From 928fb55dea730f75766f146f3a1a3e0eefd5ab05 Mon Sep 17 00:00:00 2001 From: Raffi Date: Mon, 30 Mar 2026 22:37:36 +0200 Subject: [PATCH] gestion des erreurs au sign-up --- src/app.ts | 2 ++ src/errors/AppError.ts | 17 +++++++++++++++ src/plugins/errorHandler.ts | 34 +++++++++++++++++++++++++++++ src/routes/auth.ts | 42 +++++++++++------------------------- src/schemas/auth.schema.ts | 11 +++++++--- src/services/auth.service.ts | 7 +++--- 6 files changed, 77 insertions(+), 36 deletions(-) create mode 100644 src/errors/AppError.ts create mode 100644 src/plugins/errorHandler.ts diff --git a/src/app.ts b/src/app.ts index 21bfc92..cef1094 100644 --- a/src/app.ts +++ b/src/app.ts @@ -4,10 +4,12 @@ import prismaPlugin from './plugins/prisma.js' import mailerPlugin from './plugins/mailer.js' import authRoutes from './routes/auth.js' import userRoutes from './routes/users.js' +import errorHandler from './plugins/errorHandler.js' export default function buildApp() { const app = Fastify({ logger: true }) + app.register(errorHandler) app.register(cookie) app.register(prismaPlugin) app.register(mailerPlugin) diff --git a/src/errors/AppError.ts b/src/errors/AppError.ts new file mode 100644 index 0000000..dc413a4 --- /dev/null +++ b/src/errors/AppError.ts @@ -0,0 +1,17 @@ +export class AppError extends Error { + constructor( + public readonly code: string, + public readonly statusCode: number, + message: string + ) { + super(message) + this.name = 'AppError' + } +} + +// Erreurs prédéfinies +export const Errors = { + EMAIL_TAKEN: new AppError('EMAIL_TAKEN', 409, 'Cette adresse email est déjà utilisée.'), + PASSWORD_TOO_WEAK: new AppError('PASSWORD_TOO_WEAK', 400, 'Le mot de passe doit contenir au moins 8 caractères.'), + VALIDATION_ERROR: (message: string) => new AppError('VALIDATION_ERROR', 400, message), +} \ No newline at end of file diff --git a/src/plugins/errorHandler.ts b/src/plugins/errorHandler.ts new file mode 100644 index 0000000..6e01771 --- /dev/null +++ b/src/plugins/errorHandler.ts @@ -0,0 +1,34 @@ +import fp from 'fastify-plugin' +import { FastifyInstance } from 'fastify' +import { ZodError } from 'zod' +import { AppError } from '../errors/AppError.js' + +export default fp(async (fastify: FastifyInstance) => { + fastify.setErrorHandler((error, request, reply) => { + // Erreur Zod + if (error instanceof ZodError) { + return reply.status(400).send({ + error: 'VALIDATION_ERROR', + details: error.issues.map((e) => ({ + field: e.path.join('.'), + message: e.message, + })), + }) + } + + // Erreur métier + if (error instanceof AppError) { + return reply.status(error.statusCode).send({ + error: error.code, + message: error.message, + }) + } + + // Erreur inconnue + fastify.log.error(error) + return reply.status(500).send({ + error: 'INTERNAL_ERROR', + message: 'Une erreur interne est survenue.', + }) + }) +}) \ No newline at end of file diff --git a/src/routes/auth.ts b/src/routes/auth.ts index 2b2b827..db067dd 100644 --- a/src/routes/auth.ts +++ b/src/routes/auth.ts @@ -1,41 +1,23 @@ import { FastifyInstance } from 'fastify' -import { ZodError } from 'zod' import { RegisterSchema } from '../schemas/auth.schema.js' import { registerUser } from '../services/auth.service.js' export default async function authRoutes(fastify: FastifyInstance) { fastify.post('/auth/register', async (request, reply) => { - // Validation Zod - let body - try { - body = RegisterSchema.parse(request.body) - } catch (err) { - if (err instanceof ZodError) { - return reply.status(400).send({ error: 'VALIDATION_ERROR', details: err.errors }) - } - throw err - } + const body = RegisterSchema.parse(request.body) // Zod throw → handler global - // Register - try { - const { user, authToken } = await registerUser( - fastify.prisma, - fastify.mailer, - body - ) + const { user, authToken } = await registerUser( + fastify.prisma, + fastify.mailer, + body + ) - reply.setCookie('authToken', authToken, { - httpOnly: true, - sameSite: 'strict', - maxAge: 60 * 60 * 24 * 7, // 7 jours en secondes - }) + reply.setCookie('authToken', authToken, { + httpOnly: true, + sameSite: 'strict', + maxAge: 60 * 60 * 24 * 7, + }) - return reply.status(201).send({ user }) - } catch (err: any) { - if (err.message === 'EMAIL_TAKEN') { - return reply.status(409).send({ error: 'EMAIL_TAKEN' }) - } - throw err - } + return reply.status(201).send({ user }) }) } \ No newline at end of file diff --git a/src/schemas/auth.schema.ts b/src/schemas/auth.schema.ts index 47936a8..0388064 100644 --- a/src/schemas/auth.schema.ts +++ b/src/schemas/auth.schema.ts @@ -1,9 +1,14 @@ import { z } from 'zod' export const RegisterSchema = z.object({ - email: z.string().email(), - password: z.string().min(8), - displayName: z.string().min(2).max(50), + email: z.email({ error: 'Adresse email invalide.' }), + password: z + .string() + .regex(/^.{8,22}$/, { error: 'Le mot de passe doit contenir entre 8 et 22 caractères.' }) + .regex(/[^A-Za-z0-9]/, { error: 'Le mot de passe doit contenir au moins un caractère spécial.' }) + .regex(/[A-Z]/, { error: 'Le mot de passe doit contenir au moins une majuscule.' }) + .regex(/[a-z]/, { error: 'Le mot de passe doit contenir au moins une minuscule.' }) + .regex(/\d/, { error: 'Le mot de passe doit contenir au moins un chiffre.' }), }) export type RegisterInput = z.infer \ No newline at end of file diff --git a/src/services/auth.service.ts b/src/services/auth.service.ts index d02bd02..951ff36 100644 --- a/src/services/auth.service.ts +++ b/src/services/auth.service.ts @@ -5,6 +5,7 @@ import { Transporter } from 'nodemailer' import { RegisterInput } from '../schemas/auth.schema.js' import { generateToken, generateAuthTokenExpiry, generateActionTokenExpiry } from './token.service.js' import { sendConfirmationMail } from './mail.service.js' +import { Errors } from '../errors/AppError.js' export async function registerUser( prisma: PrismaClient, @@ -16,19 +17,19 @@ export async function registerUser( where: { email: input.email }, }) if (existing) { - throw new Error('EMAIL_TAKEN') - } + throw Errors.EMAIL_TAKEN } // 2. Hash du mot de passe const passwordHash = await argon2.hash(input.password) // 3. Création de l'user + const displayName = input.email.split('@')[0] const user = await prisma.user.create({ data: { id: crypto.randomUUID(), email: input.email, passwordHash, - displayName: input.displayName, + displayName, avatar: '', }, select: {