/** * Zod Validation Schemas for API endpoints * Centralized validation logic for type-safety and security */ import { z } from 'zod'; // ========================================== // QR Code Schemas // ========================================== export const qrStyleSchema = z.object({ fgColor: z.string().regex(/^#[0-9A-F]{6}$/i, 'Invalid foreground color format').optional(), bgColor: z.string().regex(/^#[0-9A-F]{6}$/i, 'Invalid background color format').optional(), cornerStyle: z.enum(['square', 'rounded']).optional(), size: z.number().min(100).max(1000).optional(), }); export const createQRSchema = z.object({ title: z.string() .min(1, 'Title is required') .max(100, 'Title must be less than 100 characters'), content: z.record(z.any()), // Accept any object structure for content isStatic: z.boolean().optional(), contentType: z.enum(['URL', 'VCARD', 'GEO', 'PHONE', 'SMS', 'WHATSAPP', 'TEXT'], { errorMap: () => ({ message: 'Invalid content type' }) }), tags: z.array(z.string()).optional(), style: z.object({ foregroundColor: z.string().optional(), backgroundColor: z.string().optional(), cornerStyle: z.enum(['square', 'rounded']).optional(), size: z.number().optional(), }).optional(), }); export const updateQRSchema = z.object({ title: z.string() .min(1, 'Title is required') .max(100, 'Title must be less than 100 characters') .optional(), content: z.string() .min(1, 'Content is required') .max(5000, 'Content must be less than 5000 characters') .optional(), style: qrStyleSchema.optional(), isActive: z.boolean().optional(), }); export const bulkQRSchema = z.object({ qrs: z.array( z.object({ title: z.string().min(1).max(100), content: z.string().min(1).max(5000), contentType: z.enum(['URL', 'VCARD', 'GEO', 'PHONE', 'SMS', 'WHATSAPP', 'TEXT']), }) ).min(1, 'At least one QR code is required') .max(100, 'Maximum 100 QR codes per bulk creation'), }); // ========================================== // Authentication Schemas // ========================================== export const loginSchema = z.object({ email: z.string() .email('Invalid email format') .toLowerCase(), password: z.string() .min(1, 'Password is required'), }); export const signupSchema = z.object({ name: z.string() .min(2, 'Name must be at least 2 characters') .max(100, 'Name must be less than 100 characters') .trim(), email: z.string() .email('Invalid email format') .toLowerCase() .trim(), password: z.string() .min(8, 'Password must be at least 8 characters') .max(100, 'Password must be less than 100 characters'), // Password complexity rules removed for easier testing }); export const forgotPasswordSchema = z.object({ email: z.string() .email('Invalid email format') .toLowerCase() .trim(), }); export const resetPasswordSchema = z.object({ token: z.string().min(1, 'Reset token is required'), password: z.string() .min(8, 'Password must be at least 8 characters') .max(100, 'Password must be less than 100 characters'), // Password complexity rules removed for easier testing }); // ========================================== // Settings Schemas // ========================================== export const updateProfileSchema = z.object({ name: z.string() .min(2, 'Name must be at least 2 characters') .max(100, 'Name must be less than 100 characters') .trim(), }); export const changePasswordSchema = z.object({ currentPassword: z.string() .min(1, 'Current password is required'), newPassword: z.string() .min(8, 'Password must be at least 8 characters') .max(100, 'Password must be less than 100 characters'), // Password complexity rules removed for easier testing }); // ========================================== // Stripe Schemas // ========================================== export const createCheckoutSchema = z.object({ priceId: z.string().min(1, 'Price ID is required'), }); // ========================================== // Newsletter Schemas // ========================================== export const newsletterSubscribeSchema = z.object({ email: z.string() .email('Invalid email format') .toLowerCase() .trim() .max(255, 'Email must be less than 255 characters'), }); // ========================================== // Helper: Format Zod Errors // ========================================== export function formatZodError(error: z.ZodError) { return { error: 'Validation failed', details: error.errors.map(err => ({ field: err.path.join('.'), message: err.message, })), }; } // ========================================== // Helper: Validate with Zod // ========================================== export async function validateRequest( schema: z.ZodSchema, data: unknown ): Promise<{ success: true; data: T } | { success: false; error: any }> { try { const validatedData = schema.parse(data); return { success: true, data: validatedData }; } catch (error) { if (error instanceof z.ZodError) { return { success: false, error: formatZodError(error) }; } return { success: false, error: { error: 'Invalid request data' } }; } }