CognxSafeTrack
Claude Sonnet 4.6
refactor(debt): resolve all 10 technical debt items from audit
a966957 Download apps/api/src/routes/auth.ts from safetrack/edtech: direct link, hf CLI and curl.
- Browser
- Download file 6.06 kB
-
https://huggingface.co/spaces/safetrack/edtech/resolve/main/apps/api/src/routes/auth.ts
- Command line
-
hf download hf://spaces/safetrack/edtech/apps/api/src/routes/auth.ts
-
curl -L -o auth.ts https://huggingface.co/spaces/safetrack/edtech/resolve/main/apps/api/src/routes/auth.ts
6.06 kB
| 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 | |
| } | |
| }; | |
| }); | |
| } | |