import type { FastifyPluginAsync } from 'fastify'; import type Database from 'better-sqlite3'; import type { Config } from '../../../config.ts'; import type { Logger } from '../../../middleware/logging.ts'; import type { LockoutService } from '../../../services/lockout.ts'; import { attemptLogin } from '../../../services/login-handler.ts'; import { makeRequireAuth, issueSessionCookie } from '../../../middleware/auth.ts'; import { SESSION_COOKIE_NAME } from '../../../constants.ts'; interface Deps { db: Database.Database; config: Config; logger: Logger; lockout: LockoutService; } interface LoginBody { username: string; password: string; } export const authApiRoutes: FastifyPluginAsync<{ deps: Deps }> = async (app, { deps }) => { const { config } = deps; const requireAuth = makeRequireAuth(config); app.post<{ Body: LoginBody }>( '/login', { config: { rateLimit: { max: config.loginRateLimitMax, timeWindow: config.loginRateLimitWindowSeconds * 1000, }, }, }, async (request, reply) => { const { username, password } = request.body ?? {}; const result = await attemptLogin(deps, { username: username ?? '', password: password ?? '', ip: request.ip, userAgent: request.headers['user-agent'] ?? '', }); if (result.kind === 'bad_request') { return reply.status(400).send({ error: 'username and password are required' }); } if (result.kind === 'locked') { return reply .status(401) .header('Retry-After', String(result.retryAfterSeconds)) .send({ error: 'Invalid credentials' }); } if (result.kind === 'bad_credentials') { return reply.status(401).send({ error: 'Invalid credentials' }); } issueSessionCookie( reply, app, { sub: result.user.id, username: result.user.username }, config.cookieSecure, ); reply.send({ ok: true }); }, ); app.post('/logout', { preHandler: requireAuth }, async (_request, reply) => { reply.clearCookie(SESSION_COOKIE_NAME, { path: '/' }).send({ ok: true }); }); };