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 { getUserByUsername } from '../../../db/users.ts'; import { verifyPassword } from '../../../services/auth.ts'; import { requireAuth } from '../../../middleware/auth.ts'; interface Deps { db: Database.Database; config: Config; logger: Logger; } interface LoginBody { username: string; password: string; } export const authApiRoutes: FastifyPluginAsync<{ deps: Deps }> = async (app, { deps }) => { const { db, config, logger } = deps; app.post<{ Body: LoginBody }>('/login', async (request, reply) => { const { username, password } = request.body ?? {}; const ip = request.ip; const userAgent = request.headers['user-agent'] ?? ''; if (!username || !password) { return reply.status(400).send({ error: 'username and password are required' }); } const user = getUserByUsername(db, username); const valid = user ? await verifyPassword(password, user.password_hash) : false; if (!user || !valid) { await logger.authFailure({ ip, userAgent, username }); return reply.status(401).send({ error: 'Invalid credentials' }); } await logger.authSuccess({ ip, userAgent, username }); const token = app.jwt.sign({ sub: user.id, username: user.username }, { expiresIn: config.jwtExpiry }); reply .setCookie('token', token, { httpOnly: true, sameSite: 'strict', secure: config.cookieSecure, path: '/', }) .send({ ok: true }); }); app.post('/logout', { preHandler: requireAuth }, async (_request, reply) => { reply.clearCookie('token', { path: '/' }).send({ ok: true }); }); };