import type { FastifyInstance, FastifyRequest } from 'fastify'; import type { LoginRequest, LoginResponse, MeResponse } from '@arboretum/shared'; import type { AuthService, LoginRateLimiter } from '../auth/service.js'; // Tailscale Serve / un reverse-proxy TLS posent x-forwarded-proto. On ne sert jamais // en TLS direct : `secure` n'est posé que derrière un front HTTPS, jamais en localhost http // (sinon le navigateur refuserait le cookie sur http://127.0.0.1 et le login local casserait). function isHttpsRequest(req: FastifyRequest): boolean { const xfp = req.headers['x-forwarded-proto']; const proto = (Array.isArray(xfp) ? xfp[0] : xfp)?.split(',')[0]?.trim(); return proto === 'https'; } export function registerAuthRoutes( app: FastifyInstance, auth: AuthService, limiter: LoginRateLimiter, serverVersion: string, ): void { app.post('/api/v1/auth/login', { config: { public: true } }, async (req, reply) => { const wait = limiter.check(); if (wait !== null) { return reply.status(429).send({ error: { code: 'RATE_LIMITED', message: `Retry in ${Math.ceil(wait / 1000)}s` } }); } const body = req.body as Partial | null; const ctx = typeof body?.token === 'string' ? auth.verifyRawToken(body.token) : null; if (!ctx) { limiter.recordFailure(); return reply.status(401).send({ error: { code: 'BAD_TOKEN', message: 'Invalid token' } }); } limiter.recordSuccess(); void reply.setCookie(auth.cookieName, auth.issueCookie(ctx), { path: '/', httpOnly: true, sameSite: 'strict', secure: isHttpsRequest(req), maxAge: 30 * 24 * 3600, }); const res: LoginResponse = { ok: true, label: ctx.label }; return reply.send(res); }); app.get('/api/v1/auth/me', async (req, reply) => { const res: MeResponse = { ok: true, tokenLabel: req.authContext?.label ?? 'unknown', serverVersion }; return reply.send(res); }); app.post('/api/v1/auth/logout', async (req, reply) => { // Les attributs doivent matcher ceux posés au login pour que le navigateur efface bien le cookie. void reply.clearCookie(auth.cookieName, { path: '/', secure: isHttpsRequest(req) }); return reply.send({ ok: true }); }); }