| import { z } from 'zod'; |
|
|
| |
|
|
| export const loginSchema = z.object({ |
| email: z.string().email('Please enter a valid email address'), |
| 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 at most 100 characters'), |
| email: z.string().email('Please enter a valid email address'), |
| password: z |
| .string() |
| .min(8, 'Password must be at least 8 characters') |
| .refine( |
| (pwd) => /[A-Z]/.test(pwd), |
| 'Password must contain at least one uppercase letter' |
| ) |
| .refine( |
| (pwd) => /[a-z]/.test(pwd), |
| 'Password must contain at least one lowercase letter' |
| ) |
| .refine( |
| (pwd) => /[0-9]/.test(pwd), |
| 'Password must contain at least one number' |
| ), |
| termsAccepted: z.literal(true, { |
| message: 'You must accept the terms and conditions', |
| }), |
| }); |
|
|
| 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') |
| .refine( |
| (pwd) => /[A-Z]/.test(pwd), |
| 'Password must contain at least one uppercase letter' |
| ) |
| .refine( |
| (pwd) => /[a-z]/.test(pwd), |
| 'Password must contain at least one lowercase letter' |
| ) |
| .refine( |
| (pwd) => /[0-9]/.test(pwd), |
| 'Password must contain at least one number' |
| ), |
| confirmPassword: z.string().min(1, 'Please confirm your new password'), |
| }) |
| .refine((data) => data.newPassword === data.confirmPassword, { |
| message: 'New passwords do not match', |
| path: ['confirmPassword'], |
| }); |
|
|
| |
|
|
| const ALLOWED_MIME_TYPES = [ |
| 'application/pdf', |
| 'image/jpeg', |
| 'image/png', |
| 'image/tiff', |
| ]; |
|
|
| const ALLOWED_EXTENSIONS = ['pdf', 'jpg', 'jpeg', 'png', 'tiff', 'tif']; |
|
|
| const MAX_FILE_SIZE = 20 * 1024 * 1024; |
|
|
| export function validateFile(file: File): string | null { |
| |
| if (file.size > MAX_FILE_SIZE) { |
| return `File "${file.name}" exceeds the 20 MB size limit (${(file.size / 1024 / 1024).toFixed(1)} MB)`; |
| } |
|
|
| |
| if (!ALLOWED_MIME_TYPES.includes(file.type)) { |
| return `File "${file.name}" has unsupported type "${file.type}". Allowed: PDF, JPEG, PNG, TIFF`; |
| } |
|
|
| |
| const ext = file.name.split('.').pop()?.toLowerCase() ?? ''; |
| if (!ALLOWED_EXTENSIONS.includes(ext)) { |
| return `File "${file.name}" has unsupported extension ".${ext}". Allowed: .pdf, .jpg, .jpeg, .png, .tiff, .tif`; |
| } |
|
|
| return null; |
| } |
|
|
| |
|
|
| export const chatSchema = z.object({ |
| message: z |
| .string() |
| .min(1, 'Message cannot be empty') |
| .max(500, 'Message must be at most 500 characters'), |
| history: z |
| .array( |
| z.object({ |
| role: z.enum(['user', 'assistant', 'system']), |
| content: z.string(), |
| }) |
| ) |
| .optional() |
| .default([]), |
| }); |
|
|
| |
|
|
| export const invoiceIdSchema = z.object({ |
| id: z.string().min(1, 'Invoice ID is required'), |
| }); |
|
|
| |
|
|
| export function getClientIp(request: Request): string { |
| const forwarded = request.headers.get('x-forwarded-for'); |
| if (forwarded) { |
| return forwarded.split(',')[0].trim(); |
| } |
| const realIp = request.headers.get('x-real-ip'); |
| if (realIp) return realIp.trim(); |
| return 'unknown'; |
| } |