feat: add colorscheme to user in db (#987)
This commit is contained in:
89
apps/nextjs/src/app/[locale]/_client-providers/mantine.tsx
Normal file
89
apps/nextjs/src/app/[locale]/_client-providers/mantine.tsx
Normal file
@@ -0,0 +1,89 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useState } from "react";
|
||||||
|
import type { PropsWithChildren } from "react";
|
||||||
|
import type { MantineColorScheme, MantineColorSchemeManager } from "@mantine/core";
|
||||||
|
import { createTheme, isMantineColorScheme, MantineProvider } from "@mantine/core";
|
||||||
|
|
||||||
|
import { clientApi } from "@homarr/api/client";
|
||||||
|
import { useSession } from "@homarr/auth/client";
|
||||||
|
|
||||||
|
export const CustomMantineProvider = ({ children }: PropsWithChildren) => {
|
||||||
|
const manager = useColorSchemeManager();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<MantineProvider
|
||||||
|
defaultColorScheme="auto"
|
||||||
|
colorSchemeManager={manager}
|
||||||
|
theme={createTheme({
|
||||||
|
primaryColor: "red",
|
||||||
|
autoContrast: true,
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</MantineProvider>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
function useColorSchemeManager(): MantineColorSchemeManager {
|
||||||
|
const key = "homarr-color-scheme";
|
||||||
|
const { data: session } = useSession();
|
||||||
|
const [sessionColorScheme, setSessionColorScheme] = useState<MantineColorScheme | undefined>(
|
||||||
|
session?.user.colorScheme,
|
||||||
|
);
|
||||||
|
const { mutate: mutateColorScheme } = clientApi.user.changeColorScheme.useMutation({
|
||||||
|
onSuccess: (_, variables) => {
|
||||||
|
setSessionColorScheme(variables.colorScheme);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
let handleStorageEvent: (event: StorageEvent) => void;
|
||||||
|
|
||||||
|
return {
|
||||||
|
get: (defaultValue) => {
|
||||||
|
if (typeof window === "undefined") {
|
||||||
|
return defaultValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sessionColorScheme) {
|
||||||
|
return sessionColorScheme;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
return (window.localStorage.getItem(key) as MantineColorScheme | undefined) ?? defaultValue;
|
||||||
|
} catch {
|
||||||
|
return defaultValue;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
set: (value) => {
|
||||||
|
try {
|
||||||
|
if (session) {
|
||||||
|
mutateColorScheme({ colorScheme: value });
|
||||||
|
}
|
||||||
|
window.localStorage.setItem(key, value);
|
||||||
|
} catch (error) {
|
||||||
|
console.warn("[@mantine/core] Local storage color scheme manager was unable to save color scheme.", error);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
subscribe: (onUpdate) => {
|
||||||
|
handleStorageEvent = (event) => {
|
||||||
|
if (session) return; // Ignore updates when session is available as we are using session color scheme
|
||||||
|
if (event.storageArea === window.localStorage && event.key === key && isMantineColorScheme(event.newValue)) {
|
||||||
|
onUpdate(event.newValue);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
window.addEventListener("storage", handleStorageEvent);
|
||||||
|
},
|
||||||
|
|
||||||
|
unsubscribe: () => {
|
||||||
|
window.removeEventListener("storage", handleStorageEvent);
|
||||||
|
},
|
||||||
|
|
||||||
|
clear: () => {
|
||||||
|
window.localStorage.removeItem(key);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -1,13 +1,11 @@
|
|||||||
import type { Metadata, Viewport } from "next";
|
import type { Metadata, Viewport } from "next";
|
||||||
import { Inter } from "next/font/google";
|
import { Inter } from "next/font/google";
|
||||||
|
|
||||||
import "@homarr/ui/styles.css";
|
|
||||||
import "@homarr/notifications/styles.css";
|
import "@homarr/notifications/styles.css";
|
||||||
import "@homarr/spotlight/styles.css";
|
import "@homarr/spotlight/styles.css";
|
||||||
|
import "@homarr/ui/styles.css";
|
||||||
import "~/styles/scroll-area.scss";
|
import "~/styles/scroll-area.scss";
|
||||||
|
|
||||||
import { ColorSchemeScript, createTheme, MantineProvider } from "@mantine/core";
|
|
||||||
|
|
||||||
import { env } from "@homarr/auth/env.mjs";
|
import { env } from "@homarr/auth/env.mjs";
|
||||||
import { auth } from "@homarr/auth/next";
|
import { auth } from "@homarr/auth/next";
|
||||||
import { ModalProvider } from "@homarr/modals";
|
import { ModalProvider } from "@homarr/modals";
|
||||||
@@ -15,6 +13,7 @@ import { Notifications } from "@homarr/notifications";
|
|||||||
|
|
||||||
import { Analytics } from "~/components/layout/analytics";
|
import { Analytics } from "~/components/layout/analytics";
|
||||||
import { JotaiProvider } from "./_client-providers/jotai";
|
import { JotaiProvider } from "./_client-providers/jotai";
|
||||||
|
import { CustomMantineProvider } from "./_client-providers/mantine";
|
||||||
import { NextInternationalProvider } from "./_client-providers/next-international";
|
import { NextInternationalProvider } from "./_client-providers/next-international";
|
||||||
import { AuthProvider } from "./_client-providers/session";
|
import { AuthProvider } from "./_client-providers/session";
|
||||||
import { TRPCReactProvider } from "./_client-providers/trpc";
|
import { TRPCReactProvider } from "./_client-providers/trpc";
|
||||||
@@ -51,34 +50,25 @@ export const viewport: Viewport = {
|
|||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function Layout(props: { children: React.ReactNode; params: { locale: string } }) {
|
export default async function Layout(props: { children: React.ReactNode; params: { locale: string } }) {
|
||||||
const colorScheme = "dark";
|
const session = await auth();
|
||||||
|
const colorScheme = session?.user.colorScheme;
|
||||||
|
|
||||||
const StackedProvider = composeWrappers([
|
const StackedProvider = composeWrappers([
|
||||||
async (innerProps) => {
|
(innerProps) => {
|
||||||
const session = await auth();
|
|
||||||
return <AuthProvider session={session} logoutUrl={env.AUTH_LOGOUT_REDIRECT_URL} {...innerProps} />;
|
return <AuthProvider session={session} logoutUrl={env.AUTH_LOGOUT_REDIRECT_URL} {...innerProps} />;
|
||||||
},
|
},
|
||||||
(innerProps) => <JotaiProvider {...innerProps} />,
|
(innerProps) => <JotaiProvider {...innerProps} />,
|
||||||
(innerProps) => <TRPCReactProvider {...innerProps} />,
|
(innerProps) => <TRPCReactProvider {...innerProps} />,
|
||||||
(innerProps) => <NextInternationalProvider {...innerProps} locale={props.params.locale} />,
|
(innerProps) => <NextInternationalProvider {...innerProps} locale={props.params.locale} />,
|
||||||
(innerProps) => (
|
(innerProps) => <CustomMantineProvider {...innerProps} />,
|
||||||
<MantineProvider
|
|
||||||
{...innerProps}
|
|
||||||
defaultColorScheme="dark"
|
|
||||||
theme={createTheme({
|
|
||||||
primaryColor: "red",
|
|
||||||
autoContrast: true,
|
|
||||||
})}
|
|
||||||
/>
|
|
||||||
),
|
|
||||||
(innerProps) => <ModalProvider {...innerProps} />,
|
(innerProps) => <ModalProvider {...innerProps} />,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<html lang="en" suppressHydrationWarning>
|
// Instead of ColorSchemScript we use data-mantine-color-scheme to prevent flickering
|
||||||
|
<html lang="en" data-mantine-color-scheme={colorScheme} suppressHydrationWarning>
|
||||||
<head>
|
<head>
|
||||||
<ColorSchemeScript defaultColorScheme={colorScheme} />
|
|
||||||
<Analytics />
|
<Analytics />
|
||||||
</head>
|
</head>
|
||||||
<body className={["font-sans", fontSans.variable].join(" ")}>
|
<body className={["font-sans", fontSans.variable].join(" ")}>
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ const defaultSession = {
|
|||||||
user: {
|
user: {
|
||||||
id: defaultCreatorId,
|
id: defaultCreatorId,
|
||||||
permissions: [],
|
permissions: [],
|
||||||
|
colorScheme: "light",
|
||||||
},
|
},
|
||||||
expires: new Date().toISOString(),
|
expires: new Date().toISOString(),
|
||||||
} satisfies Session;
|
} satisfies Session;
|
||||||
@@ -87,6 +88,7 @@ describe("getAllBoards should return all boards accessable to the current user",
|
|||||||
user: {
|
user: {
|
||||||
id: defaultCreatorId,
|
id: defaultCreatorId,
|
||||||
permissions: ["board-view-all"],
|
permissions: ["board-view-all"],
|
||||||
|
colorScheme: "light",
|
||||||
},
|
},
|
||||||
expires: new Date().toISOString(),
|
expires: new Date().toISOString(),
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ const createSessionWithPermissions = (...permissions: GroupPermissionKey[]) =>
|
|||||||
user: {
|
user: {
|
||||||
id: "1",
|
id: "1",
|
||||||
permissions,
|
permissions,
|
||||||
|
colorScheme: "light",
|
||||||
},
|
},
|
||||||
expires: new Date().toISOString(),
|
expires: new Date().toISOString(),
|
||||||
}) satisfies Session;
|
}) satisfies Session;
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ const defaultSession = {
|
|||||||
user: {
|
user: {
|
||||||
id: defaultOwnerId,
|
id: defaultOwnerId,
|
||||||
permissions: [],
|
permissions: [],
|
||||||
|
colorScheme: "light",
|
||||||
},
|
},
|
||||||
expires: new Date().toISOString(),
|
expires: new Date().toISOString(),
|
||||||
} satisfies Session;
|
} satisfies Session;
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ const defaultSessionWithPermissions = (permissions: GroupPermissionKey[] = []) =
|
|||||||
user: {
|
user: {
|
||||||
id: defaultUserId,
|
id: defaultUserId,
|
||||||
permissions,
|
permissions,
|
||||||
|
colorScheme: "light",
|
||||||
},
|
},
|
||||||
expires: new Date().toISOString(),
|
expires: new Date().toISOString(),
|
||||||
}) satisfies Session;
|
}) satisfies Session;
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ const defaultSession = {
|
|||||||
user: {
|
user: {
|
||||||
id: createId(),
|
id: createId(),
|
||||||
permissions: [],
|
permissions: [],
|
||||||
|
colorScheme: "light",
|
||||||
},
|
},
|
||||||
expires: new Date().toISOString(),
|
expires: new Date().toISOString(),
|
||||||
} satisfies Session;
|
} satisfies Session;
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ const defaultSession = {
|
|||||||
user: {
|
user: {
|
||||||
id: createId(),
|
id: createId(),
|
||||||
permissions: [],
|
permissions: [],
|
||||||
|
colorScheme: "light",
|
||||||
},
|
},
|
||||||
expires: new Date().toISOString(),
|
expires: new Date().toISOString(),
|
||||||
} satisfies Session;
|
} satisfies Session;
|
||||||
|
|||||||
@@ -246,6 +246,7 @@ describe("editProfile shoud update user", () => {
|
|||||||
image: null,
|
image: null,
|
||||||
homeBoardId: null,
|
homeBoardId: null,
|
||||||
provider: "credentials",
|
provider: "credentials",
|
||||||
|
colorScheme: "auto",
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -287,6 +288,7 @@ describe("editProfile shoud update user", () => {
|
|||||||
image: null,
|
image: null,
|
||||||
homeBoardId: null,
|
homeBoardId: null,
|
||||||
provider: "credentials",
|
provider: "credentials",
|
||||||
|
colorScheme: "auto",
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -312,6 +314,7 @@ describe("delete should delete user", () => {
|
|||||||
salt: null,
|
salt: null,
|
||||||
homeBoardId: null,
|
homeBoardId: null,
|
||||||
provider: "ldap" as const,
|
provider: "ldap" as const,
|
||||||
|
colorScheme: "auto" as const,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: userToDelete,
|
id: userToDelete,
|
||||||
@@ -322,6 +325,7 @@ describe("delete should delete user", () => {
|
|||||||
password: null,
|
password: null,
|
||||||
salt: null,
|
salt: null,
|
||||||
homeBoardId: null,
|
homeBoardId: null,
|
||||||
|
colorScheme: "auto" as const,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: createId(),
|
id: createId(),
|
||||||
@@ -333,6 +337,7 @@ describe("delete should delete user", () => {
|
|||||||
salt: null,
|
salt: null,
|
||||||
homeBoardId: null,
|
homeBoardId: null,
|
||||||
provider: "oidc" as const,
|
provider: "oidc" as const,
|
||||||
|
colorScheme: "auto" as const,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|||||||
@@ -317,6 +317,14 @@ export const userRouter = createTRPCRouter({
|
|||||||
})
|
})
|
||||||
.where(eq(users.id, input.userId));
|
.where(eq(users.id, input.userId));
|
||||||
}),
|
}),
|
||||||
|
changeColorScheme: protectedProcedure.input(validation.user.changeColorScheme).mutation(async ({ input, ctx }) => {
|
||||||
|
await ctx.db
|
||||||
|
.update(users)
|
||||||
|
.set({
|
||||||
|
colorScheme: input.colorScheme,
|
||||||
|
})
|
||||||
|
.where(eq(users.id, ctx.session.user.id));
|
||||||
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
const createUserAsync = async (db: Database, input: z.infer<typeof validation.user.create>) => {
|
const createUserAsync = async (db: Database, input: z.infer<typeof validation.user.create>) => {
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import type { NextAuthConfig } from "next-auth";
|
|||||||
|
|
||||||
import type { Database } from "@homarr/db";
|
import type { Database } from "@homarr/db";
|
||||||
import { eq, inArray } from "@homarr/db";
|
import { eq, inArray } from "@homarr/db";
|
||||||
import { groupMembers, groupPermissions } from "@homarr/db/schema/sqlite";
|
import { groupMembers, groupPermissions, users } from "@homarr/db/schema/sqlite";
|
||||||
import { getPermissionsWithChildren } from "@homarr/definitions";
|
import { getPermissionsWithChildren } from "@homarr/definitions";
|
||||||
|
|
||||||
import { env } from "./env.mjs";
|
import { env } from "./env.mjs";
|
||||||
@@ -31,10 +31,18 @@ export const getCurrentUserPermissionsAsync = async (db: Database, userId: strin
|
|||||||
|
|
||||||
export const createSessionCallback = (db: Database): NextAuthCallbackOf<"session"> => {
|
export const createSessionCallback = (db: Database): NextAuthCallbackOf<"session"> => {
|
||||||
return async ({ session, user }) => {
|
return async ({ session, user }) => {
|
||||||
|
const additionalProperties = await db.query.users.findFirst({
|
||||||
|
where: eq(users.id, user.id),
|
||||||
|
columns: {
|
||||||
|
colorScheme: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...session,
|
...session,
|
||||||
user: {
|
user: {
|
||||||
...session.user,
|
...session.user,
|
||||||
|
...additionalProperties,
|
||||||
id: user.id,
|
id: user.id,
|
||||||
name: user.name,
|
name: user.name,
|
||||||
permissions: await getCurrentUserPermissionsAsync(db, user.id),
|
permissions: await getCurrentUserPermissionsAsync(db, user.id),
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { headers } from "next/headers";
|
import { headers } from "next/headers";
|
||||||
import type { DefaultSession } from "@auth/core/types";
|
import type { DefaultSession } from "@auth/core/types";
|
||||||
|
|
||||||
import type { GroupPermissionKey } from "@homarr/definitions";
|
import type { ColorScheme, GroupPermissionKey } from "@homarr/definitions";
|
||||||
|
|
||||||
import { createConfiguration } from "./configuration";
|
import { createConfiguration } from "./configuration";
|
||||||
|
|
||||||
@@ -12,6 +12,7 @@ declare module "next-auth" {
|
|||||||
user: {
|
user: {
|
||||||
id: string;
|
id: string;
|
||||||
permissions: GroupPermissionKey[];
|
permissions: GroupPermissionKey[];
|
||||||
|
colorScheme: ColorScheme;
|
||||||
} & DefaultSession["user"];
|
} & DefaultSession["user"];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ describe("constructBoardPermissions", () => {
|
|||||||
user: {
|
user: {
|
||||||
id: "1",
|
id: "1",
|
||||||
permissions: [],
|
permissions: [],
|
||||||
|
colorScheme: "light",
|
||||||
},
|
},
|
||||||
expires: new Date().toISOString(),
|
expires: new Date().toISOString(),
|
||||||
} satisfies Session;
|
} satisfies Session;
|
||||||
@@ -47,6 +48,7 @@ describe("constructBoardPermissions", () => {
|
|||||||
user: {
|
user: {
|
||||||
id: "2",
|
id: "2",
|
||||||
permissions: getPermissionsWithChildren(["board-full-all"]),
|
permissions: getPermissionsWithChildren(["board-full-all"]),
|
||||||
|
colorScheme: "light",
|
||||||
},
|
},
|
||||||
expires: new Date().toISOString(),
|
expires: new Date().toISOString(),
|
||||||
} satisfies Session;
|
} satisfies Session;
|
||||||
@@ -74,6 +76,7 @@ describe("constructBoardPermissions", () => {
|
|||||||
user: {
|
user: {
|
||||||
id: "2",
|
id: "2",
|
||||||
permissions: getPermissionsWithChildren(["board-modify-all"]),
|
permissions: getPermissionsWithChildren(["board-modify-all"]),
|
||||||
|
colorScheme: "light",
|
||||||
},
|
},
|
||||||
expires: new Date().toISOString(),
|
expires: new Date().toISOString(),
|
||||||
} satisfies Session;
|
} satisfies Session;
|
||||||
@@ -102,6 +105,7 @@ describe("constructBoardPermissions", () => {
|
|||||||
user: {
|
user: {
|
||||||
id: "2",
|
id: "2",
|
||||||
permissions: [],
|
permissions: [],
|
||||||
|
colorScheme: "light",
|
||||||
},
|
},
|
||||||
expires: new Date().toISOString(),
|
expires: new Date().toISOString(),
|
||||||
} satisfies Session;
|
} satisfies Session;
|
||||||
@@ -129,6 +133,7 @@ describe("constructBoardPermissions", () => {
|
|||||||
user: {
|
user: {
|
||||||
id: "2",
|
id: "2",
|
||||||
permissions: [],
|
permissions: [],
|
||||||
|
colorScheme: "light",
|
||||||
},
|
},
|
||||||
expires: new Date().toISOString(),
|
expires: new Date().toISOString(),
|
||||||
} satisfies Session;
|
} satisfies Session;
|
||||||
@@ -156,6 +161,7 @@ describe("constructBoardPermissions", () => {
|
|||||||
user: {
|
user: {
|
||||||
id: "2",
|
id: "2",
|
||||||
permissions: getPermissionsWithChildren(["board-view-all"]),
|
permissions: getPermissionsWithChildren(["board-view-all"]),
|
||||||
|
colorScheme: "light",
|
||||||
},
|
},
|
||||||
expires: new Date().toISOString(),
|
expires: new Date().toISOString(),
|
||||||
} satisfies Session;
|
} satisfies Session;
|
||||||
@@ -183,6 +189,7 @@ describe("constructBoardPermissions", () => {
|
|||||||
user: {
|
user: {
|
||||||
id: "2",
|
id: "2",
|
||||||
permissions: [],
|
permissions: [],
|
||||||
|
colorScheme: "light",
|
||||||
},
|
},
|
||||||
expires: new Date().toISOString(),
|
expires: new Date().toISOString(),
|
||||||
} satisfies Session;
|
} satisfies Session;
|
||||||
@@ -210,6 +217,7 @@ describe("constructBoardPermissions", () => {
|
|||||||
user: {
|
user: {
|
||||||
id: "2",
|
id: "2",
|
||||||
permissions: [],
|
permissions: [],
|
||||||
|
colorScheme: "light",
|
||||||
},
|
},
|
||||||
expires: new Date().toISOString(),
|
expires: new Date().toISOString(),
|
||||||
} satisfies Session;
|
} satisfies Session;
|
||||||
@@ -237,6 +245,7 @@ describe("constructBoardPermissions", () => {
|
|||||||
user: {
|
user: {
|
||||||
id: "2",
|
id: "2",
|
||||||
permissions: [],
|
permissions: [],
|
||||||
|
colorScheme: "light",
|
||||||
},
|
},
|
||||||
expires: new Date().toISOString(),
|
expires: new Date().toISOString(),
|
||||||
} satisfies Session;
|
} satisfies Session;
|
||||||
@@ -264,6 +273,7 @@ describe("constructBoardPermissions", () => {
|
|||||||
user: {
|
user: {
|
||||||
id: "2",
|
id: "2",
|
||||||
permissions: [],
|
permissions: [],
|
||||||
|
colorScheme: "light",
|
||||||
},
|
},
|
||||||
expires: new Date().toISOString(),
|
expires: new Date().toISOString(),
|
||||||
} satisfies Session;
|
} satisfies Session;
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ describe("constructIntegrationPermissions", () => {
|
|||||||
user: {
|
user: {
|
||||||
id: "2",
|
id: "2",
|
||||||
permissions: getPermissionsWithChildren(["integration-full-all"]),
|
permissions: getPermissionsWithChildren(["integration-full-all"]),
|
||||||
|
colorScheme: "light",
|
||||||
},
|
},
|
||||||
expires: new Date().toISOString(),
|
expires: new Date().toISOString(),
|
||||||
} satisfies Session;
|
} satisfies Session;
|
||||||
@@ -39,6 +40,7 @@ describe("constructIntegrationPermissions", () => {
|
|||||||
user: {
|
user: {
|
||||||
id: "2",
|
id: "2",
|
||||||
permissions: getPermissionsWithChildren(["integration-interact-all"]),
|
permissions: getPermissionsWithChildren(["integration-interact-all"]),
|
||||||
|
colorScheme: "light",
|
||||||
},
|
},
|
||||||
expires: new Date().toISOString(),
|
expires: new Date().toISOString(),
|
||||||
} satisfies Session;
|
} satisfies Session;
|
||||||
@@ -62,6 +64,7 @@ describe("constructIntegrationPermissions", () => {
|
|||||||
user: {
|
user: {
|
||||||
id: "2",
|
id: "2",
|
||||||
permissions: [],
|
permissions: [],
|
||||||
|
colorScheme: "light",
|
||||||
},
|
},
|
||||||
expires: new Date().toISOString(),
|
expires: new Date().toISOString(),
|
||||||
} satisfies Session;
|
} satisfies Session;
|
||||||
@@ -85,6 +88,7 @@ describe("constructIntegrationPermissions", () => {
|
|||||||
user: {
|
user: {
|
||||||
id: "2",
|
id: "2",
|
||||||
permissions: [],
|
permissions: [],
|
||||||
|
colorScheme: "light",
|
||||||
},
|
},
|
||||||
expires: new Date().toISOString(),
|
expires: new Date().toISOString(),
|
||||||
} satisfies Session;
|
} satisfies Session;
|
||||||
@@ -108,6 +112,7 @@ describe("constructIntegrationPermissions", () => {
|
|||||||
user: {
|
user: {
|
||||||
id: "2",
|
id: "2",
|
||||||
permissions: getPermissionsWithChildren(["integration-use-all"]),
|
permissions: getPermissionsWithChildren(["integration-use-all"]),
|
||||||
|
colorScheme: "light",
|
||||||
},
|
},
|
||||||
expires: new Date().toISOString(),
|
expires: new Date().toISOString(),
|
||||||
} satisfies Session;
|
} satisfies Session;
|
||||||
@@ -131,6 +136,7 @@ describe("constructIntegrationPermissions", () => {
|
|||||||
user: {
|
user: {
|
||||||
id: "2",
|
id: "2",
|
||||||
permissions: [],
|
permissions: [],
|
||||||
|
colorScheme: "light",
|
||||||
},
|
},
|
||||||
expires: new Date().toISOString(),
|
expires: new Date().toISOString(),
|
||||||
} satisfies Session;
|
} satisfies Session;
|
||||||
@@ -154,6 +160,7 @@ describe("constructIntegrationPermissions", () => {
|
|||||||
user: {
|
user: {
|
||||||
id: "2",
|
id: "2",
|
||||||
permissions: [],
|
permissions: [],
|
||||||
|
colorScheme: "light",
|
||||||
},
|
},
|
||||||
expires: new Date().toISOString(),
|
expires: new Date().toISOString(),
|
||||||
} satisfies Session;
|
} satisfies Session;
|
||||||
@@ -177,6 +184,7 @@ describe("constructIntegrationPermissions", () => {
|
|||||||
user: {
|
user: {
|
||||||
id: "2",
|
id: "2",
|
||||||
permissions: [],
|
permissions: [],
|
||||||
|
colorScheme: "light",
|
||||||
},
|
},
|
||||||
expires: new Date().toISOString(),
|
expires: new Date().toISOString(),
|
||||||
} satisfies Session;
|
} satisfies Session;
|
||||||
@@ -190,40 +198,3 @@ describe("constructIntegrationPermissions", () => {
|
|||||||
expect(result.hasUseAccess).toBe(false);
|
expect(result.hasUseAccess).toBe(false);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
/*
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
test("should return hasViewAccess as true when board is public", () => {
|
|
||||||
// Arrange
|
|
||||||
const board = {
|
|
||||||
creator: {
|
|
||||||
id: "1",
|
|
||||||
},
|
|
||||||
userPermissions: [],
|
|
||||||
groupPermissions: [],
|
|
||||||
isPublic: true,
|
|
||||||
};
|
|
||||||
const session = {
|
|
||||||
user: {
|
|
||||||
id: "2",
|
|
||||||
permissions: [],
|
|
||||||
},
|
|
||||||
expires: new Date().toISOString(),
|
|
||||||
} satisfies Session;
|
|
||||||
|
|
||||||
// Act
|
|
||||||
const result = constructBoardPermissions(board, session);
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
expect(result.hasFullAccess).toBe(false);
|
|
||||||
expect(result.hasChangeAccess).toBe(false);
|
|
||||||
expect(result.hasViewAccess).toBe(true);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
*/
|
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ const createSession = (user: Partial<Session["user"]>): Session => ({
|
|||||||
user: {
|
user: {
|
||||||
id: "1",
|
id: "1",
|
||||||
permissions: [],
|
permissions: [],
|
||||||
|
colorScheme: "light",
|
||||||
...user,
|
...user,
|
||||||
},
|
},
|
||||||
expires: new Date().toISOString(),
|
expires: new Date().toISOString(),
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ export const getSessionFromTokenAsync = async (db: Database, token: string | und
|
|||||||
name: true,
|
name: true,
|
||||||
email: true,
|
email: true,
|
||||||
image: true,
|
image: true,
|
||||||
|
colorScheme: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -101,6 +101,7 @@ describe("session callback", () => {
|
|||||||
email: "no-email",
|
email: "no-email",
|
||||||
emailVerified: new Date("2023-01-13"),
|
emailVerified: new Date("2023-01-13"),
|
||||||
permissions: [],
|
permissions: [],
|
||||||
|
colorScheme: "dark",
|
||||||
},
|
},
|
||||||
expires: "2023-01-13" as Date & string,
|
expires: "2023-01-13" as Date & string,
|
||||||
sessionToken: "token",
|
sessionToken: "token",
|
||||||
|
|||||||
1
packages/db/migrations/mysql/0007_boring_nocturne.sql
Normal file
1
packages/db/migrations/mysql/0007_boring_nocturne.sql
Normal file
@@ -0,0 +1 @@
|
|||||||
|
ALTER TABLE `user` ADD `colorScheme` varchar(5) DEFAULT 'auto' NOT NULL;
|
||||||
1373
packages/db/migrations/mysql/meta/0007_snapshot.json
Normal file
1373
packages/db/migrations/mysql/meta/0007_snapshot.json
Normal file
File diff suppressed because it is too large
Load Diff
@@ -50,6 +50,13 @@
|
|||||||
"when": 1722517058725,
|
"when": 1722517058725,
|
||||||
"tag": "0006_young_micromax",
|
"tag": "0006_young_micromax",
|
||||||
"breakpoints": true
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 7,
|
||||||
|
"version": "5",
|
||||||
|
"when": 1723749320706,
|
||||||
|
"tag": "0007_boring_nocturne",
|
||||||
|
"breakpoints": true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
1
packages/db/migrations/sqlite/0007_known_ultragirl.sql
Normal file
1
packages/db/migrations/sqlite/0007_known_ultragirl.sql
Normal file
@@ -0,0 +1 @@
|
|||||||
|
ALTER TABLE `user` ADD `colorScheme` text DEFAULT 'auto' NOT NULL;
|
||||||
1316
packages/db/migrations/sqlite/meta/0007_snapshot.json
Normal file
1316
packages/db/migrations/sqlite/meta/0007_snapshot.json
Normal file
File diff suppressed because it is too large
Load Diff
@@ -50,6 +50,13 @@
|
|||||||
"when": 1722517033483,
|
"when": 1722517033483,
|
||||||
"tag": "0006_windy_doctor_faustus",
|
"tag": "0006_windy_doctor_faustus",
|
||||||
"breakpoints": true
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 7,
|
||||||
|
"version": "6",
|
||||||
|
"when": 1723746828385,
|
||||||
|
"tag": "0007_known_ultragirl",
|
||||||
|
"breakpoints": true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import type {
|
|||||||
BackgroundImageRepeat,
|
BackgroundImageRepeat,
|
||||||
BackgroundImageSize,
|
BackgroundImageSize,
|
||||||
BoardPermission,
|
BoardPermission,
|
||||||
|
ColorScheme,
|
||||||
GroupPermissionKey,
|
GroupPermissionKey,
|
||||||
IntegrationKind,
|
IntegrationKind,
|
||||||
IntegrationPermission,
|
IntegrationPermission,
|
||||||
@@ -30,6 +31,7 @@ export const users = mysqlTable("user", {
|
|||||||
homeBoardId: varchar("homeBoardId", { length: 64 }).references((): AnyMySqlColumn => boards.id, {
|
homeBoardId: varchar("homeBoardId", { length: 64 }).references((): AnyMySqlColumn => boards.id, {
|
||||||
onDelete: "set null",
|
onDelete: "set null",
|
||||||
}),
|
}),
|
||||||
|
colorScheme: varchar("colorScheme", { length: 5 }).$type<ColorScheme>().default("auto").notNull(),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const accounts = mysqlTable(
|
export const accounts = mysqlTable(
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import type {
|
|||||||
BackgroundImageRepeat,
|
BackgroundImageRepeat,
|
||||||
BackgroundImageSize,
|
BackgroundImageSize,
|
||||||
BoardPermission,
|
BoardPermission,
|
||||||
|
ColorScheme,
|
||||||
GroupPermissionKey,
|
GroupPermissionKey,
|
||||||
IntegrationKind,
|
IntegrationKind,
|
||||||
IntegrationPermission,
|
IntegrationPermission,
|
||||||
@@ -31,6 +32,7 @@ export const users = sqliteTable("user", {
|
|||||||
homeBoardId: text("homeBoardId").references((): AnySQLiteColumn => boards.id, {
|
homeBoardId: text("homeBoardId").references((): AnySQLiteColumn => boards.id, {
|
||||||
onDelete: "set null",
|
onDelete: "set null",
|
||||||
}),
|
}),
|
||||||
|
colorScheme: text("colorScheme").$type<ColorScheme>().default("auto").notNull(),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const accounts = sqliteTable(
|
export const accounts = sqliteTable(
|
||||||
|
|||||||
@@ -5,3 +5,4 @@ export * from "./widget";
|
|||||||
export * from "./permissions";
|
export * from "./permissions";
|
||||||
export * from "./docker";
|
export * from "./docker";
|
||||||
export * from "./auth";
|
export * from "./auth";
|
||||||
|
export * from "./user";
|
||||||
|
|||||||
2
packages/definitions/src/user.ts
Normal file
2
packages/definitions/src/user.ts
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
export const colorSchemes = ["light", "dark", "auto"] as const;
|
||||||
|
export type ColorScheme = (typeof colorSchemes)[number];
|
||||||
@@ -1,7 +1,9 @@
|
|||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { colorSchemes } from "@homarr/definitions";
|
||||||
import type { TranslationObject } from "@homarr/translation";
|
import type { TranslationObject } from "@homarr/translation";
|
||||||
|
|
||||||
|
import { zodEnumFromArray } from "./enums";
|
||||||
import { createCustomErrorParams } from "./form/i18n";
|
import { createCustomErrorParams } from "./form/i18n";
|
||||||
|
|
||||||
const usernameSchema = z.string().min(3).max(255);
|
const usernameSchema = z.string().min(3).max(255);
|
||||||
@@ -98,6 +100,10 @@ const changeHomeBoardSchema = z.object({
|
|||||||
homeBoardId: z.string().min(1),
|
homeBoardId: z.string().min(1),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const changeColorSchemeSchema = z.object({
|
||||||
|
colorScheme: zodEnumFromArray(colorSchemes),
|
||||||
|
});
|
||||||
|
|
||||||
export const userSchemas = {
|
export const userSchemas = {
|
||||||
signIn: signInSchema,
|
signIn: signInSchema,
|
||||||
registration: registrationSchema,
|
registration: registrationSchema,
|
||||||
@@ -109,4 +115,5 @@ export const userSchemas = {
|
|||||||
changePassword: changePasswordSchema,
|
changePassword: changePasswordSchema,
|
||||||
changeHomeBoard: changeHomeBoardSchema,
|
changeHomeBoard: changeHomeBoardSchema,
|
||||||
changePasswordApi: changePasswordApiSchema,
|
changePasswordApi: changePasswordApiSchema,
|
||||||
|
changeColorScheme: changeColorSchemeSchema,
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user