/** * Validation schemas using Zod * Provides strict client-side validation for all forms and user inputs */ import { z } from 'zod'; // Schémas de base pour les types communs export const emailSchema = z .string() .min(1, "L'email est requis") .email("Format d'email invalide") .max(255, "L'email est trop long") .toLowerCase(); // T0197: Updated to match backend validation (min 8 characters) export const passwordSchema = z .string() .min(8, 'Le mot de passe doit contenir au moins 8 caractères') .max(128, 'Le mot de passe est trop long') .regex(/[A-Z]/, 'Le mot de passe doit contenir au moins une majuscule') .regex(/[a-z]/, 'Le mot de passe doit contenir au moins une minuscule') .regex(/[0-9]/, 'Le mot de passe doit contenir au moins un chiffre') .regex( /[!@#$%^&*(),.?":{}|<>]/, 'Le mot de passe doit contenir au moins un caractère spécial', ) .refine( (password) => !/(.)\1{3,}/.test(password), 'Le mot de passe ne doit pas contenir de répétition de caractères', ) .refine( (password) => !/123456|password|qwerty/i.test(password), 'Le mot de passe ne doit pas contenir de patterns communs', ); export const usernameSchema = z .string() .min(3, "Le nom d'utilisateur doit contenir au moins 3 caractères") .max(30, "Le nom d'utilisateur est trop long") .regex( /^[a-zA-Z0-9_-]+$/, "Le nom d'utilisateur ne peut contenir que des lettres, chiffres, tirets et underscores", ) .refine( (username) => !/^(admin|root|user|test|demo)$/i.test(username), "Ce nom d'utilisateur est réservé", ); export const displayNameSchema = z .string() .min(1, "Le nom d'affichage est requis") .max(50, "Le nom d'affichage est trop long") .regex( /^[a-zA-ZÀ-ÿ0-9\s\-'.]+$/, "Le nom d'affichage contient des caractères non autorisés", ); // Schémas pour l'authentification export const loginSchema = z.object({ email: emailSchema, password: z.string().min(1, 'Le mot de passe est requis'), remember_me: z.boolean().optional(), captcha: z.string().optional(), }); export const registerSchema = z .object({ email: emailSchema, password: passwordSchema, confirmPassword: z.string(), username: usernameSchema, displayName: displayNameSchema, acceptTerms: z .boolean() .refine( (val) => val === true, "Vous devez accepter les conditions d'utilisation", ), acceptPrivacy: z .boolean() .refine( (val) => val === true, 'Vous devez accepter la politique de confidentialité', ), }) .refine((data) => data.password === data.confirmPassword, { message: 'Les mots de passe ne correspondent pas', path: ['confirmPassword'], }); export const forgotPasswordSchema = z.object({ email: emailSchema, }); export const resetPasswordSchema = z .object({ token: z.string().min(1, 'Le token de réinitialisation est requis'), password: passwordSchema, confirmPassword: z.string(), }) .refine((data) => data.password === data.confirmPassword, { message: 'Les mots de passe ne correspondent pas', path: ['confirmPassword'], }); export const changePasswordSchema = z .object({ currentPassword: z.string().min(1, 'Le mot de passe actuel est requis'), newPassword: passwordSchema, confirmNewPassword: z.string(), }) .refine((data) => data.newPassword === data.confirmNewPassword, { message: 'Les nouveaux mots de passe ne correspondent pas', path: ['confirmNewPassword'], }) .refine((data) => data.currentPassword !== data.newPassword, { message: "Le nouveau mot de passe doit être différent de l'actuel", path: ['newPassword'], }); // Schémas pour les fichiers export const fileUploadSchema = z .object({ file: z.instanceof(File, { message: 'Un fichier est requis' }), title: z .string() .min(1, 'Le titre est requis') .max(255, 'Le titre est trop long'), artist: z .string() .min(1, "L'artiste est requis") .max(255, "L'artiste est trop long"), description: z .string() .max(1000, 'La description est trop longue') .optional(), tags: z .array(z.string().max(50)) .max(10, 'Maximum 10 tags autorisés') .optional(), isPublic: z.boolean().optional(), }) .refine( (data) => { const maxSize = 100 * 1024 * 1024; // 100MB return data.file.size <= maxSize; }, { message: 'Le fichier est trop volumineux (maximum 100MB)', path: ['file'], }, ) .refine( (data) => { const allowedTypes = [ 'audio/mpeg', 'audio/mp3', 'audio/wav', 'audio/flac', 'audio/aac', 'audio/ogg', 'audio/m4a', ]; return allowedTypes.includes(data.file.type); }, { message: 'Type de fichier non supporté', path: ['file'], }, ); export const imageUploadSchema = z .object({ file: z.instanceof(File, { message: 'Une image est requise' }), alt: z.string().max(255, 'Le texte alternatif est trop long').optional(), caption: z.string().max(500, 'La légende est trop longue').optional(), }) .refine( (data) => { const maxSize = 10 * 1024 * 1024; // 10MB return data.file.size <= maxSize; }, { message: "L'image est trop volumineuse (maximum 10MB)", path: ['file'], }, ) .refine( (data) => { const allowedTypes = [ 'image/jpeg', 'image/png', 'image/gif', 'image/webp', 'image/svg+xml', ]; return allowedTypes.includes(data.file.type); }, { message: "Type d'image non supporté", path: ['file'], }, ); // Schémas pour les messages de chat export const chatMessageSchema = z.object({ content: z .string() .min(1, 'Le message ne peut pas être vide') .max(2000, 'Le message est trop long (maximum 2000 caractères)') .refine( (content) => !/ ({ message: 'Langue non supportée' }), }) .optional(), timezone: z.string().max(50, 'Timezone invalide').optional(), notifications: z .object({ email: z.boolean().optional(), push: z.boolean().optional(), chat: z.boolean().optional(), mentions: z.boolean().optional(), }) .optional(), privacy: z .object({ showOnlineStatus: z.boolean().optional(), showLastSeen: z.boolean().optional(), allowDirectMessages: z.boolean().optional(), showInSearch: z.boolean().optional(), }) .optional(), }); // Schémas pour la recherche export const searchSchema = z.object({ query: z .string() .min(1, 'La requête de recherche est requise') .max(100, 'La requête est trop longue'), type: z .enum(['all', 'users', 'tracks', 'conversations'], { errorMap: () => ({ message: 'Type de recherche invalide' }), }) .optional(), limit: z.number().min(1).max(100).optional(), offset: z.number().min(0).optional(), }); // Schémas pour la pagination export const paginationSchema = z.object({ page: z .number() .min(1, 'Le numéro de page doit être supérieur à 0') .optional(), limit: z .number() .min(1) .max(100, 'La limite doit être entre 1 et 100') .optional(), cursor: z.string().optional(), sortBy: z.string().max(50).optional(), sortOrder: z.enum(['asc', 'desc']).optional(), }); // Schémas pour les filtres export const trackFiltersSchema = z.object({ genre: z.string().max(50).optional(), artist: z.string().max(100).optional(), year: z.number().min(1900).max(new Date().getFullYear()).optional(), duration: z .object({ min: z.number().min(0).optional(), max: z.number().min(0).optional(), }) .optional(), isPublic: z.boolean().optional(), }); // Schémas pour les réponses API export const apiResponseSchema = (dataSchema: T) => z.object({ success: z.boolean(), data: dataSchema.optional(), error: z .object({ code: z.number(), message: z.string(), details: z.any().optional(), }) .optional(), meta: z .object({ timestamp: z.string(), requestId: z.string().optional(), pagination: paginationSchema.optional(), }) .optional(), }); // Schémas pour les erreurs export const errorSchema = z.object({ code: z.number(), message: z.string(), details: z.any().optional(), field: z.string().optional(), timestamp: z.string(), }); // Types TypeScript générés à partir des schémas export type LoginFormData = z.infer; export type RegisterFormData = z.infer; export type ForgotPasswordFormData = z.infer; export type ResetPasswordFormData = z.infer; export type ChangePasswordFormData = z.infer; export type FileUploadFormData = z.infer; export type ImageUploadFormData = z.infer; export type ChatMessageFormData = z.infer; export type CreateConversationFormData = z.infer< typeof createConversationSchema >; export type UpdateConversationFormData = z.infer< typeof updateConversationSchema >; export type UpdateProfileFormData = z.infer; export type UpdateSettingsFormData = z.infer; export type SearchFormData = z.infer; export type PaginationFormData = z.infer; export type TrackFiltersFormData = z.infer; export type ErrorFormData = z.infer; // Utilitaires de validation export function validateForm( schema: z.ZodSchema, data: unknown, ): { success: boolean; data?: T; errors?: Record; } { try { const result = schema.parse(data); return { success: true, data: result }; } catch (error) { if (error instanceof z.ZodError) { const errors: Record = {}; error.errors.forEach((err) => { const path = err.path.join('.'); if (!errors[path]) { errors[path] = []; } errors[path].push(err.message); }); return { success: false, errors }; } return { success: false, errors: { general: ['Erreur de validation inattendue'] }, }; } } // Hook React pour la validation export function useValidation(schema: z.ZodSchema) { return (data: unknown) => validateForm(schema, data); }