import { FastifyInstance } from 'fastify'; import { AuthService } from '../services/auth'; import { logger } from '../logger'; import { prisma } from '../services/prisma'; export async function authRoutes(fastify: FastifyInstance) { // Login with Email/Password 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 }; // If organizationId is omitted (older frontend), resolve it from the user record. // Security: password is still verified — this doesn't bypass auth. 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 } }; }); // Request a password reset link 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' }); // Always return 200 to avoid email enumeration const user = await prisma.user.findFirst({ where: { email } }); if (!user) return reply.send({ ok: true }); // Sign a short-lived reset token (1 hour) with extra 'purpose' claim not in the standard payload shape 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: `

Bonjour ${user.name || ''},

Cliquez sur le lien ci-dessous pour réinitialiser votre mot de passe. Ce lien expire dans 1 heure.

Réinitialiser mon mot de passe

Si vous n'avez pas demandé cette réinitialisation, ignorez cet email.

` }); logger.info({ email }, '[AUTH] Password reset link sent'); return reply.send({ ok: true }); }); // Set new password via reset token 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 }); }); // Get current user profile (guarded by JWT) 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 } }; }); }