@payez/next-mvp 3.2.1 → 3.2.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
|
@@ -96,5 +96,7 @@ export interface MvpMiddlewareOptions {
|
|
|
96
96
|
onRefreshFailure?: (status: number, isNetworkError: boolean) => void;
|
|
97
97
|
/** Additional paths to bypass middleware (beyond /api/auth/ and /api/session/) */
|
|
98
98
|
bypassPaths?: string[];
|
|
99
|
+
/** Paths exempt from RBAC checks (auth still enforced, just no page-permission check) */
|
|
100
|
+
rbacExemptPaths?: string[];
|
|
99
101
|
}
|
|
100
102
|
export declare function createMvpMiddleware(options?: MvpMiddlewareOptions): (request: NextRequest) => Promise<NextResponse>;
|
|
@@ -232,6 +232,7 @@ function createMvpMiddleware(options = {}) {
|
|
|
232
232
|
const viabilityEndpoint = options.viabilityEndpoint || '/api/session/viability';
|
|
233
233
|
const refreshEndpoint = options.refreshEndpoint || '/api/auth/refresh';
|
|
234
234
|
const bypassPaths = options.bypassPaths || [];
|
|
235
|
+
const rbacExemptPaths = options.rbacExemptPaths || [];
|
|
235
236
|
return async function middleware(request) {
|
|
236
237
|
const { pathname, searchParams } = request.nextUrl;
|
|
237
238
|
// =========================================================================
|
|
@@ -287,6 +288,7 @@ function createMvpMiddleware(options = {}) {
|
|
|
287
288
|
circuitBreaker: cb,
|
|
288
289
|
logger: log,
|
|
289
290
|
refreshEndpoint,
|
|
291
|
+
rbacExemptPaths,
|
|
290
292
|
onRefreshSuccess: options.onRefreshSuccess,
|
|
291
293
|
onRefreshFailure: options.onRefreshFailure,
|
|
292
294
|
});
|
|
@@ -317,7 +319,8 @@ async function checkViability(request, endpoint, log) {
|
|
|
317
319
|
'Cache-Control': 'no-store',
|
|
318
320
|
'Cookie': request.headers.get('cookie') || ''
|
|
319
321
|
},
|
|
320
|
-
credentials: 'include'
|
|
322
|
+
credentials: 'include',
|
|
323
|
+
signal: AbortSignal.timeout(5000),
|
|
321
324
|
});
|
|
322
325
|
if (response.ok) {
|
|
323
326
|
const data = await response.json();
|
|
@@ -350,7 +353,7 @@ async function executeDecision(request, decision, pathname, sessionPointer, sess
|
|
|
350
353
|
const safeCallback = getSafeCallbackUrl(pathname);
|
|
351
354
|
switch (decision.type) {
|
|
352
355
|
case 'allow':
|
|
353
|
-
return handleAllow(request, pathname, sessionPointer, sessionStatus);
|
|
356
|
+
return handleAllow(request, pathname, sessionPointer, sessionStatus, opts.rbacExemptPaths);
|
|
354
357
|
case 'redirect':
|
|
355
358
|
return redirectTo(request, decision.location, decision.clearCookies);
|
|
356
359
|
case 'service_error':
|
|
@@ -362,16 +365,20 @@ async function executeDecision(request, decision, pathname, sessionPointer, sess
|
|
|
362
365
|
/** Paths that must never be RBAC-checked (they are RBAC redirect targets) */
|
|
363
366
|
const RBAC_EXEMPT_PATHS = ['/error', '/unauthorized', '/service-unavailable'];
|
|
364
367
|
/** Handle 'allow' decision - run RBAC if enabled */
|
|
365
|
-
async function handleAllow(request, pathname, sessionPointer, sessionStatus) {
|
|
368
|
+
async function handleAllow(request, pathname, sessionPointer, sessionStatus, rbacExemptPaths = []) {
|
|
366
369
|
const isPublic = (0, route_config_1.isUnauthenticatedRoute)(pathname);
|
|
367
|
-
if ((0, rbac_check_1.isRBACEnabled)() && !isPublic) {
|
|
368
|
-
// Skip RBAC for error/fallback pages
|
|
369
|
-
if (RBAC_EXEMPT_PATHS.some(p => pathname.startsWith(p))
|
|
370
|
+
if ((0, rbac_check_1.isRBACEnabled)() && !isPublic && sessionPointer.exists) {
|
|
371
|
+
// Skip RBAC for error/fallback pages (prevent redirect loops) and app-configured exempt paths
|
|
372
|
+
if (RBAC_EXEMPT_PATHS.some(p => pathname.startsWith(p)) ||
|
|
373
|
+
rbacExemptPaths.some(p => pathname.startsWith(p))) {
|
|
370
374
|
return server_1.NextResponse.next();
|
|
371
375
|
}
|
|
372
376
|
if (!sessionPointer.clientId) {
|
|
373
|
-
console.error('[MIDDLEWARE] RBAC: No clientId');
|
|
374
|
-
|
|
377
|
+
console.error('[MIDDLEWARE] RBAC: No clientId — returning 401');
|
|
378
|
+
if (pathname.startsWith('/api/')) {
|
|
379
|
+
return server_1.NextResponse.json({ error: 'Unauthorized — missing clientId for RBAC' }, { status: 401 });
|
|
380
|
+
}
|
|
381
|
+
return server_1.NextResponse.redirect(new URL('/unauthorized', request.url));
|
|
375
382
|
}
|
|
376
383
|
try {
|
|
377
384
|
const result = await (0, rbac_check_1.checkPagePermission)(pathname, sessionPointer.roles, sessionPointer.clientId);
|
|
@@ -419,7 +426,8 @@ async function handleRefresh(request, safeCallback, opts) {
|
|
|
419
426
|
'x-session-token': request.cookies.get((0, app_slug_1.getSessionCookieName)())?.value ||
|
|
420
427
|
request.cookies.get((0, app_slug_1.getSecureSessionCookieName)())?.value || ''
|
|
421
428
|
},
|
|
422
|
-
credentials: 'include'
|
|
429
|
+
credentials: 'include',
|
|
430
|
+
signal: AbortSignal.timeout(5000),
|
|
423
431
|
});
|
|
424
432
|
if (response.ok) {
|
|
425
433
|
const data = await response.json();
|
package/package.json
CHANGED
|
@@ -108,6 +108,8 @@ export interface MvpMiddlewareOptions {
|
|
|
108
108
|
onRefreshFailure?: (status: number, isNetworkError: boolean) => void;
|
|
109
109
|
/** Additional paths to bypass middleware (beyond /api/auth/ and /api/session/) */
|
|
110
110
|
bypassPaths?: string[];
|
|
111
|
+
/** Paths exempt from RBAC checks (auth still enforced, just no page-permission check) */
|
|
112
|
+
rbacExemptPaths?: string[];
|
|
111
113
|
}
|
|
112
114
|
|
|
113
115
|
// =============================================================================
|
|
@@ -336,6 +338,7 @@ export function createMvpMiddleware(options: MvpMiddlewareOptions = {}) {
|
|
|
336
338
|
const viabilityEndpoint = options.viabilityEndpoint || '/api/session/viability';
|
|
337
339
|
const refreshEndpoint = options.refreshEndpoint || '/api/auth/refresh';
|
|
338
340
|
const bypassPaths = options.bypassPaths || [];
|
|
341
|
+
const rbacExemptPaths = options.rbacExemptPaths || [];
|
|
339
342
|
|
|
340
343
|
return async function middleware(request: NextRequest): Promise<NextResponse> {
|
|
341
344
|
const { pathname, searchParams } = request.nextUrl;
|
|
@@ -400,6 +403,7 @@ export function createMvpMiddleware(options: MvpMiddlewareOptions = {}) {
|
|
|
400
403
|
circuitBreaker: cb,
|
|
401
404
|
logger: log,
|
|
402
405
|
refreshEndpoint,
|
|
406
|
+
rbacExemptPaths,
|
|
403
407
|
onRefreshSuccess: options.onRefreshSuccess,
|
|
404
408
|
onRefreshFailure: options.onRefreshFailure,
|
|
405
409
|
});
|
|
@@ -439,7 +443,8 @@ async function checkViability(
|
|
|
439
443
|
'Cache-Control': 'no-store',
|
|
440
444
|
'Cookie': request.headers.get('cookie') || ''
|
|
441
445
|
},
|
|
442
|
-
credentials: 'include'
|
|
446
|
+
credentials: 'include',
|
|
447
|
+
signal: AbortSignal.timeout(5000),
|
|
443
448
|
});
|
|
444
449
|
|
|
445
450
|
if (response.ok) {
|
|
@@ -473,6 +478,7 @@ interface ExecuteOptions {
|
|
|
473
478
|
circuitBreaker: CircuitBreakerProvider;
|
|
474
479
|
logger: MiddlewareLogger;
|
|
475
480
|
refreshEndpoint: string;
|
|
481
|
+
rbacExemptPaths: string[];
|
|
476
482
|
onRefreshSuccess?: () => void;
|
|
477
483
|
onRefreshFailure?: (status: number, isNetworkError: boolean) => void;
|
|
478
484
|
}
|
|
@@ -490,7 +496,7 @@ async function executeDecision(
|
|
|
490
496
|
|
|
491
497
|
switch (decision.type) {
|
|
492
498
|
case 'allow':
|
|
493
|
-
return handleAllow(request, pathname, sessionPointer, sessionStatus);
|
|
499
|
+
return handleAllow(request, pathname, sessionPointer, sessionStatus, opts.rbacExemptPaths);
|
|
494
500
|
|
|
495
501
|
case 'redirect':
|
|
496
502
|
return redirectTo(request, decision.location, decision.clearCookies);
|
|
@@ -511,19 +517,27 @@ async function handleAllow(
|
|
|
511
517
|
request: NextRequest,
|
|
512
518
|
pathname: string,
|
|
513
519
|
sessionPointer: SessionPointer,
|
|
514
|
-
sessionStatus: SessionStatus
|
|
520
|
+
sessionStatus: SessionStatus,
|
|
521
|
+
rbacExemptPaths: string[] = []
|
|
515
522
|
): Promise<NextResponse> {
|
|
516
523
|
const isPublic = isUnauthenticatedRoute(pathname);
|
|
517
524
|
|
|
518
|
-
if (isRBACEnabled() && !isPublic) {
|
|
519
|
-
// Skip RBAC for error/fallback pages
|
|
520
|
-
if (RBAC_EXEMPT_PATHS.some(p => pathname.startsWith(p))
|
|
525
|
+
if (isRBACEnabled() && !isPublic && sessionPointer.exists) {
|
|
526
|
+
// Skip RBAC for error/fallback pages (prevent redirect loops) and app-configured exempt paths
|
|
527
|
+
if (RBAC_EXEMPT_PATHS.some(p => pathname.startsWith(p)) ||
|
|
528
|
+
rbacExemptPaths.some(p => pathname.startsWith(p))) {
|
|
521
529
|
return NextResponse.next();
|
|
522
530
|
}
|
|
523
531
|
|
|
524
532
|
if (!sessionPointer.clientId) {
|
|
525
|
-
console.error('[MIDDLEWARE] RBAC: No clientId');
|
|
526
|
-
|
|
533
|
+
console.error('[MIDDLEWARE] RBAC: No clientId — returning 401');
|
|
534
|
+
if (pathname.startsWith('/api/')) {
|
|
535
|
+
return NextResponse.json(
|
|
536
|
+
{ error: 'Unauthorized — missing clientId for RBAC' },
|
|
537
|
+
{ status: 401 }
|
|
538
|
+
);
|
|
539
|
+
}
|
|
540
|
+
return NextResponse.redirect(new URL('/unauthorized', request.url));
|
|
527
541
|
}
|
|
528
542
|
|
|
529
543
|
try {
|
|
@@ -591,7 +605,8 @@ async function handleRefresh(
|
|
|
591
605
|
'x-session-token': request.cookies.get(getSessionCookieName())?.value ||
|
|
592
606
|
request.cookies.get(getSecureSessionCookieName())?.value || ''
|
|
593
607
|
},
|
|
594
|
-
credentials: 'include'
|
|
608
|
+
credentials: 'include',
|
|
609
|
+
signal: AbortSignal.timeout(5000),
|
|
595
610
|
});
|
|
596
611
|
|
|
597
612
|
if (response.ok) {
|