diff --git a/package-lock.json b/package-lock.json index 2b83785..4314750 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,6 +9,7 @@ "version": "1.0.0", "license": "ISC", "dependencies": { + "@fastify/cookie": "^11.0.2", "@prisma/adapter-pg": "^7.6.0", "@prisma/client": "^7.6.0", "argon2": "^0.44.0", @@ -548,6 +549,26 @@ "fast-uri": "^3.0.0" } }, + "node_modules/@fastify/cookie": { + "version": "11.0.2", + "resolved": "https://registry.npmjs.org/@fastify/cookie/-/cookie-11.0.2.tgz", + "integrity": "sha512-GWdwdGlgJxyvNv+QcKiGNevSspMQXncjMZ1J8IvuDQk0jvkzgWWZFNC2En3s+nHndZBGV8IbLwOI/sxCZw/mzA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT", + "dependencies": { + "cookie": "^1.0.0", + "fastify-plugin": "^5.0.0" + } + }, "node_modules/@fastify/error": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/@fastify/error/-/error-4.2.0.tgz", diff --git a/package.json b/package.json index 289898d..a7f5d32 100644 --- a/package.json +++ b/package.json @@ -16,6 +16,7 @@ "author": "Raffi", "license": "ISC", "dependencies": { + "@fastify/cookie": "^11.0.2", "@prisma/adapter-pg": "^7.6.0", "@prisma/client": "^7.6.0", "argon2": "^0.44.0", diff --git a/src/errors/AppError.ts b/src/errors/AppError.ts index dc413a4..90e4f43 100644 --- a/src/errors/AppError.ts +++ b/src/errors/AppError.ts @@ -13,5 +13,6 @@ export class AppError extends Error { 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.'), + INVALID_CREDENTIALS: new AppError('INVALID_CREDENTIALS', 401, 'Email ou mot de passe incorrect.'), VALIDATION_ERROR: (message: string) => new AppError('VALIDATION_ERROR', 400, message), } \ No newline at end of file diff --git a/src/routes/auth.ts b/src/routes/auth.ts index db067dd..ae718d0 100644 --- a/src/routes/auth.ts +++ b/src/routes/auth.ts @@ -2,6 +2,9 @@ import { FastifyInstance } from 'fastify' import { RegisterSchema } from '../schemas/auth.schema.js' import { registerUser } from '../services/auth.service.js' +import { LoginSchema } from '../schemas/auth.schema.js' +import { loginUser } from '../services/auth.service.js' + export default async function authRoutes(fastify: FastifyInstance) { fastify.post('/auth/register', async (request, reply) => { const body = RegisterSchema.parse(request.body) // Zod throw → handler global @@ -20,4 +23,18 @@ export default async function authRoutes(fastify: FastifyInstance) { return reply.status(201).send({ user }) }) + + fastify.post('/auth/login', async (request, reply) => { + const body = LoginSchema.parse(request.body) + + const { user, authToken } = await loginUser(fastify.prisma, body) + + reply.setCookie('authToken', authToken, { + httpOnly: true, + sameSite: 'strict', + maxAge: 60 * 60 * 24 * 7, + }) + + return reply.status(200).send({ user }) + }) } \ No newline at end of file diff --git a/src/schemas/auth.schema.ts b/src/schemas/auth.schema.ts index 0388064..85cb5af 100644 --- a/src/schemas/auth.schema.ts +++ b/src/schemas/auth.schema.ts @@ -11,4 +11,11 @@ export const RegisterSchema = z.object({ .regex(/\d/, { error: 'Le mot de passe doit contenir au moins un chiffre.' }), }) -export type RegisterInput = z.infer \ No newline at end of file +export type RegisterInput = z.infer + +export const LoginSchema = z.object({ + email: z.email({ error: 'Adresse email invalide.' }), + password: z.string().min(1, { error: 'Le mot de passe est requis.' }), +}) + +export type LoginInput = z.infer \ No newline at end of file diff --git a/src/services/auth.service.ts b/src/services/auth.service.ts index 951ff36..17777c9 100644 --- a/src/services/auth.service.ts +++ b/src/services/auth.service.ts @@ -2,7 +2,7 @@ import argon2 from 'argon2' import crypto from 'crypto' import { PrismaClient } from '../generated/prisma/client.js' import { Transporter } from 'nodemailer' -import { RegisterInput } from '../schemas/auth.schema.js' +import { RegisterInput, LoginInput } 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' @@ -68,4 +68,46 @@ export async function registerUser( }) return { user, authToken } +} + +export async function loginUser( + prisma: PrismaClient, + input: LoginInput +) { + // 1. Vérif user existe + const user = await prisma.user.findUnique({ + where: { email: input.email }, + }) + + if (!user) { + throw Errors.INVALID_CREDENTIALS + } + + // 2. Vérif password + const valid = await argon2.verify(user.passwordHash, input.password) + if (!valid) { + throw Errors.INVALID_CREDENTIALS + } + + // 3. Création AuthToken + const authToken = generateToken() + await prisma.authToken.create({ + data: { + id: crypto.randomUUID(), + userId: user.id, + token: authToken, + expiresAt: generateAuthTokenExpiry(), + }, + }) + + return { + user: { + id: user.id, + email: user.email, + displayName: user.displayName, + isConfirmed: user.isConfirmed, + createdAt: user.createdAt, + }, + authToken, + } } \ No newline at end of file