diff --git a/apps/nextjs/package.json b/apps/nextjs/package.json index 8b05651a2..5130741e5 100644 --- a/apps/nextjs/package.json +++ b/apps/nextjs/package.json @@ -29,6 +29,7 @@ "@homarr/modals": "workspace:^0.1.0", "@homarr/modals-collection": "workspace:^0.1.0", "@homarr/notifications": "workspace:^0.1.0", + "@homarr/old-import": "workspace:^0.1.0", "@homarr/old-schema": "workspace:^0.1.0", "@homarr/redis": "workspace:^0.1.0", "@homarr/server-settings": "workspace:^0.1.0", @@ -39,6 +40,7 @@ "@homarr/widgets": "workspace:^0.1.0", "@mantine/colors-generator": "^7.15.1", "@mantine/core": "^7.15.1", + "@mantine/dropzone": "^7.15.1", "@mantine/hooks": "^7.15.1", "@mantine/modals": "^7.15.1", "@mantine/tiptap": "^7.15.1", diff --git a/apps/nextjs/src/app/[locale]/_client-providers/trpc.tsx b/apps/nextjs/src/app/[locale]/_client-providers/trpc.tsx index 8a2e6c581..45249c800 100644 --- a/apps/nextjs/src/app/[locale]/_client-providers/trpc.tsx +++ b/apps/nextjs/src/app/[locale]/_client-providers/trpc.tsx @@ -15,6 +15,7 @@ import { wsLink, } from "@trpc/client"; import superjson from "superjson"; +import type { SuperJSONResult } from "superjson"; import type { AppRouter } from "@homarr/api"; import { clientApi, createHeadersCallbackForSource, getTrpcUrl } from "@homarr/api/client"; @@ -82,8 +83,8 @@ export function TRPCReactProvider(props: PropsWithChildren) { serialize(object: unknown) { return object; }, - deserialize(data: unknown) { - return data; + deserialize(data: SuperJSONResult) { + return superjson.deserialize(data); }, }, url: getTrpcUrl(), diff --git a/apps/nextjs/src/app/[locale]/init/_steps/back.tsx b/apps/nextjs/src/app/[locale]/init/_steps/back.tsx new file mode 100644 index 000000000..7fa98d283 --- /dev/null +++ b/apps/nextjs/src/app/[locale]/init/_steps/back.tsx @@ -0,0 +1,23 @@ +"use client"; + +import { Button } from "@mantine/core"; + +import { clientApi } from "@homarr/api/client"; +import { revalidatePathActionAsync } from "@homarr/common/client"; +import { useI18n } from "@homarr/translation/client"; + +export const BackToStart = () => { + const t = useI18n(); + const { mutateAsync, isPending } = clientApi.onboard.previousStep.useMutation(); + + const handleBackToStartAsync = async () => { + await mutateAsync(); + await revalidatePathActionAsync("/init"); + }; + + return ( + + ); +}; diff --git a/apps/nextjs/src/app/[locale]/init/_steps/finish/init-finish.tsx b/apps/nextjs/src/app/[locale]/init/_steps/finish/init-finish.tsx new file mode 100644 index 000000000..d60da53ab --- /dev/null +++ b/apps/nextjs/src/app/[locale]/init/_steps/finish/init-finish.tsx @@ -0,0 +1,87 @@ +import Link from "next/link"; +import type { MantineColor } from "@mantine/core"; +import { Button, Card, Stack, Text } from "@mantine/core"; +import { IconBook2, IconCategoryPlus, IconLayoutDashboard, IconMailForward } from "@tabler/icons-react"; + +import { isProviderEnabled } from "@homarr/auth/server"; +import { getMantineColor } from "@homarr/common"; +import { db } from "@homarr/db"; +import { createDocumentationLink } from "@homarr/definitions"; +import { getScopedI18n } from "@homarr/translation/server"; +import type { TablerIcon } from "@homarr/ui"; + +export const InitFinish = async () => { + const firstBoard = await db.query.boards.findFirst({ columns: { name: true } }); + const tFinish = await getScopedI18n("init.step.finish"); + + return ( + + + {tFinish("description")} + + {firstBoard ? ( + + {tFinish("action.goToBoard", { name: firstBoard.name })} + + ) : ( + + {tFinish("action.createBoard")} + + )} + + {isProviderEnabled("credentials") && ( + + {tFinish("action.inviteUser")} + + )} + + + {tFinish("action.docs")} + + + + ); +}; + +interface LinkButtonProps { + href: string; + children: string; + iconProps: IconProps; +} + +interface IconProps { + icon: TablerIcon; + color: MantineColor; +} + +const Icon = ({ icon: IcomComponent, color }: IconProps) => { + return ; +}; + +const InternalLinkButton = ({ href, children, iconProps }: LinkButtonProps) => { + return ( + + ); +}; + +const ExternalLinkButton = ({ href, children, iconProps }: LinkButtonProps) => { + return ( + + ); +}; diff --git a/apps/nextjs/src/app/[locale]/init/_steps/group/init-group.tsx b/apps/nextjs/src/app/[locale]/init/_steps/group/init-group.tsx new file mode 100644 index 000000000..93839a457 --- /dev/null +++ b/apps/nextjs/src/app/[locale]/init/_steps/group/init-group.tsx @@ -0,0 +1,52 @@ +"use client"; + +import { Button, Card, Stack, TextInput } from "@mantine/core"; +import { IconArrowRight } from "@tabler/icons-react"; + +import { clientApi } from "@homarr/api/client"; +import { revalidatePathActionAsync } from "@homarr/common/client"; +import { useZodForm } from "@homarr/form"; +import { useI18n } from "@homarr/translation/client"; +import type { z } from "@homarr/validation"; +import { validation } from "@homarr/validation"; + +export const InitGroup = () => { + const t = useI18n(); + const { mutateAsync } = clientApi.group.createInitialExternalGroup.useMutation(); + const form = useZodForm(validation.group.create, { + initialValues: { + name: "", + }, + }); + + const handleSubmitAsync = async (values: z.infer) => { + await mutateAsync(values, { + async onSuccess() { + await revalidatePathActionAsync("/init"); + }, + onError(error) { + if (error.data?.code === "CONFLICT") { + form.setErrors({ name: t("common.zod.errors.custom.groupNameTaken") }); + } + }, + }); + }; + + return ( + +
+ + + + +
+
+ ); +}; diff --git a/apps/nextjs/src/app/[locale]/init/_steps/import/file-info-card.tsx b/apps/nextjs/src/app/[locale]/init/_steps/import/file-info-card.tsx new file mode 100644 index 000000000..7c031ae15 --- /dev/null +++ b/apps/nextjs/src/app/[locale]/init/_steps/import/file-info-card.tsx @@ -0,0 +1,41 @@ +import { ActionIcon, Button, Card, Group, Text } from "@mantine/core"; +import type { FileWithPath } from "@mantine/dropzone"; +import { IconPencil } from "@tabler/icons-react"; + +import { humanFileSize } from "@homarr/common"; +import { useScopedI18n } from "@homarr/translation/client"; + +interface FileInfoCardProps { + file: FileWithPath; + onRemove: () => void; +} + +export const FileInfoCard = ({ file, onRemove }: FileInfoCardProps) => { + const tFileInfo = useScopedI18n("init.step.import.fileInfo"); + return ( + + + + + {file.name} + + + {humanFileSize(file.size)} + + + + + + + + + ); +}; diff --git a/apps/nextjs/src/app/[locale]/init/_steps/import/import-dropzone.tsx b/apps/nextjs/src/app/[locale]/init/_steps/import/import-dropzone.tsx new file mode 100644 index 000000000..b425267cf --- /dev/null +++ b/apps/nextjs/src/app/[locale]/init/_steps/import/import-dropzone.tsx @@ -0,0 +1,54 @@ +import { Group, rem, Text } from "@mantine/core"; +import type { FileWithPath } from "@mantine/dropzone"; +import { Dropzone, MIME_TYPES } from "@mantine/dropzone"; +import { IconFileZip, IconUpload, IconX } from "@tabler/icons-react"; + +import "@mantine/dropzone/styles.css"; + +import { useScopedI18n } from "@homarr/translation/client"; + +interface ImportDropZoneProps { + loading: boolean; + updateFile: (file: FileWithPath) => void; +} + +export const ImportDropZone = ({ loading, updateFile }: ImportDropZoneProps) => { + const tDropzone = useScopedI18n("init.step.import.dropzone"); + return ( + { + const firstFile = files[0]; + if (!firstFile) return; + + updateFile(firstFile); + }} + acceptColor="blue.6" + rejectColor="red.6" + accept={[MIME_TYPES.zip]} + loading={loading} + multiple={false} + maxSize={1024 * 1024 * 1024 * 64} // 64 MB + > + + + + + + + + + + + +
+ + {tDropzone("title")} + + + {tDropzone("description")} + +
+
+
+ ); +}; diff --git a/apps/nextjs/src/app/[locale]/init/_steps/import/init-import.tsx b/apps/nextjs/src/app/[locale]/init/_steps/import/init-import.tsx new file mode 100644 index 000000000..8b7124a03 --- /dev/null +++ b/apps/nextjs/src/app/[locale]/init/_steps/import/init-import.tsx @@ -0,0 +1,53 @@ +"use client"; + +import { startTransition, useState } from "react"; +import { Card, Stack } from "@mantine/core"; +import type { FileWithPath } from "@mantine/dropzone"; + +import type { RouterOutputs } from "@homarr/api"; +import { clientApi } from "@homarr/api/client"; +import { InitialOldmarrImport } from "@homarr/old-import/components"; + +import { FileInfoCard } from "./file-info-card"; +import { ImportDropZone } from "./import-dropzone"; + +export const InitImport = () => { + const [file, setFile] = useState(null); + const { isPending, mutate } = clientApi.import.analyseInitialOldmarrImport.useMutation(); + const [analyseResult, setAnalyseResult] = useState( + null, + ); + + if (!file) { + return ( + + { + const formData = new FormData(); + formData.append("file", file); + + mutate(formData, { + onSuccess: (result) => { + startTransition(() => { + setAnalyseResult(result); + setFile(file); + }); + }, + onError: (error) => { + console.error(error); + }, + }); + }} + /> + + ); + } + + return ( + + setFile(null)} /> + {analyseResult !== null && } + + ); +}; diff --git a/apps/nextjs/src/app/[locale]/init/_steps/settings/init-settings.tsx b/apps/nextjs/src/app/[locale]/init/_steps/settings/init-settings.tsx new file mode 100644 index 000000000..90e8c7a8d --- /dev/null +++ b/apps/nextjs/src/app/[locale]/init/_steps/settings/init-settings.tsx @@ -0,0 +1,156 @@ +"use client"; + +import { startTransition } from "react"; +import { Button, Card, Group, Stack, Switch, Text } from "@mantine/core"; +import { IconArrowRight } from "@tabler/icons-react"; + +import { clientApi } from "@homarr/api/client"; +import { revalidatePathActionAsync } from "@homarr/common/client"; +import { useZodForm } from "@homarr/form"; +import type { CheckboxProps } from "@homarr/form/types"; +import { defaultServerSettings } from "@homarr/server-settings"; +import type { TranslationObject } from "@homarr/translation"; +import { useI18n, useScopedI18n } from "@homarr/translation/client"; +import type { z } from "@homarr/validation"; +import { validation } from "@homarr/validation"; + +export const InitSettings = () => { + const tSection = useScopedI18n("management.page.settings.section"); + const t = useI18n(); + const { mutateAsync } = clientApi.serverSettings.initSettings.useMutation(); + const form = useZodForm(validation.settings.init, { initialValues: defaultServerSettings }); + + form.watch("analytics.enableGeneral", ({ value }) => { + if (!value) { + startTransition(() => { + form.setFieldValue("analytics.enableWidgetData", false); + form.setFieldValue("analytics.enableIntegrationData", false); + form.setFieldValue("analytics.enableUserData", false); + }); + } + }); + + const handleSubmitAsync = async (values: z.infer) => { + await mutateAsync(values, { + async onSuccess() { + await revalidatePathActionAsync("/init"); + }, + }); + }; + + return ( +
+ + + + {tSection("analytics.title")} + + + + + + + + + + + + + + + {tSection("crawlingAndIndexing.title")} + + + + + + + + + + + + +
+ ); +}; + +interface AnalyticsRowProps { + kind: Exclude; + disabled?: boolean; +} + +const AnalyticsRow = ({ kind, ...props }: AnalyticsRowProps & CheckboxProps) => { + const tSection = useI18n("management.page.settings.section"); + + return ( + + ); +}; + +interface CrawlingRowProps { + kind: Exclude< + keyof TranslationObject["management"]["page"]["settings"]["section"]["crawlingAndIndexing"], + "title" | "warning" + >; +} + +const CrawlingRow = ({ kind, ...inputProps }: CrawlingRowProps & CheckboxProps) => { + const tSection = useI18n("management.page.settings.section"); + + return ( + + ); +}; + +const SettingRow = ({ + title, + text, + disabled, + ...inputProps +}: { title: string; text: string; disabled?: boolean } & CheckboxProps) => { + return ( + + + + {title} + + + {text} + + + + + + ); +}; diff --git a/apps/nextjs/src/app/[locale]/init/_steps/start/init-start.tsx b/apps/nextjs/src/app/[locale]/init/_steps/start/init-start.tsx new file mode 100644 index 000000000..b5005f8b4 --- /dev/null +++ b/apps/nextjs/src/app/[locale]/init/_steps/start/init-start.tsx @@ -0,0 +1,32 @@ +import { Card, Stack, Text } from "@mantine/core"; +import { IconFileImport, IconPlayerPlay } from "@tabler/icons-react"; + +import { getMantineColor } from "@homarr/common"; +import { getScopedI18n } from "@homarr/translation/server"; + +import { InitStartButton } from "./next-button"; + +export const InitStart = async () => { + const tStart = await getScopedI18n("init.step.start"); + + return ( + + + {tStart("description")} + + } + > + {tStart("action.scratch")} + + } + > + {tStart("action.importOldmarr")} + + + + ); +}; diff --git a/apps/nextjs/src/app/[locale]/init/_steps/start/next-button.tsx b/apps/nextjs/src/app/[locale]/init/_steps/start/next-button.tsx new file mode 100644 index 000000000..00ecf6f7d --- /dev/null +++ b/apps/nextjs/src/app/[locale]/init/_steps/start/next-button.tsx @@ -0,0 +1,28 @@ +"use client"; + +import type { PropsWithChildren, ReactNode } from "react"; +import { Button } from "@mantine/core"; + +import { clientApi } from "@homarr/api/client"; +import { revalidatePathActionAsync } from "@homarr/common/client"; +import type { OnboardingStep } from "@homarr/definitions"; + +interface InitStartButtonProps { + icon: ReactNode; + preferredStep: OnboardingStep | undefined; +} + +export const InitStartButton = ({ preferredStep, icon, children }: PropsWithChildren) => { + const { mutateAsync } = clientApi.onboard.nextStep.useMutation(); + + const handleClickAsync = async () => { + await mutateAsync({ preferredStep }); + await revalidatePathActionAsync("/init"); + }; + + return ( + + ); +}; diff --git a/apps/nextjs/src/app/[locale]/init/user/_init-user-form.tsx b/apps/nextjs/src/app/[locale]/init/_steps/user/init-user-form.tsx similarity index 77% rename from apps/nextjs/src/app/[locale]/init/user/_init-user-form.tsx rename to apps/nextjs/src/app/[locale]/init/_steps/user/init-user-form.tsx index e22e81e47..2dcfac8f1 100644 --- a/apps/nextjs/src/app/[locale]/init/user/_init-user-form.tsx +++ b/apps/nextjs/src/app/[locale]/init/_steps/user/init-user-form.tsx @@ -1,9 +1,9 @@ "use client"; -import { useRouter } from "next/navigation"; import { Button, PasswordInput, Stack, TextInput } from "@mantine/core"; import { clientApi } from "@homarr/api/client"; +import { revalidatePathActionAsync } from "@homarr/common/client"; import { useZodForm } from "@homarr/form"; import { showErrorNotification, showSuccessNotification } from "@homarr/notifications"; import { useScopedI18n } from "@homarr/translation/client"; @@ -12,9 +12,9 @@ import type { z } from "@homarr/validation"; import { validation } from "@homarr/validation"; export const InitUserForm = () => { - const router = useRouter(); const t = useScopedI18n("user"); - const { mutateAsync, error, isPending } = clientApi.user.initUser.useMutation(); + const tUser = useScopedI18n("init.step.user"); + const { mutateAsync, isPending } = clientApi.user.initUser.useMutation(); const form = useZodForm(validation.user.init, { initialValues: { username: "", @@ -25,17 +25,17 @@ export const InitUserForm = () => { const handleSubmitAsync = async (values: FormType) => { await mutateAsync(values, { - onSuccess: () => { + async onSuccess() { showSuccessNotification({ - title: "User created", - message: "You can now log in", + title: tUser("notification.success.title"), + message: tUser("notification.success.message"), }); - router.push("/auth/login"); + await revalidatePathActionAsync("/init"); }, - onError: () => { + onError: (error) => { showErrorNotification({ - title: "User creation failed", - message: error?.message ?? "Unknown error", + title: tUser("notification.error.title"), + message: error.message, }); }, }); diff --git a/apps/nextjs/src/app/[locale]/init/_steps/user/init-user.tsx b/apps/nextjs/src/app/[locale]/init/_steps/user/init-user.tsx new file mode 100644 index 000000000..0f52de3dc --- /dev/null +++ b/apps/nextjs/src/app/[locale]/init/_steps/user/init-user.tsx @@ -0,0 +1,11 @@ +import { Card } from "@mantine/core"; + +import { InitUserForm } from "./init-user-form"; + +export const InitUser = () => { + return ( + + + + ); +}; diff --git a/apps/nextjs/src/app/[locale]/init/page.tsx b/apps/nextjs/src/app/[locale]/init/page.tsx new file mode 100644 index 000000000..5684e5709 --- /dev/null +++ b/apps/nextjs/src/app/[locale]/init/page.tsx @@ -0,0 +1,56 @@ +import type { JSX } from "react"; +import { Box, Center, Stack, Text, Title } from "@mantine/core"; + +import { api } from "@homarr/api/server"; +import type { MaybePromise } from "@homarr/common/types"; +import type { OnboardingStep } from "@homarr/definitions"; +import { getScopedI18n } from "@homarr/translation/server"; + +import { CurrentColorSchemeCombobox } from "~/components/color-scheme/current-color-scheme-combobox"; +import { CurrentLanguageCombobox } from "~/components/language/current-language-combobox"; +import { HomarrLogoWithTitle } from "~/components/layout/logo/homarr-logo"; +import { BackToStart } from "./_steps/back"; +import { InitFinish } from "./_steps/finish/init-finish"; +import { InitGroup } from "./_steps/group/init-group"; +import { InitImport } from "./_steps/import/init-import"; +import { InitSettings } from "./_steps/settings/init-settings"; +import { InitStart } from "./_steps/start/init-start"; +import { InitUser } from "./_steps/user/init-user"; + +const stepComponents: Record MaybePromise)> = { + start: InitStart, + import: InitImport, + user: InitUser, + group: InitGroup, + settings: InitSettings, + finish: InitFinish, +}; + +export default async function InitPage() { + const t = await getScopedI18n("init.step"); + const currentStep = await api.onboard.currentStep(); + + const CurrentComponent = stepComponents[currentStep.current]; + + return ( + +
+ + + + + {t(`${currentStep.current}.title`)} + + + {t(`${currentStep.current}.subtitle`)} + + + + + {CurrentComponent && } + {currentStep.previous === "start" && } + +
+
+ ); +} diff --git a/apps/nextjs/src/app/[locale]/init/user/page.tsx b/apps/nextjs/src/app/[locale]/init/user/page.tsx deleted file mode 100644 index 95fdaa145..000000000 --- a/apps/nextjs/src/app/[locale]/init/user/page.tsx +++ /dev/null @@ -1,41 +0,0 @@ -import { notFound } from "next/navigation"; -import { Card, Center, Stack, Text, Title } from "@mantine/core"; - -import { db } from "@homarr/db"; -import { getScopedI18n } from "@homarr/translation/server"; - -import { HomarrLogoWithTitle } from "~/components/layout/logo/homarr-logo"; -import { InitUserForm } from "./_init-user-form"; - -export default async function InitUser() { - const firstUser = await db.query.users.findFirst({ - columns: { - id: true, - }, - }); - - if (firstUser) { - notFound(); - } - - const t = await getScopedI18n("user.page.init"); - - return ( -
- - - - - {t("title")} - - - {t("subtitle")} - - - - - - -
- ); -} diff --git a/apps/nextjs/src/app/[locale]/manage/integrations/_components/secrets/integration-secret-card.tsx b/apps/nextjs/src/app/[locale]/manage/integrations/_components/secrets/integration-secret-card.tsx index 38ac5ec52..06c4baff3 100644 --- a/apps/nextjs/src/app/[locale]/manage/integrations/_components/secrets/integration-secret-card.tsx +++ b/apps/nextjs/src/app/[locale]/manage/integrations/_components/secrets/integration-secret-card.tsx @@ -1,13 +1,14 @@ "use client"; import { useState } from "react"; -import { ActionIcon, Avatar, Button, Card, Collapse, Group, Kbd, Stack, Text } from "@mantine/core"; +import { ActionIcon, Avatar, Badge, Button, Card, Collapse, Group, Kbd, Stack, Text, Tooltip } from "@mantine/core"; import { useDisclosure } from "@mantine/hooks"; import { IconEye, IconEyeOff } from "@tabler/icons-react"; import dayjs from "dayjs"; import relativeTime from "dayjs/plugin/relativeTime"; import type { RouterOutputs } from "@homarr/api"; +import type { IntegrationSecretKind } from "@homarr/definitions"; import { integrationSecretKindObject } from "@homarr/definitions"; import { useI18n } from "@homarr/translation/client"; @@ -16,7 +17,9 @@ import { integrationSecretIcons } from "./integration-secret-icons"; dayjs.extend(relativeTime); interface SecretCardProps { - secret: RouterOutputs["integration"]["byId"]["secrets"][number]; + secret: + | RouterOutputs["integration"]["byId"]["secrets"][number] + | { kind: IntegrationSecretKind; value: null; updatedAt: null }; children: React.ReactNode; onCancel: () => Promise; } @@ -41,11 +44,19 @@ export const SecretCard = ({ secret, children, onCancel }: SecretCardProps) => { {publicSecretDisplayOpened ? {secret.value} : null} - - {t("integration.secrets.lastUpdated", { - date: dayjs().to(dayjs(secret.updatedAt)), - })} - + {secret.updatedAt ? ( + + {t("integration.secrets.lastUpdated", { + date: dayjs().to(dayjs(secret.updatedAt)), + })} + + ) : ( + + + {t("integration.secrets.notSet.label")} + + + )} {isPublic ? ( diff --git a/apps/nextjs/src/app/[locale]/manage/integrations/edit/[id]/_integration-edit-form.tsx b/apps/nextjs/src/app/[locale]/manage/integrations/edit/[id]/_integration-edit-form.tsx index 60707e35a..867689b7e 100644 --- a/apps/nextjs/src/app/[locale]/manage/integrations/edit/[id]/_integration-edit-form.tsx +++ b/apps/nextjs/src/app/[locale]/manage/integrations/edit/[id]/_integration-edit-form.tsx @@ -98,8 +98,7 @@ export const EditIntegrationForm = ({ integration }: EditIntegrationForm) => { {secretsKinds.map((kind, index) => ( new Promise((resolve) => { // When nothing changed, just close the secret card diff --git a/apps/nextjs/src/components/color-scheme/current-color-scheme-combobox.tsx b/apps/nextjs/src/components/color-scheme/current-color-scheme-combobox.tsx new file mode 100644 index 000000000..b3534c3aa --- /dev/null +++ b/apps/nextjs/src/components/color-scheme/current-color-scheme-combobox.tsx @@ -0,0 +1,49 @@ +"use client"; + +import { Group, Text, useMantineColorScheme } from "@mantine/core"; +import { IconMoon, IconSun } from "@tabler/icons-react"; + +import type { ColorScheme } from "@homarr/definitions"; +import { colorSchemes } from "@homarr/definitions"; +import { useScopedI18n } from "@homarr/translation/client"; +import { SelectWithCustomItems } from "@homarr/ui"; + +interface CurrentColorSchemeComboboxProps { + w?: string; +} + +export const CurrentColorSchemeCombobox = ({ w }: CurrentColorSchemeComboboxProps) => { + const tOptions = useScopedI18n("common.colorScheme.options"); + const { colorScheme, setColorScheme } = useMantineColorScheme(); + + return ( + setColorScheme((value as ColorScheme | null) ?? "light")} + data={colorSchemes.map((scheme) => ({ + value: scheme, + label: tOptions(scheme), + }))} + SelectOption={ColorSchemeCustomOption} + w={w} + /> + ); +}; + +const appearanceIcons = { + light: IconSun, + dark: IconMoon, +}; + +const ColorSchemeCustomOption = ({ value, label }: { value: ColorScheme; label: string }) => { + const Icon = appearanceIcons[value]; + + return ( + + + + {label} + + + ); +}; diff --git a/apps/nextjs/src/components/language/current-language-combobox.tsx b/apps/nextjs/src/components/language/current-language-combobox.tsx index cdb4709d6..e27f34c39 100644 --- a/apps/nextjs/src/components/language/current-language-combobox.tsx +++ b/apps/nextjs/src/components/language/current-language-combobox.tsx @@ -4,9 +4,13 @@ import { useChangeLocale, useCurrentLocale } from "@homarr/translation/client"; import { LanguageCombobox } from "./language-combobox"; -export const CurrentLanguageCombobox = () => { +interface CurrentLanguageComboboxProps { + width?: string; +} + +export const CurrentLanguageCombobox = ({ width }: CurrentLanguageComboboxProps) => { const currentLocale = useCurrentLocale(); const { changeLocale, isPending } = useChangeLocale(); - return ; + return ; }; diff --git a/apps/nextjs/src/components/language/language-combobox.tsx b/apps/nextjs/src/components/language/language-combobox.tsx index 91e968686..80ea2549b 100644 --- a/apps/nextjs/src/components/language/language-combobox.tsx +++ b/apps/nextjs/src/components/language/language-combobox.tsx @@ -9,14 +9,17 @@ import { localeConfigurations, supportedLanguages } from "@homarr/translation"; import classes from "./language-combobox.module.css"; +import "flag-icons/css/flag-icons.min.css"; + interface LanguageComboboxProps { label?: string; value: SupportedLanguage; onChange: (value: SupportedLanguage) => void; isPending?: boolean; + width?: string; } -export const LanguageCombobox = ({ label, value, onChange, isPending }: LanguageComboboxProps) => { +export const LanguageCombobox = ({ label, value, onChange, isPending, width }: LanguageComboboxProps) => { const combobox = useCombobox({ onDropdownClose: () => combobox.resetSelectedOption(), }); @@ -49,6 +52,7 @@ export const LanguageCombobox = ({ label, value, onChange, isPending }: Language rightSectionPointerEvents="none" onClick={handleOnClick} variant="filled" + w={width} > diff --git a/apps/nextjs/src/components/user-avatar-menu.tsx b/apps/nextjs/src/components/user-avatar-menu.tsx index 173d95ff3..df5f63549 100644 --- a/apps/nextjs/src/components/user-avatar-menu.tsx +++ b/apps/nextjs/src/components/user-avatar-menu.tsx @@ -18,14 +18,11 @@ import { IconTool, } from "@tabler/icons-react"; +import type { RouterOutputs } from "@homarr/api"; import { signOut, useSession } from "@homarr/auth/client"; import { createModal, useModalAction } from "@homarr/modals"; import { useScopedI18n } from "@homarr/translation/client"; -import "flag-icons/css/flag-icons.min.css"; - -import type { RouterOutputs } from "@homarr/api"; - import { useAuthContext } from "~/app/[locale]/_client-providers/session"; import { CurrentLanguageCombobox } from "./language/current-language-combobox"; diff --git a/apps/nextjs/src/middleware.ts b/apps/nextjs/src/middleware.ts index f20def82b..7d38cfa48 100644 --- a/apps/nextjs/src/middleware.ts +++ b/apps/nextjs/src/middleware.ts @@ -1,3 +1,4 @@ +import { NextResponse } from "next/server"; import type { NextRequest } from "next/server"; import { createTRPCClient, httpLink } from "@trpc/client"; import SuperJSON from "superjson"; @@ -11,6 +12,15 @@ export async function middleware(request: NextRequest) { // In next 15 we will be able to use node apis and such the db directly const culture = await serverFetchApi.serverSettings.getCulture.query(); + // Redirect to onboarding if it's not finished yet + const pathname = request.nextUrl.pathname; + if (!pathname.endsWith("/init")) { + const currentOnboardingStep = await serverFetchApi.onboard.currentStep.query(); + if (currentOnboardingStep.current !== "finish") { + return NextResponse.redirect(new URL("/init", request.url)); + } + } + // We don't want to fallback to accept-language header so we clear it request.headers.set("accept-language", ""); const next = createI18nMiddleware(culture.defaultLocale); diff --git a/packages/api/src/root.ts b/packages/api/src/root.ts index 107adf6d2..fa2b22209 100644 --- a/packages/api/src/root.ts +++ b/packages/api/src/root.ts @@ -6,11 +6,13 @@ import { dockerRouter } from "./router/docker/docker-router"; import { groupRouter } from "./router/group"; import { homeRouter } from "./router/home"; import { iconsRouter } from "./router/icons"; +import { importRouter } from "./router/import/import-router"; import { integrationRouter } from "./router/integration/integration-router"; import { inviteRouter } from "./router/invite"; import { locationRouter } from "./router/location"; import { logRouter } from "./router/log"; import { mediaRouter } from "./router/medias/media-router"; +import { onboardRouter } from "./router/onboard/onboard-router"; import { searchEngineRouter } from "./router/search-engine/search-engine-router"; import { serverSettingsRouter } from "./router/serverSettings"; import { updateCheckerRouter } from "./router/update-checker"; @@ -30,6 +32,8 @@ export const appRouter = createTRPCRouter({ location: locationRouter, log: logRouter, icon: iconsRouter, + import: importRouter, + onboard: onboardRouter, home: homeRouter, docker: dockerRouter, serverSettings: serverSettingsRouter, diff --git a/packages/api/src/router/board.ts b/packages/api/src/router/board.ts index accbf7aa3..64863e2f2 100644 --- a/packages/api/src/router/board.ts +++ b/packages/api/src/router/board.ts @@ -18,12 +18,12 @@ import { } from "@homarr/db/schema/sqlite"; import type { WidgetKind } from "@homarr/definitions"; import { getPermissionsWithParents, widgetKinds } from "@homarr/definitions"; -import { importAsync } from "@homarr/old-import"; +import { importOldmarrAsync } from "@homarr/old-import"; +import { importJsonFileSchema } from "@homarr/old-import/shared"; import { oldmarrConfigSchema } from "@homarr/old-schema"; import type { BoardItemAdvancedOptions } from "@homarr/validation"; -import { createSectionSchema, sharedItemSchema, validation, z } from "@homarr/validation"; +import { createSectionSchema, sharedItemSchema, validation, z, zodUnionFromArray } from "@homarr/validation"; -import { zodUnionFromArray } from "../../../validation/src/enums"; import { createTRPCRouter, permissionRequiredProcedure, protectedProcedure, publicProcedure } from "../trpc"; import { throwIfActionForbiddenAsync } from "./board/board-access"; @@ -575,13 +575,11 @@ export const boardRouter = createTRPCRouter({ ); }); }), - importOldmarrConfig: protectedProcedure - .input(validation.board.importOldmarrConfig) - .mutation(async ({ input, ctx }) => { - const content = await input.file.text(); - const oldmarr = oldmarrConfigSchema.parse(JSON.parse(content)); - await importAsync(ctx.db, oldmarr, input.configuration); - }), + importOldmarrConfig: protectedProcedure.input(importJsonFileSchema).mutation(async ({ input, ctx }) => { + const content = await input.file.text(); + const oldmarr = oldmarrConfigSchema.parse(JSON.parse(content)); + await importOldmarrAsync(ctx.db, oldmarr, input.configuration); + }), }); const noBoardWithSimilarNameAsync = async (db: Database, name: string, ignoredIds: string[] = []) => { diff --git a/packages/api/src/router/group.ts b/packages/api/src/router/group.ts index 83a0f8868..7d3ed049b 100644 --- a/packages/api/src/router/group.ts +++ b/packages/api/src/router/group.ts @@ -6,8 +6,9 @@ import { groupMembers, groupPermissions, groups } from "@homarr/db/schema/sqlite import { everyoneGroup } from "@homarr/definitions"; import { validation, z } from "@homarr/validation"; -import { createTRPCRouter, permissionRequiredProcedure, protectedProcedure } from "../trpc"; +import { createTRPCRouter, onboardingProcedure, permissionRequiredProcedure, protectedProcedure } from "../trpc"; import { throwIfCredentialsDisabled } from "./invite/checks"; +import { nextOnboardingStepAsync } from "./onboard/onboard-queries"; export const groupRouter = createTRPCRouter({ getPaginated: permissionRequiredProcedure @@ -145,6 +146,19 @@ export const groupRouter = createTRPCRouter({ limit: input.limit, }); }), + createInitialExternalGroup: onboardingProcedure + .requiresStep("group") + .input(validation.group.create) + .mutation(async ({ input, ctx }) => { + await checkSimilarNameAndThrowAsync(ctx.db, input.name); + + await ctx.db.insert(groups).values({ + id: createId(), + name: input.name, + }); + + await nextOnboardingStepAsync(ctx.db, undefined); + }), createGroup: permissionRequiredProcedure .requiresPermission("admin") .input(validation.group.create) diff --git a/packages/api/src/router/import/import-router.ts b/packages/api/src/router/import/import-router.ts new file mode 100644 index 000000000..7bcc453ec --- /dev/null +++ b/packages/api/src/router/import/import-router.ts @@ -0,0 +1,42 @@ +import { analyseOldmarrImportForRouterAsync, analyseOldmarrImportInputSchema } from "@homarr/old-import/analyse"; +import { + ensureValidTokenOrThrow, + importInitialOldmarrAsync, + importInitialOldmarrInputSchema, +} from "@homarr/old-import/import"; +import { z } from "@homarr/validation"; + +import { createTRPCRouter, onboardingProcedure } from "../../trpc"; +import { nextOnboardingStepAsync } from "../onboard/onboard-queries"; + +export const importRouter = createTRPCRouter({ + analyseInitialOldmarrImport: onboardingProcedure + .requiresStep("import") + .input(analyseOldmarrImportInputSchema) + .mutation(async ({ input }) => { + return await analyseOldmarrImportForRouterAsync(input); + }), + validateToken: onboardingProcedure + .requiresStep("import") + .input( + z.object({ + checksum: z.string(), + token: z.string(), + }), + ) + .mutation(({ input }) => { + try { + ensureValidTokenOrThrow(input.checksum, input.token); + return true; + } catch { + return false; + } + }), + importInitialOldmarrImport: onboardingProcedure + .requiresStep("import") + .input(importInitialOldmarrInputSchema) + .mutation(async ({ ctx, input }) => { + await importInitialOldmarrAsync(ctx.db, input); + await nextOnboardingStepAsync(ctx.db, undefined); + }), +}); diff --git a/packages/api/src/router/onboard/onboard-queries.ts b/packages/api/src/router/onboard/onboard-queries.ts new file mode 100644 index 000000000..6aeb0386c --- /dev/null +++ b/packages/api/src/router/onboard/onboard-queries.ts @@ -0,0 +1,81 @@ +import { isProviderEnabled } from "@homarr/auth/server"; +import { objectEntries } from "@homarr/common"; +import type { MaybePromise } from "@homarr/common/types"; +import type { Database } from "@homarr/db"; +import { eq } from "@homarr/db"; +import { groups, onboarding } from "@homarr/db/schema/sqlite"; +import type { OnboardingStep } from "@homarr/definitions"; +import { credentialsAdminGroup } from "@homarr/definitions"; + +export const nextOnboardingStepAsync = async (db: Database, preferredStep: OnboardingStep | undefined) => { + const { current } = await getOnboardingOrFallbackAsync(db); + const nextStepConfiguration = nextSteps[current]; + if (!nextStepConfiguration) return; + + for (const conditionalStep of objectEntries(nextStepConfiguration)) { + if (!conditionalStep) continue; + const [nextStep, condition] = conditionalStep; + if (condition === "preferred" && nextStep !== preferredStep) continue; + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition + if (typeof condition === "boolean" && !condition) continue; + if (typeof condition === "function" && !(await condition(db))) continue; + + await db.update(onboarding).set({ + previousStep: current, + step: nextStep, + }); + return; + } +}; + +export const getOnboardingOrFallbackAsync = async (db: Database) => { + const value = await db.query.onboarding.findFirst(); + if (!value) return { current: "start" as const, previous: null }; + + return { current: value.step, previous: value.previousStep }; +}; + +type NextStepCondition = true | "preferred" | ((db: Database) => MaybePromise); + +/** + * The below object is a definition of which can be the next step of the current one. + * If the value is `true`, it means the step can always be the next one. + * If the value is `preferred`, it means that the step can only be reached if the input `preferredStep` is set to the step. + * If the value is a function, it will be called with the database instance and should return a boolean. + * If the value or result is `false`, the step has to be skipped and the next value or callback should be checked. + */ +const nextSteps: Partial>>> = { + start: { + import: "preferred" as const, + user: () => isProviderEnabled("credentials"), + group: () => isProviderEnabled("ldap") || isProviderEnabled("oidc"), + settings: true, + }, + import: { + // eslint-disable-next-line no-restricted-syntax + user: async (db: Database) => { + if (!isProviderEnabled("credentials")) return false; + + const adminGroup = await db.query.groups.findFirst({ + where: eq(groups.name, credentialsAdminGroup), + with: { + members: true, + }, + }); + + return !adminGroup || adminGroup.members.length === 0; + }, + group: () => isProviderEnabled("ldap") || isProviderEnabled("oidc"), + settings: true, + }, + user: { + group: () => isProviderEnabled("ldap") || isProviderEnabled("oidc"), + settings: true, + }, + group: { + settings: true, + }, + settings: { + finish: true, + }, +}; diff --git a/packages/api/src/router/onboard/onboard-router.ts b/packages/api/src/router/onboard/onboard-router.ts new file mode 100644 index 000000000..8b6120655 --- /dev/null +++ b/packages/api/src/router/onboard/onboard-router.ts @@ -0,0 +1,34 @@ +import { onboarding } from "@homarr/db/schema/sqlite"; +import { onboardingSteps } from "@homarr/definitions"; +import { z, zodEnumFromArray } from "@homarr/validation"; + +import { createTRPCRouter, publicProcedure } from "../../trpc"; +import { getOnboardingOrFallbackAsync, nextOnboardingStepAsync } from "./onboard-queries"; + +export const onboardRouter = createTRPCRouter({ + currentStep: publicProcedure.query(async ({ ctx }) => { + return await getOnboardingOrFallbackAsync(ctx.db); + }), + nextStep: publicProcedure + .input( + z.object({ + // Preferred step is only needed for 'preferred' conditions + preferredStep: zodEnumFromArray(onboardingSteps).optional(), + }), + ) + .mutation(async ({ ctx, input }) => { + await nextOnboardingStepAsync(ctx.db, input.preferredStep); + }), + previousStep: publicProcedure.mutation(async ({ ctx }) => { + const { previous } = await getOnboardingOrFallbackAsync(ctx.db); + + if (previous !== "start") { + return; + } + + await ctx.db.update(onboarding).set({ + previousStep: null, + step: "start", + }); + }), +}); diff --git a/packages/api/src/router/serverSettings.ts b/packages/api/src/router/serverSettings.ts index d1ac27b7a..f1aa6ecdd 100644 --- a/packages/api/src/router/serverSettings.ts +++ b/packages/api/src/router/serverSettings.ts @@ -1,9 +1,10 @@ import { getServerSettingByKeyAsync, getServerSettingsAsync, updateServerSettingByKeyAsync } from "@homarr/db/queries"; import type { ServerSettings } from "@homarr/server-settings"; import { defaultServerSettingsKeys } from "@homarr/server-settings"; -import { z } from "@homarr/validation"; +import { validation, z } from "@homarr/validation"; -import { createTRPCRouter, protectedProcedure, publicProcedure } from "../trpc"; +import { createTRPCRouter, onboardingProcedure, protectedProcedure, publicProcedure } from "../trpc"; +import { nextOnboardingStepAsync } from "./onboard/onboard-queries"; export const serverSettingsRouter = createTRPCRouter({ getCulture: publicProcedure.query(async ({ ctx }) => { @@ -26,4 +27,12 @@ export const serverSettingsRouter = createTRPCRouter({ input.value as ServerSettings[keyof ServerSettings], ); }), + initSettings: onboardingProcedure + .requiresStep("settings") + .input(validation.settings.init) + .mutation(async ({ ctx, input }) => { + await updateServerSettingByKeyAsync(ctx.db, "analytics", input.analytics); + await updateServerSettingByKeyAsync(ctx.db, "crawlingAndIndexing", input.crawlingAndIndexing); + await nextOnboardingStepAsync(ctx.db, undefined); + }), }); diff --git a/packages/api/src/router/test/user.spec.ts b/packages/api/src/router/test/user.spec.ts index ce02a5bc6..1ae7c2dc7 100644 --- a/packages/api/src/router/test/user.spec.ts +++ b/packages/api/src/router/test/user.spec.ts @@ -1,10 +1,11 @@ import { describe, expect, it, test, vi } from "vitest"; import type { Session } from "@homarr/auth"; +import type { Database } from "@homarr/db"; import { createId, eq, schema } from "@homarr/db"; -import { users } from "@homarr/db/schema/sqlite"; +import { onboarding, users } from "@homarr/db/schema/sqlite"; import { createDb } from "@homarr/db/test"; -import type { GroupPermissionKey } from "@homarr/definitions"; +import type { GroupPermissionKey, OnboardingStep } from "@homarr/definitions"; import { userRouter } from "../user"; @@ -36,31 +37,9 @@ vi.mock("@homarr/auth/env.mjs", () => { }); describe("initUser should initialize the first user", () => { - it("should throw an error if a user already exists", async () => { - const db = createDb(); - const caller = userRouter.createCaller({ - db, - session: null, - }); - - await db.insert(schema.users).values({ - id: "test", - name: "test", - password: "test", - }); - - const actAsync = async () => - await caller.initUser({ - username: "test", - password: "123ABCdef+/-", - confirmPassword: "123ABCdef+/-", - }); - - await expect(actAsync()).rejects.toThrow("User already exists"); - }); - it("should create a user if none exists", async () => { const db = createDb(); + await createOnboardingStepAsync(db, "user"); const caller = userRouter.createCaller({ db, session: null, @@ -83,6 +62,7 @@ describe("initUser should initialize the first user", () => { it("should not create a user if the password and confirmPassword do not match", async () => { const db = createDb(); + await createOnboardingStepAsync(db, "user"); const caller = userRouter.createCaller({ db, session: null, @@ -106,6 +86,7 @@ describe("initUser should initialize the first user", () => { ["abc123+/-"], // does not contain uppercase ])("should throw error that password requirements do not match for '%s' as password", async (password) => { const db = createDb(); + await createOnboardingStepAsync(db, "user"); const caller = userRouter.createCaller({ db, session: null, @@ -324,3 +305,10 @@ describe("delete should delete user", () => { expect(usersInDb[1]).containSubset(initialUsers[2]); }); }); + +const createOnboardingStepAsync = async (db: Database, step: OnboardingStep) => { + await db.insert(onboarding).values({ + id: createId(), + step, + }); +}; diff --git a/packages/api/src/router/user.ts b/packages/api/src/router/user.ts index ae3b8cd88..4a412365d 100644 --- a/packages/api/src/router/user.ts +++ b/packages/api/src/router/user.ts @@ -5,47 +5,46 @@ import type { Database } from "@homarr/db"; import { and, createId, eq, like, schema } from "@homarr/db"; import { groupMembers, groupPermissions, groups, invites, users } from "@homarr/db/schema/sqlite"; import { selectUserSchema } from "@homarr/db/validationSchemas"; +import { credentialsAdminGroup } from "@homarr/definitions"; import type { SupportedAuthProvider } from "@homarr/definitions"; import { logger } from "@homarr/log"; import { validation, z } from "@homarr/validation"; import { convertIntersectionToZodObject } from "../schema-merger"; -import { createTRPCRouter, permissionRequiredProcedure, protectedProcedure, publicProcedure } from "../trpc"; +import { + createTRPCRouter, + onboardingProcedure, + permissionRequiredProcedure, + protectedProcedure, + publicProcedure, +} from "../trpc"; import { throwIfCredentialsDisabled } from "./invite/checks"; +import { nextOnboardingStepAsync } from "./onboard/onboard-queries"; export const userRouter = createTRPCRouter({ - initUser: publicProcedure.input(validation.user.init).mutation(async ({ ctx, input }) => { - throwIfCredentialsDisabled(); - - const firstUser = await ctx.db.query.users.findFirst({ - columns: { - id: true, - }, - }); + initUser: onboardingProcedure + .requiresStep("user") + .input(validation.user.init) + .mutation(async ({ ctx, input }) => { + throwIfCredentialsDisabled(); - if (firstUser) { - throw new TRPCError({ - code: "FORBIDDEN", - message: "User already exists", + const userId = await createUserAsync(ctx.db, input); + const groupId = createId(); + await ctx.db.insert(groups).values({ + id: groupId, + name: credentialsAdminGroup, + ownerId: userId, }); - } - - const userId = await createUserAsync(ctx.db, input); - const groupId = createId(); - await ctx.db.insert(groups).values({ - id: groupId, - name: "admin", - ownerId: userId, - }); - await ctx.db.insert(groupPermissions).values({ - groupId, - permission: "admin", - }); - await ctx.db.insert(groupMembers).values({ - groupId, - userId, - }); - }), + await ctx.db.insert(groupPermissions).values({ + groupId, + permission: "admin", + }); + await ctx.db.insert(groupMembers).values({ + groupId, + userId, + }); + await nextOnboardingStepAsync(ctx.db, undefined); + }), register: publicProcedure .input(validation.user.registrationApi) .output(z.void()) diff --git a/packages/api/src/trpc.ts b/packages/api/src/trpc.ts index aec73eaf1..64a2bf1e3 100644 --- a/packages/api/src/trpc.ts +++ b/packages/api/src/trpc.ts @@ -13,10 +13,12 @@ import type { OpenApiMeta } from "trpc-to-openapi"; import type { Session } from "@homarr/auth"; import { FlattenError } from "@homarr/common"; import { db } from "@homarr/db"; -import type { GroupPermissionKey } from "@homarr/definitions"; +import type { GroupPermissionKey, OnboardingStep } from "@homarr/definitions"; import { logger } from "@homarr/log"; import { ZodError } from "@homarr/validation"; +import { getOnboardingOrFallbackAsync } from "./router/onboard/onboard-queries"; + /** * 1. CONTEXT * @@ -138,3 +140,19 @@ export const permissionRequiredProcedure = { }); }, }; + +export const onboardingProcedure = { + requiresStep: (step: OnboardingStep) => { + return publicProcedure.use(async ({ ctx, input, next }) => { + const currentStep = await getOnboardingOrFallbackAsync(ctx.db).then(({ current }) => current); + if (currentStep !== step) { + throw new TRPCError({ + code: "FORBIDDEN", + message: "Step denied", + }); + } + + return next({ input, ctx }); + }); + }, +}; diff --git a/packages/common/src/encryption.ts b/packages/common/src/encryption.ts index cd253a89a..369fac3b2 100644 --- a/packages/common/src/encryption.ts +++ b/packages/common/src/encryption.ts @@ -25,6 +25,10 @@ export function encryptSecret(text: string): `${string}.${string}` { } export function decryptSecret(value: `${string}.${string}`) { + return decryptSecretWithKey(value, key); +} + +export function decryptSecretWithKey(value: `${string}.${string}`, key: Buffer) { const [data, dataIv] = value.split(".") as [string, string]; const initializationVector = Buffer.from(dataIv, "hex"); const encryptedText = Buffer.from(data, "hex"); diff --git a/packages/common/src/index.ts b/packages/common/src/index.ts index f45300e32..cbcec5e2a 100644 --- a/packages/common/src/index.ts +++ b/packages/common/src/index.ts @@ -8,3 +8,4 @@ export * from "./url"; export * from "./number"; export * from "./error"; export * from "./fetch-with-timeout"; +export * from "./theme"; diff --git a/packages/common/src/theme.ts b/packages/common/src/theme.ts new file mode 100644 index 000000000..413b1eee3 --- /dev/null +++ b/packages/common/src/theme.ts @@ -0,0 +1,5 @@ +import type { DefaultMantineColor, MantineColorShade } from "@mantine/core"; +import { DEFAULT_THEME } from "@mantine/core"; + +export const getMantineColor = (color: DefaultMantineColor, shade: MantineColorShade) => + DEFAULT_THEME.colors[color]?.[shade] ?? "#fff"; diff --git a/packages/db/migrations/mysql/0017_tired_penance.sql b/packages/db/migrations/mysql/0017_tired_penance.sql new file mode 100644 index 000000000..e27e77f30 --- /dev/null +++ b/packages/db/migrations/mysql/0017_tired_penance.sql @@ -0,0 +1,6 @@ +CREATE TABLE `onboarding` ( + `id` varchar(64) NOT NULL, + `step` varchar(64) NOT NULL, + `previous_step` varchar(64), + CONSTRAINT `onboarding_id` PRIMARY KEY(`id`) +); diff --git a/packages/db/migrations/mysql/meta/0017_snapshot.json b/packages/db/migrations/mysql/meta/0017_snapshot.json new file mode 100644 index 000000000..fc3653698 --- /dev/null +++ b/packages/db/migrations/mysql/meta/0017_snapshot.json @@ -0,0 +1,1663 @@ +{ + "version": "5", + "dialect": "mysql", + "id": "a47534f2-313d-4234-a5d3-0b48da85f84c", + "prevId": "50b295e1-1802-477c-9ee1-b2cad1e9f5bb", + "tables": { + "account": { + "name": "account", + "columns": { + "user_id": { + "name": "user_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "provider": { + "name": "provider", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "provider_account_id": { + "name": "provider_account_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "refresh_token": { + "name": "refresh_token", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "access_token": { + "name": "access_token", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "token_type": { + "name": "token_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "id_token": { + "name": "id_token", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "session_state": { + "name": "session_state", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "userId_idx": { + "name": "userId_idx", + "columns": ["user_id"], + "isUnique": false + } + }, + "foreignKeys": { + "account_user_id_user_id_fk": { + "name": "account_user_id_user_id_fk", + "tableFrom": "account", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "account_provider_provider_account_id_pk": { + "name": "account_provider_provider_account_id_pk", + "columns": ["provider", "provider_account_id"] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "apiKey": { + "name": "apiKey", + "columns": { + "id": { + "name": "id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "api_key": { + "name": "api_key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "salt": { + "name": "salt", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "apiKey_user_id_user_id_fk": { + "name": "apiKey_user_id_user_id_fk", + "tableFrom": "apiKey", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "apiKey_id": { + "name": "apiKey_id", + "columns": ["id"] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "app": { + "name": "app", + "columns": { + "id": { + "name": "id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "icon_url": { + "name": "icon_url", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "href": { + "name": "href", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "app_id": { + "name": "app_id", + "columns": ["id"] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "boardGroupPermission": { + "name": "boardGroupPermission", + "columns": { + "board_id": { + "name": "board_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "group_id": { + "name": "group_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "permission": { + "name": "permission", + "type": "varchar(128)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "boardGroupPermission_board_id_board_id_fk": { + "name": "boardGroupPermission_board_id_board_id_fk", + "tableFrom": "boardGroupPermission", + "tableTo": "board", + "columnsFrom": ["board_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "boardGroupPermission_group_id_group_id_fk": { + "name": "boardGroupPermission_group_id_group_id_fk", + "tableFrom": "boardGroupPermission", + "tableTo": "group", + "columnsFrom": ["group_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "boardGroupPermission_board_id_group_id_permission_pk": { + "name": "boardGroupPermission_board_id_group_id_permission_pk", + "columns": ["board_id", "group_id", "permission"] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "boardUserPermission": { + "name": "boardUserPermission", + "columns": { + "board_id": { + "name": "board_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "permission": { + "name": "permission", + "type": "varchar(128)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "boardUserPermission_board_id_board_id_fk": { + "name": "boardUserPermission_board_id_board_id_fk", + "tableFrom": "boardUserPermission", + "tableTo": "board", + "columnsFrom": ["board_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "boardUserPermission_user_id_user_id_fk": { + "name": "boardUserPermission_user_id_user_id_fk", + "tableFrom": "boardUserPermission", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "boardUserPermission_board_id_user_id_permission_pk": { + "name": "boardUserPermission_board_id_user_id_permission_pk", + "columns": ["board_id", "user_id", "permission"] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "board": { + "name": "board", + "columns": { + "id": { + "name": "id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "varchar(256)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "is_public": { + "name": "is_public", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "creator_id": { + "name": "creator_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "page_title": { + "name": "page_title", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "meta_title": { + "name": "meta_title", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "logo_image_url": { + "name": "logo_image_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "favicon_image_url": { + "name": "favicon_image_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "background_image_url": { + "name": "background_image_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "background_image_attachment": { + "name": "background_image_attachment", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "('fixed')" + }, + "background_image_repeat": { + "name": "background_image_repeat", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "('no-repeat')" + }, + "background_image_size": { + "name": "background_image_size", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "('cover')" + }, + "primary_color": { + "name": "primary_color", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "('#fa5252')" + }, + "secondary_color": { + "name": "secondary_color", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "('#fd7e14')" + }, + "opacity": { + "name": "opacity", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 100 + }, + "custom_css": { + "name": "custom_css", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "column_count": { + "name": "column_count", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 10 + } + }, + "indexes": {}, + "foreignKeys": { + "board_creator_id_user_id_fk": { + "name": "board_creator_id_user_id_fk", + "tableFrom": "board", + "tableTo": "user", + "columnsFrom": ["creator_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "board_id": { + "name": "board_id", + "columns": ["id"] + } + }, + "uniqueConstraints": { + "board_name_unique": { + "name": "board_name_unique", + "columns": ["name"] + } + }, + "checkConstraint": {} + }, + "groupMember": { + "name": "groupMember", + "columns": { + "group_id": { + "name": "group_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "groupMember_group_id_group_id_fk": { + "name": "groupMember_group_id_group_id_fk", + "tableFrom": "groupMember", + "tableTo": "group", + "columnsFrom": ["group_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "groupMember_user_id_user_id_fk": { + "name": "groupMember_user_id_user_id_fk", + "tableFrom": "groupMember", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "groupMember_group_id_user_id_pk": { + "name": "groupMember_group_id_user_id_pk", + "columns": ["group_id", "user_id"] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "groupPermission": { + "name": "groupPermission", + "columns": { + "group_id": { + "name": "group_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "permission": { + "name": "permission", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "groupPermission_group_id_group_id_fk": { + "name": "groupPermission_group_id_group_id_fk", + "tableFrom": "groupPermission", + "tableTo": "group", + "columnsFrom": ["group_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "group": { + "name": "group", + "columns": { + "id": { + "name": "id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "owner_id": { + "name": "owner_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "group_owner_id_user_id_fk": { + "name": "group_owner_id_user_id_fk", + "tableFrom": "group", + "tableTo": "user", + "columnsFrom": ["owner_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "group_id": { + "name": "group_id", + "columns": ["id"] + } + }, + "uniqueConstraints": { + "group_name_unique": { + "name": "group_name_unique", + "columns": ["name"] + } + }, + "checkConstraint": {} + }, + "iconRepository": { + "name": "iconRepository", + "columns": { + "id": { + "name": "id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "slug": { + "name": "slug", + "type": "varchar(150)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "iconRepository_id": { + "name": "iconRepository_id", + "columns": ["id"] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "icon": { + "name": "icon", + "columns": { + "id": { + "name": "id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "varchar(250)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "checksum": { + "name": "checksum", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "icon_repository_id": { + "name": "icon_repository_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "icon_icon_repository_id_iconRepository_id_fk": { + "name": "icon_icon_repository_id_iconRepository_id_fk", + "tableFrom": "icon", + "tableTo": "iconRepository", + "columnsFrom": ["icon_repository_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "icon_id": { + "name": "icon_id", + "columns": ["id"] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "integrationGroupPermissions": { + "name": "integrationGroupPermissions", + "columns": { + "integration_id": { + "name": "integration_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "group_id": { + "name": "group_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "permission": { + "name": "permission", + "type": "varchar(128)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "integrationGroupPermissions_integration_id_integration_id_fk": { + "name": "integrationGroupPermissions_integration_id_integration_id_fk", + "tableFrom": "integrationGroupPermissions", + "tableTo": "integration", + "columnsFrom": ["integration_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "integrationGroupPermissions_group_id_group_id_fk": { + "name": "integrationGroupPermissions_group_id_group_id_fk", + "tableFrom": "integrationGroupPermissions", + "tableTo": "group", + "columnsFrom": ["group_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "integration_group_permission__pk": { + "name": "integration_group_permission__pk", + "columns": ["integration_id", "group_id", "permission"] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "integration_item": { + "name": "integration_item", + "columns": { + "item_id": { + "name": "item_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "integration_id": { + "name": "integration_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "integration_item_item_id_item_id_fk": { + "name": "integration_item_item_id_item_id_fk", + "tableFrom": "integration_item", + "tableTo": "item", + "columnsFrom": ["item_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "integration_item_integration_id_integration_id_fk": { + "name": "integration_item_integration_id_integration_id_fk", + "tableFrom": "integration_item", + "tableTo": "integration", + "columnsFrom": ["integration_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "integration_item_item_id_integration_id_pk": { + "name": "integration_item_item_id_integration_id_pk", + "columns": ["item_id", "integration_id"] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "integrationSecret": { + "name": "integrationSecret", + "columns": { + "kind": { + "name": "kind", + "type": "varchar(16)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "integration_id": { + "name": "integration_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "integration_secret__kind_idx": { + "name": "integration_secret__kind_idx", + "columns": ["kind"], + "isUnique": false + }, + "integration_secret__updated_at_idx": { + "name": "integration_secret__updated_at_idx", + "columns": ["updated_at"], + "isUnique": false + } + }, + "foreignKeys": { + "integrationSecret_integration_id_integration_id_fk": { + "name": "integrationSecret_integration_id_integration_id_fk", + "tableFrom": "integrationSecret", + "tableTo": "integration", + "columnsFrom": ["integration_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "integrationSecret_integration_id_kind_pk": { + "name": "integrationSecret_integration_id_kind_pk", + "columns": ["integration_id", "kind"] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "integrationUserPermission": { + "name": "integrationUserPermission", + "columns": { + "integration_id": { + "name": "integration_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "permission": { + "name": "permission", + "type": "varchar(128)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "integrationUserPermission_integration_id_integration_id_fk": { + "name": "integrationUserPermission_integration_id_integration_id_fk", + "tableFrom": "integrationUserPermission", + "tableTo": "integration", + "columnsFrom": ["integration_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "integrationUserPermission_user_id_user_id_fk": { + "name": "integrationUserPermission_user_id_user_id_fk", + "tableFrom": "integrationUserPermission", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "integrationUserPermission_integration_id_user_id_permission_pk": { + "name": "integrationUserPermission_integration_id_user_id_permission_pk", + "columns": ["integration_id", "user_id", "permission"] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "integration": { + "name": "integration", + "columns": { + "id": { + "name": "id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "kind": { + "name": "kind", + "type": "varchar(128)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "integration__kind_idx": { + "name": "integration__kind_idx", + "columns": ["kind"], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "integration_id": { + "name": "integration_id", + "columns": ["id"] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "invite": { + "name": "invite", + "columns": { + "id": { + "name": "id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "token": { + "name": "token", + "type": "varchar(512)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "expiration_date": { + "name": "expiration_date", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "creator_id": { + "name": "creator_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "invite_creator_id_user_id_fk": { + "name": "invite_creator_id_user_id_fk", + "tableFrom": "invite", + "tableTo": "user", + "columnsFrom": ["creator_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "invite_id": { + "name": "invite_id", + "columns": ["id"] + } + }, + "uniqueConstraints": { + "invite_token_unique": { + "name": "invite_token_unique", + "columns": ["token"] + } + }, + "checkConstraint": {} + }, + "item": { + "name": "item", + "columns": { + "id": { + "name": "id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "section_id": { + "name": "section_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "x_offset": { + "name": "x_offset", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "y_offset": { + "name": "y_offset", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "width": { + "name": "width", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "height": { + "name": "height", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "options": { + "name": "options", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "('{\"json\": {}}')" + }, + "advanced_options": { + "name": "advanced_options", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "('{\"json\": {}}')" + } + }, + "indexes": {}, + "foreignKeys": { + "item_section_id_section_id_fk": { + "name": "item_section_id_section_id_fk", + "tableFrom": "item", + "tableTo": "section", + "columnsFrom": ["section_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "item_id": { + "name": "item_id", + "columns": ["id"] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "media": { + "name": "media", + "columns": { + "id": { + "name": "id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "varchar(512)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "content": { + "name": "content", + "type": "BLOB", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "content_type": { + "name": "content_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "size": { + "name": "size", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + }, + "creator_id": { + "name": "creator_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "media_creator_id_user_id_fk": { + "name": "media_creator_id_user_id_fk", + "tableFrom": "media", + "tableTo": "user", + "columnsFrom": ["creator_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "media_id": { + "name": "media_id", + "columns": ["id"] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "onboarding": { + "name": "onboarding", + "columns": { + "id": { + "name": "id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "step": { + "name": "step", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "previous_step": { + "name": "previous_step", + "type": "varchar(64)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "onboarding_id": { + "name": "onboarding_id", + "columns": ["id"] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "search_engine": { + "name": "search_engine", + "columns": { + "id": { + "name": "id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "icon_url": { + "name": "icon_url", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "short": { + "name": "short", + "type": "varchar(8)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "url_template": { + "name": "url_template", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "type": { + "name": "type", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'generic'" + }, + "integration_id": { + "name": "integration_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "search_engine_integration_id_integration_id_fk": { + "name": "search_engine_integration_id_integration_id_fk", + "tableFrom": "search_engine", + "tableTo": "integration", + "columnsFrom": ["integration_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "search_engine_id": { + "name": "search_engine_id", + "columns": ["id"] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "section": { + "name": "section", + "columns": { + "id": { + "name": "id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "board_id": { + "name": "board_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "x_offset": { + "name": "x_offset", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "y_offset": { + "name": "y_offset", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "width": { + "name": "width", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "height": { + "name": "height", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "parent_section_id": { + "name": "parent_section_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "section_board_id_board_id_fk": { + "name": "section_board_id_board_id_fk", + "tableFrom": "section", + "tableTo": "board", + "columnsFrom": ["board_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "section_parent_section_id_section_id_fk": { + "name": "section_parent_section_id_section_id_fk", + "tableFrom": "section", + "tableTo": "section", + "columnsFrom": ["parent_section_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "section_id": { + "name": "section_id", + "columns": ["id"] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "serverSetting": { + "name": "serverSetting", + "columns": { + "setting_key": { + "name": "setting_key", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "('{\"json\": {}}')" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "serverSetting_setting_key": { + "name": "serverSetting_setting_key", + "columns": ["setting_key"] + } + }, + "uniqueConstraints": { + "serverSetting_settingKey_unique": { + "name": "serverSetting_settingKey_unique", + "columns": ["setting_key"] + } + }, + "checkConstraint": {} + }, + "session": { + "name": "session", + "columns": { + "session_token": { + "name": "session_token", + "type": "varchar(512)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "expires": { + "name": "expires", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "user_id_idx": { + "name": "user_id_idx", + "columns": ["user_id"], + "isUnique": false + } + }, + "foreignKeys": { + "session_user_id_user_id_fk": { + "name": "session_user_id_user_id_fk", + "tableFrom": "session", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "session_session_token": { + "name": "session_session_token", + "columns": ["session_token"] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "user": { + "name": "user", + "columns": { + "id": { + "name": "id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "email_verified": { + "name": "email_verified", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "image": { + "name": "image", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "salt": { + "name": "salt", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "provider": { + "name": "provider", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'credentials'" + }, + "home_board_id": { + "name": "home_board_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "color_scheme": { + "name": "color_scheme", + "type": "varchar(5)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'dark'" + }, + "first_day_of_week": { + "name": "first_day_of_week", + "type": "tinyint", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 1 + }, + "ping_icons_enabled": { + "name": "ping_icons_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + } + }, + "indexes": {}, + "foreignKeys": { + "user_home_board_id_board_id_fk": { + "name": "user_home_board_id_board_id_fk", + "tableFrom": "user", + "tableTo": "board", + "columnsFrom": ["home_board_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "user_id": { + "name": "user_id", + "columns": ["id"] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "verificationToken": { + "name": "verificationToken", + "columns": { + "identifier": { + "name": "identifier", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "token": { + "name": "token", + "type": "varchar(512)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "expires": { + "name": "expires", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "verificationToken_identifier_token_pk": { + "name": "verificationToken_identifier_token_pk", + "columns": ["identifier", "token"] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + } + }, + "views": {}, + "_meta": { + "schemas": {}, + "tables": {}, + "columns": {} + }, + "internal": { + "tables": {}, + "indexes": {} + } +} diff --git a/packages/db/migrations/mysql/meta/_journal.json b/packages/db/migrations/mysql/meta/_journal.json index d740783f1..75446c520 100644 --- a/packages/db/migrations/mysql/meta/_journal.json +++ b/packages/db/migrations/mysql/meta/_journal.json @@ -120,6 +120,13 @@ "when": 1732212709518, "tag": "0016_change_all_to_snake_case", "breakpoints": true + }, + { + "idx": 17, + "version": "5", + "when": 1733777544067, + "tag": "0017_tired_penance", + "breakpoints": true } ] } diff --git a/packages/db/migrations/seed.ts b/packages/db/migrations/seed.ts index 79a90c45e..15a82ea20 100644 --- a/packages/db/migrations/seed.ts +++ b/packages/db/migrations/seed.ts @@ -7,10 +7,11 @@ import { defaultServerSettings, defaultServerSettingsKeys } from "@homarr/server import { createId, eq } from ".."; import type { Database } from ".."; import { groups } from "../schema/mysql"; -import { serverSettings } from "../schema/sqlite"; +import { onboarding, serverSettings } from "../schema/sqlite"; export const seedDataAsync = async (db: Database) => { await seedEveryoneGroupAsync(db); + await seedOnboardingAsync(db); await seedServerSettingsAsync(db); }; @@ -31,6 +32,21 @@ const seedEveryoneGroupAsync = async (db: Database) => { console.log("Created group 'everyone' through seed"); }; +const seedOnboardingAsync = async (db: Database) => { + const existing = await db.query.onboarding.findFirst(); + + if (existing) { + console.log("Skipping seeding of onboarding as it already exists"); + return; + } + + await db.insert(onboarding).values({ + id: createId(), + step: "start", + }); + console.log("Created onboarding step through seed"); +}; + const seedServerSettingsAsync = async (db: Database) => { const serverSettingsData = await db.query.serverSettings.findMany(); diff --git a/packages/db/migrations/sqlite/0017_small_rumiko_fujikawa.sql b/packages/db/migrations/sqlite/0017_small_rumiko_fujikawa.sql new file mode 100644 index 000000000..c7e3aa09f --- /dev/null +++ b/packages/db/migrations/sqlite/0017_small_rumiko_fujikawa.sql @@ -0,0 +1,5 @@ +CREATE TABLE `onboarding` ( + `id` text PRIMARY KEY NOT NULL, + `step` text NOT NULL, + `previous_step` text +); diff --git a/packages/db/migrations/sqlite/meta/0017_snapshot.json b/packages/db/migrations/sqlite/meta/0017_snapshot.json new file mode 100644 index 000000000..73cf3303b --- /dev/null +++ b/packages/db/migrations/sqlite/meta/0017_snapshot.json @@ -0,0 +1,1587 @@ +{ + "version": "6", + "dialect": "sqlite", + "id": "8946270f-5c29-4ecb-b3c9-af35b9b38f68", + "prevId": "e1f073f2-71de-489f-8268-4754a7ccff10", + "tables": { + "account": { + "name": "account", + "columns": { + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "provider_account_id": { + "name": "provider_account_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "refresh_token": { + "name": "refresh_token", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "access_token": { + "name": "access_token", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "token_type": { + "name": "token_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "id_token": { + "name": "id_token", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "session_state": { + "name": "session_state", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "userId_idx": { + "name": "userId_idx", + "columns": ["user_id"], + "isUnique": false + } + }, + "foreignKeys": { + "account_user_id_user_id_fk": { + "name": "account_user_id_user_id_fk", + "tableFrom": "account", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "account_provider_provider_account_id_pk": { + "columns": ["provider", "provider_account_id"], + "name": "account_provider_provider_account_id_pk" + } + }, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "apiKey": { + "name": "apiKey", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "api_key": { + "name": "api_key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "salt": { + "name": "salt", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "apiKey_user_id_user_id_fk": { + "name": "apiKey_user_id_user_id_fk", + "tableFrom": "apiKey", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "app": { + "name": "app", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "icon_url": { + "name": "icon_url", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "href": { + "name": "href", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "boardGroupPermission": { + "name": "boardGroupPermission", + "columns": { + "board_id": { + "name": "board_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "group_id": { + "name": "group_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "permission": { + "name": "permission", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "boardGroupPermission_board_id_board_id_fk": { + "name": "boardGroupPermission_board_id_board_id_fk", + "tableFrom": "boardGroupPermission", + "tableTo": "board", + "columnsFrom": ["board_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "boardGroupPermission_group_id_group_id_fk": { + "name": "boardGroupPermission_group_id_group_id_fk", + "tableFrom": "boardGroupPermission", + "tableTo": "group", + "columnsFrom": ["group_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "boardGroupPermission_board_id_group_id_permission_pk": { + "columns": ["board_id", "group_id", "permission"], + "name": "boardGroupPermission_board_id_group_id_permission_pk" + } + }, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "boardUserPermission": { + "name": "boardUserPermission", + "columns": { + "board_id": { + "name": "board_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "permission": { + "name": "permission", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "boardUserPermission_board_id_board_id_fk": { + "name": "boardUserPermission_board_id_board_id_fk", + "tableFrom": "boardUserPermission", + "tableTo": "board", + "columnsFrom": ["board_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "boardUserPermission_user_id_user_id_fk": { + "name": "boardUserPermission_user_id_user_id_fk", + "tableFrom": "boardUserPermission", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "boardUserPermission_board_id_user_id_permission_pk": { + "columns": ["board_id", "user_id", "permission"], + "name": "boardUserPermission_board_id_user_id_permission_pk" + } + }, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "board": { + "name": "board", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "is_public": { + "name": "is_public", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "creator_id": { + "name": "creator_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "page_title": { + "name": "page_title", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "meta_title": { + "name": "meta_title", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "logo_image_url": { + "name": "logo_image_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "favicon_image_url": { + "name": "favicon_image_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "background_image_url": { + "name": "background_image_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "background_image_attachment": { + "name": "background_image_attachment", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'fixed'" + }, + "background_image_repeat": { + "name": "background_image_repeat", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'no-repeat'" + }, + "background_image_size": { + "name": "background_image_size", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'cover'" + }, + "primary_color": { + "name": "primary_color", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'#fa5252'" + }, + "secondary_color": { + "name": "secondary_color", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'#fd7e14'" + }, + "opacity": { + "name": "opacity", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 100 + }, + "custom_css": { + "name": "custom_css", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "column_count": { + "name": "column_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 10 + } + }, + "indexes": { + "board_name_unique": { + "name": "board_name_unique", + "columns": ["name"], + "isUnique": true + } + }, + "foreignKeys": { + "board_creator_id_user_id_fk": { + "name": "board_creator_id_user_id_fk", + "tableFrom": "board", + "tableTo": "user", + "columnsFrom": ["creator_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "groupMember": { + "name": "groupMember", + "columns": { + "group_id": { + "name": "group_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "groupMember_group_id_group_id_fk": { + "name": "groupMember_group_id_group_id_fk", + "tableFrom": "groupMember", + "tableTo": "group", + "columnsFrom": ["group_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "groupMember_user_id_user_id_fk": { + "name": "groupMember_user_id_user_id_fk", + "tableFrom": "groupMember", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "groupMember_group_id_user_id_pk": { + "columns": ["group_id", "user_id"], + "name": "groupMember_group_id_user_id_pk" + } + }, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "groupPermission": { + "name": "groupPermission", + "columns": { + "group_id": { + "name": "group_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "permission": { + "name": "permission", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "groupPermission_group_id_group_id_fk": { + "name": "groupPermission_group_id_group_id_fk", + "tableFrom": "groupPermission", + "tableTo": "group", + "columnsFrom": ["group_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "group": { + "name": "group", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "owner_id": { + "name": "owner_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "group_name_unique": { + "name": "group_name_unique", + "columns": ["name"], + "isUnique": true + } + }, + "foreignKeys": { + "group_owner_id_user_id_fk": { + "name": "group_owner_id_user_id_fk", + "tableFrom": "group", + "tableTo": "user", + "columnsFrom": ["owner_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "iconRepository": { + "name": "iconRepository", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "slug": { + "name": "slug", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "icon": { + "name": "icon", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "checksum": { + "name": "checksum", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "icon_repository_id": { + "name": "icon_repository_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "icon_icon_repository_id_iconRepository_id_fk": { + "name": "icon_icon_repository_id_iconRepository_id_fk", + "tableFrom": "icon", + "tableTo": "iconRepository", + "columnsFrom": ["icon_repository_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "integrationGroupPermissions": { + "name": "integrationGroupPermissions", + "columns": { + "integration_id": { + "name": "integration_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "group_id": { + "name": "group_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "permission": { + "name": "permission", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "integrationGroupPermissions_integration_id_integration_id_fk": { + "name": "integrationGroupPermissions_integration_id_integration_id_fk", + "tableFrom": "integrationGroupPermissions", + "tableTo": "integration", + "columnsFrom": ["integration_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "integrationGroupPermissions_group_id_group_id_fk": { + "name": "integrationGroupPermissions_group_id_group_id_fk", + "tableFrom": "integrationGroupPermissions", + "tableTo": "group", + "columnsFrom": ["group_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "integrationGroupPermissions_integration_id_group_id_permission_pk": { + "columns": ["integration_id", "group_id", "permission"], + "name": "integrationGroupPermissions_integration_id_group_id_permission_pk" + } + }, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "integration_item": { + "name": "integration_item", + "columns": { + "item_id": { + "name": "item_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "integration_id": { + "name": "integration_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "integration_item_item_id_item_id_fk": { + "name": "integration_item_item_id_item_id_fk", + "tableFrom": "integration_item", + "tableTo": "item", + "columnsFrom": ["item_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "integration_item_integration_id_integration_id_fk": { + "name": "integration_item_integration_id_integration_id_fk", + "tableFrom": "integration_item", + "tableTo": "integration", + "columnsFrom": ["integration_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "integration_item_item_id_integration_id_pk": { + "columns": ["item_id", "integration_id"], + "name": "integration_item_item_id_integration_id_pk" + } + }, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "integrationSecret": { + "name": "integrationSecret", + "columns": { + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "integration_id": { + "name": "integration_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "integration_secret__kind_idx": { + "name": "integration_secret__kind_idx", + "columns": ["kind"], + "isUnique": false + }, + "integration_secret__updated_at_idx": { + "name": "integration_secret__updated_at_idx", + "columns": ["updated_at"], + "isUnique": false + } + }, + "foreignKeys": { + "integrationSecret_integration_id_integration_id_fk": { + "name": "integrationSecret_integration_id_integration_id_fk", + "tableFrom": "integrationSecret", + "tableTo": "integration", + "columnsFrom": ["integration_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "integrationSecret_integration_id_kind_pk": { + "columns": ["integration_id", "kind"], + "name": "integrationSecret_integration_id_kind_pk" + } + }, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "integrationUserPermission": { + "name": "integrationUserPermission", + "columns": { + "integration_id": { + "name": "integration_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "permission": { + "name": "permission", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "integrationUserPermission_integration_id_integration_id_fk": { + "name": "integrationUserPermission_integration_id_integration_id_fk", + "tableFrom": "integrationUserPermission", + "tableTo": "integration", + "columnsFrom": ["integration_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "integrationUserPermission_user_id_user_id_fk": { + "name": "integrationUserPermission_user_id_user_id_fk", + "tableFrom": "integrationUserPermission", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "integrationUserPermission_integration_id_user_id_permission_pk": { + "columns": ["integration_id", "user_id", "permission"], + "name": "integrationUserPermission_integration_id_user_id_permission_pk" + } + }, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "integration": { + "name": "integration", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "integration__kind_idx": { + "name": "integration__kind_idx", + "columns": ["kind"], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "invite": { + "name": "invite", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "expiration_date": { + "name": "expiration_date", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "creator_id": { + "name": "creator_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "invite_token_unique": { + "name": "invite_token_unique", + "columns": ["token"], + "isUnique": true + } + }, + "foreignKeys": { + "invite_creator_id_user_id_fk": { + "name": "invite_creator_id_user_id_fk", + "tableFrom": "invite", + "tableTo": "user", + "columnsFrom": ["creator_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "item": { + "name": "item", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "section_id": { + "name": "section_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "x_offset": { + "name": "x_offset", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "y_offset": { + "name": "y_offset", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "width": { + "name": "width", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "height": { + "name": "height", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "options": { + "name": "options", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'{\"json\": {}}'" + }, + "advanced_options": { + "name": "advanced_options", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'{\"json\": {}}'" + } + }, + "indexes": {}, + "foreignKeys": { + "item_section_id_section_id_fk": { + "name": "item_section_id_section_id_fk", + "tableFrom": "item", + "tableTo": "section", + "columnsFrom": ["section_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "media": { + "name": "media", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "content": { + "name": "content", + "type": "blob", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "content_type": { + "name": "content_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "size": { + "name": "size", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + }, + "creator_id": { + "name": "creator_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "media_creator_id_user_id_fk": { + "name": "media_creator_id_user_id_fk", + "tableFrom": "media", + "tableTo": "user", + "columnsFrom": ["creator_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "onboarding": { + "name": "onboarding", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "step": { + "name": "step", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "previous_step": { + "name": "previous_step", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "search_engine": { + "name": "search_engine", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "icon_url": { + "name": "icon_url", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "short": { + "name": "short", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "url_template": { + "name": "url_template", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'generic'" + }, + "integration_id": { + "name": "integration_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "search_engine_integration_id_integration_id_fk": { + "name": "search_engine_integration_id_integration_id_fk", + "tableFrom": "search_engine", + "tableTo": "integration", + "columnsFrom": ["integration_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "section": { + "name": "section", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "board_id": { + "name": "board_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "x_offset": { + "name": "x_offset", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "y_offset": { + "name": "y_offset", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "width": { + "name": "width", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "height": { + "name": "height", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "parent_section_id": { + "name": "parent_section_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "section_board_id_board_id_fk": { + "name": "section_board_id_board_id_fk", + "tableFrom": "section", + "tableTo": "board", + "columnsFrom": ["board_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "section_parent_section_id_section_id_fk": { + "name": "section_parent_section_id_section_id_fk", + "tableFrom": "section", + "tableTo": "section", + "columnsFrom": ["parent_section_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "serverSetting": { + "name": "serverSetting", + "columns": { + "setting_key": { + "name": "setting_key", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'{\"json\": {}}'" + } + }, + "indexes": { + "serverSetting_settingKey_unique": { + "name": "serverSetting_settingKey_unique", + "columns": ["setting_key"], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "session": { + "name": "session", + "columns": { + "session_token": { + "name": "session_token", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "expires": { + "name": "expires", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "user_id_idx": { + "name": "user_id_idx", + "columns": ["user_id"], + "isUnique": false + } + }, + "foreignKeys": { + "session_user_id_user_id_fk": { + "name": "session_user_id_user_id_fk", + "tableFrom": "session", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "user": { + "name": "user", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "email_verified": { + "name": "email_verified", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "image": { + "name": "image", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "salt": { + "name": "salt", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'credentials'" + }, + "home_board_id": { + "name": "home_board_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "color_scheme": { + "name": "color_scheme", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'dark'" + }, + "first_day_of_week": { + "name": "first_day_of_week", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 1 + }, + "ping_icons_enabled": { + "name": "ping_icons_enabled", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + } + }, + "indexes": {}, + "foreignKeys": { + "user_home_board_id_board_id_fk": { + "name": "user_home_board_id_board_id_fk", + "tableFrom": "user", + "tableTo": "board", + "columnsFrom": ["home_board_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "verificationToken": { + "name": "verificationToken", + "columns": { + "identifier": { + "name": "identifier", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "expires": { + "name": "expires", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "verificationToken_identifier_token_pk": { + "columns": ["identifier", "token"], + "name": "verificationToken_identifier_token_pk" + } + }, + "uniqueConstraints": {}, + "checkConstraints": {} + } + }, + "views": {}, + "enums": {}, + "_meta": { + "schemas": {}, + "tables": {}, + "columns": {} + }, + "internal": { + "indexes": {} + } +} diff --git a/packages/db/migrations/sqlite/meta/_journal.json b/packages/db/migrations/sqlite/meta/_journal.json index 8367929b9..a07880551 100644 --- a/packages/db/migrations/sqlite/meta/_journal.json +++ b/packages/db/migrations/sqlite/meta/_journal.json @@ -120,6 +120,13 @@ "when": 1732210918783, "tag": "0016_change_all_to_snake_case", "breakpoints": true + }, + { + "idx": 17, + "version": "6", + "when": 1733777395703, + "tag": "0017_small_rumiko_fujikawa", + "breakpoints": true } ] } diff --git a/packages/db/schema/mysql.ts b/packages/db/schema/mysql.ts index 822e06795..b3ccabcc1 100644 --- a/packages/db/schema/mysql.ts +++ b/packages/db/schema/mysql.ts @@ -25,6 +25,7 @@ import type { IntegrationKind, IntegrationPermission, IntegrationSecretKind, + OnboardingStep, SearchEngineType, SectionKind, SupportedAuthProvider, @@ -395,6 +396,12 @@ export const searchEngines = mysqlTable("search_engine", { integrationId: varchar({ length: 64 }).references(() => integrations.id, { onDelete: "cascade" }), }); +export const onboarding = mysqlTable("onboarding", { + id: varchar({ length: 64 }).notNull().primaryKey(), + step: varchar({ length: 64 }).$type().notNull(), + previousStep: varchar({ length: 64 }).$type(), +}); + export const accountRelations = relations(accounts, ({ one }) => ({ user: one(users, { fields: [accounts.userId], diff --git a/packages/db/schema/sqlite.ts b/packages/db/schema/sqlite.ts index fd4158f38..f5feffc71 100644 --- a/packages/db/schema/sqlite.ts +++ b/packages/db/schema/sqlite.ts @@ -16,6 +16,7 @@ import type { IntegrationKind, IntegrationPermission, IntegrationSecretKind, + OnboardingStep, SearchEngineType, SectionKind, SupportedAuthProvider, @@ -382,6 +383,12 @@ export const searchEngines = sqliteTable("search_engine", { integrationId: text().references(() => integrations.id, { onDelete: "cascade" }), }); +export const onboarding = sqliteTable("onboarding", { + id: text().notNull().primaryKey(), + step: text().$type().notNull(), + previousStep: text().$type(), +}); + export const accountRelations = relations(accounts, ({ one }) => ({ user: one(users, { fields: [accounts.userId], diff --git a/packages/definitions/src/group.ts b/packages/definitions/src/group.ts index ebac0daa1..40c278b95 100644 --- a/packages/definitions/src/group.ts +++ b/packages/definitions/src/group.ts @@ -1 +1,2 @@ export const everyoneGroup = "everyone"; +export const credentialsAdminGroup = "admin"; diff --git a/packages/definitions/src/index.ts b/packages/definitions/src/index.ts index 12c878462..13cc03dd5 100644 --- a/packages/definitions/src/index.ts +++ b/packages/definitions/src/index.ts @@ -10,3 +10,4 @@ export * from "./group"; export * from "./docs"; export * from "./cookie"; export * from "./search-engine"; +export * from "./onboarding"; diff --git a/packages/definitions/src/onboarding.ts b/packages/definitions/src/onboarding.ts new file mode 100644 index 000000000..d9a6ea03e --- /dev/null +++ b/packages/definitions/src/onboarding.ts @@ -0,0 +1,2 @@ +export const onboardingSteps = ["start", "import", "user", "group", "settings", "finish"] as const; +export type OnboardingStep = (typeof onboardingSteps)[number]; diff --git a/packages/form/package.json b/packages/form/package.json index 24addae12..79a5f83ae 100644 --- a/packages/form/package.json +++ b/packages/form/package.json @@ -5,7 +5,8 @@ "license": "MIT", "type": "module", "exports": { - ".": "./index.ts" + ".": "./index.ts", + "./types": "./src/types.ts" }, "typesVersions": { "*": { @@ -22,6 +23,7 @@ }, "prettier": "@homarr/prettier-config", "dependencies": { + "@homarr/common": "workspace:^0.1.0", "@homarr/translation": "workspace:^0.1.0", "@homarr/validation": "workspace:^0.1.0", "@mantine/form": "^7.15.1" diff --git a/packages/form/src/types.ts b/packages/form/src/types.ts new file mode 100644 index 000000000..4c6c1363a --- /dev/null +++ b/packages/form/src/types.ts @@ -0,0 +1,21 @@ +import type { ChangeEvent, FocusEvent } from "react"; + +export interface InputPropsFor + extends BasePropsFor { + value?: T; + defaultValue?: T; +} + +interface BasePropsFor { + onChange: (value: TOnChangeArg) => void; + error?: string; + onBlur?: (event: FocusEvent) => void; + onFocus?: (event: FocusEvent) => void; +} + +export interface CheckboxProps< + TOnChangeArg = ChangeEvent, + TComponent extends HTMLElement = HTMLInputElement, +> extends BasePropsFor { + checked?: boolean; +} diff --git a/packages/modals-collection/package.json b/packages/modals-collection/package.json index 41053dd54..93670731b 100644 --- a/packages/modals-collection/package.json +++ b/packages/modals-collection/package.json @@ -27,6 +27,7 @@ "@homarr/form": "workspace:^0.1.0", "@homarr/modals": "workspace:^0.1.0", "@homarr/notifications": "workspace:^0.1.0", + "@homarr/old-import": "workspace:^0.1.0", "@homarr/old-schema": "workspace:^0.1.0", "@homarr/translation": "workspace:^0.1.0", "@homarr/ui": "workspace:^0.1.0", diff --git a/packages/modals-collection/src/boards/import-board-modal.tsx b/packages/modals-collection/src/boards/import-board-modal.tsx index 82caa0b75..e507a29f1 100644 --- a/packages/modals-collection/src/boards/import-board-modal.tsx +++ b/packages/modals-collection/src/boards/import-board-modal.tsx @@ -1,5 +1,5 @@ import { useState } from "react"; -import { Button, Fieldset, FileInput, Grid, Group, Radio, Stack, Switch, TextInput } from "@mantine/core"; +import { Button, FileInput, Group, Radio, Stack, TextInput } from "@mantine/core"; import { IconFileUpload } from "@tabler/icons-react"; import { clientApi } from "@homarr/api/client"; @@ -7,16 +7,18 @@ import { revalidatePathActionAsync } from "@homarr/common/client"; import { useZodForm } from "@homarr/form"; import { createModal } from "@homarr/modals"; import { showErrorNotification, showSuccessNotification } from "@homarr/notifications"; +import { OldmarrImportAppsSettings, SidebarBehaviourSelect } from "@homarr/old-import/components"; +import type { OldmarrImportConfiguration } from "@homarr/old-import/shared"; +import { oldmarrImportConfigurationSchema, superRefineJsonImportFile } from "@homarr/old-import/shared"; import { oldmarrConfigSchema } from "@homarr/old-schema"; -import { useScopedI18n } from "@homarr/translation/client"; -import { SelectWithDescription } from "@homarr/ui"; -import type { OldmarrImportConfiguration } from "@homarr/validation"; -import { oldmarrImportConfigurationSchema, superRefineJsonImportFile, z } from "@homarr/validation"; +import { useI18n, useScopedI18n } from "@homarr/translation/client"; +import { z } from "@homarr/validation"; import { useBoardNameStatus } from "./add-board-modal"; export const ImportBoardModal = createModal(({ actions }) => { const tOldImport = useScopedI18n("board.action.oldImport"); + const t = useI18n(); const tCommon = useScopedI18n("common"); const [fileValid, setFileValid] = useState(true); const form = useZodForm( @@ -30,7 +32,6 @@ export const ImportBoardModal = createModal(({ actions }) => { // eslint-disable-next-line @typescript-eslint/no-non-null-assertion file: null!, configuration: { - distinctAppsByHref: true, onlyImportApps: false, screenSize: "lg", sidebarBehaviour: "last-section", @@ -119,24 +120,7 @@ export const ImportBoardModal = createModal(({ actions }) => { label={tOldImport("form.file.label")} /> -
- - - - - - - - -
+ { - - - + + + - + + + + ); +}; diff --git a/packages/old-import/src/components/initial/token-modal.tsx b/packages/old-import/src/components/initial/token-modal.tsx new file mode 100644 index 000000000..a612f029c --- /dev/null +++ b/packages/old-import/src/components/initial/token-modal.tsx @@ -0,0 +1,67 @@ +import { Button, Group, PasswordInput, Stack } from "@mantine/core"; +import { z } from "zod"; + +import { useZodForm } from "@homarr/form"; +import { createModal } from "@homarr/modals"; +import { showErrorNotification } from "@homarr/notifications"; +import { useI18n, useScopedI18n } from "@homarr/translation/client"; + +// We don't have access to the API client here, so we need to import it from the API package +// In the future we should consider having the used router also in this package +import { clientApi } from "../../../../api/src/client"; + +interface InnerProps { + checksum: string; + onSuccessAsync: (token: string) => Promise; +} + +const formSchema = z.object({ + token: z.string().min(1).max(256), +}); + +export const ImportTokenModal = createModal(({ actions, innerProps }) => { + const t = useI18n(); + const tTokenModal = useScopedI18n("init.step.import.tokenModal"); + const { mutate, isPending } = clientApi.import.validateToken.useMutation(); + const form = useZodForm(formSchema, { initialValues: { token: "" } }); + + const handleSubmit = (values: z.infer) => { + mutate( + { checksum: innerProps.checksum, token: values.token }, + { + async onSuccess(isValid) { + if (isValid) { + actions.closeModal(); + await innerProps.onSuccessAsync(values.token); + } else { + showErrorNotification({ + title: tTokenModal("notification.error.title"), + message: tTokenModal("notification.error.message"), + }); + } + }, + }, + ); + }; + + return ( +
+ + + + + + + +
+ ); +}).withOptions({ defaultTitle: (t) => t("init.step.import.tokenModal.title") }); diff --git a/packages/old-import/src/components/shared/apps-section.tsx b/packages/old-import/src/components/shared/apps-section.tsx new file mode 100644 index 000000000..23fef2cc9 --- /dev/null +++ b/packages/old-import/src/components/shared/apps-section.tsx @@ -0,0 +1,23 @@ +import { Fieldset, Switch } from "@mantine/core"; + +import type { CheckboxProps } from "@homarr/form/types"; +import { useScopedI18n } from "@homarr/translation/client"; + +interface OldmarrImportAppsSettingsProps { + onlyImportApps: CheckboxProps; + background?: string; +} + +export const OldmarrImportAppsSettings = ({ background, onlyImportApps }: OldmarrImportAppsSettingsProps) => { + const tApps = useScopedI18n("board.action.oldImport.form.apps"); + + return ( +
+ +
+ ); +}; diff --git a/packages/old-import/src/components/shared/sidebar-behaviour-select.tsx b/packages/old-import/src/components/shared/sidebar-behaviour-select.tsx new file mode 100644 index 000000000..17a0e2c50 --- /dev/null +++ b/packages/old-import/src/components/shared/sidebar-behaviour-select.tsx @@ -0,0 +1,31 @@ +import type { InputPropsFor } from "@homarr/form/types"; +import { useScopedI18n } from "@homarr/translation/client"; +import { SelectWithDescription } from "@homarr/ui"; + +import type { SidebarBehaviour } from "../../settings"; + +export const SidebarBehaviourSelect = (props: InputPropsFor) => { + const tSidebarBehaviour = useScopedI18n("board.action.oldImport.form.sidebarBehavior"); + + return ( + (value ? props.onChange(value as SidebarBehaviour) : null)} + /> + ); +}; diff --git a/packages/old-import/src/import-board.ts b/packages/old-import/src/import-board.ts index 9ea155929..822e37ed3 100644 --- a/packages/old-import/src/import-board.ts +++ b/packages/old-import/src/import-board.ts @@ -3,10 +3,10 @@ import { createId } from "@homarr/db"; import { boards } from "@homarr/db/schema/sqlite"; import { logger } from "@homarr/log"; import type { OldmarrConfig } from "@homarr/old-schema"; -import type { OldmarrImportConfiguration } from "@homarr/validation"; import { mapColor } from "./mappers/map-colors"; import { mapColumnCount } from "./mappers/map-column-count"; +import type { OldmarrImportConfiguration } from "./settings"; export const insertBoardAsync = async (db: Database, old: OldmarrConfig, configuration: OldmarrImportConfiguration) => { logger.info(`Importing old homarr board configuration=${old.configProperties.name}`); diff --git a/packages/old-import/src/import-error.ts b/packages/old-import/src/import-error.ts index 89479aa2f..9c0fd3687 100644 --- a/packages/old-import/src/import-error.ts +++ b/packages/old-import/src/import-error.ts @@ -1,5 +1,6 @@ import type { OldmarrConfig } from "@homarr/old-schema"; -import type { OldmarrImportConfiguration } from "@homarr/validation"; + +import type { OldmarrImportConfiguration } from "./settings"; export class OldHomarrImportError extends Error { constructor(oldConfig: OldmarrConfig, cause: unknown) { diff --git a/packages/old-import/src/import-items.ts b/packages/old-import/src/import-items.ts index fab64c88e..652687d21 100644 --- a/packages/old-import/src/import-items.ts +++ b/packages/old-import/src/import-items.ts @@ -5,10 +5,10 @@ import { createId } from "@homarr/db"; import { items } from "@homarr/db/schema/sqlite"; import { logger } from "@homarr/log"; import type { OldmarrApp, OldmarrWidget } from "@homarr/old-schema"; -import type { OldmarrImportConfiguration } from "@homarr/validation"; import type { WidgetComponentProps } from "../../widgets/src/definition"; import { OldHomarrScreenSizeError } from "./import-error"; +import type { OldmarrImportConfiguration } from "./settings"; import { mapKind } from "./widgets/definitions"; import { mapOptions } from "./widgets/options"; diff --git a/packages/old-import/src/import/collections/board-collection.ts b/packages/old-import/src/import/collections/board-collection.ts new file mode 100644 index 000000000..7178485d5 --- /dev/null +++ b/packages/old-import/src/import/collections/board-collection.ts @@ -0,0 +1,78 @@ +import { createId } from "@homarr/db"; +import { logger } from "@homarr/log"; + +import { fixSectionIssues } from "../../fix-section-issues"; +import { mapBoard } from "../../mappers/map-board"; +import { moveWidgetsAndAppsIfMerge } from "../../move-widgets-and-apps-merge"; +import { prepareItems } from "../../prepare/prepare-items"; +import type { prepareMultipleImports } from "../../prepare/prepare-multiple"; +import { prepareSections } from "../../prepare/prepare-sections"; +import type { InitialOldmarrImportSettings } from "../../settings"; +import { createDbInsertCollection } from "./common"; + +export const createBoardInsertCollection = ( + { preparedApps, preparedBoards }: Omit, "preparedIntegrations">, + settings: InitialOldmarrImportSettings, +) => { + const insertCollection = createDbInsertCollection(["apps", "boards", "sections", "items"]); + logger.info("Preparing boards for insert collection"); + + const appsMap = new Map( + preparedApps.flatMap(({ ids, ...app }) => { + const id = app.existingId ?? createId(); + return ids.map((oldId) => [oldId, { id, ...app }] as const); + }), + ); + + for (const app of appsMap.values()) { + // Skip duplicate apps + if (insertCollection.apps.some((appEntry) => appEntry.id === app.id)) { + continue; + } + // Skip apps that already exist in the database + if (app.existingId) { + continue; + } + + insertCollection.apps.push(app); + } + + if (settings.onlyImportApps) { + logger.info( + `Skipping boards and sections import due to onlyImportApps setting appCount=${insertCollection.apps.length}`, + ); + return insertCollection; + } + logger.debug(`Added apps to board insert collection count=${insertCollection.apps.length}`); + + preparedBoards.forEach((board) => { + const { wrappers, categories, wrapperIdsToMerge } = fixSectionIssues(board.config); + const { apps, widgets } = moveWidgetsAndAppsIfMerge(board.config, wrapperIdsToMerge, { + ...settings, + screenSize: board.size, + name: board.name, + }); + + logger.debug(`Fixed issues with sections and item positions fileName=${board.name}`); + + const mappedBoard = mapBoard(board); + logger.debug(`Mapped board fileName=${board.name} boardId=${mappedBoard.id}`); + insertCollection.boards.push(mappedBoard); + const preparedSections = prepareSections(mappedBoard.id, { wrappers, categories }); + + for (const section of preparedSections.values()) { + insertCollection.sections.push(section); + } + logger.debug(`Added sections to board insert collection count=${insertCollection.sections.length}`); + + const preparedItems = prepareItems({ apps, widgets }, board.size, appsMap, preparedSections); + preparedItems.forEach((item) => insertCollection.items.push(item)); + logger.debug(`Added items to board insert collection count=${insertCollection.items.length}`); + }); + + logger.info( + `Board collection prepared boardCount=${insertCollection.boards.length} sectionCount=${insertCollection.sections.length} itemCount=${insertCollection.items.length} appCount=${insertCollection.apps.length}`, + ); + + return insertCollection; +}; diff --git a/packages/old-import/src/import/collections/common.ts b/packages/old-import/src/import/collections/common.ts new file mode 100644 index 000000000..7dd07f087 --- /dev/null +++ b/packages/old-import/src/import/collections/common.ts @@ -0,0 +1,33 @@ +import { objectEntries } from "@homarr/common"; +import type { Database, InferInsertModel } from "@homarr/db"; +import { schema } from "@homarr/db"; + +type TableKey = { + [K in keyof typeof schema]: (typeof schema)[K] extends { _: { brand: "Table" } } ? K : never; +}[keyof typeof schema]; + +export const createDbInsertCollection = (tablesInInsertOrder: TTableKey[]) => { + const context = tablesInInsertOrder.reduce( + (acc, key) => { + acc[key] = []; + return acc; + }, + {} as { [K in TTableKey]: InferInsertModel<(typeof schema)[K]>[] }, + ); + + return { + ...context, + insertAll: (db: Database) => { + db.transaction((transaction) => { + for (const [key, values] of objectEntries(context)) { + if (values.length >= 1) { + transaction + .insert(schema[key]) + .values(values as never) + .run(); + } + } + }); + }, + }; +}; diff --git a/packages/old-import/src/import/collections/integration-collection.ts b/packages/old-import/src/import/collections/integration-collection.ts new file mode 100644 index 000000000..dd21cd59f --- /dev/null +++ b/packages/old-import/src/import/collections/integration-collection.ts @@ -0,0 +1,47 @@ +import { encryptSecret } from "@homarr/common/server"; +import { logger } from "@homarr/log"; + +import { mapAndDecryptIntegrations } from "../../mappers/map-integration"; +import type { PreparedIntegration } from "../../prepare/prepare-integrations"; +import { createDbInsertCollection } from "./common"; + +export const createIntegrationInsertCollection = ( + preparedIntegrations: PreparedIntegration[], + encryptionToken: string | null, +) => { + const insertCollection = createDbInsertCollection(["integrations", "integrationSecrets"]); + logger.info(`Preparing integrations for insert collection count=${preparedIntegrations.length}`); + + if (encryptionToken === null) { + logger.debug("Skipping integration decryption due to missing token"); + return insertCollection; + } + + const preparedIntegrationsDecrypted = mapAndDecryptIntegrations(preparedIntegrations, encryptionToken); + + preparedIntegrationsDecrypted.forEach((integration) => { + insertCollection.integrations.push({ + id: integration.id, + kind: integration.kind, + name: integration.name, + url: integration.url, + }); + + integration.secrets + .filter((secret) => secret.value !== null) + .forEach((secret) => { + insertCollection.integrationSecrets.push({ + integrationId: integration.id, + kind: secret.field, + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + value: encryptSecret(secret.value!), + }); + }); + }); + + logger.info( + `Added integrations and secrets to insert collection integrationCount=${insertCollection.integrations.length} secretCount=${insertCollection.integrationSecrets.length}`, + ); + + return insertCollection; +}; diff --git a/packages/old-import/src/import/collections/user-collection.ts b/packages/old-import/src/import/collections/user-collection.ts new file mode 100644 index 000000000..096a06591 --- /dev/null +++ b/packages/old-import/src/import/collections/user-collection.ts @@ -0,0 +1,53 @@ +import { createId } from "@homarr/db"; +import { credentialsAdminGroup } from "@homarr/definitions"; +import { logger } from "@homarr/log"; + +import { mapAndDecryptUsers } from "../../mappers/map-user"; +import type { OldmarrImportUser } from "../../user-schema"; +import { createDbInsertCollection } from "./common"; + +export const createUserInsertCollection = (importUsers: OldmarrImportUser[], encryptionToken: string | null) => { + const insertCollection = createDbInsertCollection(["users", "groups", "groupMembers", "groupPermissions"]); + + logger.info(`Preparing users for insert collection count=${importUsers.length}`); + + if (encryptionToken === null) { + logger.debug("Skipping user decryption due to missing token"); + return insertCollection; + } + + const preparedUsers = mapAndDecryptUsers(importUsers, encryptionToken); + preparedUsers.forEach((user) => insertCollection.users.push(user)); + logger.debug(`Added users to insert collection count=${insertCollection.users.length}`); + + if (!preparedUsers.some((user) => user.isAdmin)) { + logger.warn("No admin users found, skipping admin group creation"); + return insertCollection; + } + + const adminGroupId = createId(); + insertCollection.groups.push({ + id: adminGroupId, + name: credentialsAdminGroup, + }); + + insertCollection.groupPermissions.push({ + groupId: adminGroupId, + permission: "admin", + }); + + const admins = preparedUsers.filter((user) => user.isAdmin); + + admins.forEach((user) => { + insertCollection.groupMembers.push({ + groupId: adminGroupId, + userId: user.id, + }); + }); + + logger.info( + `Added admin group and permissions to insert collection adminGroupId=${adminGroupId} adminUsersCount=${admins.length}`, + ); + + return insertCollection; +}; diff --git a/packages/old-import/src/import/import-initial-oldmarr.ts b/packages/old-import/src/import/import-initial-oldmarr.ts new file mode 100644 index 000000000..90b2fea2a --- /dev/null +++ b/packages/old-import/src/import/import-initial-oldmarr.ts @@ -0,0 +1,45 @@ +import type { z } from "zod"; + +import { Stopwatch } from "@homarr/common"; +import type { Database } from "@homarr/db"; +import { logger } from "@homarr/log"; + +import { analyseOldmarrImportAsync } from "../analyse/analyse-oldmarr-import"; +import { prepareMultipleImports } from "../prepare/prepare-multiple"; +import { createBoardInsertCollection } from "./collections/board-collection"; +import { createIntegrationInsertCollection } from "./collections/integration-collection"; +import { createUserInsertCollection } from "./collections/user-collection"; +import type { importInitialOldmarrInputSchema } from "./input"; +import { ensureValidTokenOrThrow } from "./validate-token"; + +export const importInitialOldmarrAsync = async ( + db: Database, + input: z.infer, +) => { + const stopwatch = new Stopwatch(); + const { checksum, configs, users: importUsers } = await analyseOldmarrImportAsync(input.file); + ensureValidTokenOrThrow(checksum, input.token); + + const { preparedApps, preparedBoards, preparedIntegrations } = prepareMultipleImports( + configs, + input.settings, + input.boardSelections, + ); + + logger.info("Preparing import data in insert collections for database"); + + const boardInsertCollection = createBoardInsertCollection({ preparedApps, preparedBoards }, input.settings); + const userInsertCollection = createUserInsertCollection(importUsers, input.token); + const integrationInsertCollection = createIntegrationInsertCollection(preparedIntegrations, input.token); + + logger.info("Inserting import data to database"); + + // Due to a limitation with better-sqlite it's only possible to use it synchronously + db.transaction((transaction) => { + boardInsertCollection.insertAll(transaction); + userInsertCollection.insertAll(transaction); + integrationInsertCollection.insertAll(transaction); + }); + + logger.info(`Import successful (in ${stopwatch.getElapsedInHumanWords()})`); +}; diff --git a/packages/old-import/src/import/import-single-oldmarr.ts b/packages/old-import/src/import/import-single-oldmarr.ts new file mode 100644 index 000000000..568595929 --- /dev/null +++ b/packages/old-import/src/import/import-single-oldmarr.ts @@ -0,0 +1,36 @@ +import { inArray } from "@homarr/db"; +import type { Database } from "@homarr/db"; +import { apps } from "@homarr/db/schema/sqlite"; +import type { OldmarrConfig } from "@homarr/old-schema"; + +import { doAppsMatch } from "../prepare/prepare-apps"; +import { prepareSingleImport } from "../prepare/prepare-single"; +import type { OldmarrImportConfiguration } from "../settings"; +import { createBoardInsertCollection } from "./collections/board-collection"; + +export const importSingleOldmarrConfigAsync = async ( + db: Database, + config: OldmarrConfig, + settings: OldmarrImportConfiguration, +) => { + const { preparedApps, preparedBoards } = prepareSingleImport(config, settings); + const existingApps = await db.query.apps.findMany({ + where: inArray( + apps.href, + preparedApps.map((app) => app.href).filter((href) => href !== null), + ), + }); + + preparedApps.forEach((app) => { + const existingApp = existingApps.find((existingApp) => doAppsMatch(existingApp, app)); + if (existingApp) { + app.existingId = existingApp.id; + } + return app; + }); + + const boardInsertCollection = createBoardInsertCollection({ preparedApps, preparedBoards }, settings); + + // Due to a limitation with better-sqlite it's only possible to use it synchronously + boardInsertCollection.insertAll(db); +}; diff --git a/packages/old-import/src/import/index.ts b/packages/old-import/src/import/index.ts new file mode 100644 index 000000000..1c2c4cc72 --- /dev/null +++ b/packages/old-import/src/import/index.ts @@ -0,0 +1,3 @@ +export { importInitialOldmarrAsync } from "./import-initial-oldmarr"; +export * from "./input"; +export { ensureValidTokenOrThrow } from "./validate-token"; diff --git a/packages/old-import/src/import/input.ts b/packages/old-import/src/import/input.ts new file mode 100644 index 000000000..13f28c774 --- /dev/null +++ b/packages/old-import/src/import/input.ts @@ -0,0 +1,24 @@ +import SuperJSON from "superjson"; +import { z } from "zod"; +import { zfd } from "zod-form-data"; + +import { initialOldmarrImportSettings } from "../settings"; + +const boardSelectionMapSchema = z.map( + z.string(), + z.object({ + sm: z.boolean().nullable(), + md: z.boolean().nullable(), + lg: z.boolean().nullable(), + }), +); + +export const importInitialOldmarrInputSchema = zfd.formData({ + file: zfd.file(), + settings: zfd.json(initialOldmarrImportSettings), + boardSelections: zfd.text().transform((value) => { + const map = boardSelectionMapSchema.parse(SuperJSON.parse(value)); + return map; + }), + token: zfd.text().nullable(), +}); diff --git a/packages/old-import/src/import/validate-token.ts b/packages/old-import/src/import/validate-token.ts new file mode 100644 index 000000000..ac01cf20a --- /dev/null +++ b/packages/old-import/src/import/validate-token.ts @@ -0,0 +1,18 @@ +import { decryptSecretWithKey } from "@homarr/common/server"; + +export const ensureValidTokenOrThrow = (checksum: string | undefined, encryptionToken: string | null) => { + if (!encryptionToken || !checksum) return; + + const [first, second] = checksum.split("\n"); + if (!first || !second) throw new Error("Malformed checksum"); + + const key = Buffer.from(encryptionToken, "hex"); + let decrypted: string; + try { + decrypted = decryptSecretWithKey(second as `${string}.${string}`, key); + } catch { + throw new Error("Invalid checksum"); + } + const isValid = decrypted === first; + if (!isValid) throw new Error("Invalid checksum"); +}; diff --git a/packages/old-import/src/index.ts b/packages/old-import/src/index.ts index aa5ad882a..d3153a189 100644 --- a/packages/old-import/src/index.ts +++ b/packages/old-import/src/index.ts @@ -1,60 +1,13 @@ import type { Database } from "@homarr/db"; import type { OldmarrConfig } from "@homarr/old-schema"; -import type { OldmarrImportConfiguration } from "@homarr/validation"; -import { fixSectionIssues } from "./fix-section-issues"; -import { insertAppsAsync } from "./import-apps"; -import { insertBoardAsync } from "./import-board"; -import { OldHomarrImportError, OldHomarrScreenSizeError } from "./import-error"; -import { insertItemsAsync } from "./import-items"; -import { insertSectionsAsync } from "./import-sections"; -import { moveWidgetsAndAppsIfMerge } from "./move-widgets-and-apps-merge"; -import type { BookmarkApp } from "./widgets/definitions/bookmark"; +import { importSingleOldmarrConfigAsync } from "./import/import-single-oldmarr"; +import type { OldmarrImportConfiguration } from "./settings"; -export const importAsync = async (db: Database, old: OldmarrConfig, configuration: OldmarrImportConfiguration) => { - const bookmarkApps = old.widgets - .filter((widget) => widget.type === "bookmark") - .map((widget) => widget.properties.items) - .flat() as BookmarkApp[]; - - if (configuration.onlyImportApps) { - await db - .transaction(async (trasaction) => { - await insertAppsAsync( - trasaction, - old.apps, - bookmarkApps, - configuration.distinctAppsByHref, - old.configProperties.name, - ); - }) - .catch((error) => { - throw new OldHomarrImportError(old, error); - }); - return; - } - - await db - .transaction(async (trasaction) => { - const { wrappers, categories, wrapperIdsToMerge } = fixSectionIssues(old); - const { apps, widgets } = moveWidgetsAndAppsIfMerge(old, wrapperIdsToMerge, configuration); - - const boardId = await insertBoardAsync(trasaction, old, configuration); - const sectionIdMaps = await insertSectionsAsync(trasaction, categories, wrappers, boardId); - const appsMap = await insertAppsAsync( - trasaction, - apps, - bookmarkApps, - configuration.distinctAppsByHref, - old.configProperties.name, - ); - await insertItemsAsync(trasaction, widgets, apps, appsMap, sectionIdMaps, configuration); - }) - .catch((error) => { - if (error instanceof OldHomarrScreenSizeError) { - throw error; - } - - throw new OldHomarrImportError(old, error); - }); +export const importOldmarrAsync = async ( + db: Database, + old: OldmarrConfig, + configuration: OldmarrImportConfiguration, +) => { + await importSingleOldmarrConfigAsync(db, old, configuration); }; diff --git a/packages/old-import/src/mappers/map-app.ts b/packages/old-import/src/mappers/map-app.ts new file mode 100644 index 000000000..0232edc50 --- /dev/null +++ b/packages/old-import/src/mappers/map-app.ts @@ -0,0 +1,27 @@ +import type { InferSelectModel } from "@homarr/db"; +import type { apps } from "@homarr/db/schema/sqlite"; +import type { OldmarrApp } from "@homarr/old-schema"; + +import type { OldmarrBookmarkDefinition } from "../widgets/definitions/bookmark"; + +export const mapOldmarrApp = (app: OldmarrApp): InferSelectModel => { + return { + id: app.id, + name: app.name, + iconUrl: app.appearance.iconUrl, + description: app.behaviour.tooltipDescription ?? null, + href: app.behaviour.externalUrl || app.url, + }; +}; + +export const mapOldmarrBookmarkApp = ( + app: OldmarrBookmarkDefinition["options"]["items"][number], +): InferSelectModel => { + return { + id: app.id, + name: app.name, + iconUrl: app.iconUrl, + description: null, + href: app.href, + }; +}; diff --git a/packages/old-import/src/mappers/map-board.ts b/packages/old-import/src/mappers/map-board.ts new file mode 100644 index 000000000..a11b84cbd --- /dev/null +++ b/packages/old-import/src/mappers/map-board.ts @@ -0,0 +1,27 @@ +import type { InferInsertModel } from "@homarr/db"; +import { createId } from "@homarr/db"; +import type { boards } from "@homarr/db/schema/sqlite"; + +import type { prepareMultipleImports } from "../prepare/prepare-multiple"; +import { mapColor } from "./map-colors"; +import { mapColumnCount } from "./map-column-count"; + +type PreparedBoard = ReturnType["preparedBoards"][number]; + +export const mapBoard = (preparedBoard: PreparedBoard): InferInsertModel => ({ + id: createId(), + name: preparedBoard.name, + backgroundImageAttachment: preparedBoard.config.settings.customization.backgroundImageAttachment, + backgroundImageUrl: preparedBoard.config.settings.customization.backgroundImageUrl, + backgroundImageRepeat: preparedBoard.config.settings.customization.backgroundImageRepeat, + backgroundImageSize: preparedBoard.config.settings.customization.backgroundImageSize, + columnCount: mapColumnCount(preparedBoard.config, preparedBoard.size), + faviconImageUrl: preparedBoard.config.settings.customization.faviconUrl, + isPublic: preparedBoard.config.settings.access.allowGuests, + logoImageUrl: preparedBoard.config.settings.customization.logoImageUrl, + pageTitle: preparedBoard.config.settings.customization.pageTitle, + metaTitle: preparedBoard.config.settings.customization.metaTitle, + opacity: preparedBoard.config.settings.customization.appOpacity, + primaryColor: mapColor(preparedBoard.config.settings.customization.colors.primary, "#fa5252"), + secondaryColor: mapColor(preparedBoard.config.settings.customization.colors.secondary, "#fd7e14"), +}); diff --git a/packages/old-import/src/mappers/map-column-count.ts b/packages/old-import/src/mappers/map-column-count.ts index 0f38a98e5..f2ac5c09c 100644 --- a/packages/old-import/src/mappers/map-column-count.ts +++ b/packages/old-import/src/mappers/map-column-count.ts @@ -1,5 +1,6 @@ import type { OldmarrConfig } from "@homarr/old-schema"; -import type { OldmarrImportConfiguration } from "@homarr/validation"; + +import type { OldmarrImportConfiguration } from "../settings"; export const mapColumnCount = (old: OldmarrConfig, screenSize: OldmarrImportConfiguration["screenSize"]) => { switch (screenSize) { diff --git a/packages/old-import/src/mappers/map-integration.ts b/packages/old-import/src/mappers/map-integration.ts new file mode 100644 index 000000000..b51dae2dd --- /dev/null +++ b/packages/old-import/src/mappers/map-integration.ts @@ -0,0 +1,60 @@ +import { decryptSecretWithKey } from "@homarr/common/server"; +import { createId } from "@homarr/db"; +import type { IntegrationKind } from "@homarr/definitions"; +import type { OldmarrIntegrationType } from "@homarr/old-schema"; + +import type { PreparedIntegration } from "../prepare/prepare-integrations"; + +export const mapIntegrationType = (type: OldmarrIntegrationType) => { + const kind = mapping[type]; + if (!kind) { + throw new Error(`Integration type ${type} is not supported yet`); + } + return kind; +}; + +const mapping: Record = { + adGuardHome: "adGuardHome", + deluge: "deluge", + homeAssistant: "homeAssistant", + jellyfin: "jellyfin", + jellyseerr: "jellyseerr", + lidarr: "lidarr", + nzbGet: "nzbGet", + openmediavault: "openmediavault", + overseerr: "overseerr", + pihole: "piHole", + prowlarr: "prowlarr", + proxmox: null, + qBittorrent: "qBittorrent", + radarr: "radarr", + readarr: "readarr", + sabnzbd: "sabNzbd", + sonarr: "sonarr", + tdarr: null, + transmission: "transmission", + plex: "plex", +}; + +export const mapAndDecryptIntegrations = ( + preparedIntegrations: PreparedIntegration[], + encryptionToken: string | null, +) => { + if (encryptionToken === null) { + return []; + } + + const key = Buffer.from(encryptionToken, "hex"); + + return preparedIntegrations.map(({ type, name, url, properties }) => ({ + id: createId(), + name, + url, + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + kind: mapIntegrationType(type!), + secrets: properties.map((property) => ({ + ...property, + value: property.value ? decryptSecretWithKey(property.value as `${string}.${string}`, key) : null, + })), + })); +}; diff --git a/packages/old-import/src/mappers/map-item.ts b/packages/old-import/src/mappers/map-item.ts new file mode 100644 index 000000000..f24fdfb37 --- /dev/null +++ b/packages/old-import/src/mappers/map-item.ts @@ -0,0 +1,89 @@ +import SuperJSON from "superjson"; + +import type { InferInsertModel } from "@homarr/db"; +import { createId } from "@homarr/db"; +import type { items } from "@homarr/db/schema/sqlite"; +import { logger } from "@homarr/log"; +import type { BoardSize, OldmarrApp, OldmarrWidget } from "@homarr/old-schema"; + +import type { WidgetComponentProps } from "../../../widgets/src/definition"; +import { mapKind } from "../widgets/definitions"; +import { mapOptions } from "../widgets/options"; + +export const mapApp = ( + app: OldmarrApp, + boardSize: BoardSize, + appsMap: Map, + sectionMap: Map, +): InferInsertModel => { + if (app.area.type === "sidebar") throw new Error("Mapping app in sidebar is not supported"); + + const shapeForSize = app.shape[boardSize]; + if (!shapeForSize) { + throw new Error(`Failed to find a shape for appId='${app.id}' screenSize='${boardSize}'`); + } + + const sectionId = sectionMap.get(app.area.properties.id)?.id; + if (!sectionId) { + throw new Error(`Failed to find section for app appId='${app.id}' sectionId='${app.area.properties.id}'`); + } + + return { + id: createId(), + sectionId, + height: shapeForSize.size.height, + width: shapeForSize.size.width, + xOffset: shapeForSize.location.x, + yOffset: shapeForSize.location.y, + kind: "app", + options: SuperJSON.stringify({ + // it's safe to assume that the app exists in the map + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion, @typescript-eslint/no-non-null-asserted-optional-chain + appId: appsMap.get(app.id)?.id!, + openInNewTab: app.behaviour.isOpeningNewTab, + pingEnabled: app.network.enabledStatusChecker, + showDescriptionTooltip: app.behaviour.tooltipDescription !== "", + showTitle: app.appearance.appNameStatus === "normal", + } satisfies WidgetComponentProps<"app">["options"]), + }; +}; + +export const mapWidget = ( + widget: OldmarrWidget, + boardSize: BoardSize, + appsMap: Map, + sectionMap: Map, +): InferInsertModel | null => { + if (widget.area.type === "sidebar") throw new Error("Mapping widget in sidebar is not supported"); + + const shapeForSize = widget.shape[boardSize]; + if (!shapeForSize) { + throw new Error(`Failed to find a shape for widgetId='${widget.id}' screenSize='${boardSize}'`); + } + + const kind = mapKind(widget.type); + if (!kind) { + logger.warn(`Failed to map widget type='${widget.type}'. It's no longer supported`); + return null; + } + + const sectionId = sectionMap.get(widget.area.properties.id)?.id; + if (!sectionId) { + throw new Error( + `Failed to find section for widget widgetId='${widget.id}' sectionId='${widget.area.properties.id}'`, + ); + } + + return { + id: createId(), + sectionId, + height: shapeForSize.size.height, + width: shapeForSize.size.width, + xOffset: shapeForSize.location.x, + yOffset: shapeForSize.location.y, + kind, + options: SuperJSON.stringify( + mapOptions(kind, widget.properties, new Map([...appsMap.entries()].map(([key, value]) => [key, value.id]))), + ), + }; +}; diff --git a/packages/old-import/src/mappers/map-section.ts b/packages/old-import/src/mappers/map-section.ts new file mode 100644 index 000000000..10bf47ac8 --- /dev/null +++ b/packages/old-import/src/mappers/map-section.ts @@ -0,0 +1,24 @@ +import type { InferInsertModel } from "@homarr/db"; +import { createId } from "@homarr/db"; +import type { sections } from "@homarr/db/schema/sqlite"; +import type { OldmarrCategorySection, OldmarrEmptySection } from "@homarr/old-schema"; + +export const mapCategorySection = ( + boardId: string, + category: OldmarrCategorySection, +): InferInsertModel => ({ + id: createId(), + boardId, + kind: "category", + xOffset: 0, + yOffset: category.position, + name: category.name, +}); + +export const mapEmptySection = (boardId: string, wrapper: OldmarrEmptySection): InferInsertModel => ({ + id: createId(), + boardId, + kind: "empty", + xOffset: 0, + yOffset: wrapper.position, +}); diff --git a/packages/old-import/src/mappers/map-user.ts b/packages/old-import/src/mappers/map-user.ts new file mode 100644 index 000000000..465eae455 --- /dev/null +++ b/packages/old-import/src/mappers/map-user.ts @@ -0,0 +1,35 @@ +import { decryptSecretWithKey } from "@homarr/common/server"; +import type { InferInsertModel } from "@homarr/db"; +import { createId } from "@homarr/db"; +import type { users } from "@homarr/db/schema/sqlite"; + +import type { OldmarrImportUser } from "../user-schema"; + +export const mapAndDecryptUsers = (importUsers: OldmarrImportUser[], encryptionToken: string | null) => { + if (encryptionToken === null) { + return []; + } + + const key = Buffer.from(encryptionToken, "hex"); + + return importUsers.map( + ({ + id, + password, + salt, + settings, + ...user + }): InferInsertModel & { oldId: string; isAdmin: boolean } => ({ + ...user, + oldId: id, + id: createId(), + colorScheme: settings?.colorScheme === "environment" ? undefined : settings?.colorScheme, + firstDayOfWeek: settings?.firstDayOfWeek === "sunday" ? 0 : settings?.firstDayOfWeek === "monday" ? 1 : 6, + provider: "credentials", + pingIconsEnabled: settings?.replacePingWithIcons, + isAdmin: user.isAdmin || user.isOwner, + password: decryptSecretWithKey(password, key), + salt: decryptSecretWithKey(salt, key), + }), + ); +}; diff --git a/packages/old-import/src/move-widgets-and-apps-merge.ts b/packages/old-import/src/move-widgets-and-apps-merge.ts index 0305a8832..2111e5d9e 100644 --- a/packages/old-import/src/move-widgets-and-apps-merge.ts +++ b/packages/old-import/src/move-widgets-and-apps-merge.ts @@ -1,10 +1,10 @@ import { objectEntries } from "@homarr/common"; import { logger } from "@homarr/log"; import type { OldmarrApp, OldmarrConfig, OldmarrWidget } from "@homarr/old-schema"; -import type { OldmarrImportConfiguration } from "@homarr/validation"; import { OldHomarrScreenSizeError } from "./import-error"; import { mapColumnCount } from "./mappers/map-column-count"; +import type { OldmarrImportConfiguration } from "./settings"; export const moveWidgetsAndAppsIfMerge = ( old: OldmarrConfig, diff --git a/packages/old-import/src/prepare/prepare-apps.ts b/packages/old-import/src/prepare/prepare-apps.ts new file mode 100644 index 000000000..d38820102 --- /dev/null +++ b/packages/old-import/src/prepare/prepare-apps.ts @@ -0,0 +1,59 @@ +import type { InferSelectModel } from "@homarr/db"; +import type { apps } from "@homarr/db/schema/sqlite"; +import type { OldmarrConfig } from "@homarr/old-schema"; + +import type { ValidAnalyseConfig } from "../analyse/types"; +import { mapOldmarrApp, mapOldmarrBookmarkApp } from "../mappers/map-app"; +import type { OldmarrBookmarkDefinition } from "../widgets/definitions/bookmark"; + +export type PreparedApp = Omit, "id"> & { ids: string[]; existingId?: string }; + +export const prepareApps = (analyseConfigs: ValidAnalyseConfig[]) => { + const preparedApps: PreparedApp[] = []; + + analyseConfigs.forEach(({ config }) => { + const appsFromConfig = extractAppsFromConfig(config).concat(extractBookmarkAppsFromConfig(config)); + addAppsToPreparedApps(preparedApps, appsFromConfig); + }); + + return preparedApps; +}; + +const extractAppsFromConfig = (config: OldmarrConfig) => { + return config.apps.map(mapOldmarrApp); +}; + +const extractBookmarkAppsFromConfig = (config: OldmarrConfig) => { + const bookmarkWidgets = config.widgets.filter((widget) => widget.type === "bookmark"); + return bookmarkWidgets.flatMap((widget) => + (widget.properties as OldmarrBookmarkDefinition["options"]).items.map(mapOldmarrBookmarkApp), + ); +}; + +const addAppsToPreparedApps = (preparedApps: PreparedApp[], configApps: InferSelectModel[]) => { + configApps.forEach(({ id, ...app }) => { + const existingApp = preparedApps.find((preparedApp) => doAppsMatch(preparedApp, app)); + + if (existingApp) { + existingApp.ids.push(id); + return; + } + + preparedApps.push({ + ...app, + ids: [id], + }); + }); +}; + +export const doAppsMatch = ( + app1: Omit, "id">, + app2: Omit, "id">, +) => { + return ( + app1.name === app2.name && + app1.iconUrl === app2.iconUrl && + app1.description === app2.description && + app1.href === app2.href + ); +}; diff --git a/packages/old-import/src/prepare/prepare-boards.ts b/packages/old-import/src/prepare/prepare-boards.ts new file mode 100644 index 000000000..76dc2bc2f --- /dev/null +++ b/packages/old-import/src/prepare/prepare-boards.ts @@ -0,0 +1,34 @@ +import { objectEntries } from "@homarr/common"; +import type { BoardSize } from "@homarr/old-schema"; + +import type { ValidAnalyseConfig } from "../analyse/types"; +import type { BoardSelectionMap } from "../components/initial/board-selection-card"; + +const boardSizeSuffix: Record = { + lg: "large", + md: "medium", + sm: "small", +}; + +export const createBoardName = (fileName: string, boardSize: BoardSize) => { + return `${fileName.replace(".json", "")}-${boardSizeSuffix[boardSize]}`; +}; + +export const prepareBoards = (analyseConfigs: ValidAnalyseConfig[], selections: BoardSelectionMap) => { + return analyseConfigs.flatMap(({ name, config }) => { + const selectedSizes = selections.get(name); + if (!selectedSizes) return []; + + return objectEntries(selectedSizes) + .map(([size, selected]) => { + if (!selected) return null; + + return { + name: createBoardName(name, size), + size, + config, + }; + }) + .filter((board) => board !== null); + }); +}; diff --git a/packages/old-import/src/prepare/prepare-integrations.ts b/packages/old-import/src/prepare/prepare-integrations.ts new file mode 100644 index 000000000..6d55ae3d9 --- /dev/null +++ b/packages/old-import/src/prepare/prepare-integrations.ts @@ -0,0 +1,19 @@ +import type { ValidAnalyseConfig } from "../analyse/types"; + +export type PreparedIntegration = ReturnType[number]; + +export const prepareIntegrations = (analyseConfigs: ValidAnalyseConfig[]) => { + return analyseConfigs.flatMap(({ config }) => { + return config.apps + .map((app) => + app.integration?.type + ? { + ...app.integration, + name: app.name, + url: app.url, + } + : null, + ) + .filter((integration) => integration !== null); + }); +}; diff --git a/packages/old-import/src/prepare/prepare-items.ts b/packages/old-import/src/prepare/prepare-items.ts new file mode 100644 index 000000000..e82d6043c --- /dev/null +++ b/packages/old-import/src/prepare/prepare-items.ts @@ -0,0 +1,14 @@ +import type { BoardSize, OldmarrConfig } from "@homarr/old-schema"; + +import { mapApp, mapWidget } from "../mappers/map-item"; + +export const prepareItems = ( + { apps, widgets }: Pick, + boardSize: BoardSize, + appsMap: Map, + sectionMap: Map, +) => + widgets + .map((widget) => mapWidget(widget, boardSize, appsMap, sectionMap)) + .filter((widget) => widget !== null) + .concat(apps.map((app) => mapApp(app, boardSize, appsMap, sectionMap))); diff --git a/packages/old-import/src/prepare/prepare-multiple.ts b/packages/old-import/src/prepare/prepare-multiple.ts new file mode 100644 index 000000000..860d65b75 --- /dev/null +++ b/packages/old-import/src/prepare/prepare-multiple.ts @@ -0,0 +1,25 @@ +import type { AnalyseConfig, ValidAnalyseConfig } from "../analyse/types"; +import type { BoardSelectionMap } from "../components/initial/board-selection-card"; +import type { InitialOldmarrImportSettings } from "../settings"; +import { prepareApps } from "./prepare-apps"; +import { prepareBoards } from "./prepare-boards"; +import { prepareIntegrations } from "./prepare-integrations"; + +export const prepareMultipleImports = ( + analyseConfigs: AnalyseConfig[], + settings: InitialOldmarrImportSettings, + selections: BoardSelectionMap, +) => { + const invalidConfigs = analyseConfigs.filter((item) => item.config === null); + invalidConfigs.forEach(({ name }) => { + console.warn(`Skipping import of ${name} due to error in configuration. See logs of container for more details.`); + }); + + const filteredConfigs = analyseConfigs.filter((item): item is ValidAnalyseConfig => item.config !== null); + + return { + preparedApps: prepareApps(filteredConfigs), + preparedBoards: settings.onlyImportApps ? [] : prepareBoards(filteredConfigs, selections), + preparedIntegrations: prepareIntegrations(filteredConfigs), + }; +}; diff --git a/packages/old-import/src/prepare/prepare-sections.ts b/packages/old-import/src/prepare/prepare-sections.ts new file mode 100644 index 000000000..28c8c7136 --- /dev/null +++ b/packages/old-import/src/prepare/prepare-sections.ts @@ -0,0 +1,13 @@ +import type { OldmarrConfig } from "@homarr/old-schema"; + +import { mapCategorySection, mapEmptySection } from "../mappers/map-section"; + +export const prepareSections = ( + boardId: string, + { categories, wrappers }: Pick, +) => + new Map( + categories + .map((category) => [category.id, mapCategorySection(boardId, category)] as const) + .concat(wrappers.map((wrapper) => [wrapper.id, mapEmptySection(boardId, wrapper)] as const)), + ); diff --git a/packages/old-import/src/prepare/prepare-single.ts b/packages/old-import/src/prepare/prepare-single.ts new file mode 100644 index 000000000..c762beada --- /dev/null +++ b/packages/old-import/src/prepare/prepare-single.ts @@ -0,0 +1,21 @@ +import type { OldmarrConfig } from "@homarr/old-schema"; + +import type { OldmarrImportConfiguration } from "../settings"; +import { prepareApps } from "./prepare-apps"; + +export const prepareSingleImport = (config: OldmarrConfig, settings: OldmarrImportConfiguration) => { + const validAnalyseConfigs = [{ name: settings.name, config, isError: false }]; + + return { + preparedApps: prepareApps(validAnalyseConfigs), + preparedBoards: settings.onlyImportApps + ? [] + : [ + { + name: settings.name, + size: settings.screenSize, + config, + }, + ], + }; +}; diff --git a/packages/old-import/src/settings.ts b/packages/old-import/src/settings.ts new file mode 100644 index 000000000..a0700c8a0 --- /dev/null +++ b/packages/old-import/src/settings.ts @@ -0,0 +1,63 @@ +import { z } from "zod"; +import { zfd } from "zod-form-data"; + +import { boardSizes } from "@homarr/old-schema"; +import { validation, zodEnumFromArray } from "@homarr/validation"; +import { createCustomErrorParams } from "@homarr/validation/form"; + +export const sidebarBehaviours = ["remove-items", "last-section"] as const; +export const defaultSidebarBehaviour = "last-section"; +export type SidebarBehaviour = (typeof sidebarBehaviours)[number]; + +export const oldmarrImportConfigurationSchema = z.object({ + name: validation.board.name, + onlyImportApps: z.boolean().default(false), + screenSize: zodEnumFromArray(boardSizes).default("lg"), + sidebarBehaviour: z.enum(sidebarBehaviours).default(defaultSidebarBehaviour), +}); + +export type OldmarrImportConfiguration = z.infer; + +export const initialOldmarrImportSettings = oldmarrImportConfigurationSchema.pick({ + onlyImportApps: true, + sidebarBehaviour: true, +}); + +export type InitialOldmarrImportSettings = z.infer; + +export const superRefineJsonImportFile = (value: File | null, context: z.RefinementCtx) => { + if (!value) { + return context.addIssue({ + code: "invalid_type", + expected: "object", + received: "null", + }); + } + + if (value.type !== "application/json") { + return context.addIssue({ + code: "custom", + params: createCustomErrorParams({ + key: "invalidFileType", + params: { expected: "JSON" }, + }), + }); + } + + if (value.size > 1024 * 1024) { + return context.addIssue({ + code: "custom", + params: createCustomErrorParams({ + key: "fileTooLarge", + params: { maxSize: "1 MB" }, + }), + }); + } + + return null; +}; + +export const importJsonFileSchema = zfd.formData({ + file: zfd.file().superRefine(superRefineJsonImportFile), + configuration: zfd.json(oldmarrImportConfigurationSchema), +}); diff --git a/packages/old-import/src/shared.ts b/packages/old-import/src/shared.ts new file mode 100644 index 000000000..4276a510c --- /dev/null +++ b/packages/old-import/src/shared.ts @@ -0,0 +1,2 @@ +export { importJsonFileSchema, superRefineJsonImportFile, oldmarrImportConfigurationSchema } from "./settings"; +export type { OldmarrImportConfiguration } from "./settings"; diff --git a/packages/old-import/src/user-schema.ts b/packages/old-import/src/user-schema.ts new file mode 100644 index 000000000..f7a84c917 --- /dev/null +++ b/packages/old-import/src/user-schema.ts @@ -0,0 +1,27 @@ +import { z } from "zod"; + +const regexEncryptedSchema = z.string().regex(/^[a-f0-9]+\.[a-f0-9]+$/g); + +const encryptedSchema = z.custom<`${string}.${string}`>((value) => regexEncryptedSchema.safeParse(value).success); + +export const oldmarrImportUserSchema = z.object({ + id: z.string(), + name: z.string(), + email: z.string().email().nullable(), + emailVerified: z.date().nullable(), + image: z.string().nullable(), + isAdmin: z.boolean(), + isOwner: z.boolean(), + settings: z + .object({ + colorScheme: z.enum(["environment", "light", "dark"]), + defaultBoard: z.string(), + firstDayOfWeek: z.enum(["monday", "saturday", "sunday"]), + replacePingWithIcons: z.boolean(), + }) + .nullable(), + password: encryptedSchema, + salt: encryptedSchema, +}); + +export type OldmarrImportUser = z.infer; diff --git a/packages/old-schema/package.json b/packages/old-schema/package.json index 6e334a780..25c082f6f 100644 --- a/packages/old-schema/package.json +++ b/packages/old-schema/package.json @@ -22,6 +22,7 @@ }, "prettier": "@homarr/prettier-config", "dependencies": { + "@homarr/common": "workspace:^0.1.0", "zod": "^3.24.1" }, "devDependencies": { diff --git a/packages/old-schema/src/app.ts b/packages/old-schema/src/app.ts index 5a4fb5421..79732af92 100644 --- a/packages/old-schema/src/app.ts +++ b/packages/old-schema/src/app.ts @@ -47,6 +47,8 @@ const integrationSchema = z.enum([ "tdarr", ]); +export type OldmarrIntegrationType = z.infer; + const appIntegrationPropertySchema = z.object({ type: z.enum(["private", "public"]), field: z.enum(["apiKey", "password", "username"]), diff --git a/packages/old-schema/src/config.ts b/packages/old-schema/src/config.ts index 5ca103739..d120d31c7 100644 --- a/packages/old-schema/src/config.ts +++ b/packages/old-schema/src/config.ts @@ -28,3 +28,5 @@ export const oldmarrConfigSchema = z.object({ }); export type OldmarrConfig = z.infer; +export type OldmarrCategorySection = z.infer; +export type OldmarrEmptySection = z.infer; diff --git a/packages/old-schema/src/index.ts b/packages/old-schema/src/index.ts index 6b2e36d27..c328cd8e7 100644 --- a/packages/old-schema/src/index.ts +++ b/packages/old-schema/src/index.ts @@ -1,5 +1,7 @@ -export type { OldmarrConfig } from "./config"; +export type { OldmarrConfig, OldmarrCategorySection, OldmarrEmptySection } from "./config"; export { oldmarrConfigSchema } from "./config"; -export type { OldmarrApp } from "./app"; +export type { OldmarrApp, OldmarrIntegrationType } from "./app"; export type { OldmarrWidget, OldmarrWidgetKind } from "./widget"; export { oldmarrWidgetKinds } from "./widget"; +export { boardSizes } from "./tile"; +export type { BoardSize } from "./tile"; diff --git a/packages/old-schema/src/setting.ts b/packages/old-schema/src/setting.ts index 56c30c188..f9374a6f2 100644 --- a/packages/old-schema/src/setting.ts +++ b/packages/old-schema/src/setting.ts @@ -32,11 +32,17 @@ const accessSettingsSchema = z.object({ allowGuests: z.boolean(), }); -const gridstackSettingsSchema = z.object({ - columnCountSmall: z.number(), - columnCountMedium: z.number(), - columnCountLarge: z.number(), -}); +const gridstackSettingsSchema = z + .object({ + columnCountSmall: z.number(), + columnCountMedium: z.number(), + columnCountLarge: z.number(), + }) + .catch({ + columnCountSmall: 3, + columnCountMedium: 6, + columnCountLarge: 12, + }); const layoutSettingsSchema = z.object({ enabledLeftSidebar: z.boolean(), diff --git a/packages/old-schema/src/tile.ts b/packages/old-schema/src/tile.ts index f90071175..92c66d5af 100644 --- a/packages/old-schema/src/tile.ts +++ b/packages/old-schema/src/tile.ts @@ -1,5 +1,7 @@ import { z } from "zod"; +import { objectKeys } from "@homarr/common"; + const createAreaSchema = ( type: TType, propertiesSchema: TPropertiesSchema, @@ -53,3 +55,6 @@ export const tileBaseSchema = z.object({ area: areaSchema, shape: shapeSchema, }); + +export const boardSizes = objectKeys(shapeSchema._def.shape()); +export type BoardSize = (typeof boardSizes)[number]; diff --git a/packages/translation/src/lang/en.json b/packages/translation/src/lang/en.json index 2d8dd3e76..75d42785b 100644 --- a/packages/translation/src/lang/en.json +++ b/packages/translation/src/lang/en.json @@ -1,4 +1,109 @@ { + "init": { + "step": { + "start": { + "title": "Welcome to Homarr", + "subtitle": "Let's get started with setting up your Homarr instance.", + "description": "To get started, please select how you want to set up your Homarr instance.", + "action": { + "scratch": "Start from scratch", + "importOldmarr": "Import from Homarr before 1.0" + } + }, + "import": { + "title": "Import data", + "subtitle": "You can import data from an existing Homarr instance.", + "dropzone": { + "title": "Drag the zip file here or click to browse", + "description": "The uploaded zip will be processed and you'll be able to select what you want to import" + }, + "fileInfo": { + "action": { + "change": "Change file" + } + }, + "importSettings": { + "title": "Import settings", + "description": "Configure the import behavior" + }, + "boardSelection": { + "title": "Found {count} boards", + "description": "Choose all boards with there size you want to import", + "action": { + "selectAll": "Select all", + "unselectAll": "Unselect all" + } + }, + "summary": { + "title": "Import summary", + "description": "In the below summary you can see what will be imported", + "action": { + "import": "Confirm import and continue" + }, + "entities": { + "apps": "Apps", + "boards": "Boards", + "integrations": "Integrations", + "credentialUsers": "Credential users" + } + }, + "tokenModal": { + "title": "Enter import token", + "field": { + "token": { + "label": "Token", + "description": "Enter the shown import token from your previous homarr instance" + } + }, + "notification": { + "error": { + "title": "Invalid token", + "message": "The token you entered is invalid" + } + } + } + }, + "user": { + "title": "Admin user", + "subtitle": "Specify the credentials for your administrator user.", + "notification": { + "success": { + "title": "User created", + "message": "The user was successfully created" + }, + "error": { + "title": "User creation failed" + } + } + }, + "group": { + "title": "External group", + "subtitle": "Specify the group that should be used for external users.", + "form": { + "name": { + "label": "Group name", + "description": "Name has to match admin group of external provider" + } + } + }, + "settings": { + "title": "Settings", + "subtitle": "Configure server settings." + }, + "finish": { + "title": "Finish setup", + "subtitle": "You are ready to go!", + "description": "You have successfully completed the setup process. You can now start using Homarr. Select your next action:", + "action": { + "goToBoard": "Go to {name} board", + "createBoard": "Create your first board", + "inviteUser": "Invite other users", + "docs": "Read the documentation" + } + } + }, + "backToStart": "Back to start" + }, "user": { "title": "Users", "name": "User", @@ -588,6 +693,10 @@ "secrets": { "title": "Secrets", "lastUpdated": "Last updated {date}", + "notSet": { + "label": "No value set", + "tooltip": "This required secret has not been set yet" + }, "secureNotice": "This secret cannot be retrieved after creation", "reset": { "title": "Reset secret", @@ -687,6 +796,12 @@ "label": "Icon URL", "header": "Type name or objects to filter for icons... Homarr will search through {countIcons} icons for you." }, + "colorScheme": { + "options": { + "light": "Light", + "dark": "Dark" + } + }, "information": { "min": "Min", "max": "Max", @@ -768,7 +883,8 @@ "boardAlreadyExists": "A board with this name already exists", "invalidFileType": "Invalid file type, expected {expected}", "fileTooLarge": "File is too large, maximum size is {maxSize}", - "invalidConfiguration": "Invalid configuration" + "invalidConfiguration": "Invalid configuration", + "groupNameTaken": "Group name already taken" } } } @@ -1606,6 +1722,7 @@ }, "screenSize": { "label": "Screen size", + "description": "In versions before 1.0 three different modes existed, so you were able to choose the amount of columns for each screen size.", "option": { "sm": "Small", "md": "Medium", diff --git a/packages/ui/src/components/select-with-custom-items.tsx b/packages/ui/src/components/select-with-custom-items.tsx index 410fe3429..bbed3cd1f 100644 --- a/packages/ui/src/components/select-with-custom-items.tsx +++ b/packages/ui/src/components/select-with-custom-items.tsx @@ -17,6 +17,7 @@ export interface SelectWithCustomItemsProps withAsterisk?: boolean; onBlur?: (event: React.FocusEvent) => void; onFocus?: (event: React.FocusEvent) => void; + w?: string; } type Props = SelectWithCustomItemsProps & { @@ -30,6 +31,7 @@ export const SelectWithCustomItems = ({ defaultValue, placeholder, SelectOption, + w, ...props }: Props) => { const combobox = useCombobox({ @@ -75,6 +77,7 @@ export const SelectWithCustomItems = ({ onClick={toggle} rightSectionPointerEvents="none" multiline + w={w} > {selectedOption ? : {placeholder}} diff --git a/packages/validation/package.json b/packages/validation/package.json index b4580c1a9..d3d3c325c 100644 --- a/packages/validation/package.json +++ b/packages/validation/package.json @@ -24,7 +24,6 @@ "prettier": "@homarr/prettier-config", "dependencies": { "@homarr/definitions": "workspace:^0.1.0", - "@homarr/old-schema": "workspace:^0.1.0", "@homarr/translation": "workspace:^0.1.0", "zod": "^3.24.1", "zod-form-data": "^2.0.2" diff --git a/packages/validation/src/board.ts b/packages/validation/src/board.ts index 33930063c..2783f7a88 100644 --- a/packages/validation/src/board.ts +++ b/packages/validation/src/board.ts @@ -1,5 +1,4 @@ import { z } from "zod"; -import { zfd } from "zod-form-data"; import { backgroundImageAttachments, @@ -9,7 +8,6 @@ import { } from "@homarr/definitions"; import { zodEnumFromArray } from "./enums"; -import { createCustomErrorParams } from "./form/i18n"; import { createSavePermissionsSchema } from "./permissions"; import { commonItemSchema, createSectionSchema } from "./shared"; @@ -69,53 +67,6 @@ const permissionsSchema = z.object({ id: z.string(), }); -export const oldmarrImportConfigurationSchema = z.object({ - name: boardNameSchema, - onlyImportApps: z.boolean().default(false), - distinctAppsByHref: z.boolean().default(true), - screenSize: z.enum(["lg", "md", "sm"]).default("lg"), - sidebarBehaviour: z.enum(["remove-items", "last-section"]).default("last-section"), -}); - -export type OldmarrImportConfiguration = z.infer; - -export const superRefineJsonImportFile = (value: File | null, context: z.RefinementCtx) => { - if (!value) { - return context.addIssue({ - code: "invalid_type", - expected: "object", - received: "null", - }); - } - - if (value.type !== "application/json") { - return context.addIssue({ - code: "custom", - params: createCustomErrorParams({ - key: "invalidFileType", - params: { expected: "JSON" }, - }), - }); - } - - if (value.size > 1024 * 1024) { - return context.addIssue({ - code: "custom", - params: createCustomErrorParams({ - key: "fileTooLarge", - params: { maxSize: "1 MB" }, - }), - }); - } - - return null; -}; - -const importJsonFileSchema = zfd.formData({ - file: zfd.file().superRefine(superRefineJsonImportFile), - configuration: zfd.json(oldmarrImportConfigurationSchema), -}); - const savePermissionsSchema = createSavePermissionsSchema(zodEnumFromArray(boardPermissions)); z.object({ @@ -129,6 +80,7 @@ z.object({ }); export const boardSchemas = { + name: boardNameSchema, byName: byNameSchema, savePartialSettings: savePartialSettingsSchema, save: saveSchema, @@ -137,5 +89,4 @@ export const boardSchemas = { changeVisibility: changeVisibilitySchema, permissions: permissionsSchema, savePermissions: savePermissionsSchema, - importOldmarrConfig: importJsonFileSchema, }; diff --git a/packages/validation/src/index.ts b/packages/validation/src/index.ts index dd218ff7c..6791c8a54 100644 --- a/packages/validation/src/index.ts +++ b/packages/validation/src/index.ts @@ -7,6 +7,7 @@ import { integrationSchemas } from "./integration"; import { locationSchemas } from "./location"; import { mediaSchemas } from "./media"; import { searchEngineSchemas } from "./search-engine"; +import { settingsSchemas } from "./settings"; import { userSchemas } from "./user"; import { widgetSchemas } from "./widgets"; @@ -21,11 +22,10 @@ export const validation = { icons: iconsSchemas, searchEngine: searchEngineSchemas, media: mediaSchemas, + settings: settingsSchemas, common: commonSchemas, }; -export { oldmarrImportConfigurationSchema, superRefineJsonImportFile } from "./board"; -export type { OldmarrImportConfiguration } from "./board"; export { createSectionSchema, itemAdvancedOptionsSchema, @@ -35,3 +35,4 @@ export { } from "./shared"; export { passwordRequirements } from "./user"; export { supportedMediaUploadFormats } from "./media"; +export { zodEnumFromArray, zodUnionFromArray } from "./enums"; diff --git a/packages/validation/src/settings.ts b/packages/validation/src/settings.ts new file mode 100644 index 000000000..8eb5225ac --- /dev/null +++ b/packages/validation/src/settings.ts @@ -0,0 +1,20 @@ +import { z } from "zod"; + +const initSettingsSchema = z.object({ + analytics: z.object({ + enableGeneral: z.boolean(), + enableWidgetData: z.boolean(), + enableIntegrationData: z.boolean(), + enableUserData: z.boolean(), + }), + crawlingAndIndexing: z.object({ + noIndex: z.boolean(), + noFollow: z.boolean(), + noTranslate: z.boolean(), + noSiteLinksSearchBox: z.boolean(), + }), +}); + +export const settingsSchemas = { + init: initSettingsSchema, +}; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index bca93d04f..12271478c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -97,6 +97,9 @@ importers: '@homarr/notifications': specifier: workspace:^0.1.0 version: link:../../packages/notifications + '@homarr/old-import': + specifier: workspace:^0.1.0 + version: link:../../packages/old-import '@homarr/old-schema': specifier: workspace:^0.1.0 version: link:../../packages/old-schema @@ -127,6 +130,9 @@ importers: '@mantine/core': specifier: ^7.15.1 version: 7.15.1(@mantine/hooks@7.15.1(react@19.0.0))(@types/react@19.0.1)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + '@mantine/dropzone': + specifier: ^7.15.1 + version: 7.15.1(@mantine/core@7.15.1(@mantine/hooks@7.15.1(react@19.0.0))(@types/react@19.0.1)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(@mantine/hooks@7.15.1(react@19.0.0))(react-dom@19.0.0(react@19.0.0))(react@19.0.0) '@mantine/hooks': specifier: ^7.15.1 version: 7.15.1(react@19.0.0) @@ -939,6 +945,9 @@ importers: packages/form: dependencies: + '@homarr/common': + specifier: workspace:^0.1.0 + version: link:../common '@homarr/translation': specifier: workspace:^0.1.0 version: link:../translation @@ -1130,6 +1139,9 @@ importers: '@homarr/notifications': specifier: workspace:^0.1.0 version: link:../notifications + '@homarr/old-import': + specifier: workspace:^0.1.0 + version: link:../old-import '@homarr/old-schema': specifier: workspace:^0.1.0 version: link:../old-schema @@ -1213,18 +1225,54 @@ importers: '@homarr/definitions': specifier: workspace:^0.1.0 version: link:../definitions + '@homarr/form': + specifier: workspace:^0.1.0 + version: link:../form '@homarr/log': specifier: workspace:^0.1.0 version: link:../log + '@homarr/modals': + specifier: workspace:^0.1.0 + version: link:../modals + '@homarr/notifications': + specifier: workspace:^0.1.0 + version: link:../notifications '@homarr/old-schema': specifier: workspace:^0.1.0 version: link:../old-schema + '@homarr/translation': + specifier: workspace:^0.1.0 + version: link:../translation + '@homarr/ui': + specifier: workspace:^0.1.0 + version: link:../ui '@homarr/validation': specifier: workspace:^0.1.0 version: link:../validation + '@mantine/core': + specifier: ^7.15.1 + version: 7.15.1(@mantine/hooks@7.15.1(react@19.0.0))(@types/react@18.3.13)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + '@mantine/hooks': + specifier: ^7.15.1 + version: 7.15.1(react@19.0.0) + adm-zip: + specifier: 0.5.16 + version: 0.5.16 + next: + specifier: ^14.2.20 + version: 14.2.20(@babel/core@7.26.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(sass@1.83.0) + react: + specifier: ^19.0.0 + version: 19.0.0 superjson: specifier: 2.2.2 version: 2.2.2 + zod: + specifier: ^3.24.1 + version: 3.24.1 + zod-form-data: + specifier: ^2.0.2 + version: 2.0.2(zod@3.24.1) devDependencies: '@homarr/eslint-config': specifier: workspace:^0.2.0 @@ -1235,6 +1283,9 @@ importers: '@homarr/tsconfig': specifier: workspace:^0.1.0 version: link:../../tooling/typescript + '@types/adm-zip': + specifier: 0.5.7 + version: 0.5.7 eslint: specifier: ^9.17.0 version: 9.17.0 @@ -1244,6 +1295,9 @@ importers: packages/old-schema: dependencies: + '@homarr/common': + specifier: workspace:^0.1.0 + version: link:../common zod: specifier: ^3.24.1 version: 3.24.1 @@ -1576,9 +1630,6 @@ importers: '@homarr/definitions': specifier: workspace:^0.1.0 version: link:../definitions - '@homarr/old-schema': - specifier: workspace:^0.1.0 - version: link:../old-schema '@homarr/translation': specifier: workspace:^0.1.0 version: link:../translation @@ -2916,6 +2967,14 @@ packages: react: ^18.x || ^19.x react-dom: ^18.x || ^19.x + '@mantine/dropzone@7.15.1': + resolution: {integrity: sha512-1kaIc8ReKI+xxh0Q+pTkPk9vW0hvTC++GttZ9kLQUH3Rd/VEVKC+gTqj2WFnkwaCSqe6rcGR7Xnb0deNvJ1mwA==} + peerDependencies: + '@mantine/core': 7.15.1 + '@mantine/hooks': 7.15.1 + react: ^18.x || ^19.x + react-dom: ^18.x || ^19.x + '@mantine/form@7.15.1': resolution: {integrity: sha512-DA2syNlPsG1f+UtnbEG+pf4Pc/DoZUgcdvZn3LsCRxJZYNuwz8qQ16oL/wQ6UIk3klGlJI/RDzRJ9rvn6E28YQ==} peerDependencies: @@ -3803,6 +3862,9 @@ packages: resolution: {integrity: sha512-PSys7Hy5NuX76HBleOkd8wlRtI4GCzLHS2XUpKeGIj0vpzH4fqE+tpi7fBb5t9U7UiyM6E6pyabSKjoD2zUsoQ==} hasBin: true + '@types/adm-zip@0.5.7': + resolution: {integrity: sha512-DNEs/QvmyRLurdQPChqq0Md4zGvPwHerAJYWk9l2jCbD1VPpnzRJorOdiq4zsw09NFbYnhfsoEhWtxIzXpn2yw==} + '@types/asn1@0.2.4': resolution: {integrity: sha512-V91DSJ2l0h0gRhVP4oBfBzRBN9lAbPUkGDMCnwedqPKX2d84aAMc9CulOvxdw1f7DfEYx99afab+Rsm3e52jhA==} @@ -4205,6 +4267,10 @@ packages: engines: {node: '>=0.4.0'} hasBin: true + adm-zip@0.5.16: + resolution: {integrity: sha512-TGw5yVi4saajsSEgz25grObGHEUaDrniwvA2qwSC060KfqGPdglhvPMA2lPIoxs3PQIItj2iag35fONcQqgUaQ==} + engines: {node: '>=12.0'} + aes-decrypter@4.0.2: resolution: {integrity: sha512-lc+/9s6iJvuaRe5qDlMTpCFjnwpkeOXp8qP3oiZ5jsj1MRg+SBVUmmICrhxHvc8OELSmc+fEyyxAuppY6hrWzw==} @@ -7056,6 +7122,12 @@ packages: peerDependencies: react: ^19.0.0 + react-dropzone-esm@15.2.0: + resolution: {integrity: sha512-pPwR8xWVL+tFLnbAb8KVH5f6Vtl397tck8dINkZ1cPMxHWH+l9dFmIgRWgbh7V7jbjIcuKXCsVrXbhQz68+dVA==} + engines: {node: '>= 10.13'} + peerDependencies: + react: '>= 16.8 || 18.0.0' + react-error-boundary@4.1.2: resolution: {integrity: sha512-GQDxZ5Jd+Aq/qUxbCm1UtzmL/s++V7zKgE8yMktJiCQXCCFZnMZh9ng+6/Ne6PjNSXH0L9CjeOEREfRnq6Duag==} peerDependencies: @@ -9368,6 +9440,14 @@ snapshots: react: 19.0.0 react-dom: 19.0.0(react@19.0.0) + '@mantine/dropzone@7.15.1(@mantine/core@7.15.1(@mantine/hooks@7.15.1(react@19.0.0))(@types/react@19.0.1)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(@mantine/hooks@7.15.1(react@19.0.0))(react-dom@19.0.0(react@19.0.0))(react@19.0.0)': + dependencies: + '@mantine/core': 7.15.1(@mantine/hooks@7.15.1(react@19.0.0))(@types/react@19.0.1)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + '@mantine/hooks': 7.15.1(react@19.0.0) + react: 19.0.0 + react-dom: 19.0.0(react@19.0.0) + react-dropzone-esm: 15.2.0(react@19.0.0) + '@mantine/form@7.15.1(react@19.0.0)': dependencies: fast-deep-equal: 3.1.3 @@ -10520,6 +10600,10 @@ snapshots: semver: 7.6.2 update-check: 1.5.4 + '@types/adm-zip@0.5.7': + dependencies: + '@types/node': 22.10.2 + '@types/asn1@0.2.4': dependencies: '@types/node': 22.10.2 @@ -11044,6 +11128,8 @@ snapshots: acorn@8.14.0: {} + adm-zip@0.5.16: {} + aes-decrypter@4.0.2: dependencies: '@babel/runtime': 7.25.6 @@ -14225,6 +14311,11 @@ snapshots: react: 19.0.0 scheduler: 0.25.0 + react-dropzone-esm@15.2.0(react@19.0.0): + dependencies: + prop-types: 15.8.1 + react: 19.0.0 + react-error-boundary@4.1.2(react@19.0.0): dependencies: '@babel/runtime': 7.25.6