import type { FastifyPluginAsync } from 'fastify'; import { z } from 'zod'; import { hashPassword, verifyPassword } from '../auth/passwords.js'; import type { AuthUser } from '../auth/plugin.js'; import { pool } from '../db/pool.js'; const credentialsSchema = z.object({ email: z.string().email().transform((email) => email.toLowerCase()), password: z.string().min(12).max(200), }); const registerSchema = credentialsSchema.extend({ name: z.string().trim().min(2).max(120), }); type UserRow = { id: string; email: string; display_name: string; role: AuthUser['role']; password_hash: string; is_active: boolean; }; const serializeUser = (user: UserRow): AuthUser => ({ id: user.id, email: user.email, name: user.display_name, role: user.role, }); export const authRoutes: FastifyPluginAsync = async (app) => { app.post('/register', async (request, reply) => { const input = registerSchema.parse(request.body); const passwordHash = await hashPassword(input.password); try { const result = await pool.query( `insert into users (email, password_hash, display_name) values ($1, $2, $3) returning id, email, display_name, role, password_hash, is_active`, [input.email, passwordHash, input.name], ); const user = serializeUser(result.rows[0]); const token = await reply.jwtSign(user, { expiresIn: '7d' }); return reply.code(201).send({ token, user }); } catch (error: unknown) { if (typeof error === 'object' && error && 'code' in error && error.code === '23505') { return reply.code(409).send({ error: 'An account with this email already exists' }); } throw error; } }); app.post('/login', async (request, reply) => { const input = credentialsSchema.parse(request.body); const result = await pool.query( `select id, email, display_name, role, password_hash, is_active from users where email = $1`, [input.email], ); const account = result.rows[0]; if (!account || !account.is_active || !(await verifyPassword(input.password, account.password_hash))) { return reply.code(401).send({ error: 'Invalid email or password' }); } const user = serializeUser(account); const token = await reply.jwtSign(user, { expiresIn: '7d' }); return { token, user }; }); app.get('/me', { preHandler: app.authenticate }, async (request) => ({ user: request.user })); };