| import { FastifyInstance } from 'fastify'; |
|
|
| import { AuthService } from '../services/auth'; |
| import { logger } from '../logger'; |
| import { prisma } from '../services/prisma'; |
|
|
| export async function authRoutes(fastify: FastifyInstance) { |
| |
| |
| fastify.post('/login', { |
| config: { rateLimit: { max: 10, timeWindow: '1 minute' } }, |
| schema: { |
| body: { |
| type: 'object', |
| required: ['email', 'password'], |
| properties: { |
| email: { type: 'string', format: 'email' }, |
| password: { type: 'string' }, |
| organizationId: { type: 'string' } |
| } |
| } |
| } |
| }, async (request, reply) => { |
| const { email, password, organizationId: bodyOrgId } = request.body as { email: string; password: string; organizationId?: string }; |
|
|
| |
| |
| let user; |
| if (bodyOrgId) { |
| logger.info(`[AUTH] Login attempt for ${email} (Org: ${bodyOrgId})`); |
| user = await AuthService.findUserByEmail(email, bodyOrgId); |
| } else { |
| logger.info(`[AUTH] Login attempt for ${email} (no org — resolving from email)`); |
| user = await AuthService.findUserByEmailOnly(email); |
| } |
| |
| if (!user || !user.passwordHash) { |
| logger.warn(`[AUTH] User not found: ${email} in Org: ${bodyOrgId ?? 'unknown'}`); |
| return reply.code(401).send({ error: 'Unauthorized', message: 'Invalid email or password' }); |
| } |
|
|
| const isValid = await AuthService.verifyPassword(password, user.passwordHash); |
| if (!isValid) { |
| return reply.code(401).send({ error: 'Unauthorized', message: 'Invalid email or password' }); |
| } |
|
|
| const token = fastify.jwt.sign({ |
| id: user.id, |
| organizationId: user.organizationId, |
| role: user.role |
| }); |
|
|
| logger.info(`[AUTH] User ${email} logged in successfully for org ${user.organizationId}`); |
|
|
| return { |
| token, |
| user: { |
| id: user.id, |
| name: user.name, |
| email: user.email, |
| role: user.role, |
| organizationId: user.organizationId, |
| organization: user.organization |
| } |
| }; |
| }); |
|
|
| |
| fastify.post('/forgot-password', { config: { rateLimit: { max: 3, timeWindow: '1 minute' } } }, async (request, reply) => { |
| const { email } = request.body as { email: string }; |
| if (!email) return reply.code(400).send({ error: 'Email required' }); |
|
|
| |
| const user = await prisma.user.findFirst({ where: { email } }); |
| if (!user) return reply.send({ ok: true }); |
|
|
| |
| const resetToken = (fastify.jwt.sign as Function)( |
| { id: user.id, purpose: 'reset' }, |
| { expiresIn: '1h' } |
| ) as string; |
|
|
| const adminUrl = process.env.ADMIN_URL || 'https://edtechadmin.netlify.app'; |
| const resetLink = `${adminUrl}/reset-password?token=${resetToken}`; |
|
|
| const { scheduleEmail } = await import('../services/queue'); |
| await scheduleEmail({ |
| to: email, |
| subject: 'Réinitialisation de votre mot de passe — XAMLÉ', |
| htmlContent: `<p>Bonjour ${user.name || ''},</p> |
| <p>Cliquez sur le lien ci-dessous pour réinitialiser votre mot de passe. Ce lien expire dans 1 heure.</p> |
| <p><a href="${resetLink}" style="background:#1e293b;color:#fff;padding:12px 24px;border-radius:8px;text-decoration:none;font-weight:bold;">Réinitialiser mon mot de passe</a></p> |
| <p>Si vous n'avez pas demandé cette réinitialisation, ignorez cet email.</p>` |
| }); |
|
|
| logger.info({ email }, '[AUTH] Password reset link sent'); |
| return reply.send({ ok: true }); |
| }); |
|
|
| |
| fastify.post('/reset-password', async (request, reply) => { |
| const { token, password } = request.body as { token: string; password: string }; |
| if (!token || !password) return reply.code(400).send({ error: 'Token and password required' }); |
| if (password.length < 6) return reply.code(400).send({ error: 'Password must be at least 6 characters' }); |
|
|
| let payload: { id: string; purpose?: string }; |
| try { |
| payload = fastify.jwt.verify(token) as { id: string; purpose?: string }; |
| } catch { |
| return reply.code(401).send({ error: 'Invalid or expired token' }); |
| } |
|
|
| if (payload.purpose !== 'reset') { |
| return reply.code(401).send({ error: 'Invalid token purpose' }); |
| } |
|
|
| const passwordHash = await AuthService.hashPassword(password); |
| await prisma.user.update({ |
| where: { id: payload.id }, |
| data: { passwordHash } |
| }); |
|
|
| logger.info({ userId: payload.id }, '[AUTH] Password reset successfully'); |
| return reply.send({ ok: true }); |
| }); |
|
|
| |
| fastify.get('/me', async (request, reply) => { |
| const { id } = request.user; |
| |
| const user = await prisma.user.findUnique({ |
| where: { id }, |
| include: { organization: true } |
| }); |
|
|
| if (!user) { |
| return reply.code(404).send({ error: 'Not Found', message: 'User not found' }); |
| } |
|
|
| return { |
| user: { |
| id: user.id, |
| name: user.name, |
| email: user.email, |
| role: user.role, |
| organizationId: user.organizationId, |
| organization: user.organization |
| } |
| }; |
| }); |
| } |
|
|