import { type Next, type RouteParams, type RouterContext, type State, } from "@oak/oak"; import { authenticate } from "../lib/auth.ts"; import { APIErrorCode } from "../../shared/api.ts"; import { APIException } from "../lib/api-exception.ts"; import type { AuthPayload } from "../model/auth.ts"; import { can } from "../lib/permissions.ts"; import type { Permission } from "../../shared/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); // Also checks the user still exists and hasn't changed their password since // the token was issued, and takes the live role from the DB. const payload = await authenticate(token); if (!payload) { throw new APIException(APIErrorCode.UNAUTHORIZED, 401, "Invalid token"); } ctx.state.user = payload; 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(); }; }