216 lines
6.3 KiB
TypeScript
216 lines
6.3 KiB
TypeScript
/**
|
|
* Zod schemas for route input validation.
|
|
*/
|
|
|
|
import { z } from "astro/zod";
|
|
|
|
/** Matches http(s) scheme at start of URL */
|
|
const HTTP_SCHEME_RE = /^https?:\/\//i;
|
|
|
|
/** Validates that a URL string uses http or https scheme. Rejects javascript:/data: URI XSS vectors. */
|
|
const httpUrl = z
|
|
.string()
|
|
.url()
|
|
.refine((url) => HTTP_SCHEME_RE.test(url), "URL must use http or https");
|
|
|
|
// ─── Field Schemas ───────────────────────────────────────────────
|
|
|
|
const fieldOptionSchema = z.object({
|
|
label: z.string().min(1),
|
|
value: z.string().min(1),
|
|
});
|
|
|
|
const fieldValidationSchema = z
|
|
.object({
|
|
minLength: z.number().int().min(0).optional(),
|
|
maxLength: z.number().int().min(1).optional(),
|
|
min: z.number().optional(),
|
|
max: z.number().optional(),
|
|
pattern: z.string().optional(),
|
|
patternMessage: z.string().optional(),
|
|
accept: z.string().optional(),
|
|
maxFileSize: z.number().int().min(1).optional(),
|
|
})
|
|
.optional();
|
|
|
|
const fieldConditionSchema = z
|
|
.object({
|
|
field: z.string().min(1),
|
|
op: z.enum(["eq", "neq", "filled", "empty"]),
|
|
value: z.string().optional(),
|
|
})
|
|
.optional();
|
|
|
|
export const fieldTypeSchema = z.enum([
|
|
"text",
|
|
"email",
|
|
"textarea",
|
|
"number",
|
|
"tel",
|
|
"url",
|
|
"date",
|
|
"select",
|
|
"radio",
|
|
"checkbox",
|
|
"checkbox-group",
|
|
"file",
|
|
"hidden",
|
|
]);
|
|
|
|
const formFieldSchema = z.object({
|
|
id: z.string().min(1),
|
|
type: fieldTypeSchema,
|
|
label: z.string().min(1),
|
|
name: z
|
|
.string()
|
|
.min(1)
|
|
.regex(/^[a-zA-Z][a-zA-Z0-9_-]*$/, "Invalid field name"),
|
|
placeholder: z.string().optional(),
|
|
helpText: z.string().optional(),
|
|
required: z.boolean(),
|
|
validation: fieldValidationSchema,
|
|
options: z.array(fieldOptionSchema).optional(),
|
|
defaultValue: z.string().optional(),
|
|
width: z.enum(["full", "half"]).default("full"),
|
|
condition: fieldConditionSchema,
|
|
});
|
|
|
|
const formPageSchema = z.object({
|
|
title: z.string().optional(),
|
|
fields: z.array(formFieldSchema).min(1, "Each page must have at least one field"),
|
|
});
|
|
|
|
// ─── Settings Schema ─────────────────────────────────────────────
|
|
|
|
const autoresponderSchema = z
|
|
.object({
|
|
subject: z.string().min(1),
|
|
body: z.string().min(1),
|
|
})
|
|
.optional();
|
|
|
|
const formSettingsSchema = z.object({
|
|
confirmationMessage: z.string().min(1).default("Thank you for your submission."),
|
|
redirectUrl: httpUrl.optional().or(z.literal("")),
|
|
notifyEmails: z.array(z.string().email()).default([]),
|
|
digestEnabled: z.boolean().default(false),
|
|
digestHour: z.number().int().min(0).max(23).default(9),
|
|
autoresponder: autoresponderSchema,
|
|
webhookUrl: httpUrl.optional().or(z.literal("")),
|
|
retentionDays: z.number().int().min(0).default(0),
|
|
spamProtection: z.enum(["none", "honeypot", "turnstile"]).default("honeypot"),
|
|
submitLabel: z.string().min(1).default("Submit"),
|
|
nextLabel: z.string().optional(),
|
|
prevLabel: z.string().optional(),
|
|
});
|
|
|
|
// ─── Form CRUD Schemas ──────────────────────────────────────────
|
|
|
|
export const formCreateSchema = z.object({
|
|
name: z.string().min(1).max(200),
|
|
slug: z
|
|
.string()
|
|
.min(1)
|
|
.max(100)
|
|
.regex(/^[a-z][a-z0-9-]*$/, "Slug must be lowercase alphanumeric with hyphens"),
|
|
pages: z.array(formPageSchema).min(1),
|
|
settings: formSettingsSchema,
|
|
});
|
|
|
|
export const formUpdateSchema = z.object({
|
|
id: z.string().min(1),
|
|
name: z.string().min(1).max(200).optional(),
|
|
slug: z
|
|
.string()
|
|
.min(1)
|
|
.max(100)
|
|
.regex(/^[a-z][a-z0-9-]*$/)
|
|
.optional(),
|
|
pages: z.array(formPageSchema).min(1).optional(),
|
|
settings: formSettingsSchema.partial().optional(),
|
|
status: z.enum(["active", "paused"]).optional(),
|
|
});
|
|
|
|
export const formDeleteSchema = z.object({
|
|
id: z.string().min(1),
|
|
deleteSubmissions: z.boolean().default(true),
|
|
});
|
|
|
|
export const formDuplicateSchema = z.object({
|
|
id: z.string().min(1),
|
|
name: z.string().min(1).max(200).optional(),
|
|
slug: z
|
|
.string()
|
|
.min(1)
|
|
.max(100)
|
|
.regex(/^[a-z][a-z0-9-]*$/)
|
|
.optional(),
|
|
});
|
|
|
|
export const definitionSchema = z.object({
|
|
id: z.string().min(1),
|
|
});
|
|
|
|
export type DefinitionInput = z.infer<typeof definitionSchema>;
|
|
|
|
// ─── Submission Schemas ──────────────────────────────────────────
|
|
|
|
export const submitSchema = z.object({
|
|
formId: z.string().min(1),
|
|
data: z.record(z.string(), z.unknown()),
|
|
files: z
|
|
.record(
|
|
z.string(),
|
|
z.object({
|
|
filename: z.string(),
|
|
contentType: z.string(),
|
|
bytes: z.custom<ArrayBuffer>(),
|
|
}),
|
|
)
|
|
.optional(),
|
|
});
|
|
|
|
export const submissionsListSchema = z.object({
|
|
formId: z.string().min(1),
|
|
status: z.enum(["new", "read", "archived"]).optional(),
|
|
starred: z.boolean().optional(),
|
|
cursor: z.string().optional(),
|
|
limit: z.number().int().min(1).max(100).default(50),
|
|
});
|
|
|
|
export const submissionGetSchema = z.object({
|
|
id: z.string().min(1),
|
|
});
|
|
|
|
export const submissionUpdateSchema = z.object({
|
|
id: z.string().min(1),
|
|
status: z.enum(["new", "read", "archived"]).optional(),
|
|
starred: z.boolean().optional(),
|
|
notes: z.string().optional(),
|
|
});
|
|
|
|
export const submissionDeleteSchema = z.object({
|
|
id: z.string().min(1),
|
|
});
|
|
|
|
export const exportSchema = z.object({
|
|
formId: z.string().min(1),
|
|
format: z.enum(["csv", "json"]).default("csv"),
|
|
status: z.enum(["new", "read", "archived"]).optional(),
|
|
from: z.string().datetime().optional(),
|
|
to: z.string().datetime().optional(),
|
|
});
|
|
|
|
// ─── Type Exports ────────────────────────────────────────────────
|
|
|
|
export type FormCreateInput = z.infer<typeof formCreateSchema>;
|
|
export type FormUpdateInput = z.infer<typeof formUpdateSchema>;
|
|
export type FormDeleteInput = z.infer<typeof formDeleteSchema>;
|
|
export type FormDuplicateInput = z.infer<typeof formDuplicateSchema>;
|
|
export type SubmitInput = z.infer<typeof submitSchema>;
|
|
export type SubmissionsListInput = z.infer<typeof submissionsListSchema>;
|
|
export type SubmissionGetInput = z.infer<typeof submissionGetSchema>;
|
|
export type SubmissionUpdateInput = z.infer<typeof submissionUpdateSchema>;
|
|
export type SubmissionDeleteInput = z.infer<typeof submissionDeleteSchema>;
|
|
export type ExportInput = z.infer<typeof exportSchema>;
|