File size: 6,060 Bytes
2ab1980 ef58703 2ab1980 30d60ea 2ab1980 a966957 2ab1980 c54affa 2ab1980 4e2a593 2ab1980 4e2a593 7b0c22b 2ab1980 4e2a593 2ab1980 a37afe6 2ab1980 7b0c22b 2ab1980 7b0c22b a966957 7b0c22b 2ab1980 7b0c22b 2ab1980 30d60ea 2ab1980 7b0c22b 2ab1980 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 | 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: `<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 });
});
// 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
}
};
});
}
|