import { type Next, type RouteParams, type RouterContext, type State, } from "@oak/oak"; import { verifyJWT } from "../lib/jwt.ts"; import { APIErrorCode, APIException, type AuthPayload, } from "../model/interfaces.ts"; import { getUserById } from "../services/user-service.ts"; import { can, type Permission } from "../lib/permissions.ts"; export interface AuthState extends State { user: AuthPayload; } // A router context whose state is guaranteed to carry the authenticated user // (populated by authMiddleware). Generic over the route path so `ctx.params` // stays typed per-route — e.g. `AuthContext<"/:userId/role">`. export type AuthContext< R extends string = string, P extends RouteParams = RouteParams, > = RouterContext; export async function authMiddleware( ctx: AuthContext, next: Next, ): Promise { const authHeader = ctx.request.headers.get("Authorization"); if (!authHeader || !authHeader.startsWith("Bearer ")) { throw new APIException( APIErrorCode.UNAUTHORIZED, 401, "Missing or invalid token", ); } const token = authHeader.substring(7); const payload = await verifyJWT(token); if (!payload) { throw new APIException(APIErrorCode.UNAUTHORIZED, 401, "Invalid token"); } let user; try { user = getUserById(payload.userId); } catch { throw new APIException(APIErrorCode.UNAUTHORIZED, 401, "User not found"); } // Trust the live role from the DB, not the role baked into the JWT at login. // Tokens last 7 days, so a role change (e.g. promotion to admin or a demotion) // would otherwise not take effect until the user re-logs in. ctx.state.user = { ...payload, role: user.role }; await next(); } export async function adminOnlyMiddleware( ctx: AuthContext, next: Next, ): Promise { if (ctx.state.user.role !== "admin") { throw new APIException( APIErrorCode.UNAUTHORIZED, 403, "Admin access required", ); } await next(); } // Gates a route on a named permission. Must run after authMiddleware. Generic // over the route path so it composes with path-typed route handlers. export function requirePermission(permission: Permission) { return async ( ctx: AuthContext, next: Next, ): Promise => { if (!ctx.state.user || !can(ctx.state.user, permission)) { throw new APIException( APIErrorCode.UNAUTHORIZED, 403, "Insufficient permissions", ); } await next(); }; }