refactor: remove central validation export to improve typescript performance (#2810)

* refactor: remove central validation export to improve typescript performance

* fix: missing package exports change in validation package

* chore: address pull request feedback
This commit is contained in:
Meier Lukas
2025-04-06 12:37:28 +02:00
committed by GitHub
parent c1cd563048
commit 75ba3f2ae7
81 changed files with 450 additions and 582 deletions

View File

@@ -9,7 +9,7 @@ import { useZodForm } from "@homarr/form";
import { showErrorNotification, showSuccessNotification } from "@homarr/notifications"; import { showErrorNotification, showSuccessNotification } from "@homarr/notifications";
import { useScopedI18n } from "@homarr/translation/client"; import { useScopedI18n } from "@homarr/translation/client";
import { CustomPasswordInput } from "@homarr/ui"; import { CustomPasswordInput } from "@homarr/ui";
import { validation } from "@homarr/validation"; import { userRegistrationSchema } from "@homarr/validation/user";
interface RegistrationFormProps { interface RegistrationFormProps {
invite: { invite: {
@@ -22,7 +22,7 @@ export const RegistrationForm = ({ invite }: RegistrationFormProps) => {
const t = useScopedI18n("user"); const t = useScopedI18n("user");
const router = useRouter(); const router = useRouter();
const { mutate, isPending } = clientApi.user.register.useMutation(); const { mutate, isPending } = clientApi.user.register.useMutation();
const form = useZodForm(validation.user.registration, { const form = useZodForm(userRegistrationSchema, {
initialValues: { initialValues: {
username: "", username: "",
password: "", password: "",
@@ -30,7 +30,7 @@ export const RegistrationForm = ({ invite }: RegistrationFormProps) => {
}, },
}); });
const handleSubmit = (values: z.infer<typeof validation.user.registration>) => { const handleSubmit = (values: z.infer<typeof userRegistrationSchema>) => {
mutate( mutate(
{ {
...values, ...values,

View File

@@ -13,7 +13,7 @@ import type { useForm } from "@homarr/form";
import { useZodForm } from "@homarr/form"; import { useZodForm } from "@homarr/form";
import { showErrorNotification, showSuccessNotification } from "@homarr/notifications"; import { showErrorNotification, showSuccessNotification } from "@homarr/notifications";
import { useScopedI18n } from "@homarr/translation/client"; import { useScopedI18n } from "@homarr/translation/client";
import { validation } from "@homarr/validation"; import { userSignInSchema } from "@homarr/validation/user";
interface LoginFormProps { interface LoginFormProps {
providers: string[]; providers: string[];
@@ -22,7 +22,7 @@ interface LoginFormProps {
callbackUrl: string; 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) => { export const LoginForm = ({ providers, oidcClientName, isOidcAutoLoginEnabled, callbackUrl }: LoginFormProps) => {
const t = useScopedI18n("user"); const t = useScopedI18n("user");

View File

@@ -20,7 +20,7 @@ import { useDisclosure } from "@mantine/hooks";
import { useZodForm } from "@homarr/form"; import { useZodForm } from "@homarr/form";
import { useI18n } from "@homarr/translation/client"; import { useI18n } from "@homarr/translation/client";
import { validation } from "@homarr/validation"; import { boardSavePartialSettingsSchema } from "@homarr/validation/board";
import type { Board } from "../../_types"; import type { Board } from "../../_types";
import { generateColors } from "../../(content)/_theme"; import { generateColors } from "../../(content)/_theme";
@@ -35,7 +35,7 @@ const hexRegex = /^#[0-9a-fA-F]{6}$/;
const progressPercentageLabel = (value: number) => `${value}%`; const progressPercentageLabel = (value: number) => `${value}%`;
export const ColorSettingsContent = ({ board }: Props) => { export const ColorSettingsContent = ({ board }: Props) => {
const form = useZodForm(validation.board.savePartialSettings, { const form = useZodForm(boardSavePartialSettingsSchema, {
initialValues: { initialValues: {
primaryColor: board.primaryColor, primaryColor: board.primaryColor,
secondaryColor: board.secondaryColor, secondaryColor: board.secondaryColor,

View File

@@ -12,7 +12,7 @@ import type { TranslationObject } from "@homarr/translation";
import { useI18n } from "@homarr/translation/client"; import { useI18n } from "@homarr/translation/client";
import type { SelectItemWithDescriptionBadge } from "@homarr/ui"; import type { SelectItemWithDescriptionBadge } from "@homarr/ui";
import { SelectWithDescriptionBadge } 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 type { Board } from "../../_types";
import { useSavePartialSettingsMutation } from "./_shared"; import { useSavePartialSettingsMutation } from "./_shared";
@@ -24,7 +24,7 @@ export const BackgroundSettingsContent = ({ board }: Props) => {
const t = useI18n(); const t = useI18n();
const { data: session } = useSession(); const { data: session } = useSession();
const { mutate: savePartialSettings, isPending } = useSavePartialSettingsMutation(board); const { mutate: savePartialSettings, isPending } = useSavePartialSettingsMutation(board);
const form = useZodForm(validation.board.savePartialSettings, { const form = useZodForm(boardSavePartialSettingsSchema, {
initialValues: { initialValues: {
backgroundImageUrl: board.backgroundImageUrl ?? "", backgroundImageUrl: board.backgroundImageUrl ?? "",
backgroundImageAttachment: board.backgroundImageAttachment, backgroundImageAttachment: board.backgroundImageAttachment,

View File

@@ -8,7 +8,7 @@ import { useUpdateBoard } from "@homarr/boards/updater";
import { useZodForm } from "@homarr/form"; import { useZodForm } from "@homarr/form";
import { IconPicker } from "@homarr/forms-collection"; import { IconPicker } from "@homarr/forms-collection";
import { useI18n } from "@homarr/translation/client"; import { useI18n } from "@homarr/translation/client";
import { validation } from "@homarr/validation"; import { boardSavePartialSettingsSchema } from "@homarr/validation/board";
import { createMetaTitle } from "~/metadata"; import { createMetaTitle } from "~/metadata";
import type { Board } from "../../_types"; import type { Board } from "../../_types";
@@ -28,7 +28,7 @@ export const GeneralSettingsContent = ({ board }: Props) => {
const { mutate: savePartialSettings, isPending } = useSavePartialSettingsMutation(board); const { mutate: savePartialSettings, isPending } = useSavePartialSettingsMutation(board);
const form = useZodForm( const form = useZodForm(
validation.board.savePartialSettings boardSavePartialSettingsSchema
.pick({ .pick({
pageTitle: true, pageTitle: true,
logoImageUrl: true, logoImageUrl: true,

View File

@@ -6,7 +6,7 @@ import { clientApi } from "@homarr/api/client";
import { createId } from "@homarr/db/client"; import { createId } from "@homarr/db/client";
import { useZodForm } from "@homarr/form"; import { useZodForm } from "@homarr/form";
import { useI18n } from "@homarr/translation/client"; import { useI18n } from "@homarr/translation/client";
import { validation } from "@homarr/validation"; import { boardSaveLayoutsSchema } from "@homarr/validation/board";
import type { Board } from "../../_types"; import type { Board } from "../../_types";
@@ -22,7 +22,7 @@ export const LayoutSettingsContent = ({ board }: Props) => {
void utils.board.getHomeBoard.invalidate(); void utils.board.getHomeBoard.invalidate();
}, },
}); });
const form = useZodForm(validation.board.saveLayouts.omit({ id: true }).required(), { const form = useZodForm(boardSaveLayoutsSchema.omit({ id: true }).required(), {
initialValues: { initialValues: {
layouts: board.layouts, layouts: board.layouts,
}, },

View File

@@ -8,18 +8,18 @@ import { clientApi } from "@homarr/api/client";
import { revalidatePathActionAsync } from "@homarr/common/client"; import { revalidatePathActionAsync } from "@homarr/common/client";
import { useZodForm } from "@homarr/form"; import { useZodForm } from "@homarr/form";
import { useI18n } from "@homarr/translation/client"; import { useI18n } from "@homarr/translation/client";
import { validation } from "@homarr/validation"; import { groupCreateSchema } from "@homarr/validation/group";
export const InitGroup = () => { export const InitGroup = () => {
const t = useI18n(); const t = useI18n();
const { mutateAsync } = clientApi.group.createInitialExternalGroup.useMutation(); const { mutateAsync } = clientApi.group.createInitialExternalGroup.useMutation();
const form = useZodForm(validation.group.create, { const form = useZodForm(groupCreateSchema, {
initialValues: { initialValues: {
name: "", name: "",
}, },
}); });
const handleSubmitAsync = async (values: z.infer<typeof validation.group.create>) => { const handleSubmitAsync = async (values: z.infer<typeof groupCreateSchema>) => {
await mutateAsync(values, { await mutateAsync(values, {
async onSuccess() { async onSuccess() {
await revalidatePathActionAsync("/init"); await revalidatePathActionAsync("/init");

View File

@@ -12,13 +12,13 @@ import type { CheckboxProps } from "@homarr/form/types";
import { defaultServerSettings } from "@homarr/server-settings"; import { defaultServerSettings } from "@homarr/server-settings";
import type { TranslationObject } from "@homarr/translation"; import type { TranslationObject } from "@homarr/translation";
import { useI18n, useScopedI18n } from "@homarr/translation/client"; import { useI18n, useScopedI18n } from "@homarr/translation/client";
import { validation } from "@homarr/validation"; import { settingsInitSchema } from "@homarr/validation/settings";
export const InitSettings = () => { export const InitSettings = () => {
const tSection = useScopedI18n("management.page.settings.section"); const tSection = useScopedI18n("management.page.settings.section");
const t = useI18n(); const t = useI18n();
const { mutateAsync } = clientApi.serverSettings.initSettings.useMutation(); 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 }) => { form.watch("analytics.enableGeneral", ({ value }) => {
if (!value) { if (!value) {
@@ -30,7 +30,7 @@ export const InitSettings = () => {
} }
}); });
const handleSubmitAsync = async (values: z.infer<typeof validation.settings.init>) => { const handleSubmitAsync = async (values: z.infer<typeof settingsInitSchema>) => {
await mutateAsync(values, { await mutateAsync(values, {
async onSuccess() { async onSuccess() {
await revalidatePathActionAsync("/init"); await revalidatePathActionAsync("/init");

View File

@@ -10,13 +10,13 @@ import { useZodForm } from "@homarr/form";
import { showErrorNotification, showSuccessNotification } from "@homarr/notifications"; import { showErrorNotification, showSuccessNotification } from "@homarr/notifications";
import { useScopedI18n } from "@homarr/translation/client"; import { useScopedI18n } from "@homarr/translation/client";
import { CustomPasswordInput } from "@homarr/ui"; import { CustomPasswordInput } from "@homarr/ui";
import { validation } from "@homarr/validation"; import { userInitSchema } from "@homarr/validation/user";
export const InitUserForm = () => { export const InitUserForm = () => {
const t = useScopedI18n("user"); const t = useScopedI18n("user");
const tUser = useScopedI18n("init.step.user"); const tUser = useScopedI18n("init.step.user");
const { mutateAsync, isPending } = clientApi.user.initUser.useMutation(); const { mutateAsync, isPending } = clientApi.user.initUser.useMutation();
const form = useZodForm(validation.user.init, { const form = useZodForm(userInitSchema, {
initialValues: { initialValues: {
username: "", username: "",
password: "", password: "",
@@ -74,4 +74,4 @@ export const InitUserForm = () => {
); );
}; };
type FormType = z.infer<typeof validation.user.init>; type FormType = z.infer<typeof userInitSchema>;

View File

@@ -10,7 +10,7 @@ import { revalidatePathActionAsync } from "@homarr/common/client";
import { AppForm } from "@homarr/forms-collection"; import { AppForm } from "@homarr/forms-collection";
import { showErrorNotification, showSuccessNotification } from "@homarr/notifications"; import { showErrorNotification, showSuccessNotification } from "@homarr/notifications";
import { useI18n, useScopedI18n } from "@homarr/translation/client"; import { useI18n, useScopedI18n } from "@homarr/translation/client";
import type { validation } from "@homarr/validation"; import type { appManageSchema } from "@homarr/validation/app";
interface AppEditFormProps { interface AppEditFormProps {
app: RouterOutputs["app"]["byId"]; app: RouterOutputs["app"]["byId"];
@@ -40,7 +40,7 @@ export const AppEditForm = ({ app }: AppEditFormProps) => {
}); });
const handleSubmit = useCallback( const handleSubmit = useCallback(
(values: z.infer<typeof validation.app.manage>) => { (values: z.infer<typeof appManageSchema>) => {
mutate({ mutate({
id: app.id, id: app.id,
...values, ...values,

View File

@@ -14,7 +14,7 @@ import { convertIntegrationTestConnectionError } from "@homarr/integrations/clie
import { useConfirmModal } from "@homarr/modals"; import { useConfirmModal } from "@homarr/modals";
import { showErrorNotification, showSuccessNotification } from "@homarr/notifications"; import { showErrorNotification, showSuccessNotification } from "@homarr/notifications";
import { useI18n } from "@homarr/translation/client"; 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 { SecretCard } from "../../_components/secrets/integration-secret-card";
import { IntegrationSecretInput } from "../../_components/secrets/integration-secret-inputs"; import { IntegrationSecretInput } from "../../_components/secrets/integration-secret-inputs";
@@ -32,7 +32,7 @@ export const EditIntegrationForm = ({ integration }: EditIntegrationForm) => {
) ?? getDefaultSecretKinds(integration.kind); ) ?? getDefaultSecretKinds(integration.kind);
const router = useRouter(); const router = useRouter();
const form = useZodForm(validation.integration.update.omit({ id: true }), { const form = useZodForm(integrationUpdateSchema.omit({ id: true }), {
initialValues: { initialValues: {
name: integration.name, name: integration.name,
url: integration.url, url: integration.url,
@@ -141,4 +141,4 @@ export const EditIntegrationForm = ({ integration }: EditIntegrationForm) => {
); );
}; };
type FormType = Omit<z.infer<typeof validation.integration.update>, "id">; type FormType = Omit<z.infer<typeof integrationUpdateSchema>, "id">;

View File

@@ -27,20 +27,21 @@ import { useZodForm } from "@homarr/form";
import { convertIntegrationTestConnectionError } from "@homarr/integrations/client"; import { convertIntegrationTestConnectionError } from "@homarr/integrations/client";
import { showErrorNotification, showSuccessNotification } from "@homarr/notifications"; import { showErrorNotification, showSuccessNotification } from "@homarr/notifications";
import { useI18n, useScopedI18n } from "@homarr/translation/client"; 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"; import { IntegrationSecretInput } from "../_components/secrets/integration-secret-inputs";
interface NewIntegrationFormProps { interface NewIntegrationFormProps {
searchParams: Partial<z.infer<typeof validation.integration.create>> & { searchParams: Partial<z.infer<typeof integrationCreateSchema>> & {
kind: IntegrationKind; kind: IntegrationKind;
}; };
} }
const formSchema = validation.integration.create.omit({ kind: true }).and( const formSchema = integrationCreateSchema.omit({ kind: true }).and(
z.object({ z.object({
createApp: z.boolean(), createApp: z.boolean(),
appHref: validation.app.manage.shape.href, appHref: appHrefSchema,
}), }),
); );

View File

@@ -7,14 +7,14 @@ import type { IntegrationKind } from "@homarr/definitions";
import { getIntegrationName, integrationKinds } from "@homarr/definitions"; import { getIntegrationName, integrationKinds } from "@homarr/definitions";
import { getScopedI18n } from "@homarr/translation/server"; import { getScopedI18n } from "@homarr/translation/server";
import { IntegrationAvatar } from "@homarr/ui"; 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 { DynamicBreadcrumb } from "~/components/navigation/dynamic-breadcrumb";
import { NewIntegrationForm } from "./_integration-new-form"; import { NewIntegrationForm } from "./_integration-new-form";
interface NewIntegrationPageProps { interface NewIntegrationPageProps {
searchParams: Promise< searchParams: Promise<
Partial<z.infer<typeof validation.integration.create>> & { Partial<z.infer<typeof integrationCreateSchema>> & {
kind: IntegrationKind; kind: IntegrationKind;
} }
>; >;

View File

@@ -12,9 +12,9 @@ import { useZodForm } from "@homarr/form";
import { IconPicker } from "@homarr/forms-collection"; import { IconPicker } from "@homarr/forms-collection";
import type { TranslationFunction } from "@homarr/translation"; import type { TranslationFunction } from "@homarr/translation";
import { useI18n } from "@homarr/translation/client"; import { useI18n } from "@homarr/translation/client";
import { validation } from "@homarr/validation"; import { searchEngineManageSchema } from "@homarr/validation/search-engine";
type FormType = z.infer<typeof validation.searchEngine.manage>; type FormType = z.infer<typeof searchEngineManageSchema>;
interface SearchEngineFormProps { interface SearchEngineFormProps {
submitButtonTranslation: (t: TranslationFunction) => string; submitButtonTranslation: (t: TranslationFunction) => string;
@@ -30,7 +30,7 @@ export const SearchEngineForm = (props: SearchEngineFormProps) => {
const [integrationData] = clientApi.integration.allThatSupportSearch.useSuspenseQuery(); const [integrationData] = clientApi.integration.allThatSupportSearch.useSuspenseQuery();
const form = useZodForm(validation.searchEngine.manage, { const form = useZodForm(searchEngineManageSchema, {
initialValues: initialValues ?? { initialValues: initialValues ?? {
name: "", name: "",
short: "", short: "",

View File

@@ -10,7 +10,7 @@ import { revalidatePathActionAsync } from "@homarr/common/client";
import { showErrorNotification, showSuccessNotification } from "@homarr/notifications"; import { showErrorNotification, showSuccessNotification } from "@homarr/notifications";
import type { TranslationFunction } from "@homarr/translation"; import type { TranslationFunction } from "@homarr/translation";
import { useScopedI18n } from "@homarr/translation/client"; import { useScopedI18n } from "@homarr/translation/client";
import type { validation } from "@homarr/validation"; import type { searchEngineManageSchema } from "@homarr/validation/search-engine";
import { SearchEngineForm } from "../../_form"; import { SearchEngineForm } from "../../_form";
@@ -41,7 +41,7 @@ export const SearchEngineEditForm = ({ searchEngine }: SearchEngineEditFormProps
}); });
const handleSubmit = useCallback( const handleSubmit = useCallback(
(values: z.infer<typeof validation.searchEngine.manage>) => { (values: z.infer<typeof searchEngineManageSchema>) => {
mutate({ mutate({
id: searchEngine.id, id: searchEngine.id,
...values, ...values,

View File

@@ -9,7 +9,7 @@ import { revalidatePathActionAsync } from "@homarr/common/client";
import { showErrorNotification, showSuccessNotification } from "@homarr/notifications"; import { showErrorNotification, showSuccessNotification } from "@homarr/notifications";
import type { TranslationFunction } from "@homarr/translation"; import type { TranslationFunction } from "@homarr/translation";
import { useScopedI18n } from "@homarr/translation/client"; import { useScopedI18n } from "@homarr/translation/client";
import type { validation } from "@homarr/validation"; import type { searchEngineManageSchema } from "@homarr/validation/search-engine";
import { SearchEngineForm } from "../_form"; import { SearchEngineForm } from "../_form";
@@ -35,7 +35,7 @@ export const SearchEngineNewForm = () => {
}); });
const handleSubmit = useCallback( const handleSubmit = useCallback(
(values: z.infer<typeof validation.searchEngine.manage>) => { (values: z.infer<typeof searchEngineManageSchema>) => {
mutate(values); mutate(values);
}, },
[mutate], [mutate],

View File

@@ -10,7 +10,7 @@ import { useZodForm } from "@homarr/form";
import { createModal, useModalAction } from "@homarr/modals"; import { createModal, useModalAction } from "@homarr/modals";
import { showErrorNotification, showSuccessNotification } from "@homarr/notifications"; import { showErrorNotification, showSuccessNotification } from "@homarr/notifications";
import { useI18n } from "@homarr/translation/client"; import { useI18n } from "@homarr/translation/client";
import { superRefineCertificateFile } from "@homarr/validation"; import { superRefineCertificateFile } from "@homarr/validation/certificates";
export const AddCertificateButton = () => { export const AddCertificateButton = () => {
const { openModal } = useModalAction(AddCertificateModal); const { openModal } = useModalAction(AddCertificateModal);

View File

@@ -9,7 +9,7 @@ import { revalidatePathActionAsync } from "@homarr/common/client";
import { useZodForm } from "@homarr/form"; import { useZodForm } from "@homarr/form";
import { showErrorNotification, showSuccessNotification } from "@homarr/notifications"; import { showErrorNotification, showSuccessNotification } from "@homarr/notifications";
import { useI18n } from "@homarr/translation/client"; 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 type { Board } from "~/app/[locale]/boards/_types";
import { BoardSelect } from "~/components/board/board-select"; 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: { initialValues: {
homeBoardId: user.homeBoardId, homeBoardId: user.homeBoardId,
mobileHomeBoardId: user.mobileHomeBoardId, mobileHomeBoardId: user.mobileHomeBoardId,
@@ -82,4 +82,4 @@ export const ChangeHomeBoardForm = ({ user, boardsData }: ChangeHomeBoardFormPro
); );
}; };
type FormType = z.infer<typeof validation.user.changeHomeBoards>; type FormType = z.infer<typeof userChangeHomeBoardsSchema>;

View File

@@ -9,7 +9,7 @@ import { revalidatePathActionAsync } from "@homarr/common/client";
import { useZodForm } from "@homarr/form"; import { useZodForm } from "@homarr/form";
import { showErrorNotification, showSuccessNotification } from "@homarr/notifications"; import { showErrorNotification, showSuccessNotification } from "@homarr/notifications";
import { useI18n } from "@homarr/translation/client"; import { useI18n } from "@homarr/translation/client";
import { validation } from "@homarr/validation"; import { userChangeSearchPreferencesSchema } from "@homarr/validation/user";
interface ChangeSearchPreferencesFormProps { interface ChangeSearchPreferencesFormProps {
user: RouterOutputs["user"]["getById"]; 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: { initialValues: {
defaultSearchEngineId: user.defaultSearchEngineId, defaultSearchEngineId: user.defaultSearchEngineId,
openInNewTab: user.openSearchInNewTab, openInNewTab: user.openSearchInNewTab,
@@ -75,4 +75,4 @@ export const ChangeSearchPreferencesForm = ({ user, searchEnginesData }: ChangeS
); );
}; };
type FormType = z.infer<typeof validation.user.changeSearchPreferences>; type FormType = z.infer<typeof userChangeSearchPreferencesSchema>;

View File

@@ -12,7 +12,7 @@ import { revalidatePathActionAsync } from "@homarr/common/client";
import { useZodForm } from "@homarr/form"; import { useZodForm } from "@homarr/form";
import { showErrorNotification, showSuccessNotification } from "@homarr/notifications"; import { showErrorNotification, showSuccessNotification } from "@homarr/notifications";
import { useI18n } from "@homarr/translation/client"; import { useI18n } from "@homarr/translation/client";
import { validation } from "@homarr/validation"; import { userFirstDayOfWeekSchema } from "@homarr/validation/user";
dayjs.extend(localeData); dayjs.extend(localeData);
@@ -42,7 +42,7 @@ export const FirstDayOfWeek = ({ user }: FirstDayOfWeekProps) => {
}); });
}, },
}); });
const form = useZodForm(validation.user.firstDayOfWeek, { const form = useZodForm(userFirstDayOfWeekSchema, {
initialValues: { initialValues: {
firstDayOfWeek: user.firstDayOfWeek as DayOfWeek, firstDayOfWeek: user.firstDayOfWeek as DayOfWeek,
}, },
@@ -80,4 +80,4 @@ export const FirstDayOfWeek = ({ user }: FirstDayOfWeekProps) => {
); );
}; };
type FormType = z.infer<typeof validation.user.firstDayOfWeek>; type FormType = z.infer<typeof userFirstDayOfWeekSchema>;

View File

@@ -9,7 +9,7 @@ import { revalidatePathActionAsync } from "@homarr/common/client";
import { useZodForm } from "@homarr/form"; import { useZodForm } from "@homarr/form";
import { showErrorNotification, showSuccessNotification } from "@homarr/notifications"; import { showErrorNotification, showSuccessNotification } from "@homarr/notifications";
import { useI18n } from "@homarr/translation/client"; import { useI18n } from "@homarr/translation/client";
import { validation } from "@homarr/validation"; import { userPingIconsEnabledSchema } from "@homarr/validation/user";
interface PingIconsEnabledProps { interface PingIconsEnabledProps {
user: RouterOutputs["user"]["getById"]; user: RouterOutputs["user"]["getById"];
@@ -35,7 +35,7 @@ export const PingIconsEnabled = ({ user }: PingIconsEnabledProps) => {
}); });
}, },
}); });
const form = useZodForm(validation.user.pingIconsEnabled, { const form = useZodForm(userPingIconsEnabledSchema, {
initialValues: { initialValues: {
pingIconsEnabled: user.pingIconsEnabled, pingIconsEnabled: user.pingIconsEnabled,
}, },
@@ -66,4 +66,4 @@ export const PingIconsEnabled = ({ user }: PingIconsEnabledProps) => {
); );
}; };
type FormType = z.infer<typeof validation.user.pingIconsEnabled>; type FormType = z.infer<typeof userPingIconsEnabledSchema>;

View File

@@ -9,7 +9,7 @@ import { revalidatePathActionAsync } from "@homarr/common/client";
import { useZodForm } from "@homarr/form"; import { useZodForm } from "@homarr/form";
import { showErrorNotification, showSuccessNotification } from "@homarr/notifications"; import { showErrorNotification, showSuccessNotification } from "@homarr/notifications";
import { useI18n } from "@homarr/translation/client"; import { useI18n } from "@homarr/translation/client";
import { validation } from "@homarr/validation"; import { userEditProfileSchema } from "@homarr/validation/user";
interface UserProfileFormProps { interface UserProfileFormProps {
user: RouterOutputs["user"]["getById"]; 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: { initialValues: {
name: user.name ?? "", name: user.name ?? "",
email: user.email ?? "", email: user.email ?? "",

View File

@@ -10,7 +10,7 @@ import { useZodForm } from "@homarr/form";
import { showErrorNotification, showSuccessNotification } from "@homarr/notifications"; import { showErrorNotification, showSuccessNotification } from "@homarr/notifications";
import { useI18n } from "@homarr/translation/client"; import { useI18n } from "@homarr/translation/client";
import { CustomPasswordInput } from "@homarr/ui"; import { CustomPasswordInput } from "@homarr/ui";
import { validation } from "@homarr/validation"; import { userChangePasswordSchema } from "@homarr/validation/user";
interface ChangePasswordFormProps { interface ChangePasswordFormProps {
user: RouterOutputs["user"]["getById"]; user: RouterOutputs["user"]["getById"];
@@ -34,7 +34,7 @@ export const ChangePasswordForm = ({ user }: ChangePasswordFormProps) => {
}); });
}, },
}); });
const form = useZodForm(validation.user.changePassword, { const form = useZodForm(userChangePasswordSchema, {
initialValues: { initialValues: {
/* Require previous password if the current user want's to change his password */ /* Require previous password if the current user want's to change his password */
previousPassword: session?.user.id === user.id ? "" : "_", previousPassword: session?.user.id === user.id ? "" : "_",

View File

@@ -27,8 +27,8 @@ import { useModalAction } from "@homarr/modals";
import { showErrorNotification } from "@homarr/notifications"; import { showErrorNotification } from "@homarr/notifications";
import { useI18n, useScopedI18n } from "@homarr/translation/client"; import { useI18n, useScopedI18n } from "@homarr/translation/client";
import { CustomPasswordInput, UserAvatar } from "@homarr/ui"; import { CustomPasswordInput, UserAvatar } from "@homarr/ui";
import { validation } from "@homarr/validation"; import { createCustomErrorParams } from "@homarr/validation/form/i18n";
import { createCustomErrorParams } from "@homarr/validation/form"; import { userPasswordSchema } from "@homarr/validation/user";
import { GroupSelectModal } from "~/components/access/group-select-modal"; import { GroupSelectModal } from "~/components/access/group-select-modal";
import { StepperNavigationComponent } from "./stepper-navigation"; import { StepperNavigationComponent } from "./stepper-navigation";
@@ -84,7 +84,7 @@ export const UserCreateStepperComponent = ({ initialGroups }: UserCreateStepperC
const securityForm = useZodForm( const securityForm = useZodForm(
z z
.object({ .object({
password: validation.user.password, password: userPasswordSchema,
confirmPassword: z.string(), confirmPassword: z.string(),
}) })
.refine((data) => data.password === data.confirmPassword, { .refine((data) => data.password === data.confirmPassword, {

View File

@@ -8,7 +8,7 @@ import { revalidatePathActionAsync } from "@homarr/common/client";
import { useZodForm } from "@homarr/form"; import { useZodForm } from "@homarr/form";
import { showErrorNotification, showSuccessNotification } from "@homarr/notifications"; import { showErrorNotification, showSuccessNotification } from "@homarr/notifications";
import { useI18n } from "@homarr/translation/client"; import { useI18n } from "@homarr/translation/client";
import { validation } from "@homarr/validation"; import { groupUpdateSchema } from "@homarr/validation/group";
interface RenameGroupFormProps { interface RenameGroupFormProps {
group: { group: {
@@ -21,7 +21,7 @@ interface RenameGroupFormProps {
export const RenameGroupForm = ({ group, disabled }: RenameGroupFormProps) => { export const RenameGroupForm = ({ group, disabled }: RenameGroupFormProps) => {
const t = useI18n(); const t = useI18n();
const { mutate, isPending } = clientApi.group.updateGroup.useMutation(); const { mutate, isPending } = clientApi.group.updateGroup.useMutation();
const form = useZodForm(validation.group.update.pick({ name: true }), { const form = useZodForm(groupUpdateSchema.pick({ name: true }), {
initialValues: { initialValues: {
name: group.name, name: group.name,
}, },

View File

@@ -6,7 +6,7 @@ import { clientApi } from "@homarr/api/client";
import { useZodForm } from "@homarr/form"; import { useZodForm } from "@homarr/form";
import { showErrorNotification, showSuccessNotification } from "@homarr/notifications"; import { showErrorNotification, showSuccessNotification } from "@homarr/notifications";
import { useI18n } from "@homarr/translation/client"; import { useI18n } from "@homarr/translation/client";
import { validation } from "@homarr/validation"; import { groupSettingsSchema } from "@homarr/validation/group";
import { BoardSelect } from "~/components/board/board-select"; import { BoardSelect } from "~/components/board/board-select";
@@ -19,7 +19,7 @@ interface GroupHomeBoardsProps {
export const GroupHomeBoards = ({ homeBoardId, mobileHomeBoardId, groupId }: GroupHomeBoardsProps) => { export const GroupHomeBoards = ({ homeBoardId, mobileHomeBoardId, groupId }: GroupHomeBoardsProps) => {
const t = useI18n(); const t = useI18n();
const [availableBoards] = clientApi.board.getBoardsForGroup.useSuspenseQuery({ groupId }); 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: { initialValues: {
homeBoardId, homeBoardId,
mobileHomeBoardId, mobileHomeBoardId,

View File

@@ -11,7 +11,7 @@ import { useModalAction } from "@homarr/modals";
import { showSuccessNotification } from "@homarr/notifications"; import { showSuccessNotification } from "@homarr/notifications";
import { useSettings } from "@homarr/settings"; import { useSettings } from "@homarr/settings";
import { useScopedI18n } from "@homarr/translation/client"; 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 { loadWidgetDynamic, reduceWidgetOptionsWithDefaultValues, widgetImports } from "@homarr/widgets";
import { WidgetError } from "@homarr/widgets/errors"; import { WidgetError } from "@homarr/widgets/errors";
import { WidgetEditModal } from "@homarr/widgets/modals"; import { WidgetEditModal } from "@homarr/widgets/modals";

View File

@@ -1,7 +1,7 @@
import { useCallback } from "react"; import { useCallback } from "react";
import { useUpdateBoard } from "@homarr/boards/updater"; 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 type { CreateItemInput } from "./actions/create-item";
import { createItemCallback } from "./actions/create-item"; import { createItemCallback } from "./actions/create-item";

View File

@@ -7,7 +7,7 @@ import { clientApi } from "@homarr/api/client";
import { useZodForm } from "@homarr/form"; import { useZodForm } from "@homarr/form";
import { createModal } from "@homarr/modals"; import { createModal } from "@homarr/modals";
import { useI18n } from "@homarr/translation/client"; import { useI18n } from "@homarr/translation/client";
import { validation } from "@homarr/validation"; import { boardRenameSchema } from "@homarr/validation/board";
interface InnerProps { interface InnerProps {
id: string; id: string;
@@ -26,7 +26,7 @@ export const BoardRenameModal = createModal<InnerProps>(({ actions, innerProps }
void utils.board.getHomeBoard.invalidate(); void utils.board.getHomeBoard.invalidate();
}, },
}); });
const form = useZodForm(validation.board.rename.omit({ id: true }), { const form = useZodForm(boardRenameSchema.omit({ id: true }), {
initialValues: { initialValues: {
name: innerProps.previousName, name: innerProps.previousName,
}, },
@@ -66,4 +66,4 @@ export const BoardRenameModal = createModal<InnerProps>(({ actions, innerProps }
defaultTitle: (t) => t("board.setting.section.dangerZone.action.rename.modal.title"), defaultTitle: (t) => t("board.setting.section.dangerZone.action.rename.modal.title"),
}); });
type FormType = Omit<z.infer<(typeof validation)["board"]["rename"]>, "id">; type FormType = Omit<z.infer<typeof boardRenameSchema>, "id">;

View File

@@ -2,7 +2,7 @@ import { useCallback } from "react";
import type { z } from "zod"; import type { z } from "zod";
import { useUpdateBoard } from "@homarr/boards/updater"; 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 { addDynamicSectionCallback } from "./actions/add-dynamic-section";
import type { RemoveDynamicSectionInput } from "./actions/remove-dynamic-section"; import type { RemoveDynamicSectionInput } from "./actions/remove-dynamic-section";

View File

@@ -6,7 +6,7 @@ import type { z } from "zod";
import { useZodForm } from "@homarr/form"; import { useZodForm } from "@homarr/form";
import { createModal } from "@homarr/modals"; import { createModal } from "@homarr/modals";
import { useI18n } from "@homarr/translation/client"; import { useI18n } from "@homarr/translation/client";
import { dynamicSectionOptionsSchema } from "@homarr/validation"; import { dynamicSectionOptionsSchema } from "@homarr/validation/shared";
interface ModalProps { interface ModalProps {
value: z.infer<typeof dynamicSectionOptionsSchema>; value: z.infer<typeof dynamicSectionOptionsSchema>;

View File

@@ -5,7 +5,8 @@ import { asc, createId, eq, inArray, like } from "@homarr/db";
import { apps } from "@homarr/db/schema"; import { apps } from "@homarr/db/schema";
import { selectAppSchema } from "@homarr/db/validationSchemas"; import { selectAppSchema } from "@homarr/db/validationSchemas";
import { getIconForName } from "@homarr/icons"; 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 { convertIntersectionToZodObject } from "../schema-merger";
import { createTRPCRouter, permissionRequiredProcedure, protectedProcedure, publicProcedure } from "../trpc"; 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({ export const appRouter = createTRPCRouter({
getPaginated: protectedProcedure getPaginated: protectedProcedure
.input(validation.common.paginated) .input(paginatedSchema)
.output(z.object({ items: z.array(selectAppSchema), totalCount: z.number() })) .output(z.object({ items: z.array(selectAppSchema), totalCount: z.number() }))
.meta({ openapi: { method: "GET", path: "/api/apps/paginated", tags: ["apps"], protect: true } }) .meta({ openapi: { method: "GET", path: "/api/apps/paginated", tags: ["apps"], protect: true } })
.query(async ({ input, ctx }) => { .query(async ({ input, ctx }) => {
@@ -83,7 +84,7 @@ export const appRouter = createTRPCRouter({
}); });
}), }),
byId: publicProcedure byId: publicProcedure
.input(validation.common.byId) .input(byIdSchema)
.output(selectAppSchema) .output(selectAppSchema)
.meta({ openapi: { method: "GET", path: "/api/apps/{id}", tags: ["apps"], protect: true } }) .meta({ openapi: { method: "GET", path: "/api/apps/{id}", tags: ["apps"], protect: true } })
.query(async ({ ctx, input }) => { .query(async ({ ctx, input }) => {
@@ -115,7 +116,7 @@ export const appRouter = createTRPCRouter({
}), }),
create: permissionRequiredProcedure create: permissionRequiredProcedure
.requiresPermission("app-create") .requiresPermission("app-create")
.input(validation.app.manage) .input(appManageSchema)
.output(z.object({ appId: z.string() })) .output(z.object({ appId: z.string() }))
.meta({ openapi: { method: "POST", path: "/api/apps", tags: ["apps"], protect: true } }) .meta({ openapi: { method: "POST", path: "/api/apps", tags: ["apps"], protect: true } })
.mutation(async ({ ctx, input }) => { .mutation(async ({ ctx, input }) => {
@@ -133,7 +134,7 @@ export const appRouter = createTRPCRouter({
}), }),
createMany: permissionRequiredProcedure createMany: permissionRequiredProcedure
.requiresPermission("app-create") .requiresPermission("app-create")
.input(validation.app.createMany) .input(appCreateManySchema)
.output(z.void()) .output(z.void())
.mutation(async ({ ctx, input }) => { .mutation(async ({ ctx, input }) => {
await ctx.db.insert(apps).values( await ctx.db.insert(apps).values(
@@ -148,7 +149,7 @@ export const appRouter = createTRPCRouter({
}), }),
update: permissionRequiredProcedure update: permissionRequiredProcedure
.requiresPermission("app-modify-all") .requiresPermission("app-modify-all")
.input(convertIntersectionToZodObject(validation.app.edit)) .input(convertIntersectionToZodObject(appEditSchema))
.output(z.void()) .output(z.void())
.meta({ openapi: { method: "PATCH", path: "/api/apps/{id}", tags: ["apps"], protect: true } }) .meta({ openapi: { method: "PATCH", path: "/api/apps/{id}", tags: ["apps"], protect: true } })
.mutation(async ({ ctx, input }) => { .mutation(async ({ ctx, input }) => {
@@ -178,7 +179,7 @@ export const appRouter = createTRPCRouter({
.requiresPermission("app-full-all") .requiresPermission("app-full-all")
.output(z.void()) .output(z.void())
.meta({ openapi: { method: "DELETE", path: "/api/apps/{id}", tags: ["apps"], protect: true } }) .meta({ openapi: { method: "DELETE", path: "/api/apps/{id}", tags: ["apps"], protect: true } })
.input(validation.common.byId) .input(byIdSchema)
.mutation(async ({ ctx, input }) => { .mutation(async ({ ctx, input }) => {
await ctx.db.delete(apps).where(eq(apps.id, input.id)); await ctx.db.delete(apps).where(eq(apps.id, input.id));
}), }),

View File

@@ -37,8 +37,21 @@ import {
import { importOldmarrAsync } from "@homarr/old-import"; import { importOldmarrAsync } from "@homarr/old-import";
import { importJsonFileSchema } from "@homarr/old-import/shared"; import { importJsonFileSchema } from "@homarr/old-import/shared";
import { oldmarrConfigSchema } from "@homarr/old-schema"; import { oldmarrConfigSchema } from "@homarr/old-schema";
import type { BoardItemAdvancedOptions } from "@homarr/validation"; import {
import { sectionSchema, sharedItemSchema, validation, zodUnionFromArray } from "@homarr/validation"; 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 { createTRPCRouter, permissionRequiredProcedure, protectedProcedure, publicProcedure } from "../trpc";
import { throwIfActionForbiddenAsync } from "./board/board-access"; import { throwIfActionForbiddenAsync } from "./board/board-access";
@@ -247,7 +260,7 @@ export const boardRouter = createTRPCRouter({
}), }),
createBoard: permissionRequiredProcedure createBoard: permissionRequiredProcedure
.requiresPermission("board-create") .requiresPermission("board-create")
.input(validation.board.create) .input(boardCreateSchema)
.mutation(async ({ ctx, input }) => { .mutation(async ({ ctx, input }) => {
const boardId = createId(); const boardId = createId();
@@ -291,7 +304,7 @@ export const boardRouter = createTRPCRouter({
}), }),
duplicateBoard: permissionRequiredProcedure duplicateBoard: permissionRequiredProcedure
.requiresPermission("board-create") .requiresPermission("board-create")
.input(validation.board.duplicate) .input(boardDuplicateSchema)
.mutation(async ({ ctx, input }) => { .mutation(async ({ ctx, input }) => {
await throwIfActionForbiddenAsync(ctx, eq(boards.id, input.id), "view"); await throwIfActionForbiddenAsync(ctx, eq(boards.id, input.id), "view");
await noBoardWithSimilarNameAsync(ctx.db, input.name); 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 throwIfActionForbiddenAsync(ctx, eq(boards.id, input.id), "full");
await noBoardWithSimilarNameAsync(ctx.db, input.name, [input.id]); await noBoardWithSimilarNameAsync(ctx.db, input.name, [input.id]);
await ctx.db.update(boards).set({ name: input.name }).where(eq(boards.id, input.id)); await ctx.db.update(boards).set({ name: input.name }).where(eq(boards.id, input.id));
}), }),
changeBoardVisibility: protectedProcedure changeBoardVisibility: protectedProcedure.input(boardChangeVisibilitySchema).mutation(async ({ ctx, input }) => {
.input(validation.board.changeVisibility) await throwIfActionForbiddenAsync(ctx, eq(boards.id, input.id), "full");
.mutation(async ({ ctx, input }) => { const boardSettings = await getServerSettingByKeyAsync(ctx.db, "board");
await throwIfActionForbiddenAsync(ctx, eq(boards.id, input.id), "full");
const boardSettings = await getServerSettingByKeyAsync(ctx.db, "board");
if ( if (
input.visibility !== "public" && input.visibility !== "public" &&
(boardSettings.homeBoardId === input.id || boardSettings.mobileHomeBoardId === input.id) (boardSettings.homeBoardId === input.id || boardSettings.mobileHomeBoardId === input.id)
) { ) {
throw new TRPCError({ throw new TRPCError({
code: "BAD_REQUEST", code: "BAD_REQUEST",
message: "Cannot make home board private", message: "Cannot make home board private",
}); });
} }
await ctx.db await ctx.db
.update(boards) .update(boards)
.set({ isPublic: input.visibility === "public" }) .set({ isPublic: input.visibility === "public" })
.where(eq(boards.id, input.id)); .where(eq(boards.id, input.id));
}), }),
deleteBoard: protectedProcedure.input(z.object({ id: z.string() })).mutation(async ({ ctx, input }) => { deleteBoard: protectedProcedure.input(z.object({ id: z.string() })).mutation(async ({ ctx, input }) => {
await throwIfActionForbiddenAsync(ctx, eq(boards.id, input.id), "full"); 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); 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()); const boardWhere = eq(sql`UPPER(${boards.name})`, input.name.toUpperCase());
await throwIfActionForbiddenAsync(ctx, boardWhere, "view"); await throwIfActionForbiddenAsync(ctx, boardWhere, "view");
return await getFullBoardWithWhereAsync(ctx.db, boardWhere, ctx.session?.user.id ?? null); 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"); await throwIfActionForbiddenAsync(ctx, eq(boards.id, input.id), "modify");
const board = await getFullBoardWithWhereAsync(ctx.db, eq(boards.id, input.id), ctx.session.user.id); 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 savePartialBoardSettings: protectedProcedure
.input(validation.board.savePartialSettings.and(z.object({ id: z.string() }))) .input(boardSavePartialSettingsSchema.and(z.object({ id: z.string() })))
.mutation(async ({ ctx, input }) => { .mutation(async ({ ctx, input }) => {
await throwIfActionForbiddenAsync(ctx, eq(boards.id, input.id), "modify"); await throwIfActionForbiddenAsync(ctx, eq(boards.id, input.id), "modify");
@@ -738,7 +749,7 @@ export const boardRouter = createTRPCRouter({
}) })
.where(eq(boards.id, input.id)); .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"); await throwIfActionForbiddenAsync(ctx, eq(boards.id, input.id), "modify");
const dbBoard = await getFullBoardWithWhereAsync(ctx.db, eq(boards.id, input.id), ctx.session.user.id); 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(byIdSchema).query(async ({ input, ctx }) => {
getBoardPermissions: protectedProcedure.input(validation.board.permissions).query(async ({ input, ctx }) => {
await throwIfActionForbiddenAsync(ctx, eq(boards.id, input.id), "full"); await throwIfActionForbiddenAsync(ctx, eq(boards.id, input.id), "full");
const dbGroupPermissions = await ctx.db.query.groupPermissions.findMany({ const dbGroupPermissions = await ctx.db.query.groupPermissions.findMany({
@@ -1226,92 +1236,86 @@ export const boardRouter = createTRPCRouter({
}), }),
}; };
}), }),
saveUserBoardPermissions: protectedProcedure saveUserBoardPermissions: protectedProcedure.input(boardSavePermissionsSchema).mutation(async ({ input, ctx }) => {
.input(validation.board.savePermissions) await throwIfActionForbiddenAsync(ctx, eq(boards.id, input.entityId), "full");
.mutation(async ({ input, ctx }) => {
await throwIfActionForbiddenAsync(ctx, eq(boards.id, input.entityId), "full");
await handleTransactionsAsync(ctx.db, { await handleTransactionsAsync(ctx.db, {
async handleAsync(db, schema) { async handleAsync(db, schema) {
await db.transaction(async (transaction) => { await db.transaction(async (transaction) => {
await transaction await transaction.delete(schema.boardUserPermissions).where(eq(boardUserPermissions.boardId, input.entityId));
.delete(schema.boardUserPermissions) if (input.permissions.length === 0) {
.where(eq(boardUserPermissions.boardId, input.entityId)); return;
if (input.permissions.length === 0) { }
return; await transaction.insert(schema.boardUserPermissions).values(
} input.permissions.map((permission) => ({
await transaction.insert(schema.boardUserPermissions).values( 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) => ({ input.permissions.map((permission) => ({
userId: permission.principalId, userId: permission.principalId,
permission: permission.permission, permission: permission.permission,
boardId: input.entityId, boardId: input.entityId,
})), })),
); )
}); .run();
}, });
handleSync(db) { },
db.transaction((transaction) => { });
transaction.delete(boardUserPermissions).where(eq(boardUserPermissions.boardId, input.entityId)).run(); }),
if (input.permissions.length === 0) { saveGroupBoardPermissions: protectedProcedure.input(boardSavePermissionsSchema).mutation(async ({ input, ctx }) => {
return; await throwIfActionForbiddenAsync(ctx, eq(boards.id, input.entityId), "full");
}
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");
await handleTransactionsAsync(ctx.db, { await handleTransactionsAsync(ctx.db, {
async handleAsync(db, schema) { async handleAsync(db, schema) {
await db.transaction(async (transaction) => { await db.transaction(async (transaction) => {
await transaction await transaction
.delete(schema.boardGroupPermissions) .delete(schema.boardGroupPermissions)
.where(eq(boardGroupPermissions.boardId, input.entityId)); .where(eq(boardGroupPermissions.boardId, input.entityId));
if (input.permissions.length === 0) { if (input.permissions.length === 0) {
return; return;
} }
await transaction.insert(schema.boardGroupPermissions).values( 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) => ({ input.permissions.map((permission) => ({
groupId: permission.principalId, groupId: permission.principalId,
permission: permission.permission, permission: permission.permission,
boardId: input.entityId, boardId: input.entityId,
})), })),
); )
}); .run();
}, });
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();
});
},
});
}),
importOldmarrConfig: permissionRequiredProcedure importOldmarrConfig: permissionRequiredProcedure
.requiresPermission("board-create") .requiresPermission("board-create")
.input(importJsonFileSchema) .input(importJsonFileSchema)

View File

@@ -2,7 +2,7 @@ import { z } from "zod";
import { zfd } from "zod-form-data"; import { zfd } from "zod-form-data";
import { addCustomRootCertificateAsync, removeCustomRootCertificateAsync } from "@homarr/certificates/server"; 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"; import { createTRPCRouter, permissionRequiredProcedure } from "../../trpc";
@@ -20,7 +20,7 @@ export const certificateRouter = createTRPCRouter({
}), }),
removeCertificate: permissionRequiredProcedure removeCertificate: permissionRequiredProcedure
.requiresPermission("admin") .requiresPermission("admin")
.input(z.object({ fileName: validation.certificates.validFileNameSchema })) .input(z.object({ fileName: certificateValidFileNameSchema }))
.mutation(async ({ input }) => { .mutation(async ({ input }) => {
await removeCustomRootCertificateAsync(input.fileName); await removeCustomRootCertificateAsync(input.fileName);
}), }),

View File

@@ -6,7 +6,15 @@ import { and, createId, eq, handleTransactionsAsync, like, not } from "@homarr/d
import { getMaxGroupPositionAsync } from "@homarr/db/queries"; import { getMaxGroupPositionAsync } from "@homarr/db/queries";
import { groupMembers, groupPermissions, groups } from "@homarr/db/schema"; import { groupMembers, groupPermissions, groups } from "@homarr/db/schema";
import { everyoneGroup } from "@homarr/definitions"; 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 { createTRPCRouter, onboardingProcedure, permissionRequiredProcedure, protectedProcedure } from "../trpc";
import { throwIfCredentialsDisabled } from "./invite/checks"; import { throwIfCredentialsDisabled } from "./invite/checks";
@@ -39,7 +47,7 @@ export const groupRouter = createTRPCRouter({
getPaginated: permissionRequiredProcedure getPaginated: permissionRequiredProcedure
.requiresPermission("admin") .requiresPermission("admin")
.input(validation.common.paginated) .input(paginatedSchema)
.query(async ({ input, ctx }) => { .query(async ({ input, ctx }) => {
const whereQuery = input.search ? like(groups.name, `%${input.search.trim()}%`) : undefined; const whereQuery = input.search ? like(groups.name, `%${input.search.trim()}%`) : undefined;
const groupCount = await ctx.db.$count(groups, whereQuery); const groupCount = await ctx.db.$count(groups, whereQuery);
@@ -74,7 +82,7 @@ export const groupRouter = createTRPCRouter({
}), }),
getById: permissionRequiredProcedure getById: permissionRequiredProcedure
.requiresPermission("admin") .requiresPermission("admin")
.input(validation.common.byId) .input(byIdSchema)
.query(async ({ input, ctx }) => { .query(async ({ input, ctx }) => {
const group = await ctx.db.query.groups.findFirst({ const group = await ctx.db.query.groups.findFirst({
where: eq(groups.id, input.id), where: eq(groups.id, input.id),
@@ -169,7 +177,7 @@ export const groupRouter = createTRPCRouter({
}), }),
createInitialExternalGroup: onboardingProcedure createInitialExternalGroup: onboardingProcedure
.requiresStep("group") .requiresStep("group")
.input(validation.group.create) .input(groupCreateSchema)
.mutation(async ({ input, ctx }) => { .mutation(async ({ input, ctx }) => {
await checkSimilarNameAndThrowAsync(ctx.db, input.name); await checkSimilarNameAndThrowAsync(ctx.db, input.name);
@@ -191,7 +199,7 @@ export const groupRouter = createTRPCRouter({
}), }),
createGroup: permissionRequiredProcedure createGroup: permissionRequiredProcedure
.requiresPermission("admin") .requiresPermission("admin")
.input(validation.group.create) .input(groupCreateSchema)
.mutation(async ({ input, ctx }) => { .mutation(async ({ input, ctx }) => {
await checkSimilarNameAndThrowAsync(ctx.db, input.name); await checkSimilarNameAndThrowAsync(ctx.db, input.name);
@@ -209,7 +217,7 @@ export const groupRouter = createTRPCRouter({
}), }),
updateGroup: permissionRequiredProcedure updateGroup: permissionRequiredProcedure
.requiresPermission("admin") .requiresPermission("admin")
.input(validation.group.update) .input(groupUpdateSchema)
.mutation(async ({ input, ctx }) => { .mutation(async ({ input, ctx }) => {
await throwIfGroupNotFoundAsync(ctx.db, input.id); await throwIfGroupNotFoundAsync(ctx.db, input.id);
await throwIfGroupNameIsReservedAsync(ctx.db, input.id); await throwIfGroupNameIsReservedAsync(ctx.db, input.id);
@@ -225,7 +233,7 @@ export const groupRouter = createTRPCRouter({
}), }),
savePartialSettings: permissionRequiredProcedure savePartialSettings: permissionRequiredProcedure
.requiresPermission("admin") .requiresPermission("admin")
.input(validation.group.savePartialSettings) .input(groupSavePartialSettingsSchema)
.mutation(async ({ input, ctx }) => { .mutation(async ({ input, ctx }) => {
await throwIfGroupNotFoundAsync(ctx.db, input.id); await throwIfGroupNotFoundAsync(ctx.db, input.id);
@@ -239,7 +247,7 @@ export const groupRouter = createTRPCRouter({
}), }),
savePositions: permissionRequiredProcedure savePositions: permissionRequiredProcedure
.requiresPermission("admin") .requiresPermission("admin")
.input(validation.group.savePositions) .input(groupSavePositionsSchema)
.mutation(async ({ input, ctx }) => { .mutation(async ({ input, ctx }) => {
const positions = input.positions.map((id, index) => ({ id, position: index + 1 })); const positions = input.positions.map((id, index) => ({ id, position: index + 1 }));
@@ -262,7 +270,7 @@ export const groupRouter = createTRPCRouter({
}), }),
savePermissions: permissionRequiredProcedure savePermissions: permissionRequiredProcedure
.requiresPermission("admin") .requiresPermission("admin")
.input(validation.group.savePermissions) .input(groupSavePermissionsSchema)
.mutation(async ({ input, ctx }) => { .mutation(async ({ input, ctx }) => {
await throwIfGroupNotFoundAsync(ctx.db, input.groupId); await throwIfGroupNotFoundAsync(ctx.db, input.groupId);
@@ -277,7 +285,7 @@ export const groupRouter = createTRPCRouter({
}), }),
transferOwnership: permissionRequiredProcedure transferOwnership: permissionRequiredProcedure
.requiresPermission("admin") .requiresPermission("admin")
.input(validation.group.groupUser) .input(groupUserSchema)
.mutation(async ({ input, ctx }) => { .mutation(async ({ input, ctx }) => {
await throwIfGroupNotFoundAsync(ctx.db, input.groupId); await throwIfGroupNotFoundAsync(ctx.db, input.groupId);
await throwIfGroupNameIsReservedAsync(ctx.db, input.groupId); await throwIfGroupNameIsReservedAsync(ctx.db, input.groupId);
@@ -291,7 +299,7 @@ export const groupRouter = createTRPCRouter({
}), }),
deleteGroup: permissionRequiredProcedure deleteGroup: permissionRequiredProcedure
.requiresPermission("admin") .requiresPermission("admin")
.input(validation.common.byId) .input(byIdSchema)
.mutation(async ({ input, ctx }) => { .mutation(async ({ input, ctx }) => {
await throwIfGroupNotFoundAsync(ctx.db, input.id); await throwIfGroupNotFoundAsync(ctx.db, input.id);
await throwIfGroupNameIsReservedAsync(ctx.db, input.id); await throwIfGroupNameIsReservedAsync(ctx.db, input.id);
@@ -300,7 +308,7 @@ export const groupRouter = createTRPCRouter({
}), }),
addMember: permissionRequiredProcedure addMember: permissionRequiredProcedure
.requiresPermission("admin") .requiresPermission("admin")
.input(validation.group.groupUser) .input(groupUserSchema)
.mutation(async ({ input, ctx }) => { .mutation(async ({ input, ctx }) => {
await throwIfGroupNotFoundAsync(ctx.db, input.groupId); await throwIfGroupNotFoundAsync(ctx.db, input.groupId);
await throwIfGroupNameIsReservedAsync(ctx.db, input.groupId); await throwIfGroupNameIsReservedAsync(ctx.db, input.groupId);
@@ -324,7 +332,7 @@ export const groupRouter = createTRPCRouter({
}), }),
removeMember: permissionRequiredProcedure removeMember: permissionRequiredProcedure
.requiresPermission("admin") .requiresPermission("admin")
.input(validation.group.groupUser) .input(groupUserSchema)
.mutation(async ({ input, ctx }) => { .mutation(async ({ input, ctx }) => {
await throwIfGroupNotFoundAsync(ctx.db, input.groupId); await throwIfGroupNotFoundAsync(ctx.db, input.groupId);
await throwIfGroupNameIsReservedAsync(ctx.db, input.groupId); await throwIfGroupNameIsReservedAsync(ctx.db, input.groupId);

View File

@@ -1,11 +1,11 @@
import { and, like } from "@homarr/db"; import { and, like } from "@homarr/db";
import { icons } from "@homarr/db/schema"; import { icons } from "@homarr/db/schema";
import { validation } from "@homarr/validation"; import { iconsFindSchema } from "@homarr/validation/icons";
import { createTRPCRouter, publicProcedure } from "../trpc"; import { createTRPCRouter, publicProcedure } from "../trpc";
export const iconsRouter = createTRPCRouter({ export const iconsRouter = createTRPCRouter({
findIcons: publicProcedure.input(validation.icons.findIcons).query(async ({ ctx, input }) => { findIcons: publicProcedure.input(iconsFindSchema).query(async ({ ctx, input }) => {
return { return {
icons: await ctx.db.query.iconRepositories.findMany({ icons: await ctx.db.query.iconRepositories.findMany({
with: { with: {

View File

@@ -24,7 +24,12 @@ import {
integrationSecretKindObject, integrationSecretKindObject,
} from "@homarr/definitions"; } from "@homarr/definitions";
import { createIntegrationAsync } from "@homarr/integrations"; 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 { createOneIntegrationMiddleware } from "../../middlewares/integration";
import { createTRPCRouter, permissionRequiredProcedure, protectedProcedure, publicProcedure } from "../../trpc"; 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"); await throwIfActionForbiddenAsync(ctx, eq(integrations.id, input.id), "full");
const integration = await ctx.db.query.integrations.findFirst({ const integration = await ctx.db.query.integrations.findFirst({
where: eq(integrations.id, input.id), where: eq(integrations.id, input.id),
@@ -178,7 +183,7 @@ export const integrationRouter = createTRPCRouter({
}), }),
create: permissionRequiredProcedure create: permissionRequiredProcedure
.requiresPermission("integration-create") .requiresPermission("integration-create")
.input(validation.integration.create) .input(integrationCreateSchema)
.mutation(async ({ ctx, input }) => { .mutation(async ({ ctx, input }) => {
await testConnectionAsync({ await testConnectionAsync({
id: "new", 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"); await throwIfActionForbiddenAsync(ctx, eq(integrations.id, input.id), "full");
const integration = await ctx.db.query.integrations.findFirst({ 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"); await throwIfActionForbiddenAsync(ctx, eq(integrations.id, input.id), "full");
const integration = await ctx.db.query.integrations.findFirst({ 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)); 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"); await throwIfActionForbiddenAsync(ctx, eq(integrations.id, input.id), "full");
const dbGroupPermissions = await ctx.db.query.groupPermissions.findMany({ const dbGroupPermissions = await ctx.db.query.groupPermissions.findMany({
@@ -370,7 +375,7 @@ export const integrationRouter = createTRPCRouter({
}; };
}), }),
saveUserIntegrationPermissions: protectedProcedure saveUserIntegrationPermissions: protectedProcedure
.input(validation.integration.savePermissions) .input(integrationSavePermissionsSchema)
.mutation(async ({ input, ctx }) => { .mutation(async ({ input, ctx }) => {
await throwIfActionForbiddenAsync(ctx, eq(integrations.id, input.entityId), "full"); await throwIfActionForbiddenAsync(ctx, eq(integrations.id, input.entityId), "full");
@@ -416,7 +421,7 @@ export const integrationRouter = createTRPCRouter({
}); });
}), }),
saveGroupIntegrationPermissions: protectedProcedure saveGroupIntegrationPermissions: protectedProcedure
.input(validation.integration.savePermissions) .input(integrationSavePermissionsSchema)
.mutation(async ({ input, ctx }) => { .mutation(async ({ input, ctx }) => {
await throwIfActionForbiddenAsync(ctx, eq(integrations.id, input.entityId), "full"); await throwIfActionForbiddenAsync(ctx, eq(integrations.id, input.entityId), "full");

View File

@@ -1,16 +1,42 @@
import type { z } from "zod"; import { z } from "zod";
import { fetchWithTimeout } from "@homarr/common"; import { fetchWithTimeout } from "@homarr/common";
import { validation } from "@homarr/validation";
import { createTRPCRouter, publicProcedure } from "../trpc"; 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({ export const locationRouter = createTRPCRouter({
searchCity: publicProcedure searchCity: publicProcedure
.input(validation.location.searchCity.input) .input(locationSearchCityInput)
.output(validation.location.searchCity.output) .output(locationSearchCityOutput)
.query(async ({ input }) => { .query(async ({ input }) => {
const res = await fetchWithTimeout(`https://geocoding-api.open-meteo.com/v1/search?name=${input.query}`); const res = await fetchWithTimeout(`https://geocoding-api.open-meteo.com/v1/search?name=${input.query}`);
return (await res.json()) as z.infer<typeof validation.location.searchCity.output>; return (await res.json()) as z.infer<typeof locationSearchCityOutput>;
}), }),
}); });

View File

@@ -5,14 +5,15 @@ import type { InferInsertModel } from "@homarr/db";
import { and, createId, desc, eq, like } from "@homarr/db"; import { and, createId, desc, eq, like } from "@homarr/db";
import { iconRepositories, icons, medias } from "@homarr/db/schema"; import { iconRepositories, icons, medias } from "@homarr/db/schema";
import { createLocalImageUrl, LOCAL_ICON_REPOSITORY_SLUG, mapMediaToIcon } from "@homarr/icons/local"; 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"; import { createTRPCRouter, permissionRequiredProcedure, protectedProcedure } from "../../trpc";
export const mediaRouter = createTRPCRouter({ export const mediaRouter = createTRPCRouter({
getPaginated: protectedProcedure getPaginated: protectedProcedure
.input( .input(
validation.common.paginated.and( paginatedSchema.and(
z.object({ includeFromAllUsers: z.boolean().default(false), search: z.string().trim().default("") }), z.object({ includeFromAllUsers: z.boolean().default(false), search: z.string().trim().default("") }),
), ),
) )
@@ -51,7 +52,7 @@ export const mediaRouter = createTRPCRouter({
}), }),
uploadMedia: permissionRequiredProcedure uploadMedia: permissionRequiredProcedure
.requiresPermission("media-upload") .requiresPermission("media-upload")
.input(validation.media.uploadMedia) .input(mediaUploadSchema)
.mutation(async ({ ctx, input }) => { .mutation(async ({ ctx, input }) => {
const content = Buffer.from(await input.file.arrayBuffer()); const content = Buffer.from(await input.file.arrayBuffer());
const id = createId(); const id = createId();
@@ -82,7 +83,7 @@ export const mediaRouter = createTRPCRouter({
return id; 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({ const dbMedia = await ctx.db.query.medias.findFirst({
where: eq(medias.id, input.id), where: eq(medias.id, input.id),
columns: { columns: {

View File

@@ -2,7 +2,7 @@ import { z } from "zod";
import { onboarding } from "@homarr/db/schema"; import { onboarding } from "@homarr/db/schema";
import { onboardingSteps } from "@homarr/definitions"; import { onboardingSteps } from "@homarr/definitions";
import { zodEnumFromArray } from "@homarr/validation"; import { zodEnumFromArray } from "@homarr/validation/enums";
import { createTRPCRouter, publicProcedure } from "../../trpc"; import { createTRPCRouter, publicProcedure } from "../../trpc";
import { getOnboardingOrFallbackAsync, nextOnboardingStepAsync } from "./onboard-queries"; import { getOnboardingOrFallbackAsync, nextOnboardingStepAsync } from "./onboard-queries";

View File

@@ -5,13 +5,15 @@ import { asc, createId, eq, like } from "@homarr/db";
import { getServerSettingByKeyAsync } from "@homarr/db/queries"; import { getServerSettingByKeyAsync } from "@homarr/db/queries";
import { searchEngines, users } from "@homarr/db/schema"; import { searchEngines, users } from "@homarr/db/schema";
import { createIntegrationAsync } from "@homarr/integrations"; 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 { createOneIntegrationMiddleware } from "../../middlewares/integration";
import { createTRPCRouter, permissionRequiredProcedure, protectedProcedure, publicProcedure } from "../../trpc"; import { createTRPCRouter, permissionRequiredProcedure, protectedProcedure, publicProcedure } from "../../trpc";
export const searchEngineRouter = createTRPCRouter({ 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 whereQuery = input.search ? like(searchEngines.name, `%${input.search.trim()}%`) : undefined;
const searchEngineCount = await ctx.db.$count(searchEngines, whereQuery); 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 }))); .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({ const searchEngine = await ctx.db.query.searchEngines.findFirst({
where: eq(searchEngines.id, input.id), where: eq(searchEngines.id, input.id),
}); });
@@ -115,7 +117,7 @@ export const searchEngineRouter = createTRPCRouter({
return null; 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({ return await ctx.db.query.searchEngines.findMany({
where: like(searchEngines.short, `${input.query.toLowerCase().trim()}%`), where: like(searchEngines.short, `${input.query.toLowerCase().trim()}%`),
with: { with: {
@@ -132,21 +134,21 @@ export const searchEngineRouter = createTRPCRouter({
}), }),
getMediaRequestOptions: protectedProcedure getMediaRequestOptions: protectedProcedure
.unstable_concat(createOneIntegrationMiddleware("query", "jellyseerr", "overseerr")) .unstable_concat(createOneIntegrationMiddleware("query", "jellyseerr", "overseerr"))
.input(validation.common.mediaRequestOptions) .input(mediaRequestOptionsSchema)
.query(async ({ ctx, input }) => { .query(async ({ ctx, input }) => {
const integration = await createIntegrationAsync(ctx.integration); const integration = await createIntegrationAsync(ctx.integration);
return await integration.getSeriesInformationAsync(input.mediaType, input.mediaId); return await integration.getSeriesInformationAsync(input.mediaType, input.mediaId);
}), }),
requestMedia: protectedProcedure requestMedia: protectedProcedure
.unstable_concat(createOneIntegrationMiddleware("interact", "jellyseerr", "overseerr")) .unstable_concat(createOneIntegrationMiddleware("interact", "jellyseerr", "overseerr"))
.input(validation.common.requestMedia) .input(mediaRequestRequestSchema)
.mutation(async ({ ctx, input }) => { .mutation(async ({ ctx, input }) => {
const integration = await createIntegrationAsync(ctx.integration); const integration = await createIntegrationAsync(ctx.integration);
return await integration.requestMediaAsync(input.mediaType, input.mediaId, input.seasons); return await integration.requestMediaAsync(input.mediaType, input.mediaId, input.seasons);
}), }),
create: permissionRequiredProcedure create: permissionRequiredProcedure
.requiresPermission("search-engine-create") .requiresPermission("search-engine-create")
.input(validation.searchEngine.manage) .input(searchEngineManageSchema)
.mutation(async ({ ctx, input }) => { .mutation(async ({ ctx, input }) => {
await ctx.db.insert(searchEngines).values({ await ctx.db.insert(searchEngines).values({
id: createId(), id: createId(),
@@ -161,7 +163,7 @@ export const searchEngineRouter = createTRPCRouter({
}), }),
update: permissionRequiredProcedure update: permissionRequiredProcedure
.requiresPermission("search-engine-modify-all") .requiresPermission("search-engine-modify-all")
.input(validation.searchEngine.edit) .input(searchEngineEditSchema)
.mutation(async ({ ctx, input }) => { .mutation(async ({ ctx, input }) => {
const searchEngine = await ctx.db.query.searchEngines.findFirst({ const searchEngine = await ctx.db.query.searchEngines.findFirst({
where: eq(searchEngines.id, input.id), where: eq(searchEngines.id, input.id),
@@ -188,7 +190,7 @@ export const searchEngineRouter = createTRPCRouter({
}), }),
delete: permissionRequiredProcedure delete: permissionRequiredProcedure
.requiresPermission("search-engine-full-all") .requiresPermission("search-engine-full-all")
.input(validation.common.byId) .input(byIdSchema)
.mutation(async ({ ctx, input }) => { .mutation(async ({ ctx, input }) => {
await ctx.db await ctx.db
.update(users) .update(users)

View File

@@ -3,7 +3,7 @@ import { z } from "zod";
import { getServerSettingByKeyAsync, getServerSettingsAsync, updateServerSettingByKeyAsync } from "@homarr/db/queries"; import { getServerSettingByKeyAsync, getServerSettingsAsync, updateServerSettingByKeyAsync } from "@homarr/db/queries";
import type { ServerSettings } from "@homarr/server-settings"; import type { ServerSettings } from "@homarr/server-settings";
import { defaultServerSettingsKeys } 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 { createTRPCRouter, onboardingProcedure, permissionRequiredProcedure, publicProcedure } from "../trpc";
import { nextOnboardingStepAsync } from "./onboard/onboard-queries"; import { nextOnboardingStepAsync } from "./onboard/onboard-queries";
@@ -32,7 +32,7 @@ export const serverSettingsRouter = createTRPCRouter({
}), }),
initSettings: onboardingProcedure initSettings: onboardingProcedure
.requiresStep("settings") .requiresStep("settings")
.input(validation.settings.init) .input(settingsInitSchema)
.mutation(async ({ ctx, input }) => { .mutation(async ({ ctx, input }) => {
await updateServerSettingByKeyAsync(ctx.db, "analytics", input.analytics); await updateServerSettingByKeyAsync(ctx.db, "analytics", input.analytics);
await updateServerSettingByKeyAsync(ctx.db, "crawlingAndIndexing", input.crawlingAndIndexing); await updateServerSettingByKeyAsync(ctx.db, "crawlingAndIndexing", input.crawlingAndIndexing);

View File

@@ -10,7 +10,20 @@ import { selectUserSchema } from "@homarr/db/validationSchemas";
import { credentialsAdminGroup } from "@homarr/definitions"; import { credentialsAdminGroup } from "@homarr/definitions";
import type { SupportedAuthProvider } from "@homarr/definitions"; import type { SupportedAuthProvider } from "@homarr/definitions";
import { logger } from "@homarr/log"; 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 { convertIntersectionToZodObject } from "../schema-merger";
import { import {
@@ -28,7 +41,7 @@ import { changeSearchPreferencesAsync, changeSearchPreferencesInputSchema } from
export const userRouter = createTRPCRouter({ export const userRouter = createTRPCRouter({
initUser: onboardingProcedure initUser: onboardingProcedure
.requiresStep("user") .requiresStep("user")
.input(validation.user.init) .input(userInitSchema)
.mutation(async ({ ctx, input }) => { .mutation(async ({ ctx, input }) => {
throwIfCredentialsDisabled(); throwIfCredentialsDisabled();
@@ -52,7 +65,7 @@ export const userRouter = createTRPCRouter({
await nextOnboardingStepAsync(ctx.db, undefined); await nextOnboardingStepAsync(ctx.db, undefined);
}), }),
register: publicProcedure register: publicProcedure
.input(validation.user.registrationApi) .input(userRegistrationApiSchema)
.output(z.void()) .output(z.void())
.mutation(async ({ ctx, input }) => { .mutation(async ({ ctx, input }) => {
throwIfCredentialsDisabled(); throwIfCredentialsDisabled();
@@ -82,7 +95,7 @@ export const userRouter = createTRPCRouter({
create: permissionRequiredProcedure create: permissionRequiredProcedure
.requiresPermission("admin") .requiresPermission("admin")
.meta({ openapi: { method: "POST", path: "/api/users", tags: ["users"], protect: true } }) .meta({ openapi: { method: "POST", path: "/api/users", tags: ["users"], protect: true } })
.input(validation.user.create) .input(userCreateSchema)
.output(z.void()) .output(z.void())
.mutation(async ({ ctx, input }) => { .mutation(async ({ ctx, input }) => {
throwIfCredentialsDisabled(); throwIfCredentialsDisabled();
@@ -259,7 +272,7 @@ export const userRouter = createTRPCRouter({
return user; return user;
}), }),
editProfile: protectedProcedure editProfile: protectedProcedure
.input(validation.user.editProfile) .input(userEditProfileSchema)
.output(z.void()) .output(z.void())
.meta({ openapi: { method: "PUT", path: "/api/users/profile", tags: ["users"], protect: true } }) .meta({ openapi: { method: "PUT", path: "/api/users/profile", tags: ["users"], protect: true } })
.mutation(async ({ input, ctx }) => { .mutation(async ({ input, ctx }) => {
@@ -318,7 +331,7 @@ export const userRouter = createTRPCRouter({
await ctx.db.delete(users).where(eq(users.id, input.userId)); await ctx.db.delete(users).where(eq(users.id, input.userId));
}), }),
changePassword: protectedProcedure changePassword: protectedProcedure
.input(validation.user.changePasswordApi) .input(userChangePasswordApiSchema)
.output(z.void()) .output(z.void())
.meta({ openapi: { method: "PATCH", path: "/api/users/{userId}/changePassword", tags: ["users"], protect: true } }) .meta({ openapi: { method: "PATCH", path: "/api/users/{userId}/changePassword", tags: ["users"], protect: true } })
.mutation(async ({ ctx, input }) => { .mutation(async ({ ctx, input }) => {
@@ -384,7 +397,7 @@ export const userRouter = createTRPCRouter({
.where(eq(users.id, input.userId)); .where(eq(users.id, input.userId));
}), }),
changeHomeBoards: protectedProcedure 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()) .output(z.void())
.meta({ openapi: { method: "PATCH", path: "/api/users/changeHome", tags: ["users"], protect: true } }) .meta({ openapi: { method: "PATCH", path: "/api/users/changeHome", tags: ["users"], protect: true } })
.mutation(async ({ input, ctx }) => { .mutation(async ({ input, ctx }) => {
@@ -430,7 +443,7 @@ export const userRouter = createTRPCRouter({
changeDefaultSearchEngine: protectedProcedure changeDefaultSearchEngine: protectedProcedure
.input( .input(
convertIntersectionToZodObject( 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()) .output(z.void())
@@ -457,7 +470,7 @@ export const userRouter = createTRPCRouter({
await changeSearchPreferencesAsync(ctx.db, ctx.session, input); await changeSearchPreferencesAsync(ctx.db, ctx.session, input);
}), }),
changeColorScheme: protectedProcedure changeColorScheme: protectedProcedure
.input(validation.user.changeColorScheme) .input(userChangeColorSchemeSchema)
.output(z.void()) .output(z.void())
.meta({ openapi: { method: "PATCH", path: "/api/users/changeScheme", tags: ["users"], protect: true } }) .meta({ openapi: { method: "PATCH", path: "/api/users/changeScheme", tags: ["users"], protect: true } })
.mutation(async ({ input, ctx }) => { .mutation(async ({ input, ctx }) => {
@@ -469,7 +482,7 @@ export const userRouter = createTRPCRouter({
.where(eq(users.id, ctx.session.user.id)); .where(eq(users.id, ctx.session.user.id));
}), }),
changePingIconsEnabled: protectedProcedure changePingIconsEnabled: protectedProcedure
.input(validation.user.pingIconsEnabled.and(validation.common.byId)) .input(userPingIconsEnabledSchema.and(byIdSchema))
.mutation(async ({ input, ctx }) => { .mutation(async ({ input, ctx }) => {
// Only admins can change other users ping icons enabled // Only admins can change other users ping icons enabled
if (!ctx.session.user.permissions.includes("admin") && ctx.session.user.id !== input.id) { 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)); .where(eq(users.id, ctx.session.user.id));
}), }),
changeFirstDayOfWeek: protectedProcedure changeFirstDayOfWeek: protectedProcedure
.input(convertIntersectionToZodObject(validation.user.firstDayOfWeek.and(validation.common.byId))) .input(convertIntersectionToZodObject(userFirstDayOfWeekSchema.and(byIdSchema)))
.output(z.void()) .output(z.void())
.meta({ openapi: { method: "PATCH", path: "/api/users/firstDayOfWeek", tags: ["users"], protect: true } }) .meta({ openapi: { method: "PATCH", path: "/api/users/firstDayOfWeek", tags: ["users"], protect: true } })
.mutation(async ({ input, ctx }) => { .mutation(async ({ input, ctx }) => {
@@ -522,7 +535,7 @@ export const userRouter = createTRPCRouter({
}), }),
}); });
const createUserAsync = async (db: Database, input: Omit<z.infer<typeof validation.user.baseCreate>, "groupIds">) => { const createUserAsync = async (db: Database, input: Omit<z.infer<typeof userBaseCreateSchema>, "groupIds">) => {
const salt = await createSaltAsync(); const salt = await createSaltAsync();
const hashedPassword = await hashPasswordAsync(input.password, salt); const hashedPassword = await hashPasswordAsync(input.password, salt);

View File

@@ -6,9 +6,9 @@ import type { Modify } from "@homarr/common/types";
import { eq } from "@homarr/db"; import { eq } from "@homarr/db";
import type { Database } from "@homarr/db"; import type { Database } from "@homarr/db";
import { users } from "@homarr/db/schema"; 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() }), z.object({ userId: z.string() }),
); );

View File

@@ -1,6 +1,6 @@
import { getIntegrationKindsByCategory } from "@homarr/definitions"; import { getIntegrationKindsByCategory } from "@homarr/definitions";
import { mediaTranscodingRequestHandler } from "@homarr/request-handler/media-transcoding"; 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 type { IntegrationAction } from "../../middlewares/integration";
import { createOneIntegrationMiddleware } from "../../middlewares/integration"; import { createOneIntegrationMiddleware } from "../../middlewares/integration";
@@ -12,7 +12,7 @@ const createIndexerManagerIntegrationMiddleware = (action: IntegrationAction) =>
export const mediaTranscodingRouter = createTRPCRouter({ export const mediaTranscodingRouter = createTRPCRouter({
getDataAsync: publicProcedure getDataAsync: publicProcedure
.unstable_concat(createIndexerManagerIntegrationMiddleware("query")) .unstable_concat(createIndexerManagerIntegrationMiddleware("query"))
.input(validation.common.paginated.pick({ page: true, pageSize: true })) .input(paginatedSchema.pick({ page: true, pageSize: true }))
.query(async ({ ctx, input }) => { .query(async ({ ctx, input }) => {
const innerHandler = mediaTranscodingRequestHandler.handler(ctx.integration, { const innerHandler = mediaTranscodingRequestHandler.handler(ctx.integration, {
pageOffset: input.page, pageOffset: input.page,

View File

@@ -1,15 +1,39 @@
import { z } from "zod";
import { fetchWithTimeout } from "@homarr/common"; import { fetchWithTimeout } from "@homarr/common";
import { validation } from "@homarr/validation";
import { createTRPCRouter, publicProcedure } from "../../trpc"; 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({ 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( 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&current_weather=true&timezone=auto`, `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&current_weather=true&timezone=auto`,
); );
const json: unknown = await res.json(); const json: unknown = await res.json();
const weather = await validation.widget.weather.atLocationOutput.parseAsync(json); const weather = await atLocationOutput.parseAsync(json);
return { return {
current: weather.current_weather, current: weather.current_weather,
daily: weather.daily.time.map((value, index) => { daily: weather.daily.time.map((value, index) => {

View File

@@ -5,11 +5,11 @@ import type { Database } from "@homarr/db";
import { and, eq } from "@homarr/db"; import { and, eq } from "@homarr/db";
import { users } from "@homarr/db/schema"; import { users } from "@homarr/db/schema";
import { logger } from "@homarr/log"; import { logger } from "@homarr/log";
import type { validation } from "@homarr/validation"; import type { userSignInSchema } from "@homarr/validation/user";
export const authorizeWithBasicCredentialsAsync = async ( export const authorizeWithBasicCredentialsAsync = async (
db: Database, db: Database,
credentials: z.infer<typeof validation.user.signIn>, credentials: z.infer<typeof userSignInSchema>,
) => { ) => {
const user = await db.query.users.findFirst({ const user = await db.query.users.findFirst({
where: and(eq(users.name, credentials.name.toLowerCase()), eq(users.provider, "credentials")), where: and(eq(users.name, credentials.name.toLowerCase()), eq(users.provider, "credentials")),

View File

@@ -6,14 +6,14 @@ import type { Database, InferInsertModel } from "@homarr/db";
import { and, createId, eq } from "@homarr/db"; import { and, createId, eq } from "@homarr/db";
import { users } from "@homarr/db/schema"; import { users } from "@homarr/db/schema";
import { logger } from "@homarr/log"; import { logger } from "@homarr/log";
import type { validation } from "@homarr/validation"; import type { userSignInSchema } from "@homarr/validation/user";
import { env } from "../../../env"; import { env } from "../../../env";
import { LdapClient } from "../ldap-client"; import { LdapClient } from "../ldap-client";
export const authorizeWithLdapCredentialsAsync = async ( export const authorizeWithLdapCredentialsAsync = async (
db: Database, db: Database,
credentials: z.infer<typeof validation.user.signIn>, credentials: z.infer<typeof userSignInSchema>,
) => { ) => {
logger.info(`user ${credentials.name} is trying to log in using LDAP. Connecting to LDAP server...`); logger.info(`user ${credentials.name} is trying to log in using LDAP. Connecting to LDAP server...`);
const client = new LdapClient(); const client = new LdapClient();

View File

@@ -1,7 +1,7 @@
import type Credentials from "@auth/core/providers/credentials"; import type Credentials from "@auth/core/providers/credentials";
import type { Database } from "@homarr/db"; import type { Database } from "@homarr/db";
import { validation } from "@homarr/validation"; import { userSignInSchema } from "@homarr/validation/user";
import { authorizeWithBasicCredentialsAsync } from "./authorization/basic-authorization"; import { authorizeWithBasicCredentialsAsync } from "./authorization/basic-authorization";
import { authorizeWithLdapCredentialsAsync } from "./authorization/ldap-authorization"; import { authorizeWithLdapCredentialsAsync } from "./authorization/ldap-authorization";
@@ -15,7 +15,7 @@ export const createCredentialsConfiguration = (db: Database) =>
name: "Credentials", name: "Credentials",
// eslint-disable-next-line no-restricted-syntax // eslint-disable-next-line no-restricted-syntax
async authorize(credentials) { async authorize(credentials) {
const data = await validation.user.signIn.parseAsync(credentials); const data = await userSignInSchema.parseAsync(credentials);
return await authorizeWithBasicCredentialsAsync(db, data); return await authorizeWithBasicCredentialsAsync(db, data);
}, },
@@ -28,7 +28,7 @@ export const createLdapConfiguration = (db: Database) =>
name: "Ldap", name: "Ldap",
// eslint-disable-next-line no-restricted-syntax // eslint-disable-next-line no-restricted-syntax
async authorize(credentials) { async authorize(credentials) {
const data = await validation.user.signIn.parseAsync(credentials); const data = await userSignInSchema.parseAsync(credentials);
return await authorizeWithLdapCredentialsAsync(db, data).catch(() => null); return await authorizeWithLdapCredentialsAsync(db, data).catch(() => null);
}, },
}) satisfies CredentialsConfiguration; }) satisfies CredentialsConfiguration;

View File

@@ -5,7 +5,7 @@ import { generateSecureRandomToken } from "@homarr/common/server";
import { and, count, createId, db, eq } from "@homarr/db"; import { and, count, createId, db, eq } from "@homarr/db";
import { getMaxGroupPositionAsync } from "@homarr/db/queries"; import { getMaxGroupPositionAsync } from "@homarr/db/queries";
import { groupMembers, groupPermissions, groups, users } from "@homarr/db/schema"; import { groupMembers, groupPermissions, groups, users } from "@homarr/db/schema";
import { usernameSchema } from "@homarr/validation"; import { usernameSchema } from "@homarr/validation/user";
export const recreateAdmin = command({ export const recreateAdmin = command({
name: "recreate-admin", name: "recreate-admin",

View File

@@ -1,7 +1,7 @@
import { objectKeys } from "@homarr/common"; import { objectKeys } from "@homarr/common";
import type { JobGroupKeys } from "@homarr/cron-jobs"; import type { JobGroupKeys } from "@homarr/cron-jobs";
import { createSubPubChannel } from "@homarr/redis"; import { createSubPubChannel } from "@homarr/redis";
import { zodEnumFromArray } from "@homarr/validation"; import { zodEnumFromArray } from "@homarr/validation/enums";
export const cronJobRunnerChannel = createSubPubChannel<JobGroupKeys>("cron-job-runner", { persist: false }); export const cronJobRunnerChannel = createSubPubChannel<JobGroupKeys>("cron-job-runner", { persist: false });

View File

@@ -3,7 +3,7 @@ import { z } from "zod";
import type { AnyZodObject, ZodDiscriminatedUnion, ZodEffects, ZodIntersection } from "zod"; import type { AnyZodObject, ZodDiscriminatedUnion, ZodEffects, ZodIntersection } from "zod";
import { useI18n } from "@homarr/translation/client"; import { useI18n } from "@homarr/translation/client";
import { zodErrorMap } from "@homarr/validation/form"; import { zodErrorMap } from "@homarr/validation/form/i18n";
export const useZodForm = < export const useZodForm = <
TSchema extends TSchema extends

View File

@@ -8,7 +8,7 @@ import { clientApi } from "@homarr/api/client";
import { revalidatePathActionAsync } from "@homarr/common/client"; import { revalidatePathActionAsync } from "@homarr/common/client";
import { showErrorNotification, showSuccessNotification } from "@homarr/notifications"; import { showErrorNotification, showSuccessNotification } from "@homarr/notifications";
import { useI18n, useScopedI18n } from "@homarr/translation/client"; import { useI18n, useScopedI18n } from "@homarr/translation/client";
import type { validation } from "@homarr/validation"; import type { appManageSchema } from "@homarr/validation/app";
import { AppForm } from "./_form"; import { AppForm } from "./_form";
@@ -33,7 +33,7 @@ export const AppNewForm = ({
}); });
const handleSubmit = useCallback( const handleSubmit = useCallback(
(values: z.infer<typeof validation.app.manage>, redirect: boolean, afterSuccess?: () => void) => { (values: z.infer<typeof appManageSchema>, redirect: boolean, afterSuccess?: () => void) => {
mutate(values, { mutate(values, {
onSuccess() { onSuccess() {
showSuccessNotification({ showSuccessNotification({

View File

@@ -10,12 +10,12 @@ import type { z } from "zod";
import { clientApi } from "@homarr/api/client"; import { clientApi } from "@homarr/api/client";
import { useZodForm } from "@homarr/form"; import { useZodForm } from "@homarr/form";
import { useI18n } from "@homarr/translation/client"; 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 { IconPicker } from "../icon-picker/icon-picker";
import { findBestIconMatch } from "./icon-matcher"; import { findBestIconMatch } from "./icon-matcher";
type FormType = z.infer<typeof validation.app.manage>; type FormType = z.infer<typeof appManageSchema>;
interface AppFormProps { interface AppFormProps {
showBackToOverview: boolean; showBackToOverview: boolean;
@@ -37,7 +37,7 @@ export const AppForm = ({
}: AppFormProps) => { }: AppFormProps) => {
const t = useI18n(); const t = useI18n();
const form = useZodForm(validation.app.manage, { const form = useZodForm(appManageSchema, {
initialValues: { initialValues: {
name: initialValues?.name ?? "", name: initialValues?.name ?? "",
description: initialValues?.description ?? "", description: initialValues?.description ?? "",

View File

@@ -5,7 +5,7 @@ import { clientApi } from "@homarr/api/client";
import type { MaybePromise } from "@homarr/common/types"; import type { MaybePromise } from "@homarr/common/types";
import { showErrorNotification, showSuccessNotification } from "@homarr/notifications"; import { showErrorNotification, showSuccessNotification } from "@homarr/notifications";
import { useI18n } from "@homarr/translation/client"; import { useI18n } from "@homarr/translation/client";
import { supportedMediaUploadFormats } from "@homarr/validation"; import { supportedMediaUploadFormats } from "@homarr/validation/media";
interface UploadMediaProps { interface UploadMediaProps {
children: (props: { onClick: () => void; loading: boolean }) => JSX.Element; children: (props: { onClick: () => void; loading: boolean }) => JSX.Element;

View File

@@ -5,7 +5,7 @@ import { AppForm } from "@homarr/forms-collection";
import { createModal } from "@homarr/modals"; import { createModal } from "@homarr/modals";
import { showErrorNotification, showSuccessNotification } from "@homarr/notifications"; import { showErrorNotification, showSuccessNotification } from "@homarr/notifications";
import { useI18n, useScopedI18n } from "@homarr/translation/client"; import { useI18n, useScopedI18n } from "@homarr/translation/client";
import type { validation } from "@homarr/validation"; import type { appManageSchema } from "@homarr/validation/app";
interface QuickAddAppModalProps { interface QuickAddAppModalProps {
onClose: (createdAppId: string) => Promise<void>; onClose: (createdAppId: string) => Promise<void>;
@@ -24,7 +24,7 @@ export const QuickAddAppModal = createModal<QuickAddAppModalProps>(({ actions, i
}, },
}); });
const handleSubmit = (values: z.infer<typeof validation.app.manage>) => { const handleSubmit = (values: z.infer<typeof appManageSchema>) => {
mutate(values, { mutate(values, {
async onSuccess({ appId }) { async onSuccess({ appId }) {
showSuccessNotification({ showSuccessNotification({

View File

@@ -8,11 +8,11 @@ import { useZodForm } from "@homarr/form";
import { createModal } from "@homarr/modals"; import { createModal } from "@homarr/modals";
import { showErrorNotification, showSuccessNotification } from "@homarr/notifications"; import { showErrorNotification, showSuccessNotification } from "@homarr/notifications";
import { useI18n } from "@homarr/translation/client"; import { useI18n } from "@homarr/translation/client";
import { validation } from "@homarr/validation"; import { boardColumnCountSchema, boardCreateSchema, boardNameSchema } from "@homarr/validation/board";
export const AddBoardModal = createModal(({ actions }) => { export const AddBoardModal = createModal(({ actions }) => {
const t = useI18n(); const t = useI18n();
const form = useZodForm(validation.board.create, { const form = useZodForm(boardCreateSchema, {
mode: "controlled", mode: "controlled",
initialValues: { initialValues: {
name: "", name: "",
@@ -28,7 +28,7 @@ export const AddBoardModal = createModal(({ actions }) => {
const boardNameStatus = useBoardNameStatus(form.values.name); 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 minColumnCount = columnCountChecks.find((check) => check.kind === "min")?.value;
const maxColumnCount = columnCountChecks.find((check) => check.kind === "max")?.value; const maxColumnCount = columnCountChecks.find((check) => check.kind === "max")?.value;
@@ -97,7 +97,7 @@ export const useBoardNameStatus = (name: string) => {
const t = useI18n(); const t = useI18n();
const [debouncedName] = useDebouncedValue(name, 250); const [debouncedName] = useDebouncedValue(name, 250);
const { data: boardExists, isLoading } = clientApi.board.exists.useQuery(debouncedName, { const { data: boardExists, isLoading } = clientApi.board.exists.useQuery(debouncedName, {
enabled: validation.board.create.shape.name.safeParse(debouncedName).success, enabled: boardNameSchema.safeParse(debouncedName).success,
}); });
return { return {

View File

@@ -5,7 +5,7 @@ import type { MaybePromise } from "@homarr/common/types";
import { useZodForm } from "@homarr/form"; import { useZodForm } from "@homarr/form";
import { showErrorNotification, showSuccessNotification } from "@homarr/notifications"; import { showErrorNotification, showSuccessNotification } from "@homarr/notifications";
import { useI18n } from "@homarr/translation/client"; import { useI18n } from "@homarr/translation/client";
import { validation } from "@homarr/validation"; import { boardDuplicateSchema } from "@homarr/validation/board";
import { createModal } from "../../../modals/src/creator"; import { createModal } from "../../../modals/src/creator";
import { useBoardNameStatus } from "./add-board-modal"; import { useBoardNameStatus } from "./add-board-modal";
@@ -20,7 +20,7 @@ interface InnerProps {
export const DuplicateBoardModal = createModal<InnerProps>(({ actions, innerProps }) => { export const DuplicateBoardModal = createModal<InnerProps>(({ actions, innerProps }) => {
const t = useI18n(); const t = useI18n();
const form = useZodForm(validation.board.duplicate.omit({ id: true }), { const form = useZodForm(boardDuplicateSchema.omit({ id: true }), {
mode: "controlled", mode: "controlled",
initialValues: { initialValues: {
name: innerProps.board.name, name: innerProps.board.name,

View File

@@ -6,12 +6,12 @@ import { useZodForm } from "@homarr/form";
import { createModal } from "@homarr/modals"; import { createModal } from "@homarr/modals";
import { showErrorNotification, showSuccessNotification } from "@homarr/notifications"; import { showErrorNotification, showSuccessNotification } from "@homarr/notifications";
import { useI18n } from "@homarr/translation/client"; import { useI18n } from "@homarr/translation/client";
import { validation } from "@homarr/validation"; import { groupCreateSchema } from "@homarr/validation/group";
export const AddGroupModal = createModal<void>(({ actions }) => { export const AddGroupModal = createModal<void>(({ actions }) => {
const t = useI18n(); const t = useI18n();
const { mutate, isPending } = clientApi.group.createGroup.useMutation(); const { mutate, isPending } = clientApi.group.createGroup.useMutation();
const form = useZodForm(validation.group.create, { const form = useZodForm(groupCreateSchema, {
initialValues: { initialValues: {
name: "", name: "",
}, },

View File

@@ -1,15 +1,15 @@
import { z } from "zod"; import { z } from "zod";
import { zfd } from "zod-form-data"; import { zfd } from "zod-form-data";
import { validation } from "@homarr/validation"; import { boardNameSchema } from "@homarr/validation/board";
import { createCustomErrorParams } from "@homarr/validation/form"; import { createCustomErrorParams } from "@homarr/validation/form/i18n";
export const sidebarBehaviours = ["remove-items", "last-section"] as const; export const sidebarBehaviours = ["remove-items", "last-section"] as const;
export const defaultSidebarBehaviour = "last-section"; export const defaultSidebarBehaviour = "last-section";
export type SidebarBehaviour = (typeof sidebarBehaviours)[number]; export type SidebarBehaviour = (typeof sidebarBehaviours)[number];
export const oldmarrImportConfigurationSchema = z.object({ export const oldmarrImportConfigurationSchema = z.object({
name: validation.board.name, name: boardNameSchema,
onlyImportApps: z.boolean().default(false), onlyImportApps: z.boolean().default(false),
sidebarBehaviour: z.enum(sidebarBehaviours).default(defaultSidebarBehaviour), sidebarBehaviour: z.enum(sidebarBehaviours).default(defaultSidebarBehaviour),
}); });

View File

@@ -3,7 +3,7 @@ import { useState } from "react";
import { Popover, Progress } from "@mantine/core"; import { Popover, Progress } from "@mantine/core";
import { useScopedI18n } from "@homarr/translation/client"; import { useScopedI18n } from "@homarr/translation/client";
import { passwordRequirements } from "@homarr/validation"; import { passwordRequirements } from "@homarr/validation/user";
import { PasswordRequirement } from "./password-requirement"; import { PasswordRequirement } from "./password-requirement";

View File

@@ -1 +0,0 @@
export * from "./src";

View File

@@ -5,8 +5,7 @@
"license": "Apache-2.0", "license": "Apache-2.0",
"type": "module", "type": "module",
"exports": { "exports": {
".": "./index.ts", "./*": "./src/*.ts"
"./form": "./src/form/i18n.ts"
}, },
"typesVersions": { "typesVersions": {
"*": { "*": {

View File

@@ -1,6 +1,15 @@
import { z } from "zod"; 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), name: z.string().trim().min(1).max(64),
description: z description: z
.string() .string()
@@ -9,14 +18,7 @@ const manageAppSchema = z.object({
.transform((value) => (value.length === 0 ? null : value)) .transform((value) => (value.length === 0 ? null : value))
.nullable(), .nullable(),
iconUrl: z.string().trim().min(1), iconUrl: z.string().trim().min(1),
href: z href: appHrefSchema,
.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(),
pingUrl: z pingUrl: z
.string() .string()
.trim() .trim()
@@ -27,12 +29,8 @@ const manageAppSchema = z.object({
.nullable(), .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 = { export const appEditSchema = appManageSchema.and(z.object({ id: z.string() }));
manage: manageAppSchema,
createMany: z
.array(manageAppSchema.omit({ iconUrl: true }).and(z.object({ iconUrl: z.string().min(1).nullable() })))
.min(1),
edit: editAppSchema,
};

View File

@@ -18,27 +18,28 @@ const hexColorNullableSchema = hexColorSchema
.nullable() .nullable()
.transform((value) => (value?.trim().length === 0 ? null : value)); .transform((value) => (value?.trim().length === 0 ? null : value));
const boardNameSchema = z export const boardNameSchema = z
.string() .string()
.min(1) .min(1)
.max(255) .max(255)
.regex(/^[A-Za-z0-9-\\_]*$/); .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, name: boardNameSchema,
}); });
const renameSchema = z.object({ export const boardRenameSchema = z.object({
id: z.string(), id: z.string(),
name: boardNameSchema, name: boardNameSchema,
}); });
const duplicateSchema = z.object({ export const boardDuplicateSchema = z.object({
id: z.string(), id: z.string(),
name: boardNameSchema, name: boardNameSchema,
}); });
const changeVisibilitySchema = z.object({ export const boardChangeVisibilitySchema = z.object({
id: z.string(), id: z.string(),
visibility: z.enum(["public", "private"]), visibility: z.enum(["public", "private"]),
}); });
@@ -48,7 +49,7 @@ const trimmedNullableString = z
.nullable() .nullable()
.transform((value) => (value?.trim().length === 0 ? null : value)); .transform((value) => (value?.trim().length === 0 ? null : value));
const savePartialSettingsSchema = z export const boardSavePartialSettingsSchema = z
.object({ .object({
pageTitle: trimmedNullableString, pageTitle: trimmedNullableString,
metaTitle: trimmedNullableString, metaTitle: trimmedNullableString,
@@ -68,52 +69,28 @@ const savePartialSettingsSchema = z
}) })
.partial(); .partial();
const saveLayoutsSchema = z.object({ export const boardSaveLayoutsSchema = z.object({
id: z.string(), id: z.string(),
layouts: z.array( layouts: z.array(
z.object({ z.object({
id: z.string(), id: z.string(),
name: z.string().trim().nonempty().max(32), name: z.string().trim().nonempty().max(32),
columnCount: z.number().min(1).max(24), columnCount: boardColumnCountSchema,
breakpoint: z.number().min(0).max(32767), breakpoint: z.number().min(0).max(32767),
}), }),
), ),
}); });
const saveSchema = z.object({ export const boardSaveSchema = z.object({
id: z.string(), id: z.string(),
sections: z.array(sectionSchema), sections: z.array(sectionSchema),
items: z.array(commonItemSchema), items: z.array(commonItemSchema),
}); });
const createSchema = z.object({ name: boardNameSchema, columnCount: z.number().min(1).max(24), isPublic: z.boolean() }); export const boardCreateSchema = z.object({
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 = {
name: boardNameSchema, name: boardNameSchema,
byName: byNameSchema, columnCount: boardColumnCountSchema,
savePartialSettings: savePartialSettingsSchema, isPublic: z.boolean(),
saveLayouts: saveLayoutsSchema, });
save: saveSchema,
create: createSchema, export const boardSavePermissionsSchema = createSavePermissionsSchema(zodEnumFromArray(boardPermissions));
duplicate: duplicateSchema,
rename: renameSchema,
changeVisibility: changeVisibilitySchema,
permissions: permissionsSchema,
savePermissions: savePermissionsSchema,
};

View File

@@ -2,7 +2,7 @@ import { z } from "zod";
import { createCustomErrorParams } from "./form/i18n"; 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) => { export const superRefineCertificateFile = (value: File | null, context: z.RefinementCtx) => {
if (!value) { 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) { if (!result.success) {
return context.addIssue({ return context.addIssue({
code: "custom", code: "custom",
@@ -46,7 +46,3 @@ export const superRefineCertificateFile = (value: File | null, context: z.Refine
return null; return null;
}; };
export const certificateSchemas = {
validFileNameSchema,
};

View File

@@ -1,6 +1,6 @@
import { z } from "zod"; import { z } from "zod";
const paginatedSchema = z.object({ export const paginatedSchema = z.object({
search: z.string().optional(), search: z.string().optional(),
pageSize: z.number().int().positive().default(10), pageSize: z.number().int().positive().default(10),
page: z.number().int().positive().default(1), page: z.number().int().positive().default(1),
@@ -10,26 +10,7 @@ export const byIdSchema = z.object({
id: z.string(), id: z.string(),
}); });
const searchSchema = z.object({ export const searchSchema = z.object({
query: z.string(), query: z.string(),
limit: z.number().int().positive().default(10), 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,
};

View File

@@ -5,7 +5,7 @@ import { everyoneGroup, groupPermissionKeys } from "@homarr/definitions";
import { byIdSchema } from "./common"; import { byIdSchema } from "./common";
import { zodEnumFromArray } from "./enums"; import { zodEnumFromArray } from "./enums";
const createSchema = z.object({ export const groupCreateSchema = z.object({
name: z name: z
.string() .string()
.trim() .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(), homeBoardId: z.string().nullable(),
mobileHomeBoardId: z.string().nullable(), mobileHomeBoardId: z.string().nullable(),
}); });
const savePartialSettingsSchema = z.object({ export const groupSavePartialSettingsSchema = z.object({
id: z.string(), id: z.string(),
settings: settingsSchema.partial(), settings: groupSettingsSchema.partial(),
}); });
const savePermissionsSchema = z.object({ export const groupSavePermissionsSchema = z.object({
groupId: z.string(), groupId: z.string(),
permissions: z.array(zodEnumFromArray(groupPermissionKeys)), permissions: z.array(zodEnumFromArray(groupPermissionKeys)),
}); });
const savePositionsSchema = z.object({ export const groupSavePositionsSchema = z.object({
positions: z.array(z.string()), positions: z.array(z.string()),
}); });
const groupUserSchema = z.object({ groupId: z.string(), userId: z.string() }); export 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,
};

View File

@@ -1,10 +1,6 @@
import { z } from "zod"; import { z } from "zod";
const findIconsSchema = z.object({ export const iconsFindSchema = z.object({
searchText: z.string().optional(), searchText: z.string().optional(),
limitPerGroup: z.number().min(1).max(500).default(12), limitPerGroup: z.number().min(1).max(500).default(12),
}); });
export const iconsSchemas = {
findIcons: findIconsSchema,
};

View File

@@ -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";

View File

@@ -5,7 +5,7 @@ import { integrationKinds, integrationPermissions, integrationSecretKinds } from
import { zodEnumFromArray } from "./enums"; import { zodEnumFromArray } from "./enums";
import { createSavePermissionsSchema } from "./permissions"; import { createSavePermissionsSchema } from "./permissions";
const integrationCreateSchema = z.object({ export const integrationCreateSchema = z.object({
name: z.string().nonempty().max(127), name: z.string().nonempty().max(127),
url: z url: z
.string() .string()
@@ -21,7 +21,7 @@ const integrationCreateSchema = z.object({
attemptSearchEngineCreation: z.boolean(), attemptSearchEngineCreation: z.boolean(),
}); });
const integrationUpdateSchema = z.object({ export const integrationUpdateSchema = z.object({
id: z.string().cuid2(), id: z.string().cuid2(),
name: z.string().nonempty().max(127), name: z.string().nonempty().max(127),
url: z.string().url(), url: z.string().url(),
@@ -33,29 +33,4 @@ const integrationUpdateSchema = z.object({
), ),
}); });
const idSchema = z.object({ export const integrationSavePermissionsSchema = createSavePermissionsSchema(zodEnumFromArray(integrationPermissions));
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,
};

View File

@@ -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,
},
};

View File

@@ -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 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) => { file: zfd.file().superRefine((value: File | null, context: z.RefinementCtx) => {
if (!value) { if (!value) {
return context.addIssue({ return context.addIssue({
@@ -38,7 +38,3 @@ export const uploadMediaSchema = zfd.formData({
return null; return null;
}), }),
}); });
export const mediaSchemas = {
uploadMedia: uploadMediaSchema,
};

View File

@@ -13,7 +13,7 @@ const fromIntegrationSearchEngine = z.object({
integrationId: z.string().optional(), integrationId: z.string().optional(),
}); });
const manageSearchEngineSchema = z.object({ const baseSearchEngineManageSchema = z.object({
name: z.string().min(1).max(64), name: z.string().min(1).max(64),
short: z.string().min(1).max(8), short: z.string().min(1).max(8),
iconUrl: z.string().min(1), iconUrl: z.string().min(1),
@@ -21,21 +21,18 @@ const manageSearchEngineSchema = z.object({
}); });
const createManageSearchEngineSchema = <T extends ZodTypeAny>( const createManageSearchEngineSchema = <T extends ZodTypeAny>(
callback: (schema: typeof manageSearchEngineSchema) => T, callback: (schema: typeof baseSearchEngineManageSchema) => T,
) => ) =>
z z
.discriminatedUnion("type", [genericSearchEngine, fromIntegrationSearchEngine]) .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 schema
.extend({ .extend({
id: z.string(), id: z.string(),
}) })
.omit({ short: true }), .omit({ short: true }),
); );
export const searchEngineSchemas = {
manage: createManageSearchEngineSchema((schema) => schema),
edit: editSearchEngineSchema,
};

View File

@@ -1,6 +1,6 @@
import { z } from "zod"; import { z } from "zod";
const initSettingsSchema = z.object({ export const settingsInitSchema = z.object({
analytics: z.object({ analytics: z.object({
enableGeneral: z.boolean(), enableGeneral: z.boolean(),
enableWidgetData: z.boolean(), enableWidgetData: z.boolean(),
@@ -14,7 +14,3 @@ const initSettingsSchema = z.object({
noSiteLinksSearchBox: z.boolean(), noSiteLinksSearchBox: z.boolean(),
}), }),
}); });
export const settingsSchemas = {
init: initSettingsSchema,
};

View File

@@ -22,7 +22,7 @@ export const passwordRequirements = [
value: keyof TranslationObject["user"]["field"]["password"]["requirement"]; value: keyof TranslationObject["user"]["field"]["password"]["requirement"];
}[]; }[];
const passwordSchema = z export const userPasswordSchema = z
.string() .string()
.min(8) .min(8)
.max(255) .max(255)
@@ -51,39 +51,39 @@ const addConfirmPasswordRefinement = <TObj extends { password: string; confirmPa
}); });
}; };
const baseCreateUserSchema = z.object({ export const userBaseCreateSchema = z.object({
username: usernameSchema, username: usernameSchema,
password: passwordSchema, password: userPasswordSchema,
confirmPassword: z.string(), confirmPassword: z.string(),
email: z.string().email().or(z.string().length(0).optional()), email: z.string().email().or(z.string().length(0).optional()),
groupIds: z.array(z.string()), groupIds: z.array(z.string()),
}); });
const createUserSchema = addConfirmPasswordRefinement(baseCreateUserSchema); export const userCreateSchema = addConfirmPasswordRefinement(userBaseCreateSchema);
const initUserSchema = addConfirmPasswordRefinement(baseCreateUserSchema.omit({ groupIds: true })); export const userInitSchema = addConfirmPasswordRefinement(userBaseCreateSchema.omit({ groupIds: true }));
const signInSchema = z.object({ export const userSignInSchema = z.object({
name: z.string().min(1), name: z.string().min(1),
password: z.string().min(1), password: z.string().min(1),
}); });
const registrationSchema = addConfirmPasswordRefinement( export const userRegistrationSchema = addConfirmPasswordRefinement(
z.object({ z.object({
username: usernameSchema, username: usernameSchema,
password: passwordSchema, password: userPasswordSchema,
confirmPassword: z.string(), confirmPassword: z.string(),
}), }),
); );
const registrationSchemaApi = registrationSchema.and( export const userRegistrationApiSchema = userRegistrationSchema.and(
z.object({ z.object({
inviteId: z.string(), inviteId: z.string(),
token: z.string(), token: z.string(),
}), }),
); );
const editProfileSchema = z.object({ export const userEditProfileSchema = z.object({
id: z.string(), id: z.string(),
name: usernameSchema, name: usernameSchema,
email: z email: z
@@ -97,51 +97,33 @@ const editProfileSchema = z.object({
const baseChangePasswordSchema = z.object({ const baseChangePasswordSchema = z.object({
previousPassword: z.string().min(1), previousPassword: z.string().min(1),
password: passwordSchema, password: userPasswordSchema,
confirmPassword: z.string(), confirmPassword: z.string(),
userId: z.string(), userId: z.string(),
}); });
const changePasswordSchema = addConfirmPasswordRefinement(baseChangePasswordSchema.omit({ userId: true })); export const userChangePasswordSchema = addConfirmPasswordRefinement(baseChangePasswordSchema.omit({ userId: true }));
const changePasswordApiSchema = addConfirmPasswordRefinement(baseChangePasswordSchema); export const userChangePasswordApiSchema = addConfirmPasswordRefinement(baseChangePasswordSchema);
const changeHomeBoardSchema = z.object({ export const userChangeHomeBoardsSchema = z.object({
homeBoardId: z.string().nullable(), homeBoardId: z.string().nullable(),
mobileHomeBoardId: z.string().nullable(), mobileHomeBoardId: z.string().nullable(),
}); });
const changeSearchPreferencesSchema = z.object({ export const userChangeSearchPreferencesSchema = z.object({
defaultSearchEngineId: z.string().min(1).nullable(), defaultSearchEngineId: z.string().min(1).nullable(),
openInNewTab: z.boolean(), openInNewTab: z.boolean(),
}); });
const changeColorSchemeSchema = z.object({ export const userChangeColorSchemeSchema = z.object({
colorScheme: zodEnumFromArray(colorSchemes), colorScheme: zodEnumFromArray(colorSchemes),
}); });
const firstDayOfWeekSchema = z.object({ export const userFirstDayOfWeekSchema = z.object({
firstDayOfWeek: z.custom<DayOfWeek>((value) => z.number().min(0).max(6).safeParse(value).success), firstDayOfWeek: z.custom<DayOfWeek>((value) => z.number().min(0).max(6).safeParse(value).success),
}); });
const pingIconsEnabledSchema = z.object({ export const userPingIconsEnabledSchema = z.object({
pingIconsEnabled: z.boolean(), 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,
};

View File

@@ -1,5 +0,0 @@
import { weatherWidgetSchemas } from "./weather";
export const widgetSchemas = {
weather: weatherWidgetSchemas,
};

View File

@@ -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(),
});

View File

@@ -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,
};

View File

@@ -6,7 +6,7 @@ import { useForm } from "@homarr/form";
import { createModal } from "@homarr/modals"; import { createModal } from "@homarr/modals";
import { useI18n } from "@homarr/translation/client"; import { useI18n } from "@homarr/translation/client";
import { TextMultiSelect } from "@homarr/ui"; import { TextMultiSelect } from "@homarr/ui";
import type { BoardItemAdvancedOptions } from "@homarr/validation"; import type { BoardItemAdvancedOptions } from "@homarr/validation/shared";
interface InnerProps { interface InnerProps {
advancedOptions: BoardItemAdvancedOptions; advancedOptions: BoardItemAdvancedOptions;

View File

@@ -10,7 +10,7 @@ import { zodResolver } from "@homarr/form";
import { createModal, useModalAction } from "@homarr/modals"; import { createModal, useModalAction } from "@homarr/modals";
import type { SettingsContextProps } from "@homarr/settings"; import type { SettingsContextProps } from "@homarr/settings";
import { useI18n } from "@homarr/translation/client"; import { useI18n } from "@homarr/translation/client";
import { zodErrorMap } from "@homarr/validation/form"; import { zodErrorMap } from "@homarr/validation/form/i18n";
import { widgetImports } from ".."; import { widgetImports } from "..";
import { getInputForType } from "../_inputs"; import { getInputForType } from "../_inputs";