Press n or j to go to the next uncovered block, b, p or k for the previous block.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 | import { type NextRequest, NextResponse } from 'next/server' import { auth } from '@/auth' import { getRouteEnforcer } from './enforcer' import { resolveUserRole, type UserRole } from './roles' export interface AuthenticatedContext { userId: string userEmail: string | null userRole: UserRole /** Next.js dynamic route params (e.g. { id: string }). Always present — resolves to {} for non-dynamic routes. */ params: Promise<Record<string, string | string[]>> } interface WithAuthOptions { /** Minimum role required. If not specified, route-level RBAC from Casbin decides. */ role?: 'user' | 'admin' } /** The context Next.js passes as the second argument to route handlers. */ type NextRouteContext = { params?: Promise<Record<string, string | string[]>> } type RouteHandler = ( request: NextRequest, context: AuthenticatedContext ) => Promise<NextResponse | Response> /** * Wrap a Next.js API route handler with authentication and route-level authorization. * * 1. Gets the current session * 2. Resolves the user's role (guest/user/admin) * 3. Checks Layer 1 route RBAC (Casbin enforcer with URL pattern matching) * 4. Optionally checks a minimum role requirement * 5. Passes enriched context to the handler * * Usage: * ```typescript * export const GET = withAuth(async (req, { userId, userRole }) => { * return NextResponse.json({ hello: userId }) * }) * ``` */ export function withAuth(handler: RouteHandler, options?: WithAuthOptions) { return async (request: NextRequest, routeContext?: NextRouteContext) => { const session = await auth() // Determine role let role: UserRole = 'guest' let userId = '' let userEmail: string | null = null if (session?.user?.id) { userId = session.user.id userEmail = session.user.email ?? null role = await resolveUserRole({ userId, email: userEmail }) } // Check route-level RBAC const url = new URL(request.url) const pathname = url.pathname const method = request.method try { const enforcer = await getRouteEnforcer() const allowed = await enforcer.enforce(role, pathname, method) if (!allowed) { if (role === 'guest') { return NextResponse.json({ error: 'Authentication required' }, { status: 401 }) } return NextResponse.json({ error: 'Forbidden' }, { status: 403 }) } } catch (err) { console.error('[withAuth] Route enforcement error:', err) // Fail open for now — log but don't block // TODO: Consider fail-closed after confidence period } // Check minimum role requirement if specified if (options?.role) { const roleRank = { guest: 0, user: 1, admin: 2 } if (roleRank[role] < roleRank[options.role]) { if (role === 'guest') { return NextResponse.json({ error: 'Authentication required' }, { status: 401 }) } return NextResponse.json({ error: 'Forbidden' }, { status: 403 }) } } return handler(request, { userId, userEmail, userRole: role, params: routeContext?.params ?? Promise.resolve({}), }) } } |