diff --git a/apps/nextjs/src/app/[locale]/auth/invite/[id]/_registration-form.tsx b/apps/nextjs/src/app/[locale]/auth/invite/[id]/_registration-form.tsx index 3f2689234..034001c28 100644 --- a/apps/nextjs/src/app/[locale]/auth/invite/[id]/_registration-form.tsx +++ b/apps/nextjs/src/app/[locale]/auth/invite/[id]/_registration-form.tsx @@ -9,7 +9,7 @@ import { useZodForm } from "@homarr/form"; import { showErrorNotification, showSuccessNotification } from "@homarr/notifications"; import { useScopedI18n } from "@homarr/translation/client"; import { CustomPasswordInput } from "@homarr/ui"; -import { validation } from "@homarr/validation"; +import { userRegistrationSchema } from "@homarr/validation/user"; interface RegistrationFormProps { invite: { @@ -22,7 +22,7 @@ export const RegistrationForm = ({ invite }: RegistrationFormProps) => { const t = useScopedI18n("user"); const router = useRouter(); const { mutate, isPending } = clientApi.user.register.useMutation(); - const form = useZodForm(validation.user.registration, { + const form = useZodForm(userRegistrationSchema, { initialValues: { username: "", password: "", @@ -30,7 +30,7 @@ export const RegistrationForm = ({ invite }: RegistrationFormProps) => { }, }); - const handleSubmit = (values: z.infer) => { + const handleSubmit = (values: z.infer) => { mutate( { ...values, diff --git a/apps/nextjs/src/app/[locale]/auth/login/_login-form.tsx b/apps/nextjs/src/app/[locale]/auth/login/_login-form.tsx index 6a013101f..a494c42cb 100644 --- a/apps/nextjs/src/app/[locale]/auth/login/_login-form.tsx +++ b/apps/nextjs/src/app/[locale]/auth/login/_login-form.tsx @@ -13,7 +13,7 @@ import type { useForm } from "@homarr/form"; import { useZodForm } from "@homarr/form"; import { showErrorNotification, showSuccessNotification } from "@homarr/notifications"; import { useScopedI18n } from "@homarr/translation/client"; -import { validation } from "@homarr/validation"; +import { userSignInSchema } from "@homarr/validation/user"; interface LoginFormProps { providers: string[]; @@ -22,7 +22,7 @@ interface LoginFormProps { callbackUrl: string; } -const extendedValidation = validation.user.signIn.extend({ provider: z.enum(["credentials", "ldap"]) }); +const extendedValidation = userSignInSchema.extend({ provider: z.enum(["credentials", "ldap"]) }); export const LoginForm = ({ providers, oidcClientName, isOidcAutoLoginEnabled, callbackUrl }: LoginFormProps) => { const t = useScopedI18n("user"); diff --git a/apps/nextjs/src/app/[locale]/boards/[name]/settings/_appereance.tsx b/apps/nextjs/src/app/[locale]/boards/[name]/settings/_appereance.tsx index b96d99a88..0da5fa64a 100644 --- a/apps/nextjs/src/app/[locale]/boards/[name]/settings/_appereance.tsx +++ b/apps/nextjs/src/app/[locale]/boards/[name]/settings/_appereance.tsx @@ -20,7 +20,7 @@ import { useDisclosure } from "@mantine/hooks"; import { useZodForm } from "@homarr/form"; import { useI18n } from "@homarr/translation/client"; -import { validation } from "@homarr/validation"; +import { boardSavePartialSettingsSchema } from "@homarr/validation/board"; import type { Board } from "../../_types"; import { generateColors } from "../../(content)/_theme"; @@ -35,7 +35,7 @@ const hexRegex = /^#[0-9a-fA-F]{6}$/; const progressPercentageLabel = (value: number) => `${value}%`; export const ColorSettingsContent = ({ board }: Props) => { - const form = useZodForm(validation.board.savePartialSettings, { + const form = useZodForm(boardSavePartialSettingsSchema, { initialValues: { primaryColor: board.primaryColor, secondaryColor: board.secondaryColor, diff --git a/apps/nextjs/src/app/[locale]/boards/[name]/settings/_background.tsx b/apps/nextjs/src/app/[locale]/boards/[name]/settings/_background.tsx index 0a5786655..1471ff26d 100644 --- a/apps/nextjs/src/app/[locale]/boards/[name]/settings/_background.tsx +++ b/apps/nextjs/src/app/[locale]/boards/[name]/settings/_background.tsx @@ -12,7 +12,7 @@ import type { TranslationObject } from "@homarr/translation"; import { useI18n } from "@homarr/translation/client"; import type { SelectItemWithDescriptionBadge } from "@homarr/ui"; import { SelectWithDescriptionBadge } from "@homarr/ui"; -import { validation } from "@homarr/validation"; +import { boardSavePartialSettingsSchema } from "@homarr/validation/board"; import type { Board } from "../../_types"; import { useSavePartialSettingsMutation } from "./_shared"; @@ -24,7 +24,7 @@ export const BackgroundSettingsContent = ({ board }: Props) => { const t = useI18n(); const { data: session } = useSession(); const { mutate: savePartialSettings, isPending } = useSavePartialSettingsMutation(board); - const form = useZodForm(validation.board.savePartialSettings, { + const form = useZodForm(boardSavePartialSettingsSchema, { initialValues: { backgroundImageUrl: board.backgroundImageUrl ?? "", backgroundImageAttachment: board.backgroundImageAttachment, diff --git a/apps/nextjs/src/app/[locale]/boards/[name]/settings/_general.tsx b/apps/nextjs/src/app/[locale]/boards/[name]/settings/_general.tsx index 389bff5ed..10d27037f 100644 --- a/apps/nextjs/src/app/[locale]/boards/[name]/settings/_general.tsx +++ b/apps/nextjs/src/app/[locale]/boards/[name]/settings/_general.tsx @@ -8,7 +8,7 @@ import { useUpdateBoard } from "@homarr/boards/updater"; import { useZodForm } from "@homarr/form"; import { IconPicker } from "@homarr/forms-collection"; import { useI18n } from "@homarr/translation/client"; -import { validation } from "@homarr/validation"; +import { boardSavePartialSettingsSchema } from "@homarr/validation/board"; import { createMetaTitle } from "~/metadata"; import type { Board } from "../../_types"; @@ -28,7 +28,7 @@ export const GeneralSettingsContent = ({ board }: Props) => { const { mutate: savePartialSettings, isPending } = useSavePartialSettingsMutation(board); const form = useZodForm( - validation.board.savePartialSettings + boardSavePartialSettingsSchema .pick({ pageTitle: true, logoImageUrl: true, diff --git a/apps/nextjs/src/app/[locale]/boards/[name]/settings/_layout.tsx b/apps/nextjs/src/app/[locale]/boards/[name]/settings/_layout.tsx index 99c0a7f41..2c6661cde 100644 --- a/apps/nextjs/src/app/[locale]/boards/[name]/settings/_layout.tsx +++ b/apps/nextjs/src/app/[locale]/boards/[name]/settings/_layout.tsx @@ -6,7 +6,7 @@ import { clientApi } from "@homarr/api/client"; import { createId } from "@homarr/db/client"; import { useZodForm } from "@homarr/form"; import { useI18n } from "@homarr/translation/client"; -import { validation } from "@homarr/validation"; +import { boardSaveLayoutsSchema } from "@homarr/validation/board"; import type { Board } from "../../_types"; @@ -22,7 +22,7 @@ export const LayoutSettingsContent = ({ board }: Props) => { void utils.board.getHomeBoard.invalidate(); }, }); - const form = useZodForm(validation.board.saveLayouts.omit({ id: true }).required(), { + const form = useZodForm(boardSaveLayoutsSchema.omit({ id: true }).required(), { initialValues: { layouts: board.layouts, }, 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 index 849891220..976a8848a 100644 --- a/apps/nextjs/src/app/[locale]/init/_steps/group/init-group.tsx +++ b/apps/nextjs/src/app/[locale]/init/_steps/group/init-group.tsx @@ -8,18 +8,18 @@ import { clientApi } from "@homarr/api/client"; import { revalidatePathActionAsync } from "@homarr/common/client"; import { useZodForm } from "@homarr/form"; import { useI18n } from "@homarr/translation/client"; -import { validation } from "@homarr/validation"; +import { groupCreateSchema } from "@homarr/validation/group"; export const InitGroup = () => { const t = useI18n(); const { mutateAsync } = clientApi.group.createInitialExternalGroup.useMutation(); - const form = useZodForm(validation.group.create, { + const form = useZodForm(groupCreateSchema, { initialValues: { name: "", }, }); - const handleSubmitAsync = async (values: z.infer) => { + const handleSubmitAsync = async (values: z.infer) => { await mutateAsync(values, { async onSuccess() { await revalidatePathActionAsync("/init"); 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 index 3d40a4445..f83612f1a 100644 --- a/apps/nextjs/src/app/[locale]/init/_steps/settings/init-settings.tsx +++ b/apps/nextjs/src/app/[locale]/init/_steps/settings/init-settings.tsx @@ -12,13 +12,13 @@ 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 { validation } from "@homarr/validation"; +import { settingsInitSchema } from "@homarr/validation/settings"; 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 }); + const form = useZodForm(settingsInitSchema, { initialValues: defaultServerSettings }); form.watch("analytics.enableGeneral", ({ value }) => { if (!value) { @@ -30,7 +30,7 @@ export const InitSettings = () => { } }); - const handleSubmitAsync = async (values: z.infer) => { + const handleSubmitAsync = async (values: z.infer) => { await mutateAsync(values, { async onSuccess() { await revalidatePathActionAsync("/init"); diff --git a/apps/nextjs/src/app/[locale]/init/_steps/user/init-user-form.tsx b/apps/nextjs/src/app/[locale]/init/_steps/user/init-user-form.tsx index 2dd2df722..5e43cbc32 100644 --- a/apps/nextjs/src/app/[locale]/init/_steps/user/init-user-form.tsx +++ b/apps/nextjs/src/app/[locale]/init/_steps/user/init-user-form.tsx @@ -10,13 +10,13 @@ import { useZodForm } from "@homarr/form"; import { showErrorNotification, showSuccessNotification } from "@homarr/notifications"; import { useScopedI18n } from "@homarr/translation/client"; import { CustomPasswordInput } from "@homarr/ui"; -import { validation } from "@homarr/validation"; +import { userInitSchema } from "@homarr/validation/user"; export const InitUserForm = () => { const t = useScopedI18n("user"); const tUser = useScopedI18n("init.step.user"); const { mutateAsync, isPending } = clientApi.user.initUser.useMutation(); - const form = useZodForm(validation.user.init, { + const form = useZodForm(userInitSchema, { initialValues: { username: "", password: "", @@ -74,4 +74,4 @@ export const InitUserForm = () => { ); }; -type FormType = z.infer; +type FormType = z.infer; diff --git a/apps/nextjs/src/app/[locale]/manage/apps/edit/[id]/_app-edit-form.tsx b/apps/nextjs/src/app/[locale]/manage/apps/edit/[id]/_app-edit-form.tsx index 61e3693d3..2dc13dc27 100644 --- a/apps/nextjs/src/app/[locale]/manage/apps/edit/[id]/_app-edit-form.tsx +++ b/apps/nextjs/src/app/[locale]/manage/apps/edit/[id]/_app-edit-form.tsx @@ -10,7 +10,7 @@ import { revalidatePathActionAsync } from "@homarr/common/client"; import { AppForm } from "@homarr/forms-collection"; import { showErrorNotification, showSuccessNotification } from "@homarr/notifications"; import { useI18n, useScopedI18n } from "@homarr/translation/client"; -import type { validation } from "@homarr/validation"; +import type { appManageSchema } from "@homarr/validation/app"; interface AppEditFormProps { app: RouterOutputs["app"]["byId"]; @@ -40,7 +40,7 @@ export const AppEditForm = ({ app }: AppEditFormProps) => { }); const handleSubmit = useCallback( - (values: z.infer) => { + (values: z.infer) => { mutate({ id: app.id, ...values, 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 dd15d0005..06fb662ec 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 @@ -14,7 +14,7 @@ import { convertIntegrationTestConnectionError } from "@homarr/integrations/clie import { useConfirmModal } from "@homarr/modals"; import { showErrorNotification, showSuccessNotification } from "@homarr/notifications"; import { useI18n } from "@homarr/translation/client"; -import { validation } from "@homarr/validation"; +import { integrationUpdateSchema } from "@homarr/validation/integration"; import { SecretCard } from "../../_components/secrets/integration-secret-card"; import { IntegrationSecretInput } from "../../_components/secrets/integration-secret-inputs"; @@ -32,7 +32,7 @@ export const EditIntegrationForm = ({ integration }: EditIntegrationForm) => { ) ?? getDefaultSecretKinds(integration.kind); const router = useRouter(); - const form = useZodForm(validation.integration.update.omit({ id: true }), { + const form = useZodForm(integrationUpdateSchema.omit({ id: true }), { initialValues: { name: integration.name, url: integration.url, @@ -141,4 +141,4 @@ export const EditIntegrationForm = ({ integration }: EditIntegrationForm) => { ); }; -type FormType = Omit, "id">; +type FormType = Omit, "id">; diff --git a/apps/nextjs/src/app/[locale]/manage/integrations/new/_integration-new-form.tsx b/apps/nextjs/src/app/[locale]/manage/integrations/new/_integration-new-form.tsx index ed93ba822..c795aed54 100644 --- a/apps/nextjs/src/app/[locale]/manage/integrations/new/_integration-new-form.tsx +++ b/apps/nextjs/src/app/[locale]/manage/integrations/new/_integration-new-form.tsx @@ -27,20 +27,21 @@ import { useZodForm } from "@homarr/form"; import { convertIntegrationTestConnectionError } from "@homarr/integrations/client"; import { showErrorNotification, showSuccessNotification } from "@homarr/notifications"; import { useI18n, useScopedI18n } from "@homarr/translation/client"; -import { validation } from "@homarr/validation"; +import { appHrefSchema } from "@homarr/validation/app"; +import { integrationCreateSchema } from "@homarr/validation/integration"; import { IntegrationSecretInput } from "../_components/secrets/integration-secret-inputs"; interface NewIntegrationFormProps { - searchParams: Partial> & { + searchParams: Partial> & { kind: IntegrationKind; }; } -const formSchema = validation.integration.create.omit({ kind: true }).and( +const formSchema = integrationCreateSchema.omit({ kind: true }).and( z.object({ createApp: z.boolean(), - appHref: validation.app.manage.shape.href, + appHref: appHrefSchema, }), ); diff --git a/apps/nextjs/src/app/[locale]/manage/integrations/new/page.tsx b/apps/nextjs/src/app/[locale]/manage/integrations/new/page.tsx index dba8e5c1f..deb544491 100644 --- a/apps/nextjs/src/app/[locale]/manage/integrations/new/page.tsx +++ b/apps/nextjs/src/app/[locale]/manage/integrations/new/page.tsx @@ -7,14 +7,14 @@ import type { IntegrationKind } from "@homarr/definitions"; import { getIntegrationName, integrationKinds } from "@homarr/definitions"; import { getScopedI18n } from "@homarr/translation/server"; import { IntegrationAvatar } from "@homarr/ui"; -import type { validation } from "@homarr/validation"; +import type { integrationCreateSchema } from "@homarr/validation/integration"; import { DynamicBreadcrumb } from "~/components/navigation/dynamic-breadcrumb"; import { NewIntegrationForm } from "./_integration-new-form"; interface NewIntegrationPageProps { searchParams: Promise< - Partial> & { + Partial> & { kind: IntegrationKind; } >; diff --git a/apps/nextjs/src/app/[locale]/manage/search-engines/_form.tsx b/apps/nextjs/src/app/[locale]/manage/search-engines/_form.tsx index c1bcaa225..e34f78dc5 100644 --- a/apps/nextjs/src/app/[locale]/manage/search-engines/_form.tsx +++ b/apps/nextjs/src/app/[locale]/manage/search-engines/_form.tsx @@ -12,9 +12,9 @@ import { useZodForm } from "@homarr/form"; import { IconPicker } from "@homarr/forms-collection"; import type { TranslationFunction } from "@homarr/translation"; import { useI18n } from "@homarr/translation/client"; -import { validation } from "@homarr/validation"; +import { searchEngineManageSchema } from "@homarr/validation/search-engine"; -type FormType = z.infer; +type FormType = z.infer; interface SearchEngineFormProps { submitButtonTranslation: (t: TranslationFunction) => string; @@ -30,7 +30,7 @@ export const SearchEngineForm = (props: SearchEngineFormProps) => { const [integrationData] = clientApi.integration.allThatSupportSearch.useSuspenseQuery(); - const form = useZodForm(validation.searchEngine.manage, { + const form = useZodForm(searchEngineManageSchema, { initialValues: initialValues ?? { name: "", short: "", diff --git a/apps/nextjs/src/app/[locale]/manage/search-engines/edit/[id]/_search-engine-edit-form.tsx b/apps/nextjs/src/app/[locale]/manage/search-engines/edit/[id]/_search-engine-edit-form.tsx index cc4c1eb79..dbd2b949d 100644 --- a/apps/nextjs/src/app/[locale]/manage/search-engines/edit/[id]/_search-engine-edit-form.tsx +++ b/apps/nextjs/src/app/[locale]/manage/search-engines/edit/[id]/_search-engine-edit-form.tsx @@ -10,7 +10,7 @@ import { revalidatePathActionAsync } from "@homarr/common/client"; import { showErrorNotification, showSuccessNotification } from "@homarr/notifications"; import type { TranslationFunction } from "@homarr/translation"; import { useScopedI18n } from "@homarr/translation/client"; -import type { validation } from "@homarr/validation"; +import type { searchEngineManageSchema } from "@homarr/validation/search-engine"; import { SearchEngineForm } from "../../_form"; @@ -41,7 +41,7 @@ export const SearchEngineEditForm = ({ searchEngine }: SearchEngineEditFormProps }); const handleSubmit = useCallback( - (values: z.infer) => { + (values: z.infer) => { mutate({ id: searchEngine.id, ...values, diff --git a/apps/nextjs/src/app/[locale]/manage/search-engines/new/_search-engine-new-form.tsx b/apps/nextjs/src/app/[locale]/manage/search-engines/new/_search-engine-new-form.tsx index b5824d3c1..5bd5a6ae1 100644 --- a/apps/nextjs/src/app/[locale]/manage/search-engines/new/_search-engine-new-form.tsx +++ b/apps/nextjs/src/app/[locale]/manage/search-engines/new/_search-engine-new-form.tsx @@ -9,7 +9,7 @@ import { revalidatePathActionAsync } from "@homarr/common/client"; import { showErrorNotification, showSuccessNotification } from "@homarr/notifications"; import type { TranslationFunction } from "@homarr/translation"; import { useScopedI18n } from "@homarr/translation/client"; -import type { validation } from "@homarr/validation"; +import type { searchEngineManageSchema } from "@homarr/validation/search-engine"; import { SearchEngineForm } from "../_form"; @@ -35,7 +35,7 @@ export const SearchEngineNewForm = () => { }); const handleSubmit = useCallback( - (values: z.infer) => { + (values: z.infer) => { mutate(values); }, [mutate], diff --git a/apps/nextjs/src/app/[locale]/manage/tools/certificates/_components/add-certificate.tsx b/apps/nextjs/src/app/[locale]/manage/tools/certificates/_components/add-certificate.tsx index 87aaa058f..d16c60248 100644 --- a/apps/nextjs/src/app/[locale]/manage/tools/certificates/_components/add-certificate.tsx +++ b/apps/nextjs/src/app/[locale]/manage/tools/certificates/_components/add-certificate.tsx @@ -10,7 +10,7 @@ import { useZodForm } from "@homarr/form"; import { createModal, useModalAction } from "@homarr/modals"; import { showErrorNotification, showSuccessNotification } from "@homarr/notifications"; import { useI18n } from "@homarr/translation/client"; -import { superRefineCertificateFile } from "@homarr/validation"; +import { superRefineCertificateFile } from "@homarr/validation/certificates"; export const AddCertificateButton = () => { const { openModal } = useModalAction(AddCertificateModal); diff --git a/apps/nextjs/src/app/[locale]/manage/users/[userId]/general/_components/_change-home-board.tsx b/apps/nextjs/src/app/[locale]/manage/users/[userId]/general/_components/_change-home-board.tsx index d544a3c8d..1ad8c30b2 100644 --- a/apps/nextjs/src/app/[locale]/manage/users/[userId]/general/_components/_change-home-board.tsx +++ b/apps/nextjs/src/app/[locale]/manage/users/[userId]/general/_components/_change-home-board.tsx @@ -9,7 +9,7 @@ import { revalidatePathActionAsync } from "@homarr/common/client"; import { useZodForm } from "@homarr/form"; import { showErrorNotification, showSuccessNotification } from "@homarr/notifications"; import { useI18n } from "@homarr/translation/client"; -import { validation } from "@homarr/validation"; +import { userChangeHomeBoardsSchema } from "@homarr/validation/user"; import type { Board } from "~/app/[locale]/boards/_types"; import { BoardSelect } from "~/components/board/board-select"; @@ -40,7 +40,7 @@ export const ChangeHomeBoardForm = ({ user, boardsData }: ChangeHomeBoardFormPro }); }, }); - const form = useZodForm(validation.user.changeHomeBoards, { + const form = useZodForm(userChangeHomeBoardsSchema, { initialValues: { homeBoardId: user.homeBoardId, mobileHomeBoardId: user.mobileHomeBoardId, @@ -82,4 +82,4 @@ export const ChangeHomeBoardForm = ({ user, boardsData }: ChangeHomeBoardFormPro ); }; -type FormType = z.infer; +type FormType = z.infer; diff --git a/apps/nextjs/src/app/[locale]/manage/users/[userId]/general/_components/_change-search-preferences.tsx b/apps/nextjs/src/app/[locale]/manage/users/[userId]/general/_components/_change-search-preferences.tsx index 5fabe8d4f..5a8a051bd 100644 --- a/apps/nextjs/src/app/[locale]/manage/users/[userId]/general/_components/_change-search-preferences.tsx +++ b/apps/nextjs/src/app/[locale]/manage/users/[userId]/general/_components/_change-search-preferences.tsx @@ -9,7 +9,7 @@ import { revalidatePathActionAsync } from "@homarr/common/client"; import { useZodForm } from "@homarr/form"; import { showErrorNotification, showSuccessNotification } from "@homarr/notifications"; import { useI18n } from "@homarr/translation/client"; -import { validation } from "@homarr/validation"; +import { userChangeSearchPreferencesSchema } from "@homarr/validation/user"; interface ChangeSearchPreferencesFormProps { user: RouterOutputs["user"]["getById"]; @@ -37,7 +37,7 @@ export const ChangeSearchPreferencesForm = ({ user, searchEnginesData }: ChangeS }); }, }); - const form = useZodForm(validation.user.changeSearchPreferences, { + const form = useZodForm(userChangeSearchPreferencesSchema, { initialValues: { defaultSearchEngineId: user.defaultSearchEngineId, openInNewTab: user.openSearchInNewTab, @@ -75,4 +75,4 @@ export const ChangeSearchPreferencesForm = ({ user, searchEnginesData }: ChangeS ); }; -type FormType = z.infer; +type FormType = z.infer; diff --git a/apps/nextjs/src/app/[locale]/manage/users/[userId]/general/_components/_first-day-of-week.tsx b/apps/nextjs/src/app/[locale]/manage/users/[userId]/general/_components/_first-day-of-week.tsx index 1b5c27dc2..d15379b76 100644 --- a/apps/nextjs/src/app/[locale]/manage/users/[userId]/general/_components/_first-day-of-week.tsx +++ b/apps/nextjs/src/app/[locale]/manage/users/[userId]/general/_components/_first-day-of-week.tsx @@ -12,7 +12,7 @@ import { revalidatePathActionAsync } from "@homarr/common/client"; import { useZodForm } from "@homarr/form"; import { showErrorNotification, showSuccessNotification } from "@homarr/notifications"; import { useI18n } from "@homarr/translation/client"; -import { validation } from "@homarr/validation"; +import { userFirstDayOfWeekSchema } from "@homarr/validation/user"; dayjs.extend(localeData); @@ -42,7 +42,7 @@ export const FirstDayOfWeek = ({ user }: FirstDayOfWeekProps) => { }); }, }); - const form = useZodForm(validation.user.firstDayOfWeek, { + const form = useZodForm(userFirstDayOfWeekSchema, { initialValues: { firstDayOfWeek: user.firstDayOfWeek as DayOfWeek, }, @@ -80,4 +80,4 @@ export const FirstDayOfWeek = ({ user }: FirstDayOfWeekProps) => { ); }; -type FormType = z.infer; +type FormType = z.infer; diff --git a/apps/nextjs/src/app/[locale]/manage/users/[userId]/general/_components/_ping-icons-enabled.tsx b/apps/nextjs/src/app/[locale]/manage/users/[userId]/general/_components/_ping-icons-enabled.tsx index ce34378e6..6c5010d52 100644 --- a/apps/nextjs/src/app/[locale]/manage/users/[userId]/general/_components/_ping-icons-enabled.tsx +++ b/apps/nextjs/src/app/[locale]/manage/users/[userId]/general/_components/_ping-icons-enabled.tsx @@ -9,7 +9,7 @@ import { revalidatePathActionAsync } from "@homarr/common/client"; import { useZodForm } from "@homarr/form"; import { showErrorNotification, showSuccessNotification } from "@homarr/notifications"; import { useI18n } from "@homarr/translation/client"; -import { validation } from "@homarr/validation"; +import { userPingIconsEnabledSchema } from "@homarr/validation/user"; interface PingIconsEnabledProps { user: RouterOutputs["user"]["getById"]; @@ -35,7 +35,7 @@ export const PingIconsEnabled = ({ user }: PingIconsEnabledProps) => { }); }, }); - const form = useZodForm(validation.user.pingIconsEnabled, { + const form = useZodForm(userPingIconsEnabledSchema, { initialValues: { pingIconsEnabled: user.pingIconsEnabled, }, @@ -66,4 +66,4 @@ export const PingIconsEnabled = ({ user }: PingIconsEnabledProps) => { ); }; -type FormType = z.infer; +type FormType = z.infer; diff --git a/apps/nextjs/src/app/[locale]/manage/users/[userId]/general/_components/_profile-form.tsx b/apps/nextjs/src/app/[locale]/manage/users/[userId]/general/_components/_profile-form.tsx index b2d5ab87e..f94458108 100644 --- a/apps/nextjs/src/app/[locale]/manage/users/[userId]/general/_components/_profile-form.tsx +++ b/apps/nextjs/src/app/[locale]/manage/users/[userId]/general/_components/_profile-form.tsx @@ -9,7 +9,7 @@ import { revalidatePathActionAsync } from "@homarr/common/client"; import { useZodForm } from "@homarr/form"; import { showErrorNotification, showSuccessNotification } from "@homarr/notifications"; import { useI18n } from "@homarr/translation/client"; -import { validation } from "@homarr/validation"; +import { userEditProfileSchema } from "@homarr/validation/user"; interface UserProfileFormProps { user: RouterOutputs["user"]["getById"]; @@ -43,7 +43,7 @@ export const UserProfileForm = ({ user }: UserProfileFormProps) => { }); }, }); - const form = useZodForm(validation.user.editProfile.omit({ id: true }), { + const form = useZodForm(userEditProfileSchema.omit({ id: true }), { initialValues: { name: user.name ?? "", email: user.email ?? "", diff --git a/apps/nextjs/src/app/[locale]/manage/users/[userId]/security/_components/_change-password-form.tsx b/apps/nextjs/src/app/[locale]/manage/users/[userId]/security/_components/_change-password-form.tsx index e8ea4e96c..9fac22f51 100644 --- a/apps/nextjs/src/app/[locale]/manage/users/[userId]/security/_components/_change-password-form.tsx +++ b/apps/nextjs/src/app/[locale]/manage/users/[userId]/security/_components/_change-password-form.tsx @@ -10,7 +10,7 @@ import { useZodForm } from "@homarr/form"; import { showErrorNotification, showSuccessNotification } from "@homarr/notifications"; import { useI18n } from "@homarr/translation/client"; import { CustomPasswordInput } from "@homarr/ui"; -import { validation } from "@homarr/validation"; +import { userChangePasswordSchema } from "@homarr/validation/user"; interface ChangePasswordFormProps { user: RouterOutputs["user"]["getById"]; @@ -34,7 +34,7 @@ export const ChangePasswordForm = ({ user }: ChangePasswordFormProps) => { }); }, }); - const form = useZodForm(validation.user.changePassword, { + const form = useZodForm(userChangePasswordSchema, { initialValues: { /* Require previous password if the current user want's to change his password */ previousPassword: session?.user.id === user.id ? "" : "_", diff --git a/apps/nextjs/src/app/[locale]/manage/users/create/_components/create-user-stepper.tsx b/apps/nextjs/src/app/[locale]/manage/users/create/_components/create-user-stepper.tsx index e848a8182..97eeb9ff0 100644 --- a/apps/nextjs/src/app/[locale]/manage/users/create/_components/create-user-stepper.tsx +++ b/apps/nextjs/src/app/[locale]/manage/users/create/_components/create-user-stepper.tsx @@ -27,8 +27,8 @@ import { useModalAction } from "@homarr/modals"; import { showErrorNotification } from "@homarr/notifications"; import { useI18n, useScopedI18n } from "@homarr/translation/client"; import { CustomPasswordInput, UserAvatar } from "@homarr/ui"; -import { validation } from "@homarr/validation"; -import { createCustomErrorParams } from "@homarr/validation/form"; +import { createCustomErrorParams } from "@homarr/validation/form/i18n"; +import { userPasswordSchema } from "@homarr/validation/user"; import { GroupSelectModal } from "~/components/access/group-select-modal"; import { StepperNavigationComponent } from "./stepper-navigation"; @@ -84,7 +84,7 @@ export const UserCreateStepperComponent = ({ initialGroups }: UserCreateStepperC const securityForm = useZodForm( z .object({ - password: validation.user.password, + password: userPasswordSchema, confirmPassword: z.string(), }) .refine((data) => data.password === data.confirmPassword, { diff --git a/apps/nextjs/src/app/[locale]/manage/users/groups/[id]/_rename-group-form.tsx b/apps/nextjs/src/app/[locale]/manage/users/groups/[id]/_rename-group-form.tsx index 407ee2429..23585056d 100644 --- a/apps/nextjs/src/app/[locale]/manage/users/groups/[id]/_rename-group-form.tsx +++ b/apps/nextjs/src/app/[locale]/manage/users/groups/[id]/_rename-group-form.tsx @@ -8,7 +8,7 @@ import { revalidatePathActionAsync } from "@homarr/common/client"; import { useZodForm } from "@homarr/form"; import { showErrorNotification, showSuccessNotification } from "@homarr/notifications"; import { useI18n } from "@homarr/translation/client"; -import { validation } from "@homarr/validation"; +import { groupUpdateSchema } from "@homarr/validation/group"; interface RenameGroupFormProps { group: { @@ -21,7 +21,7 @@ interface RenameGroupFormProps { export const RenameGroupForm = ({ group, disabled }: RenameGroupFormProps) => { const t = useI18n(); const { mutate, isPending } = clientApi.group.updateGroup.useMutation(); - const form = useZodForm(validation.group.update.pick({ name: true }), { + const form = useZodForm(groupUpdateSchema.pick({ name: true }), { initialValues: { name: group.name, }, diff --git a/apps/nextjs/src/app/[locale]/manage/users/groups/[id]/settings/_group-home-boards.tsx b/apps/nextjs/src/app/[locale]/manage/users/groups/[id]/settings/_group-home-boards.tsx index b64ad7a61..62c7e1989 100644 --- a/apps/nextjs/src/app/[locale]/manage/users/groups/[id]/settings/_group-home-boards.tsx +++ b/apps/nextjs/src/app/[locale]/manage/users/groups/[id]/settings/_group-home-boards.tsx @@ -6,7 +6,7 @@ import { clientApi } from "@homarr/api/client"; import { useZodForm } from "@homarr/form"; import { showErrorNotification, showSuccessNotification } from "@homarr/notifications"; import { useI18n } from "@homarr/translation/client"; -import { validation } from "@homarr/validation"; +import { groupSettingsSchema } from "@homarr/validation/group"; import { BoardSelect } from "~/components/board/board-select"; @@ -19,7 +19,7 @@ interface GroupHomeBoardsProps { export const GroupHomeBoards = ({ homeBoardId, mobileHomeBoardId, groupId }: GroupHomeBoardsProps) => { const t = useI18n(); const [availableBoards] = clientApi.board.getBoardsForGroup.useSuspenseQuery({ groupId }); - const form = useZodForm(validation.group.settings.pick({ homeBoardId: true, mobileHomeBoardId: true }), { + const form = useZodForm(groupSettingsSchema.pick({ homeBoardId: true, mobileHomeBoardId: true }), { initialValues: { homeBoardId, mobileHomeBoardId, diff --git a/apps/nextjs/src/app/[locale]/widgets/[kind]/_content.tsx b/apps/nextjs/src/app/[locale]/widgets/[kind]/_content.tsx index 362236300..37ba09d52 100644 --- a/apps/nextjs/src/app/[locale]/widgets/[kind]/_content.tsx +++ b/apps/nextjs/src/app/[locale]/widgets/[kind]/_content.tsx @@ -11,7 +11,7 @@ import { useModalAction } from "@homarr/modals"; import { showSuccessNotification } from "@homarr/notifications"; import { useSettings } from "@homarr/settings"; import { useScopedI18n } from "@homarr/translation/client"; -import type { BoardItemAdvancedOptions } from "@homarr/validation"; +import type { BoardItemAdvancedOptions } from "@homarr/validation/shared"; import { loadWidgetDynamic, reduceWidgetOptionsWithDefaultValues, widgetImports } from "@homarr/widgets"; import { WidgetError } from "@homarr/widgets/errors"; import { WidgetEditModal } from "@homarr/widgets/modals"; diff --git a/apps/nextjs/src/components/board/items/item-actions.tsx b/apps/nextjs/src/components/board/items/item-actions.tsx index 609cfa0aa..29b5615e2 100644 --- a/apps/nextjs/src/components/board/items/item-actions.tsx +++ b/apps/nextjs/src/components/board/items/item-actions.tsx @@ -1,7 +1,7 @@ import { useCallback } from "react"; import { useUpdateBoard } from "@homarr/boards/updater"; -import type { BoardItemAdvancedOptions } from "@homarr/validation"; +import type { BoardItemAdvancedOptions } from "@homarr/validation/shared"; import type { CreateItemInput } from "./actions/create-item"; import { createItemCallback } from "./actions/create-item"; diff --git a/apps/nextjs/src/components/board/modals/board-rename-modal.tsx b/apps/nextjs/src/components/board/modals/board-rename-modal.tsx index fafa334b0..c2f2e08e0 100644 --- a/apps/nextjs/src/components/board/modals/board-rename-modal.tsx +++ b/apps/nextjs/src/components/board/modals/board-rename-modal.tsx @@ -7,7 +7,7 @@ import { clientApi } from "@homarr/api/client"; import { useZodForm } from "@homarr/form"; import { createModal } from "@homarr/modals"; import { useI18n } from "@homarr/translation/client"; -import { validation } from "@homarr/validation"; +import { boardRenameSchema } from "@homarr/validation/board"; interface InnerProps { id: string; @@ -26,7 +26,7 @@ export const BoardRenameModal = createModal(({ actions, innerProps } void utils.board.getHomeBoard.invalidate(); }, }); - const form = useZodForm(validation.board.rename.omit({ id: true }), { + const form = useZodForm(boardRenameSchema.omit({ id: true }), { initialValues: { name: innerProps.previousName, }, @@ -66,4 +66,4 @@ export const BoardRenameModal = createModal(({ actions, innerProps } defaultTitle: (t) => t("board.setting.section.dangerZone.action.rename.modal.title"), }); -type FormType = Omit, "id">; +type FormType = Omit, "id">; diff --git a/apps/nextjs/src/components/board/sections/dynamic/dynamic-actions.ts b/apps/nextjs/src/components/board/sections/dynamic/dynamic-actions.ts index 324f4df1b..553172905 100644 --- a/apps/nextjs/src/components/board/sections/dynamic/dynamic-actions.ts +++ b/apps/nextjs/src/components/board/sections/dynamic/dynamic-actions.ts @@ -2,7 +2,7 @@ import { useCallback } from "react"; import type { z } from "zod"; import { useUpdateBoard } from "@homarr/boards/updater"; -import type { dynamicSectionOptionsSchema } from "@homarr/validation"; +import type { dynamicSectionOptionsSchema } from "@homarr/validation/shared"; import { addDynamicSectionCallback } from "./actions/add-dynamic-section"; import type { RemoveDynamicSectionInput } from "./actions/remove-dynamic-section"; diff --git a/apps/nextjs/src/components/board/sections/dynamic/dynamic-edit-modal.tsx b/apps/nextjs/src/components/board/sections/dynamic/dynamic-edit-modal.tsx index 85ffc7e79..3ce026800 100644 --- a/apps/nextjs/src/components/board/sections/dynamic/dynamic-edit-modal.tsx +++ b/apps/nextjs/src/components/board/sections/dynamic/dynamic-edit-modal.tsx @@ -6,7 +6,7 @@ import type { z } from "zod"; import { useZodForm } from "@homarr/form"; import { createModal } from "@homarr/modals"; import { useI18n } from "@homarr/translation/client"; -import { dynamicSectionOptionsSchema } from "@homarr/validation"; +import { dynamicSectionOptionsSchema } from "@homarr/validation/shared"; interface ModalProps { value: z.infer; diff --git a/packages/api/src/router/app.ts b/packages/api/src/router/app.ts index 6bd53c948..77b18b6b8 100644 --- a/packages/api/src/router/app.ts +++ b/packages/api/src/router/app.ts @@ -5,7 +5,8 @@ import { asc, createId, eq, inArray, like } from "@homarr/db"; import { apps } from "@homarr/db/schema"; import { selectAppSchema } from "@homarr/db/validationSchemas"; import { getIconForName } from "@homarr/icons"; -import { validation } from "@homarr/validation"; +import { appCreateManySchema, appEditSchema, appManageSchema } from "@homarr/validation/app"; +import { byIdSchema, paginatedSchema } from "@homarr/validation/common"; import { convertIntersectionToZodObject } from "../schema-merger"; import { createTRPCRouter, permissionRequiredProcedure, protectedProcedure, publicProcedure } from "../trpc"; @@ -15,7 +16,7 @@ const defaultIcon = "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons@mas export const appRouter = createTRPCRouter({ getPaginated: protectedProcedure - .input(validation.common.paginated) + .input(paginatedSchema) .output(z.object({ items: z.array(selectAppSchema), totalCount: z.number() })) .meta({ openapi: { method: "GET", path: "/api/apps/paginated", tags: ["apps"], protect: true } }) .query(async ({ input, ctx }) => { @@ -83,7 +84,7 @@ export const appRouter = createTRPCRouter({ }); }), byId: publicProcedure - .input(validation.common.byId) + .input(byIdSchema) .output(selectAppSchema) .meta({ openapi: { method: "GET", path: "/api/apps/{id}", tags: ["apps"], protect: true } }) .query(async ({ ctx, input }) => { @@ -115,7 +116,7 @@ export const appRouter = createTRPCRouter({ }), create: permissionRequiredProcedure .requiresPermission("app-create") - .input(validation.app.manage) + .input(appManageSchema) .output(z.object({ appId: z.string() })) .meta({ openapi: { method: "POST", path: "/api/apps", tags: ["apps"], protect: true } }) .mutation(async ({ ctx, input }) => { @@ -133,7 +134,7 @@ export const appRouter = createTRPCRouter({ }), createMany: permissionRequiredProcedure .requiresPermission("app-create") - .input(validation.app.createMany) + .input(appCreateManySchema) .output(z.void()) .mutation(async ({ ctx, input }) => { await ctx.db.insert(apps).values( @@ -148,7 +149,7 @@ export const appRouter = createTRPCRouter({ }), update: permissionRequiredProcedure .requiresPermission("app-modify-all") - .input(convertIntersectionToZodObject(validation.app.edit)) + .input(convertIntersectionToZodObject(appEditSchema)) .output(z.void()) .meta({ openapi: { method: "PATCH", path: "/api/apps/{id}", tags: ["apps"], protect: true } }) .mutation(async ({ ctx, input }) => { @@ -178,7 +179,7 @@ export const appRouter = createTRPCRouter({ .requiresPermission("app-full-all") .output(z.void()) .meta({ openapi: { method: "DELETE", path: "/api/apps/{id}", tags: ["apps"], protect: true } }) - .input(validation.common.byId) + .input(byIdSchema) .mutation(async ({ ctx, input }) => { await ctx.db.delete(apps).where(eq(apps.id, input.id)); }), diff --git a/packages/api/src/router/board.ts b/packages/api/src/router/board.ts index 80e58393d..e8b096c09 100644 --- a/packages/api/src/router/board.ts +++ b/packages/api/src/router/board.ts @@ -37,8 +37,21 @@ 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 { sectionSchema, sharedItemSchema, validation, zodUnionFromArray } from "@homarr/validation"; +import { + boardByNameSchema, + boardChangeVisibilitySchema, + boardCreateSchema, + boardDuplicateSchema, + boardRenameSchema, + boardSaveLayoutsSchema, + boardSavePartialSettingsSchema, + boardSavePermissionsSchema, + boardSaveSchema, +} from "@homarr/validation/board"; +import { byIdSchema } from "@homarr/validation/common"; +import { zodUnionFromArray } from "@homarr/validation/enums"; +import type { BoardItemAdvancedOptions } from "@homarr/validation/shared"; +import { sectionSchema, sharedItemSchema } from "@homarr/validation/shared"; import { createTRPCRouter, permissionRequiredProcedure, protectedProcedure, publicProcedure } from "../trpc"; import { throwIfActionForbiddenAsync } from "./board/board-access"; @@ -247,7 +260,7 @@ export const boardRouter = createTRPCRouter({ }), createBoard: permissionRequiredProcedure .requiresPermission("board-create") - .input(validation.board.create) + .input(boardCreateSchema) .mutation(async ({ ctx, input }) => { const boardId = createId(); @@ -291,7 +304,7 @@ export const boardRouter = createTRPCRouter({ }), duplicateBoard: permissionRequiredProcedure .requiresPermission("board-create") - .input(validation.board.duplicate) + .input(boardDuplicateSchema) .mutation(async ({ ctx, input }) => { await throwIfActionForbiddenAsync(ctx, eq(boards.id, input.id), "view"); await noBoardWithSimilarNameAsync(ctx.db, input.name); @@ -506,34 +519,32 @@ export const boardRouter = createTRPCRouter({ }, }); }), - renameBoard: protectedProcedure.input(validation.board.rename).mutation(async ({ ctx, input }) => { + renameBoard: protectedProcedure.input(boardRenameSchema).mutation(async ({ ctx, input }) => { await throwIfActionForbiddenAsync(ctx, eq(boards.id, input.id), "full"); await noBoardWithSimilarNameAsync(ctx.db, input.name, [input.id]); await ctx.db.update(boards).set({ name: input.name }).where(eq(boards.id, input.id)); }), - changeBoardVisibility: protectedProcedure - .input(validation.board.changeVisibility) - .mutation(async ({ ctx, input }) => { - await throwIfActionForbiddenAsync(ctx, eq(boards.id, input.id), "full"); - const boardSettings = await getServerSettingByKeyAsync(ctx.db, "board"); + changeBoardVisibility: protectedProcedure.input(boardChangeVisibilitySchema).mutation(async ({ ctx, input }) => { + await throwIfActionForbiddenAsync(ctx, eq(boards.id, input.id), "full"); + const boardSettings = await getServerSettingByKeyAsync(ctx.db, "board"); - if ( - input.visibility !== "public" && - (boardSettings.homeBoardId === input.id || boardSettings.mobileHomeBoardId === input.id) - ) { - throw new TRPCError({ - code: "BAD_REQUEST", - message: "Cannot make home board private", - }); - } + if ( + input.visibility !== "public" && + (boardSettings.homeBoardId === input.id || boardSettings.mobileHomeBoardId === input.id) + ) { + throw new TRPCError({ + code: "BAD_REQUEST", + message: "Cannot make home board private", + }); + } - await ctx.db - .update(boards) - .set({ isPublic: input.visibility === "public" }) - .where(eq(boards.id, input.id)); - }), + await ctx.db + .update(boards) + .set({ isPublic: input.visibility === "public" }) + .where(eq(boards.id, input.id)); + }), deleteBoard: protectedProcedure.input(z.object({ id: z.string() })).mutation(async ({ ctx, input }) => { await throwIfActionForbiddenAsync(ctx, eq(boards.id, input.id), "full"); @@ -572,13 +583,13 @@ export const boardRouter = createTRPCRouter({ return await getFullBoardWithWhereAsync(ctx.db, boardWhere, ctx.session?.user.id ?? null); }), - getBoardByName: publicProcedure.input(validation.board.byName).query(async ({ input, ctx }) => { + getBoardByName: publicProcedure.input(boardByNameSchema).query(async ({ input, ctx }) => { const boardWhere = eq(sql`UPPER(${boards.name})`, input.name.toUpperCase()); await throwIfActionForbiddenAsync(ctx, boardWhere, "view"); return await getFullBoardWithWhereAsync(ctx.db, boardWhere, ctx.session?.user.id ?? null); }), - saveLayouts: protectedProcedure.input(validation.board.saveLayouts).mutation(async ({ ctx, input }) => { + saveLayouts: protectedProcedure.input(boardSaveLayoutsSchema).mutation(async ({ ctx, input }) => { await throwIfActionForbiddenAsync(ctx, eq(boards.id, input.id), "modify"); const board = await getFullBoardWithWhereAsync(ctx.db, eq(boards.id, input.id), ctx.session.user.id); @@ -704,7 +715,7 @@ export const boardRouter = createTRPCRouter({ } }), savePartialBoardSettings: protectedProcedure - .input(validation.board.savePartialSettings.and(z.object({ id: z.string() }))) + .input(boardSavePartialSettingsSchema.and(z.object({ id: z.string() }))) .mutation(async ({ ctx, input }) => { await throwIfActionForbiddenAsync(ctx, eq(boards.id, input.id), "modify"); @@ -738,7 +749,7 @@ export const boardRouter = createTRPCRouter({ }) .where(eq(boards.id, input.id)); }), - saveBoard: protectedProcedure.input(validation.board.save).mutation(async ({ input, ctx }) => { + saveBoard: protectedProcedure.input(boardSaveSchema).mutation(async ({ input, ctx }) => { await throwIfActionForbiddenAsync(ctx, eq(boards.id, input.id), "modify"); const dbBoard = await getFullBoardWithWhereAsync(ctx.db, eq(boards.id, input.id), ctx.session.user.id); @@ -1154,8 +1165,7 @@ export const boardRouter = createTRPCRouter({ }, }); }), - - getBoardPermissions: protectedProcedure.input(validation.board.permissions).query(async ({ input, ctx }) => { + getBoardPermissions: protectedProcedure.input(byIdSchema).query(async ({ input, ctx }) => { await throwIfActionForbiddenAsync(ctx, eq(boards.id, input.id), "full"); const dbGroupPermissions = await ctx.db.query.groupPermissions.findMany({ @@ -1226,92 +1236,86 @@ export const boardRouter = createTRPCRouter({ }), }; }), - saveUserBoardPermissions: protectedProcedure - .input(validation.board.savePermissions) - .mutation(async ({ input, ctx }) => { - await throwIfActionForbiddenAsync(ctx, eq(boards.id, input.entityId), "full"); + saveUserBoardPermissions: protectedProcedure.input(boardSavePermissionsSchema).mutation(async ({ input, ctx }) => { + await throwIfActionForbiddenAsync(ctx, eq(boards.id, input.entityId), "full"); - await handleTransactionsAsync(ctx.db, { - async handleAsync(db, schema) { - await db.transaction(async (transaction) => { - await transaction - .delete(schema.boardUserPermissions) - .where(eq(boardUserPermissions.boardId, input.entityId)); - if (input.permissions.length === 0) { - return; - } - await transaction.insert(schema.boardUserPermissions).values( + await handleTransactionsAsync(ctx.db, { + async handleAsync(db, schema) { + await db.transaction(async (transaction) => { + await transaction.delete(schema.boardUserPermissions).where(eq(boardUserPermissions.boardId, input.entityId)); + if (input.permissions.length === 0) { + return; + } + await transaction.insert(schema.boardUserPermissions).values( + input.permissions.map((permission) => ({ + userId: permission.principalId, + permission: permission.permission, + boardId: input.entityId, + })), + ); + }); + }, + handleSync(db) { + db.transaction((transaction) => { + transaction.delete(boardUserPermissions).where(eq(boardUserPermissions.boardId, input.entityId)).run(); + if (input.permissions.length === 0) { + return; + } + transaction + .insert(boardUserPermissions) + .values( input.permissions.map((permission) => ({ userId: permission.principalId, permission: permission.permission, boardId: input.entityId, })), - ); - }); - }, - handleSync(db) { - db.transaction((transaction) => { - transaction.delete(boardUserPermissions).where(eq(boardUserPermissions.boardId, input.entityId)).run(); - if (input.permissions.length === 0) { - return; - } - transaction - .insert(boardUserPermissions) - .values( - input.permissions.map((permission) => ({ - userId: permission.principalId, - permission: permission.permission, - boardId: input.entityId, - })), - ) - .run(); - }); - }, - }); - }), - saveGroupBoardPermissions: protectedProcedure - .input(validation.board.savePermissions) - .mutation(async ({ input, ctx }) => { - await throwIfActionForbiddenAsync(ctx, eq(boards.id, input.entityId), "full"); + ) + .run(); + }); + }, + }); + }), + saveGroupBoardPermissions: protectedProcedure.input(boardSavePermissionsSchema).mutation(async ({ input, ctx }) => { + await throwIfActionForbiddenAsync(ctx, eq(boards.id, input.entityId), "full"); - await handleTransactionsAsync(ctx.db, { - async handleAsync(db, schema) { - await db.transaction(async (transaction) => { - await transaction - .delete(schema.boardGroupPermissions) - .where(eq(boardGroupPermissions.boardId, input.entityId)); - if (input.permissions.length === 0) { - return; - } - await transaction.insert(schema.boardGroupPermissions).values( + await handleTransactionsAsync(ctx.db, { + async handleAsync(db, schema) { + await db.transaction(async (transaction) => { + await transaction + .delete(schema.boardGroupPermissions) + .where(eq(boardGroupPermissions.boardId, input.entityId)); + if (input.permissions.length === 0) { + return; + } + await transaction.insert(schema.boardGroupPermissions).values( + input.permissions.map((permission) => ({ + groupId: permission.principalId, + permission: permission.permission, + boardId: input.entityId, + })), + ); + }); + }, + handleSync(db) { + db.transaction((transaction) => { + transaction.delete(boardGroupPermissions).where(eq(boardGroupPermissions.boardId, input.entityId)).run(); + if (input.permissions.length === 0) { + return; + } + transaction + .insert(boardGroupPermissions) + .values( input.permissions.map((permission) => ({ groupId: permission.principalId, permission: permission.permission, boardId: input.entityId, })), - ); - }); - }, - handleSync(db) { - db.transaction((transaction) => { - transaction.delete(boardGroupPermissions).where(eq(boardGroupPermissions.boardId, input.entityId)).run(); - if (input.permissions.length === 0) { - return; - } - transaction - .insert(boardGroupPermissions) - .values( - input.permissions.map((permission) => ({ - groupId: permission.principalId, - permission: permission.permission, - boardId: input.entityId, - })), - ) - .run(); - }); - }, - }); - }), + ) + .run(); + }); + }, + }); + }), importOldmarrConfig: permissionRequiredProcedure .requiresPermission("board-create") .input(importJsonFileSchema) diff --git a/packages/api/src/router/certificates/certificate-router.ts b/packages/api/src/router/certificates/certificate-router.ts index d50150c80..96d638f70 100644 --- a/packages/api/src/router/certificates/certificate-router.ts +++ b/packages/api/src/router/certificates/certificate-router.ts @@ -2,7 +2,7 @@ import { z } from "zod"; import { zfd } from "zod-form-data"; import { addCustomRootCertificateAsync, removeCustomRootCertificateAsync } from "@homarr/certificates/server"; -import { superRefineCertificateFile, validation } from "@homarr/validation"; +import { certificateValidFileNameSchema, superRefineCertificateFile } from "@homarr/validation/certificates"; import { createTRPCRouter, permissionRequiredProcedure } from "../../trpc"; @@ -20,7 +20,7 @@ export const certificateRouter = createTRPCRouter({ }), removeCertificate: permissionRequiredProcedure .requiresPermission("admin") - .input(z.object({ fileName: validation.certificates.validFileNameSchema })) + .input(z.object({ fileName: certificateValidFileNameSchema })) .mutation(async ({ input }) => { await removeCustomRootCertificateAsync(input.fileName); }), diff --git a/packages/api/src/router/group.ts b/packages/api/src/router/group.ts index 36fa8ba3d..3407b066a 100644 --- a/packages/api/src/router/group.ts +++ b/packages/api/src/router/group.ts @@ -6,7 +6,15 @@ import { and, createId, eq, handleTransactionsAsync, like, not } from "@homarr/d import { getMaxGroupPositionAsync } from "@homarr/db/queries"; import { groupMembers, groupPermissions, groups } from "@homarr/db/schema"; import { everyoneGroup } from "@homarr/definitions"; -import { validation } from "@homarr/validation"; +import { byIdSchema, paginatedSchema } from "@homarr/validation/common"; +import { + groupCreateSchema, + groupSavePartialSettingsSchema, + groupSavePermissionsSchema, + groupSavePositionsSchema, + groupUpdateSchema, + groupUserSchema, +} from "@homarr/validation/group"; import { createTRPCRouter, onboardingProcedure, permissionRequiredProcedure, protectedProcedure } from "../trpc"; import { throwIfCredentialsDisabled } from "./invite/checks"; @@ -39,7 +47,7 @@ export const groupRouter = createTRPCRouter({ getPaginated: permissionRequiredProcedure .requiresPermission("admin") - .input(validation.common.paginated) + .input(paginatedSchema) .query(async ({ input, ctx }) => { const whereQuery = input.search ? like(groups.name, `%${input.search.trim()}%`) : undefined; const groupCount = await ctx.db.$count(groups, whereQuery); @@ -74,7 +82,7 @@ export const groupRouter = createTRPCRouter({ }), getById: permissionRequiredProcedure .requiresPermission("admin") - .input(validation.common.byId) + .input(byIdSchema) .query(async ({ input, ctx }) => { const group = await ctx.db.query.groups.findFirst({ where: eq(groups.id, input.id), @@ -169,7 +177,7 @@ export const groupRouter = createTRPCRouter({ }), createInitialExternalGroup: onboardingProcedure .requiresStep("group") - .input(validation.group.create) + .input(groupCreateSchema) .mutation(async ({ input, ctx }) => { await checkSimilarNameAndThrowAsync(ctx.db, input.name); @@ -191,7 +199,7 @@ export const groupRouter = createTRPCRouter({ }), createGroup: permissionRequiredProcedure .requiresPermission("admin") - .input(validation.group.create) + .input(groupCreateSchema) .mutation(async ({ input, ctx }) => { await checkSimilarNameAndThrowAsync(ctx.db, input.name); @@ -209,7 +217,7 @@ export const groupRouter = createTRPCRouter({ }), updateGroup: permissionRequiredProcedure .requiresPermission("admin") - .input(validation.group.update) + .input(groupUpdateSchema) .mutation(async ({ input, ctx }) => { await throwIfGroupNotFoundAsync(ctx.db, input.id); await throwIfGroupNameIsReservedAsync(ctx.db, input.id); @@ -225,7 +233,7 @@ export const groupRouter = createTRPCRouter({ }), savePartialSettings: permissionRequiredProcedure .requiresPermission("admin") - .input(validation.group.savePartialSettings) + .input(groupSavePartialSettingsSchema) .mutation(async ({ input, ctx }) => { await throwIfGroupNotFoundAsync(ctx.db, input.id); @@ -239,7 +247,7 @@ export const groupRouter = createTRPCRouter({ }), savePositions: permissionRequiredProcedure .requiresPermission("admin") - .input(validation.group.savePositions) + .input(groupSavePositionsSchema) .mutation(async ({ input, ctx }) => { const positions = input.positions.map((id, index) => ({ id, position: index + 1 })); @@ -262,7 +270,7 @@ export const groupRouter = createTRPCRouter({ }), savePermissions: permissionRequiredProcedure .requiresPermission("admin") - .input(validation.group.savePermissions) + .input(groupSavePermissionsSchema) .mutation(async ({ input, ctx }) => { await throwIfGroupNotFoundAsync(ctx.db, input.groupId); @@ -277,7 +285,7 @@ export const groupRouter = createTRPCRouter({ }), transferOwnership: permissionRequiredProcedure .requiresPermission("admin") - .input(validation.group.groupUser) + .input(groupUserSchema) .mutation(async ({ input, ctx }) => { await throwIfGroupNotFoundAsync(ctx.db, input.groupId); await throwIfGroupNameIsReservedAsync(ctx.db, input.groupId); @@ -291,7 +299,7 @@ export const groupRouter = createTRPCRouter({ }), deleteGroup: permissionRequiredProcedure .requiresPermission("admin") - .input(validation.common.byId) + .input(byIdSchema) .mutation(async ({ input, ctx }) => { await throwIfGroupNotFoundAsync(ctx.db, input.id); await throwIfGroupNameIsReservedAsync(ctx.db, input.id); @@ -300,7 +308,7 @@ export const groupRouter = createTRPCRouter({ }), addMember: permissionRequiredProcedure .requiresPermission("admin") - .input(validation.group.groupUser) + .input(groupUserSchema) .mutation(async ({ input, ctx }) => { await throwIfGroupNotFoundAsync(ctx.db, input.groupId); await throwIfGroupNameIsReservedAsync(ctx.db, input.groupId); @@ -324,7 +332,7 @@ export const groupRouter = createTRPCRouter({ }), removeMember: permissionRequiredProcedure .requiresPermission("admin") - .input(validation.group.groupUser) + .input(groupUserSchema) .mutation(async ({ input, ctx }) => { await throwIfGroupNotFoundAsync(ctx.db, input.groupId); await throwIfGroupNameIsReservedAsync(ctx.db, input.groupId); diff --git a/packages/api/src/router/icons.ts b/packages/api/src/router/icons.ts index 4a3b33051..04f15fc2d 100644 --- a/packages/api/src/router/icons.ts +++ b/packages/api/src/router/icons.ts @@ -1,11 +1,11 @@ import { and, like } from "@homarr/db"; import { icons } from "@homarr/db/schema"; -import { validation } from "@homarr/validation"; +import { iconsFindSchema } from "@homarr/validation/icons"; import { createTRPCRouter, publicProcedure } from "../trpc"; export const iconsRouter = createTRPCRouter({ - findIcons: publicProcedure.input(validation.icons.findIcons).query(async ({ ctx, input }) => { + findIcons: publicProcedure.input(iconsFindSchema).query(async ({ ctx, input }) => { return { icons: await ctx.db.query.iconRepositories.findMany({ with: { diff --git a/packages/api/src/router/integration/integration-router.ts b/packages/api/src/router/integration/integration-router.ts index e133e2b9c..014ca1429 100644 --- a/packages/api/src/router/integration/integration-router.ts +++ b/packages/api/src/router/integration/integration-router.ts @@ -24,7 +24,12 @@ import { integrationSecretKindObject, } from "@homarr/definitions"; import { createIntegrationAsync } from "@homarr/integrations"; -import { validation } from "@homarr/validation"; +import { byIdSchema } from "@homarr/validation/common"; +import { + integrationCreateSchema, + integrationSavePermissionsSchema, + integrationUpdateSchema, +} from "@homarr/validation/integration"; import { createOneIntegrationMiddleware } from "../../middlewares/integration"; import { createTRPCRouter, permissionRequiredProcedure, protectedProcedure, publicProcedure } from "../../trpc"; @@ -141,7 +146,7 @@ export const integrationRouter = createTRPCRouter({ }, }); }), - byId: protectedProcedure.input(validation.integration.byId).query(async ({ ctx, input }) => { + byId: protectedProcedure.input(byIdSchema).query(async ({ ctx, input }) => { await throwIfActionForbiddenAsync(ctx, eq(integrations.id, input.id), "full"); const integration = await ctx.db.query.integrations.findFirst({ where: eq(integrations.id, input.id), @@ -178,7 +183,7 @@ export const integrationRouter = createTRPCRouter({ }), create: permissionRequiredProcedure .requiresPermission("integration-create") - .input(validation.integration.create) + .input(integrationCreateSchema) .mutation(async ({ ctx, input }) => { await testConnectionAsync({ id: "new", @@ -221,7 +226,7 @@ export const integrationRouter = createTRPCRouter({ }); } }), - update: protectedProcedure.input(validation.integration.update).mutation(async ({ ctx, input }) => { + update: protectedProcedure.input(integrationUpdateSchema).mutation(async ({ ctx, input }) => { await throwIfActionForbiddenAsync(ctx, eq(integrations.id, input.id), "full"); const integration = await ctx.db.query.integrations.findFirst({ @@ -282,7 +287,7 @@ export const integrationRouter = createTRPCRouter({ } } }), - delete: protectedProcedure.input(validation.integration.delete).mutation(async ({ ctx, input }) => { + delete: protectedProcedure.input(byIdSchema).mutation(async ({ ctx, input }) => { await throwIfActionForbiddenAsync(ctx, eq(integrations.id, input.id), "full"); const integration = await ctx.db.query.integrations.findFirst({ @@ -298,7 +303,7 @@ export const integrationRouter = createTRPCRouter({ await ctx.db.delete(integrations).where(eq(integrations.id, input.id)); }), - getIntegrationPermissions: protectedProcedure.input(validation.board.permissions).query(async ({ input, ctx }) => { + getIntegrationPermissions: protectedProcedure.input(byIdSchema).query(async ({ input, ctx }) => { await throwIfActionForbiddenAsync(ctx, eq(integrations.id, input.id), "full"); const dbGroupPermissions = await ctx.db.query.groupPermissions.findMany({ @@ -370,7 +375,7 @@ export const integrationRouter = createTRPCRouter({ }; }), saveUserIntegrationPermissions: protectedProcedure - .input(validation.integration.savePermissions) + .input(integrationSavePermissionsSchema) .mutation(async ({ input, ctx }) => { await throwIfActionForbiddenAsync(ctx, eq(integrations.id, input.entityId), "full"); @@ -416,7 +421,7 @@ export const integrationRouter = createTRPCRouter({ }); }), saveGroupIntegrationPermissions: protectedProcedure - .input(validation.integration.savePermissions) + .input(integrationSavePermissionsSchema) .mutation(async ({ input, ctx }) => { await throwIfActionForbiddenAsync(ctx, eq(integrations.id, input.entityId), "full"); diff --git a/packages/api/src/router/location.ts b/packages/api/src/router/location.ts index 7d3f3d980..5ecae7db6 100644 --- a/packages/api/src/router/location.ts +++ b/packages/api/src/router/location.ts @@ -1,16 +1,42 @@ -import type { z } from "zod"; +import { z } from "zod"; import { fetchWithTimeout } from "@homarr/common"; -import { validation } from "@homarr/validation"; import { createTRPCRouter, publicProcedure } from "../trpc"; +const citySchema = z.object({ + id: z.number(), + name: z.string(), + country: z.string().optional(), + country_code: z.string().optional(), + latitude: z.number(), + longitude: z.number(), + population: z.number().optional(), +}); + +export const locationSearchCityInput = z.object({ + query: z.string(), +}); + +export const locationSearchCityOutput = z + .object({ + results: z.array(citySchema), + }) + .or( + z + .object({ + generationtime_ms: z.number(), + }) + .refine((data) => Object.keys(data).length === 1, { message: "Invalid response" }) + .transform(() => ({ results: [] })), // We fallback to empty array if no results + ); + export const locationRouter = createTRPCRouter({ searchCity: publicProcedure - .input(validation.location.searchCity.input) - .output(validation.location.searchCity.output) + .input(locationSearchCityInput) + .output(locationSearchCityOutput) .query(async ({ input }) => { const res = await fetchWithTimeout(`https://geocoding-api.open-meteo.com/v1/search?name=${input.query}`); - return (await res.json()) as z.infer; + return (await res.json()) as z.infer; }), }); diff --git a/packages/api/src/router/medias/media-router.ts b/packages/api/src/router/medias/media-router.ts index d6ed0223b..9b4dbfb36 100644 --- a/packages/api/src/router/medias/media-router.ts +++ b/packages/api/src/router/medias/media-router.ts @@ -5,14 +5,15 @@ import type { InferInsertModel } from "@homarr/db"; import { and, createId, desc, eq, like } from "@homarr/db"; import { iconRepositories, icons, medias } from "@homarr/db/schema"; import { createLocalImageUrl, LOCAL_ICON_REPOSITORY_SLUG, mapMediaToIcon } from "@homarr/icons/local"; -import { validation } from "@homarr/validation"; +import { byIdSchema, paginatedSchema } from "@homarr/validation/common"; +import { mediaUploadSchema } from "@homarr/validation/media"; import { createTRPCRouter, permissionRequiredProcedure, protectedProcedure } from "../../trpc"; export const mediaRouter = createTRPCRouter({ getPaginated: protectedProcedure .input( - validation.common.paginated.and( + paginatedSchema.and( z.object({ includeFromAllUsers: z.boolean().default(false), search: z.string().trim().default("") }), ), ) @@ -51,7 +52,7 @@ export const mediaRouter = createTRPCRouter({ }), uploadMedia: permissionRequiredProcedure .requiresPermission("media-upload") - .input(validation.media.uploadMedia) + .input(mediaUploadSchema) .mutation(async ({ ctx, input }) => { const content = Buffer.from(await input.file.arrayBuffer()); const id = createId(); @@ -82,7 +83,7 @@ export const mediaRouter = createTRPCRouter({ return id; }), - deleteMedia: protectedProcedure.input(validation.common.byId).mutation(async ({ ctx, input }) => { + deleteMedia: protectedProcedure.input(byIdSchema).mutation(async ({ ctx, input }) => { const dbMedia = await ctx.db.query.medias.findFirst({ where: eq(medias.id, input.id), columns: { diff --git a/packages/api/src/router/onboard/onboard-router.ts b/packages/api/src/router/onboard/onboard-router.ts index a9a97d474..b68fbd44f 100644 --- a/packages/api/src/router/onboard/onboard-router.ts +++ b/packages/api/src/router/onboard/onboard-router.ts @@ -2,7 +2,7 @@ import { z } from "zod"; import { onboarding } from "@homarr/db/schema"; import { onboardingSteps } from "@homarr/definitions"; -import { zodEnumFromArray } from "@homarr/validation"; +import { zodEnumFromArray } from "@homarr/validation/enums"; import { createTRPCRouter, publicProcedure } from "../../trpc"; import { getOnboardingOrFallbackAsync, nextOnboardingStepAsync } from "./onboard-queries"; diff --git a/packages/api/src/router/search-engine/search-engine-router.ts b/packages/api/src/router/search-engine/search-engine-router.ts index 7b6eee219..e361b3bc9 100644 --- a/packages/api/src/router/search-engine/search-engine-router.ts +++ b/packages/api/src/router/search-engine/search-engine-router.ts @@ -5,13 +5,15 @@ import { asc, createId, eq, like } from "@homarr/db"; import { getServerSettingByKeyAsync } from "@homarr/db/queries"; import { searchEngines, users } from "@homarr/db/schema"; import { createIntegrationAsync } from "@homarr/integrations"; -import { validation } from "@homarr/validation"; +import { byIdSchema, paginatedSchema, searchSchema } from "@homarr/validation/common"; +import { searchEngineEditSchema, searchEngineManageSchema } from "@homarr/validation/search-engine"; +import { mediaRequestOptionsSchema, mediaRequestRequestSchema } from "@homarr/validation/widgets/media-request"; import { createOneIntegrationMiddleware } from "../../middlewares/integration"; import { createTRPCRouter, permissionRequiredProcedure, protectedProcedure, publicProcedure } from "../../trpc"; export const searchEngineRouter = createTRPCRouter({ - getPaginated: protectedProcedure.input(validation.common.paginated).query(async ({ input, ctx }) => { + getPaginated: protectedProcedure.input(paginatedSchema).query(async ({ input, ctx }) => { const whereQuery = input.search ? like(searchEngines.name, `%${input.search.trim()}%`) : undefined; const searchEngineCount = await ctx.db.$count(searchEngines, whereQuery); @@ -41,7 +43,7 @@ export const searchEngineRouter = createTRPCRouter({ .then((engines) => engines.map((engine) => ({ value: engine.id, label: engine.name }))); }), - byId: protectedProcedure.input(validation.common.byId).query(async ({ ctx, input }) => { + byId: protectedProcedure.input(byIdSchema).query(async ({ ctx, input }) => { const searchEngine = await ctx.db.query.searchEngines.findFirst({ where: eq(searchEngines.id, input.id), }); @@ -115,7 +117,7 @@ export const searchEngineRouter = createTRPCRouter({ return null; }), - search: protectedProcedure.input(validation.common.search).query(async ({ ctx, input }) => { + search: protectedProcedure.input(searchSchema).query(async ({ ctx, input }) => { return await ctx.db.query.searchEngines.findMany({ where: like(searchEngines.short, `${input.query.toLowerCase().trim()}%`), with: { @@ -132,21 +134,21 @@ export const searchEngineRouter = createTRPCRouter({ }), getMediaRequestOptions: protectedProcedure .unstable_concat(createOneIntegrationMiddleware("query", "jellyseerr", "overseerr")) - .input(validation.common.mediaRequestOptions) + .input(mediaRequestOptionsSchema) .query(async ({ ctx, input }) => { const integration = await createIntegrationAsync(ctx.integration); return await integration.getSeriesInformationAsync(input.mediaType, input.mediaId); }), requestMedia: protectedProcedure .unstable_concat(createOneIntegrationMiddleware("interact", "jellyseerr", "overseerr")) - .input(validation.common.requestMedia) + .input(mediaRequestRequestSchema) .mutation(async ({ ctx, input }) => { const integration = await createIntegrationAsync(ctx.integration); return await integration.requestMediaAsync(input.mediaType, input.mediaId, input.seasons); }), create: permissionRequiredProcedure .requiresPermission("search-engine-create") - .input(validation.searchEngine.manage) + .input(searchEngineManageSchema) .mutation(async ({ ctx, input }) => { await ctx.db.insert(searchEngines).values({ id: createId(), @@ -161,7 +163,7 @@ export const searchEngineRouter = createTRPCRouter({ }), update: permissionRequiredProcedure .requiresPermission("search-engine-modify-all") - .input(validation.searchEngine.edit) + .input(searchEngineEditSchema) .mutation(async ({ ctx, input }) => { const searchEngine = await ctx.db.query.searchEngines.findFirst({ where: eq(searchEngines.id, input.id), @@ -188,7 +190,7 @@ export const searchEngineRouter = createTRPCRouter({ }), delete: permissionRequiredProcedure .requiresPermission("search-engine-full-all") - .input(validation.common.byId) + .input(byIdSchema) .mutation(async ({ ctx, input }) => { await ctx.db .update(users) diff --git a/packages/api/src/router/serverSettings.ts b/packages/api/src/router/serverSettings.ts index a4bb5eeca..c7035aa1f 100644 --- a/packages/api/src/router/serverSettings.ts +++ b/packages/api/src/router/serverSettings.ts @@ -3,7 +3,7 @@ import { z } from "zod"; import { getServerSettingByKeyAsync, getServerSettingsAsync, updateServerSettingByKeyAsync } from "@homarr/db/queries"; import type { ServerSettings } from "@homarr/server-settings"; import { defaultServerSettingsKeys } from "@homarr/server-settings"; -import { validation } from "@homarr/validation"; +import { settingsInitSchema } from "@homarr/validation/settings"; import { createTRPCRouter, onboardingProcedure, permissionRequiredProcedure, publicProcedure } from "../trpc"; import { nextOnboardingStepAsync } from "./onboard/onboard-queries"; @@ -32,7 +32,7 @@ export const serverSettingsRouter = createTRPCRouter({ }), initSettings: onboardingProcedure .requiresStep("settings") - .input(validation.settings.init) + .input(settingsInitSchema) .mutation(async ({ ctx, input }) => { await updateServerSettingByKeyAsync(ctx.db, "analytics", input.analytics); await updateServerSettingByKeyAsync(ctx.db, "crawlingAndIndexing", input.crawlingAndIndexing); diff --git a/packages/api/src/router/user.ts b/packages/api/src/router/user.ts index 9aefff564..ff802e1df 100644 --- a/packages/api/src/router/user.ts +++ b/packages/api/src/router/user.ts @@ -10,7 +10,20 @@ import { selectUserSchema } from "@homarr/db/validationSchemas"; import { credentialsAdminGroup } from "@homarr/definitions"; import type { SupportedAuthProvider } from "@homarr/definitions"; import { logger } from "@homarr/log"; -import { validation } from "@homarr/validation"; +import { byIdSchema } from "@homarr/validation/common"; +import type { userBaseCreateSchema } from "@homarr/validation/user"; +import { + userChangeColorSchemeSchema, + userChangeHomeBoardsSchema, + userChangePasswordApiSchema, + userChangeSearchPreferencesSchema, + userCreateSchema, + userEditProfileSchema, + userFirstDayOfWeekSchema, + userInitSchema, + userPingIconsEnabledSchema, + userRegistrationApiSchema, +} from "@homarr/validation/user"; import { convertIntersectionToZodObject } from "../schema-merger"; import { @@ -28,7 +41,7 @@ import { changeSearchPreferencesAsync, changeSearchPreferencesInputSchema } from export const userRouter = createTRPCRouter({ initUser: onboardingProcedure .requiresStep("user") - .input(validation.user.init) + .input(userInitSchema) .mutation(async ({ ctx, input }) => { throwIfCredentialsDisabled(); @@ -52,7 +65,7 @@ export const userRouter = createTRPCRouter({ await nextOnboardingStepAsync(ctx.db, undefined); }), register: publicProcedure - .input(validation.user.registrationApi) + .input(userRegistrationApiSchema) .output(z.void()) .mutation(async ({ ctx, input }) => { throwIfCredentialsDisabled(); @@ -82,7 +95,7 @@ export const userRouter = createTRPCRouter({ create: permissionRequiredProcedure .requiresPermission("admin") .meta({ openapi: { method: "POST", path: "/api/users", tags: ["users"], protect: true } }) - .input(validation.user.create) + .input(userCreateSchema) .output(z.void()) .mutation(async ({ ctx, input }) => { throwIfCredentialsDisabled(); @@ -259,7 +272,7 @@ export const userRouter = createTRPCRouter({ return user; }), editProfile: protectedProcedure - .input(validation.user.editProfile) + .input(userEditProfileSchema) .output(z.void()) .meta({ openapi: { method: "PUT", path: "/api/users/profile", tags: ["users"], protect: true } }) .mutation(async ({ input, ctx }) => { @@ -318,7 +331,7 @@ export const userRouter = createTRPCRouter({ await ctx.db.delete(users).where(eq(users.id, input.userId)); }), changePassword: protectedProcedure - .input(validation.user.changePasswordApi) + .input(userChangePasswordApiSchema) .output(z.void()) .meta({ openapi: { method: "PATCH", path: "/api/users/{userId}/changePassword", tags: ["users"], protect: true } }) .mutation(async ({ ctx, input }) => { @@ -384,7 +397,7 @@ export const userRouter = createTRPCRouter({ .where(eq(users.id, input.userId)); }), changeHomeBoards: protectedProcedure - .input(convertIntersectionToZodObject(validation.user.changeHomeBoards.and(z.object({ userId: z.string() })))) + .input(convertIntersectionToZodObject(userChangeHomeBoardsSchema.and(z.object({ userId: z.string() })))) .output(z.void()) .meta({ openapi: { method: "PATCH", path: "/api/users/changeHome", tags: ["users"], protect: true } }) .mutation(async ({ input, ctx }) => { @@ -430,7 +443,7 @@ export const userRouter = createTRPCRouter({ changeDefaultSearchEngine: protectedProcedure .input( convertIntersectionToZodObject( - validation.user.changeSearchPreferences.omit({ openInNewTab: true }).and(z.object({ userId: z.string() })), + userChangeSearchPreferencesSchema.omit({ openInNewTab: true }).and(z.object({ userId: z.string() })), ), ) .output(z.void()) @@ -457,7 +470,7 @@ export const userRouter = createTRPCRouter({ await changeSearchPreferencesAsync(ctx.db, ctx.session, input); }), changeColorScheme: protectedProcedure - .input(validation.user.changeColorScheme) + .input(userChangeColorSchemeSchema) .output(z.void()) .meta({ openapi: { method: "PATCH", path: "/api/users/changeScheme", tags: ["users"], protect: true } }) .mutation(async ({ input, ctx }) => { @@ -469,7 +482,7 @@ export const userRouter = createTRPCRouter({ .where(eq(users.id, ctx.session.user.id)); }), changePingIconsEnabled: protectedProcedure - .input(validation.user.pingIconsEnabled.and(validation.common.byId)) + .input(userPingIconsEnabledSchema.and(byIdSchema)) .mutation(async ({ input, ctx }) => { // Only admins can change other users ping icons enabled if (!ctx.session.user.permissions.includes("admin") && ctx.session.user.id !== input.id) { @@ -487,7 +500,7 @@ export const userRouter = createTRPCRouter({ .where(eq(users.id, ctx.session.user.id)); }), changeFirstDayOfWeek: protectedProcedure - .input(convertIntersectionToZodObject(validation.user.firstDayOfWeek.and(validation.common.byId))) + .input(convertIntersectionToZodObject(userFirstDayOfWeekSchema.and(byIdSchema))) .output(z.void()) .meta({ openapi: { method: "PATCH", path: "/api/users/firstDayOfWeek", tags: ["users"], protect: true } }) .mutation(async ({ input, ctx }) => { @@ -522,7 +535,7 @@ export const userRouter = createTRPCRouter({ }), }); -const createUserAsync = async (db: Database, input: Omit, "groupIds">) => { +const createUserAsync = async (db: Database, input: Omit, "groupIds">) => { const salt = await createSaltAsync(); const hashedPassword = await hashPasswordAsync(input.password, salt); diff --git a/packages/api/src/router/user/change-search-preferences.ts b/packages/api/src/router/user/change-search-preferences.ts index 1231b4e48..927418331 100644 --- a/packages/api/src/router/user/change-search-preferences.ts +++ b/packages/api/src/router/user/change-search-preferences.ts @@ -6,9 +6,9 @@ import type { Modify } from "@homarr/common/types"; import { eq } from "@homarr/db"; import type { Database } from "@homarr/db"; import { users } from "@homarr/db/schema"; -import { validation } from "@homarr/validation"; +import { userChangeSearchPreferencesSchema } from "@homarr/validation/user"; -export const changeSearchPreferencesInputSchema = validation.user.changeSearchPreferences.and( +export const changeSearchPreferencesInputSchema = userChangeSearchPreferencesSchema.and( z.object({ userId: z.string() }), ); diff --git a/packages/api/src/router/widgets/media-transcoding.ts b/packages/api/src/router/widgets/media-transcoding.ts index d6d1ce7e8..c2f0b0d9b 100644 --- a/packages/api/src/router/widgets/media-transcoding.ts +++ b/packages/api/src/router/widgets/media-transcoding.ts @@ -1,6 +1,6 @@ import { getIntegrationKindsByCategory } from "@homarr/definitions"; import { mediaTranscodingRequestHandler } from "@homarr/request-handler/media-transcoding"; -import { validation } from "@homarr/validation"; +import { paginatedSchema } from "@homarr/validation/common"; import type { IntegrationAction } from "../../middlewares/integration"; import { createOneIntegrationMiddleware } from "../../middlewares/integration"; @@ -12,7 +12,7 @@ const createIndexerManagerIntegrationMiddleware = (action: IntegrationAction) => export const mediaTranscodingRouter = createTRPCRouter({ getDataAsync: publicProcedure .unstable_concat(createIndexerManagerIntegrationMiddleware("query")) - .input(validation.common.paginated.pick({ page: true, pageSize: true })) + .input(paginatedSchema.pick({ page: true, pageSize: true })) .query(async ({ ctx, input }) => { const innerHandler = mediaTranscodingRequestHandler.handler(ctx.integration, { pageOffset: input.page, diff --git a/packages/api/src/router/widgets/weather.ts b/packages/api/src/router/widgets/weather.ts index ded3bc5b0..94555c666 100644 --- a/packages/api/src/router/widgets/weather.ts +++ b/packages/api/src/router/widgets/weather.ts @@ -1,15 +1,39 @@ +import { z } from "zod"; + import { fetchWithTimeout } from "@homarr/common"; -import { validation } from "@homarr/validation"; import { createTRPCRouter, publicProcedure } from "../../trpc"; +const atLocationInput = z.object({ + longitude: z.number(), + latitude: z.number(), +}); + +const atLocationOutput = z.object({ + current_weather: z.object({ + weathercode: z.number(), + temperature: z.number(), + windspeed: z.number(), + }), + daily: z.object({ + time: z.array(z.string()), + weathercode: z.array(z.number()), + temperature_2m_max: z.array(z.number()), + temperature_2m_min: z.array(z.number()), + sunrise: z.array(z.string()), + sunset: z.array(z.string()), + wind_speed_10m_max: z.array(z.number()), + wind_gusts_10m_max: z.array(z.number()), + }), +}); + export const weatherRouter = createTRPCRouter({ - atLocation: publicProcedure.input(validation.widget.weather.atLocationInput).query(async ({ input }) => { + atLocation: publicProcedure.input(atLocationInput).query(async ({ input }) => { const res = await fetchWithTimeout( `https://api.open-meteo.com/v1/forecast?latitude=${input.latitude}&longitude=${input.longitude}&daily=weathercode,temperature_2m_max,temperature_2m_min,sunrise,sunset,wind_speed_10m_max,wind_gusts_10m_max¤t_weather=true&timezone=auto`, ); const json: unknown = await res.json(); - const weather = await validation.widget.weather.atLocationOutput.parseAsync(json); + const weather = await atLocationOutput.parseAsync(json); return { current: weather.current_weather, daily: weather.daily.time.map((value, index) => { diff --git a/packages/auth/providers/credentials/authorization/basic-authorization.ts b/packages/auth/providers/credentials/authorization/basic-authorization.ts index 6cbd4d16a..596716c1b 100644 --- a/packages/auth/providers/credentials/authorization/basic-authorization.ts +++ b/packages/auth/providers/credentials/authorization/basic-authorization.ts @@ -5,11 +5,11 @@ import type { Database } from "@homarr/db"; import { and, eq } from "@homarr/db"; import { users } from "@homarr/db/schema"; import { logger } from "@homarr/log"; -import type { validation } from "@homarr/validation"; +import type { userSignInSchema } from "@homarr/validation/user"; export const authorizeWithBasicCredentialsAsync = async ( db: Database, - credentials: z.infer, + credentials: z.infer, ) => { const user = await db.query.users.findFirst({ where: and(eq(users.name, credentials.name.toLowerCase()), eq(users.provider, "credentials")), diff --git a/packages/auth/providers/credentials/authorization/ldap-authorization.ts b/packages/auth/providers/credentials/authorization/ldap-authorization.ts index 84f7733ac..9bfa90c2a 100644 --- a/packages/auth/providers/credentials/authorization/ldap-authorization.ts +++ b/packages/auth/providers/credentials/authorization/ldap-authorization.ts @@ -6,14 +6,14 @@ import type { Database, InferInsertModel } from "@homarr/db"; import { and, createId, eq } from "@homarr/db"; import { users } from "@homarr/db/schema"; import { logger } from "@homarr/log"; -import type { validation } from "@homarr/validation"; +import type { userSignInSchema } from "@homarr/validation/user"; import { env } from "../../../env"; import { LdapClient } from "../ldap-client"; export const authorizeWithLdapCredentialsAsync = async ( db: Database, - credentials: z.infer, + credentials: z.infer, ) => { logger.info(`user ${credentials.name} is trying to log in using LDAP. Connecting to LDAP server...`); const client = new LdapClient(); diff --git a/packages/auth/providers/credentials/credentials-provider.ts b/packages/auth/providers/credentials/credentials-provider.ts index 571541b7a..79e5eddf6 100644 --- a/packages/auth/providers/credentials/credentials-provider.ts +++ b/packages/auth/providers/credentials/credentials-provider.ts @@ -1,7 +1,7 @@ import type Credentials from "@auth/core/providers/credentials"; import type { Database } from "@homarr/db"; -import { validation } from "@homarr/validation"; +import { userSignInSchema } from "@homarr/validation/user"; import { authorizeWithBasicCredentialsAsync } from "./authorization/basic-authorization"; import { authorizeWithLdapCredentialsAsync } from "./authorization/ldap-authorization"; @@ -15,7 +15,7 @@ export const createCredentialsConfiguration = (db: Database) => name: "Credentials", // eslint-disable-next-line no-restricted-syntax async authorize(credentials) { - const data = await validation.user.signIn.parseAsync(credentials); + const data = await userSignInSchema.parseAsync(credentials); return await authorizeWithBasicCredentialsAsync(db, data); }, @@ -28,7 +28,7 @@ export const createLdapConfiguration = (db: Database) => name: "Ldap", // eslint-disable-next-line no-restricted-syntax async authorize(credentials) { - const data = await validation.user.signIn.parseAsync(credentials); + const data = await userSignInSchema.parseAsync(credentials); return await authorizeWithLdapCredentialsAsync(db, data).catch(() => null); }, }) satisfies CredentialsConfiguration; diff --git a/packages/cli/src/commands/recreate-admin.ts b/packages/cli/src/commands/recreate-admin.ts index 59af3bade..29d7027df 100644 --- a/packages/cli/src/commands/recreate-admin.ts +++ b/packages/cli/src/commands/recreate-admin.ts @@ -5,7 +5,7 @@ import { generateSecureRandomToken } from "@homarr/common/server"; import { and, count, createId, db, eq } from "@homarr/db"; import { getMaxGroupPositionAsync } from "@homarr/db/queries"; import { groupMembers, groupPermissions, groups, users } from "@homarr/db/schema"; -import { usernameSchema } from "@homarr/validation"; +import { usernameSchema } from "@homarr/validation/user"; export const recreateAdmin = command({ name: "recreate-admin", diff --git a/packages/cron-job-runner/src/index.ts b/packages/cron-job-runner/src/index.ts index 08eb3aacd..e1e851da6 100644 --- a/packages/cron-job-runner/src/index.ts +++ b/packages/cron-job-runner/src/index.ts @@ -1,7 +1,7 @@ import { objectKeys } from "@homarr/common"; import type { JobGroupKeys } from "@homarr/cron-jobs"; import { createSubPubChannel } from "@homarr/redis"; -import { zodEnumFromArray } from "@homarr/validation"; +import { zodEnumFromArray } from "@homarr/validation/enums"; export const cronJobRunnerChannel = createSubPubChannel("cron-job-runner", { persist: false }); diff --git a/packages/form/src/index.ts b/packages/form/src/index.ts index 328315b5b..59192a2a6 100644 --- a/packages/form/src/index.ts +++ b/packages/form/src/index.ts @@ -3,7 +3,7 @@ import { z } from "zod"; import type { AnyZodObject, ZodDiscriminatedUnion, ZodEffects, ZodIntersection } from "zod"; import { useI18n } from "@homarr/translation/client"; -import { zodErrorMap } from "@homarr/validation/form"; +import { zodErrorMap } from "@homarr/validation/form/i18n"; export const useZodForm = < TSchema extends diff --git a/packages/forms-collection/src/new-app/_app-new-form.tsx b/packages/forms-collection/src/new-app/_app-new-form.tsx index 846b10d7d..a8ef5d70d 100644 --- a/packages/forms-collection/src/new-app/_app-new-form.tsx +++ b/packages/forms-collection/src/new-app/_app-new-form.tsx @@ -8,7 +8,7 @@ import { clientApi } from "@homarr/api/client"; import { revalidatePathActionAsync } from "@homarr/common/client"; import { showErrorNotification, showSuccessNotification } from "@homarr/notifications"; import { useI18n, useScopedI18n } from "@homarr/translation/client"; -import type { validation } from "@homarr/validation"; +import type { appManageSchema } from "@homarr/validation/app"; import { AppForm } from "./_form"; @@ -33,7 +33,7 @@ export const AppNewForm = ({ }); const handleSubmit = useCallback( - (values: z.infer, redirect: boolean, afterSuccess?: () => void) => { + (values: z.infer, redirect: boolean, afterSuccess?: () => void) => { mutate(values, { onSuccess() { showSuccessNotification({ diff --git a/packages/forms-collection/src/new-app/_form.tsx b/packages/forms-collection/src/new-app/_form.tsx index 146ed5151..559b2160a 100644 --- a/packages/forms-collection/src/new-app/_form.tsx +++ b/packages/forms-collection/src/new-app/_form.tsx @@ -10,12 +10,12 @@ import type { z } from "zod"; import { clientApi } from "@homarr/api/client"; import { useZodForm } from "@homarr/form"; import { useI18n } from "@homarr/translation/client"; -import { validation } from "@homarr/validation"; +import { appManageSchema } from "@homarr/validation/app"; import { IconPicker } from "../icon-picker/icon-picker"; import { findBestIconMatch } from "./icon-matcher"; -type FormType = z.infer; +type FormType = z.infer; interface AppFormProps { showBackToOverview: boolean; @@ -37,7 +37,7 @@ export const AppForm = ({ }: AppFormProps) => { const t = useI18n(); - const form = useZodForm(validation.app.manage, { + const form = useZodForm(appManageSchema, { initialValues: { name: initialValues?.name ?? "", description: initialValues?.description ?? "", diff --git a/packages/forms-collection/src/upload-media/upload-media.tsx b/packages/forms-collection/src/upload-media/upload-media.tsx index ca3305f1c..2d48e9004 100644 --- a/packages/forms-collection/src/upload-media/upload-media.tsx +++ b/packages/forms-collection/src/upload-media/upload-media.tsx @@ -5,7 +5,7 @@ import { clientApi } from "@homarr/api/client"; import type { MaybePromise } from "@homarr/common/types"; import { showErrorNotification, showSuccessNotification } from "@homarr/notifications"; import { useI18n } from "@homarr/translation/client"; -import { supportedMediaUploadFormats } from "@homarr/validation"; +import { supportedMediaUploadFormats } from "@homarr/validation/media"; interface UploadMediaProps { children: (props: { onClick: () => void; loading: boolean }) => JSX.Element; diff --git a/packages/modals-collection/src/apps/quick-add-app/quick-add-app-modal.tsx b/packages/modals-collection/src/apps/quick-add-app/quick-add-app-modal.tsx index 5e007821e..9f2e5e290 100644 --- a/packages/modals-collection/src/apps/quick-add-app/quick-add-app-modal.tsx +++ b/packages/modals-collection/src/apps/quick-add-app/quick-add-app-modal.tsx @@ -5,7 +5,7 @@ import { AppForm } from "@homarr/forms-collection"; import { createModal } from "@homarr/modals"; import { showErrorNotification, showSuccessNotification } from "@homarr/notifications"; import { useI18n, useScopedI18n } from "@homarr/translation/client"; -import type { validation } from "@homarr/validation"; +import type { appManageSchema } from "@homarr/validation/app"; interface QuickAddAppModalProps { onClose: (createdAppId: string) => Promise; @@ -24,7 +24,7 @@ export const QuickAddAppModal = createModal(({ actions, i }, }); - const handleSubmit = (values: z.infer) => { + const handleSubmit = (values: z.infer) => { mutate(values, { async onSuccess({ appId }) { showSuccessNotification({ diff --git a/packages/modals-collection/src/boards/add-board-modal.tsx b/packages/modals-collection/src/boards/add-board-modal.tsx index 45990aad0..83a5d3509 100644 --- a/packages/modals-collection/src/boards/add-board-modal.tsx +++ b/packages/modals-collection/src/boards/add-board-modal.tsx @@ -8,11 +8,11 @@ import { useZodForm } from "@homarr/form"; import { createModal } from "@homarr/modals"; import { showErrorNotification, showSuccessNotification } from "@homarr/notifications"; import { useI18n } from "@homarr/translation/client"; -import { validation } from "@homarr/validation"; +import { boardColumnCountSchema, boardCreateSchema, boardNameSchema } from "@homarr/validation/board"; export const AddBoardModal = createModal(({ actions }) => { const t = useI18n(); - const form = useZodForm(validation.board.create, { + const form = useZodForm(boardCreateSchema, { mode: "controlled", initialValues: { name: "", @@ -28,7 +28,7 @@ export const AddBoardModal = createModal(({ actions }) => { const boardNameStatus = useBoardNameStatus(form.values.name); - const columnCountChecks = validation.board.create.shape.columnCount._def.checks; + const columnCountChecks = boardColumnCountSchema._def.checks; const minColumnCount = columnCountChecks.find((check) => check.kind === "min")?.value; const maxColumnCount = columnCountChecks.find((check) => check.kind === "max")?.value; @@ -97,7 +97,7 @@ export const useBoardNameStatus = (name: string) => { const t = useI18n(); const [debouncedName] = useDebouncedValue(name, 250); const { data: boardExists, isLoading } = clientApi.board.exists.useQuery(debouncedName, { - enabled: validation.board.create.shape.name.safeParse(debouncedName).success, + enabled: boardNameSchema.safeParse(debouncedName).success, }); return { diff --git a/packages/modals-collection/src/boards/duplicate-board-modal.tsx b/packages/modals-collection/src/boards/duplicate-board-modal.tsx index 083cb716b..441e16edf 100644 --- a/packages/modals-collection/src/boards/duplicate-board-modal.tsx +++ b/packages/modals-collection/src/boards/duplicate-board-modal.tsx @@ -5,7 +5,7 @@ import type { MaybePromise } from "@homarr/common/types"; import { useZodForm } from "@homarr/form"; import { showErrorNotification, showSuccessNotification } from "@homarr/notifications"; import { useI18n } from "@homarr/translation/client"; -import { validation } from "@homarr/validation"; +import { boardDuplicateSchema } from "@homarr/validation/board"; import { createModal } from "../../../modals/src/creator"; import { useBoardNameStatus } from "./add-board-modal"; @@ -20,7 +20,7 @@ interface InnerProps { export const DuplicateBoardModal = createModal(({ actions, innerProps }) => { const t = useI18n(); - const form = useZodForm(validation.board.duplicate.omit({ id: true }), { + const form = useZodForm(boardDuplicateSchema.omit({ id: true }), { mode: "controlled", initialValues: { name: innerProps.board.name, diff --git a/packages/modals-collection/src/groups/add-group-modal.tsx b/packages/modals-collection/src/groups/add-group-modal.tsx index 2ba0af1a3..178d89581 100644 --- a/packages/modals-collection/src/groups/add-group-modal.tsx +++ b/packages/modals-collection/src/groups/add-group-modal.tsx @@ -6,12 +6,12 @@ import { useZodForm } from "@homarr/form"; import { createModal } from "@homarr/modals"; import { showErrorNotification, showSuccessNotification } from "@homarr/notifications"; import { useI18n } from "@homarr/translation/client"; -import { validation } from "@homarr/validation"; +import { groupCreateSchema } from "@homarr/validation/group"; export const AddGroupModal = createModal(({ actions }) => { const t = useI18n(); const { mutate, isPending } = clientApi.group.createGroup.useMutation(); - const form = useZodForm(validation.group.create, { + const form = useZodForm(groupCreateSchema, { initialValues: { name: "", }, diff --git a/packages/old-import/src/settings.ts b/packages/old-import/src/settings.ts index 9c2fb2f9e..a4944562b 100644 --- a/packages/old-import/src/settings.ts +++ b/packages/old-import/src/settings.ts @@ -1,15 +1,15 @@ import { z } from "zod"; import { zfd } from "zod-form-data"; -import { validation } from "@homarr/validation"; -import { createCustomErrorParams } from "@homarr/validation/form"; +import { boardNameSchema } from "@homarr/validation/board"; +import { createCustomErrorParams } from "@homarr/validation/form/i18n"; 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, + name: boardNameSchema, onlyImportApps: z.boolean().default(false), sidebarBehaviour: z.enum(sidebarBehaviours).default(defaultSidebarBehaviour), }); diff --git a/packages/ui/src/components/password-input/password-requirements-popover.tsx b/packages/ui/src/components/password-input/password-requirements-popover.tsx index b340ac473..5d10611f9 100644 --- a/packages/ui/src/components/password-input/password-requirements-popover.tsx +++ b/packages/ui/src/components/password-input/password-requirements-popover.tsx @@ -3,7 +3,7 @@ import { useState } from "react"; import { Popover, Progress } from "@mantine/core"; import { useScopedI18n } from "@homarr/translation/client"; -import { passwordRequirements } from "@homarr/validation"; +import { passwordRequirements } from "@homarr/validation/user"; import { PasswordRequirement } from "./password-requirement"; diff --git a/packages/validation/index.ts b/packages/validation/index.ts deleted file mode 100644 index 3bd16e178..000000000 --- a/packages/validation/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "./src"; diff --git a/packages/validation/package.json b/packages/validation/package.json index 85534401b..c52198bb4 100644 --- a/packages/validation/package.json +++ b/packages/validation/package.json @@ -5,8 +5,7 @@ "license": "Apache-2.0", "type": "module", "exports": { - ".": "./index.ts", - "./form": "./src/form/i18n.ts" + "./*": "./src/*.ts" }, "typesVersions": { "*": { diff --git a/packages/validation/src/app.ts b/packages/validation/src/app.ts index 42d170691..278f4a8b5 100644 --- a/packages/validation/src/app.ts +++ b/packages/validation/src/app.ts @@ -1,6 +1,15 @@ import { z } from "zod"; -const manageAppSchema = z.object({ +export const appHrefSchema = z + .string() + .trim() + .url() + .regex(/^https?:\/\//) // Only allow http and https for security reasons (javascript: is not allowed) + .or(z.literal("")) + .transform((value) => (value.length === 0 ? null : value)) + .nullable(); + +export const appManageSchema = z.object({ name: z.string().trim().min(1).max(64), description: z .string() @@ -9,14 +18,7 @@ const manageAppSchema = z.object({ .transform((value) => (value.length === 0 ? null : value)) .nullable(), iconUrl: z.string().trim().min(1), - href: z - .string() - .trim() - .url() - .regex(/^https?:\/\//) // Only allow http and https for security reasons (javascript: is not allowed) - .or(z.literal("")) - .transform((value) => (value.length === 0 ? null : value)) - .nullable(), + href: appHrefSchema, pingUrl: z .string() .trim() @@ -27,12 +29,8 @@ const manageAppSchema = z.object({ .nullable(), }); -const editAppSchema = manageAppSchema.and(z.object({ id: z.string() })); +export const appCreateManySchema = z + .array(appManageSchema.omit({ iconUrl: true }).and(z.object({ iconUrl: z.string().min(1).nullable() }))) + .min(1); -export const appSchemas = { - manage: manageAppSchema, - createMany: z - .array(manageAppSchema.omit({ iconUrl: true }).and(z.object({ iconUrl: z.string().min(1).nullable() }))) - .min(1), - edit: editAppSchema, -}; +export const appEditSchema = appManageSchema.and(z.object({ id: z.string() })); diff --git a/packages/validation/src/board.ts b/packages/validation/src/board.ts index 093d2042a..87facae3e 100644 --- a/packages/validation/src/board.ts +++ b/packages/validation/src/board.ts @@ -18,27 +18,28 @@ const hexColorNullableSchema = hexColorSchema .nullable() .transform((value) => (value?.trim().length === 0 ? null : value)); -const boardNameSchema = z +export const boardNameSchema = z .string() .min(1) .max(255) .regex(/^[A-Za-z0-9-\\_]*$/); +export const boardColumnCountSchema = z.number().min(1).max(24); -const byNameSchema = z.object({ +export const boardByNameSchema = z.object({ name: boardNameSchema, }); -const renameSchema = z.object({ +export const boardRenameSchema = z.object({ id: z.string(), name: boardNameSchema, }); -const duplicateSchema = z.object({ +export const boardDuplicateSchema = z.object({ id: z.string(), name: boardNameSchema, }); -const changeVisibilitySchema = z.object({ +export const boardChangeVisibilitySchema = z.object({ id: z.string(), visibility: z.enum(["public", "private"]), }); @@ -48,7 +49,7 @@ const trimmedNullableString = z .nullable() .transform((value) => (value?.trim().length === 0 ? null : value)); -const savePartialSettingsSchema = z +export const boardSavePartialSettingsSchema = z .object({ pageTitle: trimmedNullableString, metaTitle: trimmedNullableString, @@ -68,52 +69,28 @@ const savePartialSettingsSchema = z }) .partial(); -const saveLayoutsSchema = z.object({ +export const boardSaveLayoutsSchema = z.object({ id: z.string(), layouts: z.array( z.object({ id: z.string(), name: z.string().trim().nonempty().max(32), - columnCount: z.number().min(1).max(24), + columnCount: boardColumnCountSchema, breakpoint: z.number().min(0).max(32767), }), ), }); -const saveSchema = z.object({ +export const boardSaveSchema = z.object({ id: z.string(), sections: z.array(sectionSchema), items: z.array(commonItemSchema), }); -const createSchema = z.object({ name: boardNameSchema, columnCount: z.number().min(1).max(24), isPublic: z.boolean() }); - -const permissionsSchema = z.object({ - id: z.string(), -}); - -const savePermissionsSchema = createSavePermissionsSchema(zodEnumFromArray(boardPermissions)); - -z.object({ - entityId: z.string(), - permissions: z.array( - z.object({ - principalId: z.string(), - permission: zodEnumFromArray(boardPermissions), - }), - ), -}); - -export const boardSchemas = { +export const boardCreateSchema = z.object({ name: boardNameSchema, - byName: byNameSchema, - savePartialSettings: savePartialSettingsSchema, - saveLayouts: saveLayoutsSchema, - save: saveSchema, - create: createSchema, - duplicate: duplicateSchema, - rename: renameSchema, - changeVisibility: changeVisibilitySchema, - permissions: permissionsSchema, - savePermissions: savePermissionsSchema, -}; + columnCount: boardColumnCountSchema, + isPublic: z.boolean(), +}); + +export const boardSavePermissionsSchema = createSavePermissionsSchema(zodEnumFromArray(boardPermissions)); diff --git a/packages/validation/src/certificates.ts b/packages/validation/src/certificates.ts index 7ad6d3aeb..08a11ef11 100644 --- a/packages/validation/src/certificates.ts +++ b/packages/validation/src/certificates.ts @@ -2,7 +2,7 @@ import { z } from "zod"; import { createCustomErrorParams } from "./form/i18n"; -const validFileNameSchema = z.string().regex(/^[\w\-. ]+$/); +export const certificateValidFileNameSchema = z.string().regex(/^[\w\-. ]+$/); export const superRefineCertificateFile = (value: File | null, context: z.RefinementCtx) => { if (!value) { @@ -13,7 +13,7 @@ export const superRefineCertificateFile = (value: File | null, context: z.Refine }); } - const result = validFileNameSchema.safeParse(value.name); + const result = certificateValidFileNameSchema.safeParse(value.name); if (!result.success) { return context.addIssue({ code: "custom", @@ -46,7 +46,3 @@ export const superRefineCertificateFile = (value: File | null, context: z.Refine return null; }; - -export const certificateSchemas = { - validFileNameSchema, -}; diff --git a/packages/validation/src/common.ts b/packages/validation/src/common.ts index 3decf20be..20c7a175b 100644 --- a/packages/validation/src/common.ts +++ b/packages/validation/src/common.ts @@ -1,6 +1,6 @@ import { z } from "zod"; -const paginatedSchema = z.object({ +export const paginatedSchema = z.object({ search: z.string().optional(), pageSize: z.number().int().positive().default(10), page: z.number().int().positive().default(1), @@ -10,26 +10,7 @@ export const byIdSchema = z.object({ id: z.string(), }); -const searchSchema = z.object({ +export const searchSchema = z.object({ query: z.string(), limit: z.number().int().positive().default(10), }); - -const mediaRequestOptionsSchema = z.object({ - mediaId: z.number(), - mediaType: z.enum(["tv", "movie"]), -}); - -const requestMediaSchema = z.object({ - mediaType: z.enum(["tv", "movie"]), - mediaId: z.number(), - seasons: z.array(z.number().min(0)).optional(), -}); - -export const commonSchemas = { - paginated: paginatedSchema, - byId: byIdSchema, - search: searchSchema, - mediaRequestOptions: mediaRequestOptionsSchema, - requestMedia: requestMediaSchema, -}; diff --git a/packages/validation/src/group.ts b/packages/validation/src/group.ts index 25e1fa4ed..d621a93d4 100644 --- a/packages/validation/src/group.ts +++ b/packages/validation/src/group.ts @@ -5,7 +5,7 @@ import { everyoneGroup, groupPermissionKeys } from "@homarr/definitions"; import { byIdSchema } from "./common"; import { zodEnumFromArray } from "./enums"; -const createSchema = z.object({ +export const groupCreateSchema = z.object({ name: z .string() .trim() @@ -16,35 +16,25 @@ const createSchema = z.object({ }), }); -const updateSchema = createSchema.merge(byIdSchema); +export const groupUpdateSchema = groupCreateSchema.merge(byIdSchema); -const settingsSchema = z.object({ +export const groupSettingsSchema = z.object({ homeBoardId: z.string().nullable(), mobileHomeBoardId: z.string().nullable(), }); -const savePartialSettingsSchema = z.object({ +export const groupSavePartialSettingsSchema = z.object({ id: z.string(), - settings: settingsSchema.partial(), + settings: groupSettingsSchema.partial(), }); -const savePermissionsSchema = z.object({ +export const groupSavePermissionsSchema = z.object({ groupId: z.string(), permissions: z.array(zodEnumFromArray(groupPermissionKeys)), }); -const savePositionsSchema = z.object({ +export const groupSavePositionsSchema = z.object({ positions: z.array(z.string()), }); -const groupUserSchema = z.object({ groupId: z.string(), userId: z.string() }); - -export const groupSchemas = { - create: createSchema, - update: updateSchema, - savePermissions: savePermissionsSchema, - groupUser: groupUserSchema, - savePartialSettings: savePartialSettingsSchema, - settings: settingsSchema, - savePositions: savePositionsSchema, -}; +export const groupUserSchema = z.object({ groupId: z.string(), userId: z.string() }); diff --git a/packages/validation/src/icons.ts b/packages/validation/src/icons.ts index c621ecea5..6d134097f 100644 --- a/packages/validation/src/icons.ts +++ b/packages/validation/src/icons.ts @@ -1,10 +1,6 @@ import { z } from "zod"; -const findIconsSchema = z.object({ +export const iconsFindSchema = z.object({ searchText: z.string().optional(), limitPerGroup: z.number().min(1).max(500).default(12), }); - -export const iconsSchemas = { - findIcons: findIconsSchema, -}; diff --git a/packages/validation/src/index.ts b/packages/validation/src/index.ts deleted file mode 100644 index 7bc9e9407..000000000 --- a/packages/validation/src/index.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { appSchemas } from "./app"; -import { boardSchemas } from "./board"; -import { certificateSchemas } from "./certificates"; -import { commonSchemas } from "./common"; -import { groupSchemas } from "./group"; -import { iconsSchemas } from "./icons"; -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"; - -export const validation = { - user: userSchemas, - group: groupSchemas, - integration: integrationSchemas, - board: boardSchemas, - app: appSchemas, - widget: widgetSchemas, - location: locationSchemas, - icons: iconsSchemas, - searchEngine: searchEngineSchemas, - media: mediaSchemas, - settings: settingsSchemas, - common: commonSchemas, - certificates: certificateSchemas, -}; - -export { - sectionSchema, - itemAdvancedOptionsSchema, - sharedItemSchema, - dynamicSectionOptionsSchema, - type BoardItemAdvancedOptions, - type BoardItemIntegration, -} from "./shared"; -export { superRefineCertificateFile } from "./certificates"; -export { passwordRequirements, usernameSchema } from "./user"; -export { supportedMediaUploadFormats } from "./media"; -export { zodEnumFromArray, zodUnionFromArray } from "./enums"; diff --git a/packages/validation/src/integration.ts b/packages/validation/src/integration.ts index e9606a268..00b3783fa 100644 --- a/packages/validation/src/integration.ts +++ b/packages/validation/src/integration.ts @@ -5,7 +5,7 @@ import { integrationKinds, integrationPermissions, integrationSecretKinds } from import { zodEnumFromArray } from "./enums"; import { createSavePermissionsSchema } from "./permissions"; -const integrationCreateSchema = z.object({ +export const integrationCreateSchema = z.object({ name: z.string().nonempty().max(127), url: z .string() @@ -21,7 +21,7 @@ const integrationCreateSchema = z.object({ attemptSearchEngineCreation: z.boolean(), }); -const integrationUpdateSchema = z.object({ +export const integrationUpdateSchema = z.object({ id: z.string().cuid2(), name: z.string().nonempty().max(127), url: z.string().url(), @@ -33,29 +33,4 @@ const integrationUpdateSchema = z.object({ ), }); -const idSchema = z.object({ - id: z.string(), -}); - -const testConnectionSchema = z.object({ - id: z.string().cuid2().nullable(), // Is used to use existing secrets if they have not been updated - url: z.string().url(), - kind: zodEnumFromArray(integrationKinds), - secrets: z.array( - z.object({ - kind: zodEnumFromArray(integrationSecretKinds), - value: z.string().nullable(), - }), - ), -}); - -const savePermissionsSchema = createSavePermissionsSchema(zodEnumFromArray(integrationPermissions)); - -export const integrationSchemas = { - create: integrationCreateSchema, - update: integrationUpdateSchema, - delete: idSchema, - byId: idSchema, - testConnection: testConnectionSchema, - savePermissions: savePermissionsSchema, -}; +export const integrationSavePermissionsSchema = createSavePermissionsSchema(zodEnumFromArray(integrationPermissions)); diff --git a/packages/validation/src/location.ts b/packages/validation/src/location.ts deleted file mode 100644 index b8d71ca3c..000000000 --- a/packages/validation/src/location.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { z } from "zod"; - -const citySchema = z.object({ - id: z.number(), - name: z.string(), - country: z.string().optional(), - country_code: z.string().optional(), - latitude: z.number(), - longitude: z.number(), - population: z.number().optional(), -}); - -const searchCityInput = z.object({ - query: z.string(), -}); - -const searchCityOutput = z - .object({ - results: z.array(citySchema), - }) - .or( - z - .object({ - generationtime_ms: z.number(), - }) - .refine((data) => Object.keys(data).length === 1, { message: "Invalid response" }) - .transform(() => ({ results: [] })), // We fallback to empty array if no results - ); - -export const locationSchemas = { - searchCity: { - input: searchCityInput, - output: searchCityOutput, - }, -}; diff --git a/packages/validation/src/media.ts b/packages/validation/src/media.ts index d18e87589..9fd47256f 100644 --- a/packages/validation/src/media.ts +++ b/packages/validation/src/media.ts @@ -5,7 +5,7 @@ import { createCustomErrorParams } from "./form/i18n"; export const supportedMediaUploadFormats = ["image/png", "image/jpeg", "image/webp", "image/gif", "image/svg+xml"]; -export const uploadMediaSchema = zfd.formData({ +export const mediaUploadSchema = zfd.formData({ file: zfd.file().superRefine((value: File | null, context: z.RefinementCtx) => { if (!value) { return context.addIssue({ @@ -38,7 +38,3 @@ export const uploadMediaSchema = zfd.formData({ return null; }), }); - -export const mediaSchemas = { - uploadMedia: uploadMediaSchema, -}; diff --git a/packages/validation/src/search-engine.ts b/packages/validation/src/search-engine.ts index 720f5fb86..c84c77d9e 100644 --- a/packages/validation/src/search-engine.ts +++ b/packages/validation/src/search-engine.ts @@ -13,7 +13,7 @@ const fromIntegrationSearchEngine = z.object({ integrationId: z.string().optional(), }); -const manageSearchEngineSchema = z.object({ +const baseSearchEngineManageSchema = z.object({ name: z.string().min(1).max(64), short: z.string().min(1).max(8), iconUrl: z.string().min(1), @@ -21,21 +21,18 @@ const manageSearchEngineSchema = z.object({ }); const createManageSearchEngineSchema = ( - callback: (schema: typeof manageSearchEngineSchema) => T, + callback: (schema: typeof baseSearchEngineManageSchema) => T, ) => z .discriminatedUnion("type", [genericSearchEngine, fromIntegrationSearchEngine]) - .and(callback(manageSearchEngineSchema)); + .and(callback(baseSearchEngineManageSchema)); -const editSearchEngineSchema = createManageSearchEngineSchema((schema) => +export const searchEngineManageSchema = createManageSearchEngineSchema((schema) => schema); + +export const searchEngineEditSchema = createManageSearchEngineSchema((schema) => schema .extend({ id: z.string(), }) .omit({ short: true }), ); - -export const searchEngineSchemas = { - manage: createManageSearchEngineSchema((schema) => schema), - edit: editSearchEngineSchema, -}; diff --git a/packages/validation/src/settings.ts b/packages/validation/src/settings.ts index 8eb5225ac..7110dfb3e 100644 --- a/packages/validation/src/settings.ts +++ b/packages/validation/src/settings.ts @@ -1,6 +1,6 @@ import { z } from "zod"; -const initSettingsSchema = z.object({ +export const settingsInitSchema = z.object({ analytics: z.object({ enableGeneral: z.boolean(), enableWidgetData: z.boolean(), @@ -14,7 +14,3 @@ const initSettingsSchema = z.object({ noSiteLinksSearchBox: z.boolean(), }), }); - -export const settingsSchemas = { - init: initSettingsSchema, -}; diff --git a/packages/validation/src/user.ts b/packages/validation/src/user.ts index 9e7c9da73..b8d78e8ee 100644 --- a/packages/validation/src/user.ts +++ b/packages/validation/src/user.ts @@ -22,7 +22,7 @@ export const passwordRequirements = [ value: keyof TranslationObject["user"]["field"]["password"]["requirement"]; }[]; -const passwordSchema = z +export const userPasswordSchema = z .string() .min(8) .max(255) @@ -51,39 +51,39 @@ const addConfirmPasswordRefinement = ((value) => z.number().min(0).max(6).safeParse(value).success), }); -const pingIconsEnabledSchema = z.object({ +export const userPingIconsEnabledSchema = z.object({ pingIconsEnabled: z.boolean(), }); - -export const userSchemas = { - signIn: signInSchema, - registration: registrationSchema, - registrationApi: registrationSchemaApi, - init: initUserSchema, - create: createUserSchema, - baseCreate: baseCreateUserSchema, - password: passwordSchema, - editProfile: editProfileSchema, - changePassword: changePasswordSchema, - changeHomeBoards: changeHomeBoardSchema, - changeSearchPreferences: changeSearchPreferencesSchema, - changePasswordApi: changePasswordApiSchema, - changeColorScheme: changeColorSchemeSchema, - firstDayOfWeek: firstDayOfWeekSchema, - pingIconsEnabled: pingIconsEnabledSchema, -}; diff --git a/packages/validation/src/widgets/index.ts b/packages/validation/src/widgets/index.ts deleted file mode 100644 index bf007766d..000000000 --- a/packages/validation/src/widgets/index.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { weatherWidgetSchemas } from "./weather"; - -export const widgetSchemas = { - weather: weatherWidgetSchemas, -}; diff --git a/packages/validation/src/widgets/media-request.ts b/packages/validation/src/widgets/media-request.ts new file mode 100644 index 000000000..aa0f85c6e --- /dev/null +++ b/packages/validation/src/widgets/media-request.ts @@ -0,0 +1,12 @@ +import { z } from "zod"; + +export const mediaRequestOptionsSchema = z.object({ + mediaId: z.number(), + mediaType: z.enum(["tv", "movie"]), +}); + +export const mediaRequestRequestSchema = z.object({ + mediaType: z.enum(["tv", "movie"]), + mediaId: z.number(), + seasons: z.array(z.number().min(0)).optional(), +}); diff --git a/packages/validation/src/widgets/weather.ts b/packages/validation/src/widgets/weather.ts deleted file mode 100644 index 6b9bf74b9..000000000 --- a/packages/validation/src/widgets/weather.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { z } from "zod"; - -export const atLocationInput = z.object({ - longitude: z.number(), - latitude: z.number(), -}); - -export const atLocationOutput = z.object({ - current_weather: z.object({ - weathercode: z.number(), - temperature: z.number(), - windspeed: z.number(), - }), - daily: z.object({ - time: z.array(z.string()), - weathercode: z.array(z.number()), - temperature_2m_max: z.array(z.number()), - temperature_2m_min: z.array(z.number()), - sunrise: z.array(z.string()), - sunset: z.array(z.string()), - wind_speed_10m_max: z.array(z.number()), - wind_gusts_10m_max: z.array(z.number()), - }), -}); - -export const weatherWidgetSchemas = { - atLocationInput, - atLocationOutput, -}; diff --git a/packages/widgets/src/modals/widget-advanced-options-modal.tsx b/packages/widgets/src/modals/widget-advanced-options-modal.tsx index f957b100a..f2dd98db9 100644 --- a/packages/widgets/src/modals/widget-advanced-options-modal.tsx +++ b/packages/widgets/src/modals/widget-advanced-options-modal.tsx @@ -6,7 +6,7 @@ import { useForm } from "@homarr/form"; import { createModal } from "@homarr/modals"; import { useI18n } from "@homarr/translation/client"; import { TextMultiSelect } from "@homarr/ui"; -import type { BoardItemAdvancedOptions } from "@homarr/validation"; +import type { BoardItemAdvancedOptions } from "@homarr/validation/shared"; interface InnerProps { advancedOptions: BoardItemAdvancedOptions; diff --git a/packages/widgets/src/modals/widget-edit-modal.tsx b/packages/widgets/src/modals/widget-edit-modal.tsx index 6667637ed..05fca7fd4 100644 --- a/packages/widgets/src/modals/widget-edit-modal.tsx +++ b/packages/widgets/src/modals/widget-edit-modal.tsx @@ -10,7 +10,7 @@ import { zodResolver } from "@homarr/form"; import { createModal, useModalAction } from "@homarr/modals"; import type { SettingsContextProps } from "@homarr/settings"; import { useI18n } from "@homarr/translation/client"; -import { zodErrorMap } from "@homarr/validation/form"; +import { zodErrorMap } from "@homarr/validation/form/i18n"; import { widgetImports } from ".."; import { getInputForType } from "../_inputs";