/** * @factory/shared — auth DTOs (PRODUCT-SPEC §1, §24). * * The same zod schemas validate API input (NestJS pipes) and define the * client-side types, so FE and BE can never drift (ARCHITECTURE.md §3). */ import { z } from "zod"; import { LIMITS } from "../constants.js"; /** * Password policy: min 10 chars, at least one letter and one number. * (Bcrypt only uses the first 72 bytes — enforced at MAX_PASSWORD.) */ export const passwordSchema = z .string() .min(LIMITS.MIN_PASSWORD, "Password must be at least 10 characters") .max(LIMITS.MAX_PASSWORD, "Password must be at most 72 characters") .regex(/[A-Za-z]/, "Password must contain at least one letter") .regex(/[0-9]/, "Password must contain at least one number"); export type PasswordInput = z.infer; export const emailSchema = z .string() .min(1, "Email is required") .max(254) .email("Invalid email address") .transform((v) => v.toLowerCase()); export type EmailInput = z.infer; export const registerInputSchema = z .object({ name: z.string().min(1, "Name is required").max(100), email: emailSchema, password: passwordSchema, }) .strict(); export type RegisterInput = z.infer; export const loginInputSchema = z .object({ email: emailSchema, password: z.string().min(1, "Password is required"), }) .strict(); export type LoginInput = z.infer; export const refreshInputSchema = z .object({ /** Opaque rotating refresh token. */ refreshToken: z.string().min(1, "Refresh token is required"), }) .strict(); export type RefreshInput = z.infer; export const passwordResetRequestSchema = z .object({ email: emailSchema, }) .strict(); export type PasswordResetRequest = z.infer; export const passwordResetConfirmSchema = z .object({ /** Single-use reset token (email link). */ token: z.string().min(1, "Token is required"), password: passwordSchema, }) .strict(); export type PasswordResetConfirm = z.infer; export const updateProfileInputSchema = z .object({ name: z.string().min(1).max(100).optional(), displayName: z.string().min(1).max(100).optional(), avatarUrl: z.string().url().optional().nullable(), locale: z .string() .regex(/^[a-z]{2,3}(-[A-Za-z0-9]{2,4})?$/) .optional(), timezone: z.string().min(1).max(64).optional(), theme: z.enum(["light", "dark", "system"]).optional(), }) .strict() .refine((v) => Object.keys(v).length > 0, { message: "No fields to update" }); export type UpdateProfileInput = z.infer;