import type { FastifyInstance, FastifyReply, FastifyRequest } from 'fastify'; import fp from 'fastify-plugin'; import { SESSION_COOKIE, loadSession } from './sessions'; import { isProd, env } from '../env'; import { ensureSuperadminFlag } from './superadmin'; declare module 'fastify' { interface FastifyRequest { user?: { id: string; email: string; firmId: string | null; role: string; isSuperadmin: boolean; isSuspended: boolean; emailVerified: boolean; }; } interface FastifyInstance { requireAuth: (req: FastifyRequest, reply: FastifyReply) => Promise; requireFirm: (req: FastifyRequest, reply: FastifyReply) => Promise; requireSuperadmin: (req: FastifyRequest, reply: FastifyReply) => Promise; setSessionCookie: (reply: FastifyReply, token: string, expiresAt: Date) => void; clearSessionCookie: (reply: FastifyReply) => void; } } async function plugin(app: FastifyInstance) { app.addHook('onRequest', async (req) => { const token = req.cookies?.[SESSION_COOKIE]; if (!token) return; const session = await loadSession(token); if (!session) return; // Auto-promote/demote based on SUPERADMIN_EMAILS env var, every request — cheap and self-healing. const isSuperadmin = await ensureSuperadminFlag( session.user.id, session.user.email, session.user.isSuperadmin, session.user.emailVerifiedAt, ); req.user = { id: session.user.id, email: session.user.email, firmId: session.user.firmId, role: session.user.role, isSuperadmin, isSuspended: session.user.isSuspended, emailVerified: Boolean(session.user.emailVerifiedAt), }; }); app.decorate('requireAuth', async (req: FastifyRequest, reply: FastifyReply) => { if (!req.user) return reply.code(401).send({ error: 'unauthorized' }); if (req.user.isSuspended) return reply.code(403).send({ error: 'account_suspended' }); }); app.decorate('requireFirm', async (req: FastifyRequest, reply: FastifyReply) => { if (!req.user) return reply.code(401).send({ error: 'unauthorized' }); if (req.user.isSuspended) return reply.code(403).send({ error: 'account_suspended' }); if (!req.user.firmId) return reply.code(403).send({ error: 'no_firm' }); }); app.decorate('requireSuperadmin', async (req: FastifyRequest, reply: FastifyReply) => { if (!req.user) return reply.code(401).send({ error: 'unauthorized' }); if (req.user.isSuspended) return reply.code(403).send({ error: 'account_suspended' }); if (!req.user.isSuperadmin) return reply.code(403).send({ error: 'forbidden' }); }); app.decorate('setSessionCookie', (reply: FastifyReply, token: string, expiresAt: Date) => { reply.setCookie(SESSION_COOKIE, token, { path: '/', httpOnly: true, secure: isProd, sameSite: 'lax', domain: env.COOKIE_DOMAIN || undefined, expires: expiresAt, signed: false, }); }); app.decorate('clearSessionCookie', (reply: FastifyReply) => { reply.clearCookie(SESSION_COOKIE, { path: '/', httpOnly: true, secure: isProd, sameSite: 'lax', domain: env.COOKIE_DOMAIN || undefined, }); }); } export const authPlugin = fp(plugin, { name: 'auth' });