import { NextRequest, NextResponse } from "next/server"; import { getCurrentUser } from "@/lib/auth"; import { defineAbilityFor } from "@/lib/casl/ability"; import type { AppAbility, AppActions, AppSubjects } from "@/lib/casl/types"; import type { Role } from "@prisma/client"; /** * The context passed to authorized route handlers. * Handlers receive both the session user and the built CASL ability * for fine-grained permission checks within the handler body. */ export interface AuthorizedContext { user: { id: string; email: string; tenantId: string | null; roles: Role[]; isSuperAdmin: boolean; firstName: string; lastName: string; }; ability: AppAbility; } /** * Handler signature for authorized route handlers. * Receives the request and an authorized context with user + ability. */ type AuthorizedHandler = ( req: NextRequest, ctx: AuthorizedContext ) => Promise | NextResponse; /** * Higher-order function that wraps a Next.js route handler with RBAC enforcement. * * Flow: * 1. Get current user session via getCurrentUser() * 2. If no session → return 401 Unauthorized * 3. Build CASL ability using defineAbilityFor(user) * 4. Check ability.can(action, subject) * 5. If cannot → return 403 Forbidden * 6. If can → call the wrapped handler with (req, { user, ability }) * * Usage: * ```typescript * // In route.ts: * export const GET = withPermission("read", "Subscriber")(async (req, { user, ability }) => { * // ability is available for fine-grained checks within the handler * const subscribers = await getSubscribers(user.tenantId); * return NextResponse.json(subscribers); * }); * ``` * * @param action - The CASL action to check ("create" | "read" | "update" | "delete" | "manage") * @param subject - The CASL subject to check against (e.g., "Subscriber", "Invoice") * @returns A function that takes an authorized handler and returns a Next.js route handler */ export function withPermission(action: AppActions, subject: AppSubjects) { return function (handler: AuthorizedHandler) { return async function (req: NextRequest): Promise { // Step 1: Get session user const user = await getCurrentUser(); // Step 2: No session → 401 Unauthorized if (!user) { return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); } // Step 3: Build CASL ability from session user const ability = defineAbilityFor({ id: user.id, roles: user.roles, tenantId: user.tenantId, isSuperAdmin: user.isSuperAdmin, }); // Step 4: Check permission if (!ability.can(action, subject)) { // Step 5: Cannot → 403 Forbidden return NextResponse.json({ error: "Forbidden" }, { status: 403 }); } // Step 6: Can → call handler with user and ability in context return handler(req, { user, ability }); }; }; } /** * Alternative API: authorize(handler, action, subject) * * Convenience wrapper for `withPermission` when you prefer * the handler-first style rather than curried style. * * Usage: * ```typescript * export const GET = authorize( * async (req, { user, ability }) => { * return NextResponse.json({ ok: true }); * }, * "read", * "Subscriber" * ); * ``` */ export function authorize( handler: AuthorizedHandler, action: AppActions, subject: AppSubjects ) { return withPermission(action, subject)(handler); }