From 8fb13273bf8a52cb721a4dbf939b3393202c0f8a Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Sun, 28 Dec 2025 19:36:14 +0000 Subject: [PATCH] settings --- .../src/hooks/forms/UseInitSettingsForm.tsx | 5 +- client/src/pages/settings/AdminSettings.tsx | 5 +- client/src/pages/settings/SettingsForm.tsx | 4 +- client/src/validation/index.ts | 1 + client/src/validation/system-settings.ts | 55 ++++++++++++++ client/src/validation/utils.ts | 7 ++ client/src/validation/zod.ts | 75 ------------------- 7 files changed, 69 insertions(+), 83 deletions(-) create mode 100644 client/src/validation/system-settings.ts diff --git a/client/src/hooks/forms/UseInitSettingsForm.tsx b/client/src/hooks/forms/UseInitSettingsForm.tsx index 1a98fe48a..5caad8788 100644 --- a/client/src/hooks/forms/UseInitSettingsForm.tsx +++ b/client/src/hooks/forms/UseInitSettingsForm.tsx @@ -1,11 +1,12 @@ -import { systemSettingsSchema } from "@/validation/zod"; +import { systemSettingsSchema } from "@/validation"; import { useMemo } from "react"; +import type { FormValues } from "@/pages/settings/SettingsForm"; import { z } from "zod"; export const useInitForm = ({ initialData, }: { - initialData: Partial> | undefined; + initialData: Partial | undefined; }) => { return useMemo(() => { const defaults: z.infer = { diff --git a/client/src/pages/settings/AdminSettings.tsx b/client/src/pages/settings/AdminSettings.tsx index c2d130d15..f2e7b52cd 100644 --- a/client/src/pages/settings/AdminSettings.tsx +++ b/client/src/pages/settings/AdminSettings.tsx @@ -7,12 +7,9 @@ import {} from "react-i18next"; import { useAppSelector } from "@/hooks/AppHooks"; import { useGet, usePatch, usePost } from "@/hooks/UseApi"; import type { ApiResponse } from "@/types/api"; -import { z } from "zod"; -import { systemSettingsSchema } from "@/validation/zod"; +import type { FormValues } from "@/pages/settings/SettingsForm"; const AdminSettingsPage = () => { - type FormValues = z.infer; - const user = useAppSelector((state) => state.auth.user); const orgPermissions = user?.org?.permissions || []; diff --git a/client/src/pages/settings/SettingsForm.tsx b/client/src/pages/settings/SettingsForm.tsx index d2119b4a9..a0be1ca16 100644 --- a/client/src/pages/settings/SettingsForm.tsx +++ b/client/src/pages/settings/SettingsForm.tsx @@ -6,14 +6,14 @@ import { ConfigBox } from "@/components/design-elements"; import { useTheme } from "@mui/material/styles"; import { useTranslation } from "react-i18next"; -import { systemSettingsSchema } from "@/validation/zod"; +import { systemSettingsSchema } from "@/validation"; import { z } from "zod"; import { zodResolver } from "@hookform/resolvers/zod"; import { useForm, Controller, type SubmitHandler } from "react-hook-form"; import { useInitForm } from "@/hooks/forms/UseInitSettingsForm"; import { useEffect } from "react"; -type FormValues = z.infer; +export type FormValues = z.infer; export const SettingsForm = ({ initialData, diff --git a/client/src/validation/index.ts b/client/src/validation/index.ts index b100f0428..5644732eb 100644 --- a/client/src/validation/index.ts +++ b/client/src/validation/index.ts @@ -8,3 +8,4 @@ export * from "./utils"; export * from "./notification-channel"; export * from "./maintenance"; export * from "./team"; +export * from "./system-settings"; diff --git a/client/src/validation/system-settings.ts b/client/src/validation/system-settings.ts new file mode 100644 index 000000000..b3619c3ac --- /dev/null +++ b/client/src/validation/system-settings.ts @@ -0,0 +1,55 @@ +import { z } from "zod"; +import { optionalString } from "@/validation/utils"; + +export const systemSettingsSchema = z.object({ + systemEmailHost: optionalString( + z.string().trim().min(1, { message: "SMTP host is required" }).max(255, { + message: "SMTP host must be 255 characters or fewer", + }) + ), + systemEmailPort: optionalString( + z + .union([z.number().int(), z.string().regex(/^\d+$/)]) + .transform((val) => (typeof val === "string" ? Number(val) : val)) + .refine( + (val) => + val === undefined || + (Number.isInteger(val) && val > 0 && val < 65536), + { + message: "Port must be a number between 1 and 65535", + } + ) + ), + systemEmailAddress: optionalString( + z.string().email({ message: "SMTP email must be valid" }) + ), + systemEmailPassword: optionalString( + z.string().max(255, { + message: "SMTP password must be 255 characters or fewer", + }) + ), + systemEmailUser: optionalString( + z.string().trim().max(255, { + message: "SMTP username must be 255 characters or fewer", + }) + ), + systemEmailConnectionHost: optionalString( + z.string().trim().max(255, { + message: "Connection host must be 255 characters or fewer", + }) + ), + systemEmailTLSServername: optionalString( + z.string().trim().max(255, { + message: "TLS server name must be 255 characters or fewer", + }) + ), + systemEmailSecure: z.boolean().optional(), + systemEmailPool: z.boolean().optional(), + systemEmailIgnoreTLS: z.boolean().optional(), + systemEmailRequireTLS: z.boolean().optional(), + systemEmailRejectUnauthorized: z.boolean().optional(), + checksRetentionDays: z.coerce + .number({ message: "Number required" }) + .min(1, "Retention days must be at least 1") + .max(365, "Retention days cannot exceed 365 days"), +}); diff --git a/client/src/validation/utils.ts b/client/src/validation/utils.ts index c4dc597c2..b1d99991f 100644 --- a/client/src/validation/utils.ts +++ b/client/src/validation/utils.ts @@ -21,3 +21,10 @@ export const durationSchema = (minInterval: number) => }); } }); + +export const optionalString = (schema: T) => + z.preprocess( + (val) => + val === "" || val === null || val === undefined ? undefined : val, + schema.optional() + ); diff --git a/client/src/validation/zod.ts b/client/src/validation/zod.ts index 7d94befa6..b9198315b 100644 --- a/client/src/validation/zod.ts +++ b/client/src/validation/zod.ts @@ -1,26 +1,4 @@ import { z } from "zod"; -// humanInterval moved to split schema files -// MaintenanceRepeats moved to maintenance schema - -// urlRegex moved to client/src/validation/utils.ts - -// monitor duration schema moved to client/src/validation/monitor.ts -// pagespeed duration schema moved to client/src/validation/pagespeed.ts -// infra duration schema moved to client/src/validation/infra.ts - -const optionalString = (schema: T) => - z.preprocess( - (val) => - val === "" || val === null || val === undefined ? undefined : val, - schema.optional() - ); - -// moved to client/src/validation/monitor.ts - -// moved to client/src/validation/pagespeed.ts -// moved to client/src/validation/infra.ts - -// moved to client/src/validation/team.ts export const inviteSchema = z.object({ email: z.email("Invalid email address"), @@ -29,59 +7,6 @@ export const inviteSchema = z.object({ orgRoleId: z.string().optional(), }); -export const systemSettingsSchema = z.object({ - systemEmailHost: optionalString( - z.string().trim().min(1, { message: "SMTP host is required" }).max(255, { - message: "SMTP host must be 255 characters or fewer", - }) - ), - systemEmailPort: optionalString( - z - .union([z.number().int(), z.string().regex(/^\d+$/)]) - .transform((val) => (typeof val === "string" ? Number(val) : val)) - .refine( - (val) => - val === undefined || - (Number.isInteger(val) && val > 0 && val < 65536), - { - message: "Port must be a number between 1 and 65535", - } - ) - ), - systemEmailAddress: optionalString( - z.string().email({ message: "SMTP email must be valid" }) - ), - systemEmailPassword: optionalString( - z.string().max(255, { - message: "SMTP password must be 255 characters or fewer", - }) - ), - systemEmailUser: optionalString( - z.string().trim().max(255, { - message: "SMTP username must be 255 characters or fewer", - }) - ), - systemEmailConnectionHost: optionalString( - z.string().trim().max(255, { - message: "Connection host must be 255 characters or fewer", - }) - ), - systemEmailTLSServername: optionalString( - z.string().trim().max(255, { - message: "TLS server name must be 255 characters or fewer", - }) - ), - systemEmailSecure: z.boolean().optional(), - systemEmailPool: z.boolean().optional(), - systemEmailIgnoreTLS: z.boolean().optional(), - systemEmailRequireTLS: z.boolean().optional(), - systemEmailRejectUnauthorized: z.boolean().optional(), - checksRetentionDays: z.coerce - .number({ message: "Number required" }) - .min(1, "Retention days must be at least 1") - .max(365, "Retention days cannot exceed 365 days"), -}); - const statusPageUrlRegex = /^[A-Za-z0-9]+$/; export const statusPageSchema = z.object({