mirror of
https://github.com/ajnart/homarr.git
synced 2026-02-26 08:20:56 +01:00
feat(users): add libravatar / gravatar support (#4277)
Co-authored-by: HeapReaper <kelivn@heapreaper.nl> Co-authored-by: Meier Lukas <meierschlumpf@gmail.com>
This commit is contained in:
@@ -106,6 +106,7 @@ export default async function Layout(props: {
|
||||
forceDisableStatus: serverSettings.board.forceDisableStatus,
|
||||
},
|
||||
search: { defaultSearchEngineId: serverSettings.search.defaultSearchEngineId },
|
||||
user: { enableGravatar: serverSettings.user.enableGravatar },
|
||||
}}
|
||||
{...innerProps}
|
||||
/>
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
"use client";
|
||||
|
||||
import { Switch } from "@mantine/core";
|
||||
|
||||
import type { ServerSettings } from "@homarr/server-settings";
|
||||
import { useScopedI18n } from "@homarr/translation/client";
|
||||
|
||||
import { CommonSettingsForm } from "./common-form";
|
||||
|
||||
export const UserSettingsForm = ({ defaultValues }: { defaultValues: ServerSettings["user"] }) => {
|
||||
const tUser = useScopedI18n("management.page.settings.section.user");
|
||||
|
||||
return (
|
||||
<CommonSettingsForm settingKey="user" defaultValues={defaultValues}>
|
||||
{(form) => (
|
||||
<>
|
||||
<Switch
|
||||
{...form.getInputProps("enableGravatar", { type: "checkbox" })}
|
||||
label={tUser("enableGravatar.label")}
|
||||
description={tUser("enableGravatar.description")}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</CommonSettingsForm>
|
||||
);
|
||||
};
|
||||
@@ -12,6 +12,7 @@ import { AppearanceSettingsForm } from "./_components/appearance-settings-form";
|
||||
import { BoardSettingsForm } from "./_components/board-settings-form";
|
||||
import { CultureSettingsForm } from "./_components/culture-settings-form";
|
||||
import { SearchSettingsForm } from "./_components/search-settings-form";
|
||||
import { UserSettingsForm } from "./_components/user-settings-form";
|
||||
|
||||
export async function generateMetadata() {
|
||||
const t = await getScopedI18n("management");
|
||||
@@ -42,6 +43,10 @@ export default async function SettingsPage() {
|
||||
<Title order={2}>{tSettings("section.board.title")}</Title>
|
||||
<BoardSettingsForm defaultValues={serverSettings.board} />
|
||||
</Stack>
|
||||
<Stack>
|
||||
<Title order={2}>{tSettings("section.user.title")}</Title>
|
||||
<UserSettingsForm defaultValues={serverSettings.user} />
|
||||
</Stack>
|
||||
<Stack>
|
||||
<Title order={2}>{tSettings("section.search.title")}</Title>
|
||||
<SearchSettingsForm defaultValues={serverSettings.search} />
|
||||
|
||||
@@ -200,7 +200,10 @@ export const UserCreateStepperComponent = ({ initialGroups }: UserCreateStepperC
|
||||
<Stepper.Step label={t("step.review.label")} allowStepSelect={false} allowStepClick={false}>
|
||||
<Card p="xl" shadow="md" withBorder>
|
||||
<Stack maw={300} align="center" mx="auto">
|
||||
<UserAvatar size="xl" user={{ name: generalForm.values.username, image: null }} />
|
||||
<UserAvatar
|
||||
size="xl"
|
||||
user={{ name: generalForm.values.username, email: generalForm.values.email ?? null, image: null }}
|
||||
/>
|
||||
<Text tt="uppercase" fw="bolder" size="xl">
|
||||
{generalForm.values.username}
|
||||
</Text>
|
||||
|
||||
@@ -44,7 +44,7 @@ export default async function GroupsDetailPage(props: GroupsDetailPageProps) {
|
||||
<Card>
|
||||
{group.owner ? (
|
||||
<Group>
|
||||
<UserAvatar user={{ name: group.owner.name, image: group.owner.image }} size={"lg"} />
|
||||
<UserAvatar user={group.owner} size={"lg"} />
|
||||
<Stack align={"start"} gap={3}>
|
||||
<Text fw={"bold"}>{group.owner.name}</Text>
|
||||
<Text>{group.owner.email}</Text>
|
||||
|
||||
@@ -26,6 +26,7 @@ interface UserAccessPermission<TPermission extends string> {
|
||||
user: {
|
||||
name: string | null;
|
||||
image: string | null;
|
||||
email: string | null;
|
||||
id: string;
|
||||
};
|
||||
}
|
||||
@@ -66,6 +67,7 @@ interface Props<TPermission extends string> {
|
||||
id: string;
|
||||
name: string | null;
|
||||
image: string | null;
|
||||
email: string | null;
|
||||
} | null;
|
||||
};
|
||||
translate: (key: TPermission) => string;
|
||||
|
||||
@@ -21,6 +21,7 @@ export interface FormProps<TPermission extends string> {
|
||||
id: string;
|
||||
name: string | null;
|
||||
image: string | null;
|
||||
email: string | null;
|
||||
} | null;
|
||||
};
|
||||
accessQueryData: AccessQueryData<TPermission>;
|
||||
@@ -118,6 +119,7 @@ interface UserItemContentProps {
|
||||
id: string;
|
||||
name: string | null;
|
||||
image: string | null;
|
||||
email: string | null;
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@ import { UserAvatar } from "@homarr/ui";
|
||||
interface InnerProps {
|
||||
presentUserIds: string[];
|
||||
excludeExternalProviders?: boolean;
|
||||
onSelect: (props: { id: string; name: string; image: string }) => void | Promise<void>;
|
||||
onSelect: (props: { id: string; name: string; image: string; email: string | null }) => void | Promise<void>;
|
||||
confirmLabel?: string;
|
||||
}
|
||||
|
||||
@@ -36,6 +36,7 @@ export const UserSelectModal = createModal<InnerProps>(({ actions, innerProps })
|
||||
id: currentUser.id,
|
||||
name: currentUser.name ?? "",
|
||||
image: currentUser.image ?? "",
|
||||
email: currentUser.email ?? null,
|
||||
});
|
||||
|
||||
setLoading(false);
|
||||
|
||||
@@ -34,6 +34,7 @@ export class BoardMockBuilder {
|
||||
id: createId(),
|
||||
image: null,
|
||||
name: "User",
|
||||
email: null,
|
||||
},
|
||||
groupPermissions: [],
|
||||
userPermissions: [],
|
||||
|
||||
@@ -3,17 +3,21 @@ import type { MantineSize } from "@mantine/core";
|
||||
import { auth } from "@homarr/auth/next";
|
||||
import { UserAvatar } from "@homarr/ui";
|
||||
|
||||
interface UserAvatarProps {
|
||||
interface CurrentUserAvatarProps {
|
||||
size: MantineSize;
|
||||
}
|
||||
|
||||
export const CurrentUserAvatar = async ({ size }: UserAvatarProps) => {
|
||||
export const CurrentUserAvatar = async ({ size }: CurrentUserAvatarProps) => {
|
||||
const currentSession = await auth();
|
||||
|
||||
const user = {
|
||||
name: currentSession?.user.name ?? null,
|
||||
image: currentSession?.user.image ?? null,
|
||||
};
|
||||
|
||||
return <UserAvatar user={user} size={size} />;
|
||||
return (
|
||||
<UserAvatar
|
||||
user={{
|
||||
name: currentSession?.user.name ?? null,
|
||||
image: currentSession?.user.image ?? null,
|
||||
email: currentSession?.user.email ?? null,
|
||||
}}
|
||||
size={size}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -22,6 +22,7 @@ export const apiKeysRouter = createTRPCRouter({
|
||||
id: true,
|
||||
name: true,
|
||||
image: true,
|
||||
email: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -155,6 +155,7 @@ export const boardRouter = createTRPCRouter({
|
||||
id: true,
|
||||
name: true,
|
||||
image: true,
|
||||
email: true,
|
||||
},
|
||||
},
|
||||
userPermissions: {
|
||||
@@ -1195,6 +1196,7 @@ export const boardRouter = createTRPCRouter({
|
||||
id: true,
|
||||
name: true,
|
||||
image: true,
|
||||
email: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -1537,6 +1539,7 @@ const getFullBoardWithWhereAsync = async (db: Database, where: SQL<unknown>, use
|
||||
id: true,
|
||||
name: true,
|
||||
image: true,
|
||||
email: true,
|
||||
},
|
||||
},
|
||||
sections: {
|
||||
|
||||
@@ -476,6 +476,7 @@ export const integrationRouter = createTRPCRouter({
|
||||
id: true,
|
||||
name: true,
|
||||
image: true,
|
||||
email: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -39,6 +39,7 @@ export const mediaRouter = createTRPCRouter({
|
||||
id: true,
|
||||
name: true,
|
||||
image: true,
|
||||
email: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -1220,11 +1220,11 @@ describe("getBoardPermissions should return board permissions", () => {
|
||||
expect(result.users).toEqual(
|
||||
expect.arrayContaining([
|
||||
{
|
||||
user: { id: user1, name: null, image: null },
|
||||
user: { id: user1, name: null, image: null, email: null },
|
||||
permission: "view",
|
||||
},
|
||||
{
|
||||
user: { id: user2, name: null, image: null },
|
||||
user: { id: user2, name: null, image: null, email: null },
|
||||
permission: "modify",
|
||||
},
|
||||
]),
|
||||
|
||||
@@ -174,7 +174,7 @@ export const userRouter = createTRPCRouter({
|
||||
// Is protected because also used in board access / integration access forms
|
||||
selectable: protectedProcedure
|
||||
.input(z.object({ excludeExternalProviders: z.boolean().default(false) }).optional())
|
||||
.output(z.array(selectUserSchema.pick({ id: true, name: true, image: true })))
|
||||
.output(z.array(selectUserSchema.pick({ id: true, name: true, image: true, email: true })))
|
||||
.meta({ openapi: { method: "GET", path: "/api/users/selectable", tags: ["users"], protect: true } })
|
||||
.query(({ ctx, input }) => {
|
||||
return ctx.db.query.users.findMany({
|
||||
@@ -182,6 +182,7 @@ export const userRouter = createTRPCRouter({
|
||||
id: true,
|
||||
name: true,
|
||||
image: true,
|
||||
email: true,
|
||||
},
|
||||
where: input?.excludeExternalProviders ? eq(users.provider, "credentials") : undefined,
|
||||
});
|
||||
@@ -194,7 +195,7 @@ export const userRouter = createTRPCRouter({
|
||||
limit: z.number().min(1).max(100).default(10),
|
||||
}),
|
||||
)
|
||||
.output(z.array(selectUserSchema.pick({ id: true, name: true, image: true })))
|
||||
.output(z.array(selectUserSchema.pick({ id: true, name: true, image: true, email: true })))
|
||||
.meta({ openapi: { method: "POST", path: "/api/users/search", tags: ["users"], protect: true } })
|
||||
.query(async ({ input, ctx }) => {
|
||||
const dbUsers = await ctx.db.query.users.findMany({
|
||||
@@ -202,6 +203,7 @@ export const userRouter = createTRPCRouter({
|
||||
id: true,
|
||||
name: true,
|
||||
image: true,
|
||||
email: true,
|
||||
},
|
||||
where: like(users.name, `%${input.query}%`),
|
||||
limit: input.limit,
|
||||
@@ -210,6 +212,7 @@ export const userRouter = createTRPCRouter({
|
||||
id: user.id,
|
||||
name: user.name ?? "",
|
||||
image: user.image,
|
||||
email: user.email,
|
||||
}));
|
||||
}),
|
||||
getById: protectedProcedure
|
||||
|
||||
@@ -57,12 +57,6 @@ export const createRequestIntegrationJobHandler = <
|
||||
reduceWidgetOptionsWithDefaultValues(
|
||||
itemForIntegration.kind,
|
||||
{
|
||||
defaultSearchEngineId: serverSettings.search.defaultSearchEngineId,
|
||||
openSearchInNewTab: true,
|
||||
firstDayOfWeek: 1,
|
||||
homeBoardId: serverSettings.board.homeBoardId,
|
||||
mobileHomeBoardId: serverSettings.board.mobileHomeBoardId,
|
||||
pingIconsEnabled: true,
|
||||
enableStatusByDefault: serverSettings.board.enableStatusByDefault,
|
||||
forceDisableStatus: serverSettings.board.forceDisableStatus,
|
||||
},
|
||||
|
||||
@@ -5,6 +5,7 @@ export const defaultServerSettingsKeys = [
|
||||
"analytics",
|
||||
"crawlingAndIndexing",
|
||||
"board",
|
||||
"user",
|
||||
"appearance",
|
||||
"culture",
|
||||
"search",
|
||||
@@ -31,6 +32,9 @@ export const defaultServerSettings = {
|
||||
enableStatusByDefault: true,
|
||||
forceDisableStatus: false,
|
||||
},
|
||||
user: {
|
||||
enableGravatar: true,
|
||||
},
|
||||
appearance: {
|
||||
defaultColorScheme: "light" as ColorScheme,
|
||||
},
|
||||
|
||||
@@ -23,7 +23,6 @@
|
||||
},
|
||||
"prettier": "@homarr/prettier-config",
|
||||
"dependencies": {
|
||||
"@homarr/api": "workspace:^0.1.0",
|
||||
"@homarr/db": "workspace:^0.1.0",
|
||||
"@homarr/server-settings": "workspace:^0.1.0",
|
||||
"@mantine/dates": "^8.3.10",
|
||||
|
||||
@@ -10,7 +10,8 @@ export type SettingsContextProps = Pick<
|
||||
| "openSearchInNewTab"
|
||||
| "pingIconsEnabled"
|
||||
> &
|
||||
Pick<ServerSettings["board"], "enableStatusByDefault" | "forceDisableStatus">;
|
||||
Pick<ServerSettings["board"], "enableStatusByDefault" | "forceDisableStatus"> &
|
||||
Pick<ServerSettings["user"], "enableGravatar">;
|
||||
|
||||
export interface PublicServerSettings {
|
||||
search: Pick<ServerSettings["search"], "defaultSearchEngineId">;
|
||||
@@ -18,6 +19,7 @@ export interface PublicServerSettings {
|
||||
ServerSettings["board"],
|
||||
"homeBoardId" | "mobileHomeBoardId" | "enableStatusByDefault" | "forceDisableStatus"
|
||||
>;
|
||||
user: Pick<ServerSettings["user"], "enableGravatar">;
|
||||
}
|
||||
|
||||
export type UserSettings = Pick<
|
||||
@@ -45,4 +47,5 @@ export const createSettings = ({
|
||||
pingIconsEnabled: user?.pingIconsEnabled ?? false,
|
||||
enableStatusByDefault: serverSettings.board.enableStatusByDefault,
|
||||
forceDisableStatus: serverSettings.board.forceDisableStatus,
|
||||
enableGravatar: serverSettings.user.enableGravatar,
|
||||
});
|
||||
|
||||
@@ -11,7 +11,7 @@ import { interaction } from "../../lib/interaction";
|
||||
|
||||
// This has to be type so it can be interpreted as Record<string, unknown>.
|
||||
// eslint-disable-next-line @typescript-eslint/consistent-type-definitions
|
||||
type User = { id: string; name: string; image: string | null };
|
||||
type User = { id: string; name: string; image: string | null; email: string | null };
|
||||
|
||||
const userChildrenOptions = createChildrenOptions<User>({
|
||||
useActions: () => [
|
||||
|
||||
@@ -3294,6 +3294,13 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"user": {
|
||||
"title": "Users",
|
||||
"enableGravatar": {
|
||||
"label": "Enable Gravatar",
|
||||
"description": "Falls back to user avatars from Libravatar/Gravatar when no custom avatar is set and an email is configured"
|
||||
}
|
||||
},
|
||||
"search": {
|
||||
"title": "Search",
|
||||
"defaultSearchEngine": {
|
||||
|
||||
@@ -27,12 +27,14 @@
|
||||
"dependencies": {
|
||||
"@homarr/common": "workspace:^0.1.0",
|
||||
"@homarr/definitions": "workspace:^0.1.0",
|
||||
"@homarr/settings": "workspace:^0.1.0",
|
||||
"@homarr/translation": "workspace:^0.1.0",
|
||||
"@homarr/validation": "workspace:^0.1.0",
|
||||
"@mantine/core": "^8.3.10",
|
||||
"@mantine/dates": "^8.3.10",
|
||||
"@mantine/hooks": "^8.3.10",
|
||||
"@tabler/icons-react": "^3.36.1",
|
||||
"crypto-js": "^4.2.0",
|
||||
"mantine-react-table": "2.0.0-beta.9",
|
||||
"next": "16.1.1",
|
||||
"react": "19.2.3",
|
||||
@@ -43,6 +45,7 @@
|
||||
"@homarr/eslint-config": "workspace:^0.2.0",
|
||||
"@homarr/prettier-config": "workspace:^0.1.0",
|
||||
"@homarr/tsconfig": "workspace:^0.1.0",
|
||||
"@types/crypto-js": "^4.2.2",
|
||||
"@types/css-modules": "^1.0.5",
|
||||
"eslint": "^9.39.2",
|
||||
"typescript": "^5.9.3"
|
||||
|
||||
@@ -1,9 +1,15 @@
|
||||
"use client";
|
||||
|
||||
import type { AvatarProps } from "@mantine/core";
|
||||
import { Avatar } from "@mantine/core";
|
||||
import { enc, MD5 } from "crypto-js";
|
||||
|
||||
import { useSettings } from "@homarr/settings";
|
||||
|
||||
export interface UserProps {
|
||||
name: string | null;
|
||||
image: string | null;
|
||||
email: string | null;
|
||||
}
|
||||
|
||||
interface UserAvatarProps {
|
||||
@@ -12,10 +18,18 @@ interface UserAvatarProps {
|
||||
}
|
||||
|
||||
export const UserAvatar = ({ user, size }: UserAvatarProps) => {
|
||||
const { enableGravatar } = useSettings();
|
||||
|
||||
if (!user?.name) return <Avatar size={size} />;
|
||||
|
||||
if (user.image) {
|
||||
return <Avatar src={user.image} alt={user.name} size={size} />;
|
||||
}
|
||||
|
||||
if (user.email && enableGravatar) {
|
||||
const emailHash = MD5(user.email.trim().toLowerCase()).toString(enc.Hex);
|
||||
return <Avatar src={`https://seccdn.libravatar.org/avatar/${emailHash}?d=blank`} alt={user.name} size={size} />;
|
||||
}
|
||||
|
||||
return <Avatar name={user.name} color="initials" size={size}></Avatar>;
|
||||
};
|
||||
|
||||
@@ -42,7 +42,9 @@ export interface WidgetDefinition {
|
||||
icon: TablerIcon;
|
||||
supportedIntegrations?: IntegrationKind[];
|
||||
integrationsRequired?: boolean;
|
||||
createOptions: (settings: SettingsContextProps) => WidgetOptionsRecord;
|
||||
createOptions: (
|
||||
settings: Pick<SettingsContextProps, "enableStatusByDefault" | "forceDisableStatus">,
|
||||
) => WidgetOptionsRecord;
|
||||
errors?: Partial<
|
||||
Record<
|
||||
DefaultErrorData["code"],
|
||||
|
||||
@@ -115,7 +115,7 @@ export type inferSupportedIntegrationsStrict<TKind extends WidgetKind> = (Widget
|
||||
|
||||
export const reduceWidgetOptionsWithDefaultValues = (
|
||||
kind: WidgetKind,
|
||||
settings: SettingsContextProps,
|
||||
settings: Pick<SettingsContextProps, "enableStatusByDefault" | "forceDisableStatus">,
|
||||
currentValue: Record<string, unknown> = {},
|
||||
) => {
|
||||
const definition = widgetImports[kind].definition;
|
||||
|
||||
22
pnpm-lock.yaml
generated
22
pnpm-lock.yaml
generated
@@ -1898,9 +1898,6 @@ importers:
|
||||
|
||||
packages/settings:
|
||||
dependencies:
|
||||
'@homarr/api':
|
||||
specifier: workspace:^0.1.0
|
||||
version: link:../api
|
||||
'@homarr/db':
|
||||
specifier: workspace:^0.1.0
|
||||
version: link:../db
|
||||
@@ -2066,6 +2063,9 @@ importers:
|
||||
'@homarr/definitions':
|
||||
specifier: workspace:^0.1.0
|
||||
version: link:../definitions
|
||||
'@homarr/settings':
|
||||
specifier: workspace:^0.1.0
|
||||
version: link:../settings
|
||||
'@homarr/translation':
|
||||
specifier: workspace:^0.1.0
|
||||
version: link:../translation
|
||||
@@ -2084,6 +2084,9 @@ importers:
|
||||
'@tabler/icons-react':
|
||||
specifier: ^3.36.1
|
||||
version: 3.36.1(react@19.2.3)
|
||||
crypto-js:
|
||||
specifier: ^4.2.0
|
||||
version: 4.2.0
|
||||
mantine-react-table:
|
||||
specifier: 2.0.0-beta.9
|
||||
version: 2.0.0-beta.9(@mantine/core@8.3.10(@mantine/hooks@8.3.10(react@19.2.3))(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(@mantine/dates@8.3.10(@mantine/core@8.3.10(@mantine/hooks@8.3.10(react@19.2.3))(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(@mantine/hooks@8.3.10(react@19.2.3))(dayjs@1.11.19)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(@mantine/hooks@8.3.10(react@19.2.3))(@tabler/icons-react@3.36.1(react@19.2.3))(clsx@2.1.1)(dayjs@1.11.19)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
|
||||
@@ -2109,6 +2112,9 @@ importers:
|
||||
'@homarr/tsconfig':
|
||||
specifier: workspace:^0.1.0
|
||||
version: link:../../tooling/typescript
|
||||
'@types/crypto-js':
|
||||
specifier: ^4.2.2
|
||||
version: 4.2.2
|
||||
'@types/css-modules':
|
||||
specifier: ^1.0.5
|
||||
version: 1.0.5
|
||||
@@ -4802,6 +4808,9 @@ packages:
|
||||
'@types/cors@2.8.17':
|
||||
resolution: {integrity: sha512-8CGDvrBj1zgo2qE+oS3pOCyYNqCPryMWY2bGfwA0dcfopWGgxs+78df0Rs3rc9THP4JkOhLsAa+15VdpAqkcUA==}
|
||||
|
||||
'@types/crypto-js@4.2.2':
|
||||
resolution: {integrity: sha512-sDOLlVbHhXpAUAL0YHDUUwDZf3iN4Bwi4W6a0W0b+QcAezUbRtH4FVb+9J4h+XFPW7l/gQ9F8qC7P+Ec4k8QVQ==}
|
||||
|
||||
'@types/css-font-loading-module@0.0.7':
|
||||
resolution: {integrity: sha512-nl09VhutdjINdWyXxHWN/w9zlNCfr60JUqJbd24YXUuCwgeL0TpFSdElCwb6cxfB6ybE19Gjj4g0jsgkXxKv1Q==}
|
||||
|
||||
@@ -5980,6 +5989,9 @@ packages:
|
||||
crossws@0.3.5:
|
||||
resolution: {integrity: sha512-ojKiDvcmByhwa8YYqbQI/hg7MEU0NC03+pSdEq4ZUnZR9xXpwk7E43SMNGkn+JxJGPFtNvQ48+vV2p+P1ml5PA==}
|
||||
|
||||
crypto-js@4.2.0:
|
||||
resolution: {integrity: sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q==}
|
||||
|
||||
crypto-random-string@2.0.0:
|
||||
resolution: {integrity: sha512-v1plID3y9r/lPhviJ1wrXpLeyUIGAZ2SHNYTEapm7/8A9nLPoyvVp3RK/EPFqn5kEznyWgYZNsRtYYIWbuG8KA==}
|
||||
engines: {node: '>=8'}
|
||||
@@ -13787,6 +13799,8 @@ snapshots:
|
||||
dependencies:
|
||||
'@types/node': 24.10.4
|
||||
|
||||
'@types/crypto-js@4.2.2': {}
|
||||
|
||||
'@types/css-font-loading-module@0.0.7': {}
|
||||
|
||||
'@types/css-modules@1.0.5': {}
|
||||
@@ -15148,6 +15162,8 @@ snapshots:
|
||||
dependencies:
|
||||
uncrypto: 0.1.3
|
||||
|
||||
crypto-js@4.2.0: {}
|
||||
|
||||
crypto-random-string@2.0.0: {}
|
||||
|
||||
crypto-random-string@4.0.0:
|
||||
|
||||
Reference in New Issue
Block a user