/** * Authentication handlers — login, register, and token refresh. * Mirrors `apps/interfaces/api/src/handlers/auth.rs`. * * Endpoints: * - POST /auth/login — authenticate, return JWT pair * - POST /auth/register — create account, return JWT pair * - POST /auth/refresh — exchange refresh token for a new pair * * All three are rate-limited (20 attempts / 10 min per client). */ import type { ApiState, RateLimiter } from "../state.ts"; import { ApiError } from "../error.ts"; import { loadUsers, saveUsers } from "../state.ts"; import type { AuthResponse, LoginRequest, RegisterRequest, RefreshRequest } from "../dto.ts"; /** Login/register brute-force protection: 20 attempts per 10-minute window. */ const AUTH_RATE_LIMIT_MAX = 20; const AUTH_RATE_LIMIT_WINDOW_SECS = 600; /** Extract a coarse client identity (X-Forwarded-For first hop or "unknown"). */ function clientId(headers: Headers): string { const xff = headers.get("x-forwarded-for"); if (xff) { const first = xff.split(",")[0]?.trim(); if (first) return first; } return "unknown"; } /** Enforce the auth rate limit; throws TooManyRequests when exceeded. */ function enforceRateLimit(limiter: RateLimiter, headers: Headers): void { if (!limiter.check(clientId(headers), AUTH_RATE_LIMIT_MAX, AUTH_RATE_LIMIT_WINDOW_SECS)) { throw ApiError.tooManyRequests("Too many requests, try again later"); } } function requireFields(req: LoginRequest & RegisterRequest, reg: boolean): void { if (req.username === "") { throw ApiError.badRequest("Username is required"); } if (req.password === "") { throw ApiError.badRequest("Password is required"); } if (reg && req.password.length < 6) { throw ApiError.badRequest("Password must be at least 6 characters"); } } /** Issue an AuthResponse for the given subject. */ function authResponse(state: ApiState, sub: string): AuthResponse { const [access, refresh] = state.token_service.generateTokens(sub); return { access_token: access, refresh_token: refresh, token_type: "Bearer", expires_in: 3600, // access token expiry in seconds }; } /** POST /auth/login — authenticate and issue a JWT pair. */ export async function loginHandler( state: ApiState, headers: Headers, body: LoginRequest, ): Promise { enforceRateLimit(state.auth_rate_limiter, headers); requireFields(body, false); const users = loadUsers(state.store_base_dir); if (!users) { throw ApiError.unauthorized("Invalid username or password"); } const storedHash = users[body.username]; if (!storedHash) { throw ApiError.unauthorized("Invalid username or password"); } const valid = await state.password_service.verify(body.password, storedHash); if (!valid) { throw ApiError.unauthorized("Invalid username or password"); } return authResponse(state, body.username); } /** POST /auth/register — create a new user account and issue a JWT pair. */ export async function registerHandler( state: ApiState, headers: Headers, body: RegisterRequest, ): Promise { enforceRateLimit(state.auth_rate_limiter, headers); requireFields(body, true); // Hash first (async), then serialize the read-modify-write of users.json. const hash = await state.password_service.hash(body.password); const unlock = state.users_lock.lock(); try { const users = loadUsers(state.store_base_dir) ?? {}; if (body.username in users) { throw ApiError.conflict("Username already exists"); } users[body.username] = hash; saveUsers(state.store_base_dir, users); } finally { unlock(); } return authResponse(state, body.username); } /** POST /auth/refresh — exchange a refresh token for a fresh pair. */ export async function refreshHandler( state: ApiState, headers: Headers, body: RefreshRequest, ): Promise { enforceRateLimit(state.auth_rate_limiter, headers); if (body.refresh_token === "") { throw ApiError.badRequest("Refresh token is required"); } let sub: string; try { sub = state.token_service.verifyRefreshToken(body.refresh_token); } catch { throw ApiError.unauthorized("Invalid or expired refresh token"); } return authResponse(state, sub); }