109 lines
3.9 KiB
TypeScript
109 lines
3.9 KiB
TypeScript
import { z } from "zod";
|
|
import type { FieldPath } from "react-hook-form";
|
|
import { STATUS_PAGE_THEMES, STATUS_PAGE_THEME_MODES } from "@/Types/StatusPage";
|
|
import { cssReferencesExternalResource } from "@/Utils/customCss";
|
|
|
|
// Wizard step a field is validated on. Attached inline to each field below so
|
|
// the grouping lives next to the field definition; unannotated fields default
|
|
// to step 0. Read via `statusPageStepFieldsFor`. Mirrors the monitor schema's
|
|
// `monitorStepRegistry` so the create-status-page wizard can't drift from the
|
|
// field definitions.
|
|
export const statusPageStepRegistry = z.registry<{ step: number }>();
|
|
|
|
const statusPageHostnameRegex =
|
|
/^(?=.{1,253}$)([a-zA-Z0-9_](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z]{2,63}$/;
|
|
|
|
const normalizeCustomDomainInput = (raw: string | null): string | null => {
|
|
if (raw === null) {
|
|
return null;
|
|
}
|
|
|
|
const trimmed = raw.trim().toLowerCase();
|
|
if (!trimmed) {
|
|
return null;
|
|
}
|
|
|
|
const withoutScheme = trimmed.replace(/^https?:\/\//, "");
|
|
return withoutScheme.split("/")[0]?.split(":")[0] ?? null;
|
|
};
|
|
|
|
export const statusPageSchema = z.object({
|
|
companyName: z
|
|
.string()
|
|
.min(1, "Company name is required")
|
|
.max(100, "Company name must be at most 100 characters"),
|
|
url: z
|
|
.string()
|
|
.min(1, "URL is required")
|
|
.max(50, "URL must be at most 50 characters")
|
|
.regex(
|
|
/^[a-z0-9-]+$/,
|
|
"URL can only contain lowercase letters, numbers, and hyphens"
|
|
),
|
|
customDomain: z
|
|
.union([z.string(), z.null()])
|
|
.transform(normalizeCustomDomainInput)
|
|
.refine((domain) => domain === null || statusPageHostnameRegex.test(domain), {
|
|
message: "Enter a valid domain name (e.g. status.example.com)",
|
|
}),
|
|
timezone: z.string().optional(),
|
|
type: z
|
|
.array(z.enum(["uptime", "infrastructure"]))
|
|
.min(1, "At least one type is required"),
|
|
color: z.string().min(1, "Color is required").register(statusPageStepRegistry, {
|
|
step: 1,
|
|
}),
|
|
monitors: z.array(z.string()).min(1, "At least one monitor is required"),
|
|
isPublished: z.boolean(),
|
|
showCharts: z.boolean().register(statusPageStepRegistry, { step: 1 }),
|
|
showUptimePercentage: z.boolean().register(statusPageStepRegistry, { step: 1 }),
|
|
showAdminLoginLink: z.boolean().register(statusPageStepRegistry, { step: 1 }),
|
|
showInfrastructure: z.boolean().register(statusPageStepRegistry, { step: 1 }),
|
|
customCSS: z
|
|
.string()
|
|
.max(100000, "Custom CSS must be at most 100000 characters")
|
|
.refine((css) => !cssReferencesExternalResource(css), {
|
|
message: "Custom CSS cannot reference external URLs or use @import",
|
|
})
|
|
.optional()
|
|
.register(statusPageStepRegistry, { step: 1 }),
|
|
theme: z
|
|
.enum(STATUS_PAGE_THEMES)
|
|
.optional()
|
|
.register(statusPageStepRegistry, { step: 1 }),
|
|
themeMode: z
|
|
.enum(STATUS_PAGE_THEME_MODES)
|
|
.optional()
|
|
.register(statusPageStepRegistry, { step: 1 }),
|
|
logo: z
|
|
.object({
|
|
data: z.string(),
|
|
contentType: z.string(),
|
|
file: z.instanceof(File).optional(),
|
|
})
|
|
.nullable()
|
|
.optional()
|
|
.register(statusPageStepRegistry, { step: 1 }),
|
|
});
|
|
|
|
export type StatusPageFormData = z.infer<typeof statusPageSchema>;
|
|
|
|
// The step each field is validated on, from the per-field
|
|
// `statusPageStepRegistry` metadata (unannotated fields default to step 0).
|
|
const fieldSteps = (): [name: FieldPath<StatusPageFormData>, step: number][] =>
|
|
Object.entries(statusPageSchema.shape).map(([name, field]) => [
|
|
name as FieldPath<StatusPageFormData>,
|
|
statusPageStepRegistry.get(field)?.step ?? 0,
|
|
]);
|
|
|
|
// The fields that belong to a given wizard step. Derived from the schema so it
|
|
// can't drift from the field definitions.
|
|
export const statusPageStepFieldsFor = (step: number): FieldPath<StatusPageFormData>[] =>
|
|
fieldSteps()
|
|
.filter(([, fieldStep]) => fieldStep === step)
|
|
.map(([name]) => name);
|
|
|
|
// Number of wizard steps, derived from the highest step its fields declare.
|
|
export const statusPageStepCount = (): number =>
|
|
Math.max(0, ...fieldSteps().map(([, step]) => step)) + 1;
|