import { type NextRequest, NextResponse } from "next/server"; import { auth, checkSameOrigin } from "./auth"; import { validateToken } from "./models/api-tokens"; export class ApiAuthError extends Error { status: number; constructor(message: string, status: number) { super(message); this.name = "ApiAuthError"; this.status = status; } } export class NotFoundError extends Error { constructor(message: string) { super(message); this.name = "NotFoundError"; } } export type ApiAuthResult = { userId: number; role: string; authMethod: "bearer" | "session"; }; export async function authenticateApiRequest( request: NextRequest ): Promise { // Try Bearer token first const authHeader = request.headers.get("authorization") ?? ""; if (authHeader.startsWith("Bearer ")) { const rawToken = authHeader.slice(7); if (!rawToken) { throw new ApiAuthError("Invalid Bearer token", 401); } const result = await validateToken(rawToken); if (!result) { throw new ApiAuthError("Invalid or expired API token", 401); } return { userId: result.user.id, role: result.user.role, authMethod: "bearer", }; } // Fall back to session auth const session = await auth(); if (!session?.user?.id) { throw new ApiAuthError("Unauthorized", 401); } // Deny access when role is missing rather than defaulting to "user" const role = session.user.role; if (!role) { throw new ApiAuthError("Session missing role claim", 401); } return { userId: Number(session.user.id), role, authMethod: "session", }; } export async function requireApiUser(request: NextRequest): Promise { const result = await authenticateApiRequest(request); // CSRF check for session-authenticated mutating requests if (result.authMethod === "session") { const method = request.method.toUpperCase(); if (method !== "GET" && method !== "HEAD" && method !== "OPTIONS") { const csrfResponse = checkSameOrigin(request); if (csrfResponse) { throw new ApiAuthError("Forbidden", 403); } } } return result; } export async function requireApiAdmin(request: NextRequest): Promise { const result = await requireApiUser(request); if (result.role !== "admin") { throw new ApiAuthError("Administrator privileges required", 403); } return result; } /** * Helper to build an error response from an ApiAuthError or generic error. */ export function apiErrorResponse(error: unknown): NextResponse { if (error instanceof ApiAuthError) { return NextResponse.json({ error: error.message }, { status: error.status }); } if (error instanceof NotFoundError) { return NextResponse.json({ error: error.message }, { status: 404 }); } if (error instanceof Error && error.message.toLowerCase().includes("not found")) { return NextResponse.json({ error: error.message }, { status: 404 }); } return NextResponse.json( { error: error instanceof Error ? error.message : "Internal server error" }, { status: 500 } ); }