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 100 101 | 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 3x 3x 3x 3x 3x 3x 3x 3x 3x 3x 3x 3x 3x 3x 3x 3x 3x 2x 1x 3x 3x 2x 2x 2x 2x 2x 2x 1x 1x 3x 1x 1x 1x 1x 1x 3x 3x 3x 3x | 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 — failing closed:', err)
// Authorization must fail closed: a broken enforcer means the RBAC layer
// is gone, and proceeding would silently serve every withAuth route
// without it.
return NextResponse.json({ error: 'Authorization unavailable' }, { status: 503 })
}
// 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({}),
})
}
}
|