@payez/next-mvp 3.2.2 → 3.3.0
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.
- package/dist/api-handlers/admin/analytics.js +3 -3
- package/dist/api-handlers/admin/audit.js +3 -3
- package/dist/api-handlers/admin/sessions.js +3 -3
- package/dist/api-handlers/admin/users.js +3 -3
- package/dist/api-handlers/admin/vibe-data.js +3 -3
- package/dist/lib/idp-client-config.d.ts +1 -1
- package/dist/lib/idp-client-config.js +2 -2
- package/dist/lib/nextauth-secret.js +0 -3
- package/dist/middleware/create-middleware.d.ts +2 -0
- package/dist/middleware/create-middleware.js +7 -4
- package/package.json +1 -1
- package/src/api-handlers/admin/analytics.ts +3 -3
- package/src/api-handlers/admin/audit.ts +3 -3
- package/src/api-handlers/admin/sessions.ts +3 -3
- package/src/api-handlers/admin/users.ts +3 -3
- package/src/api-handlers/admin/vibe-data.ts +3 -3
- package/src/lib/idp-client-config.ts +3 -3
- package/src/lib/nextauth-secret.ts +0 -4
- package/src/middleware/create-middleware.ts +11 -4
|
@@ -81,9 +81,9 @@ async function vibeServiceRequest(endpoint, options) {
|
|
|
81
81
|
.update(stringToSign)
|
|
82
82
|
.digest('base64');
|
|
83
83
|
const proxyUrl = `${idpUrl}/api/vibe/proxy`;
|
|
84
|
-
// Get the
|
|
84
|
+
// Get the client slug from startup config for multi-client admin support
|
|
85
85
|
const idpConfig = (0, startup_init_1.getStartupIDPConfig)();
|
|
86
|
-
const
|
|
86
|
+
const idpClientId = idpConfig?.clientSlug || idpConfig?.clientId;
|
|
87
87
|
try {
|
|
88
88
|
const res = await fetch(proxyUrl, {
|
|
89
89
|
method: 'POST',
|
|
@@ -92,7 +92,7 @@ async function vibeServiceRequest(endpoint, options) {
|
|
|
92
92
|
'X-Vibe-Client-Id': clientId,
|
|
93
93
|
'X-Vibe-Timestamp': String(timestamp),
|
|
94
94
|
'X-Vibe-Signature': signature,
|
|
95
|
-
...(
|
|
95
|
+
...(idpClientId && { 'X-Client-Id': idpClientId }),
|
|
96
96
|
},
|
|
97
97
|
body: JSON.stringify({
|
|
98
98
|
endpoint,
|
|
@@ -80,9 +80,9 @@ async function vibeServiceRequest(endpoint, options) {
|
|
|
80
80
|
.update(stringToSign)
|
|
81
81
|
.digest('base64');
|
|
82
82
|
const proxyUrl = `${idpUrl}/api/vibe/proxy`;
|
|
83
|
-
// Get the
|
|
83
|
+
// Get the client slug from startup config for multi-client admin support
|
|
84
84
|
const idpConfig = (0, startup_init_1.getStartupIDPConfig)();
|
|
85
|
-
const
|
|
85
|
+
const idpClientId = idpConfig?.clientSlug || idpConfig?.clientId;
|
|
86
86
|
try {
|
|
87
87
|
const res = await fetch(proxyUrl, {
|
|
88
88
|
method: 'POST',
|
|
@@ -91,7 +91,7 @@ async function vibeServiceRequest(endpoint, options) {
|
|
|
91
91
|
'X-Vibe-Client-Id': clientId,
|
|
92
92
|
'X-Vibe-Timestamp': String(timestamp),
|
|
93
93
|
'X-Vibe-Signature': signature,
|
|
94
|
-
...(
|
|
94
|
+
...(idpClientId && { 'X-Client-Id': idpClientId }),
|
|
95
95
|
},
|
|
96
96
|
body: JSON.stringify({
|
|
97
97
|
endpoint,
|
|
@@ -87,9 +87,9 @@ async function vibeServiceRequest(endpoint, options) {
|
|
|
87
87
|
.update(stringToSign)
|
|
88
88
|
.digest('base64');
|
|
89
89
|
const proxyUrl = `${idpUrl}/api/vibe/proxy`;
|
|
90
|
-
// Get the
|
|
90
|
+
// Get the client slug from startup config for multi-client admin support
|
|
91
91
|
const idpConfig = (0, startup_init_1.getStartupIDPConfig)();
|
|
92
|
-
const
|
|
92
|
+
const idpClientId = idpConfig?.clientSlug || idpConfig?.clientId;
|
|
93
93
|
try {
|
|
94
94
|
const res = await fetch(proxyUrl, {
|
|
95
95
|
method: 'POST',
|
|
@@ -98,7 +98,7 @@ async function vibeServiceRequest(endpoint, options) {
|
|
|
98
98
|
'X-Vibe-Client-Id': clientId,
|
|
99
99
|
'X-Vibe-Timestamp': String(timestamp),
|
|
100
100
|
'X-Vibe-Signature': signature,
|
|
101
|
-
...(
|
|
101
|
+
...(idpClientId && { 'X-Client-Id': idpClientId }),
|
|
102
102
|
},
|
|
103
103
|
body: JSON.stringify({
|
|
104
104
|
endpoint,
|
|
@@ -80,9 +80,9 @@ async function vibeServiceRequest(endpoint, options) {
|
|
|
80
80
|
.update(stringToSign)
|
|
81
81
|
.digest('base64');
|
|
82
82
|
const proxyUrl = `${idpUrl}/api/vibe/proxy`;
|
|
83
|
-
// Get the
|
|
83
|
+
// Get the client slug from startup config for multi-client admin support
|
|
84
84
|
const idpConfig = (0, startup_init_1.getStartupIDPConfig)();
|
|
85
|
-
const
|
|
85
|
+
const idpClientId = idpConfig?.clientSlug || idpConfig?.clientId;
|
|
86
86
|
try {
|
|
87
87
|
const res = await fetch(proxyUrl, {
|
|
88
88
|
method: 'POST',
|
|
@@ -91,7 +91,7 @@ async function vibeServiceRequest(endpoint, options) {
|
|
|
91
91
|
'X-Vibe-Client-Id': clientId,
|
|
92
92
|
'X-Vibe-Timestamp': String(timestamp),
|
|
93
93
|
'X-Vibe-Signature': signature,
|
|
94
|
-
...(
|
|
94
|
+
...(idpClientId && { 'X-Client-Id': idpClientId }),
|
|
95
95
|
},
|
|
96
96
|
body: JSON.stringify({
|
|
97
97
|
endpoint,
|
|
@@ -95,9 +95,9 @@ async function vibeServiceRequest(endpoint, options) {
|
|
|
95
95
|
.update(stringToSign)
|
|
96
96
|
.digest('base64');
|
|
97
97
|
const proxyUrl = `${idpUrl}/api/vibe/proxy`;
|
|
98
|
-
// Get the
|
|
98
|
+
// Get the client slug from startup config for multi-client admin support
|
|
99
99
|
const idpConfig = (0, startup_init_1.getStartupIDPConfig)();
|
|
100
|
-
const
|
|
100
|
+
const idpClientId = idpConfig?.clientSlug || idpConfig?.clientId;
|
|
101
101
|
try {
|
|
102
102
|
const res = await fetch(proxyUrl, {
|
|
103
103
|
method: 'POST',
|
|
@@ -107,7 +107,7 @@ async function vibeServiceRequest(endpoint, options) {
|
|
|
107
107
|
'X-Vibe-Timestamp': String(timestamp),
|
|
108
108
|
'X-Vibe-Signature': signature,
|
|
109
109
|
// For multi-client admins: specify which client context to use
|
|
110
|
-
...(
|
|
110
|
+
...(idpClientId && { 'X-Client-Id': idpClientId }),
|
|
111
111
|
},
|
|
112
112
|
body: JSON.stringify({
|
|
113
113
|
endpoint,
|
|
@@ -282,7 +282,7 @@ async function fetchConfigFromIDP(idpUrl, clientIdStr) {
|
|
|
282
282
|
}
|
|
283
283
|
// Map response to our interface (IDP always returns snake_case)
|
|
284
284
|
const config = {
|
|
285
|
-
clientId:
|
|
285
|
+
clientId: String(rawClientId),
|
|
286
286
|
clientSlug: configData.clientSlug ?? configData.client_slug ?? configData.slug ?? '',
|
|
287
287
|
nextAuthSecret: configData.nextAuthSecret ?? configData.next_auth_secret ?? '',
|
|
288
288
|
configCacheTtlSeconds: configData.configCacheTtlSeconds ?? configData.config_cache_ttl_seconds ?? 300,
|
|
@@ -330,7 +330,7 @@ async function fetchConfigFromIDP(idpUrl, clientIdStr) {
|
|
|
330
330
|
console.log(`[IDP_CONFIG] Parsed baseClientUrl:`, config.baseClientUrl, `| raw keys:`, Object.keys(configData).filter(k => k.toLowerCase().includes('client')));
|
|
331
331
|
// Validate we got what we need
|
|
332
332
|
if (!config.clientId) {
|
|
333
|
-
throw new Error('[IDP_CONFIG] FATAL: clientId is
|
|
333
|
+
throw new Error('[IDP_CONFIG] FATAL: clientId is empty or missing after parsing');
|
|
334
334
|
}
|
|
335
335
|
if (!config.nextAuthSecret) {
|
|
336
336
|
throw new Error('[IDP_CONFIG] FATAL: nextAuthSecret is empty after parsing');
|
|
@@ -31,9 +31,6 @@ async function resolveNextAuthSecret() {
|
|
|
31
31
|
const clientIdStr = process.env.CLIENT_ID;
|
|
32
32
|
if (!clientIdStr || clientIdStr.trim() === '')
|
|
33
33
|
throw new Error('CLIENT_ID is required (e.g., "ideal_resume_website")');
|
|
34
|
-
// Determine if clientId is numeric or string
|
|
35
|
-
const isNumeric = /^[0-9]+$/.test(clientIdStr);
|
|
36
|
-
const clientId = isNumeric ? parseInt(clientIdStr, 10) : clientIdStr;
|
|
37
34
|
// Step 1: Request IDP to sign a client assertion (IDP has the keys, not us)
|
|
38
35
|
const signingUrl = new URL(`${base.replace(/\/$/, '')}/api/ExternalAuth/sign-client-assertion`);
|
|
39
36
|
// Client ID passed via X-Client-Id header, not query string
|
|
@@ -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
|
});
|
|
@@ -351,7 +353,7 @@ async function executeDecision(request, decision, pathname, sessionPointer, sess
|
|
|
351
353
|
const safeCallback = getSafeCallbackUrl(pathname);
|
|
352
354
|
switch (decision.type) {
|
|
353
355
|
case 'allow':
|
|
354
|
-
return handleAllow(request, pathname, sessionPointer, sessionStatus);
|
|
356
|
+
return handleAllow(request, pathname, sessionPointer, sessionStatus, opts.rbacExemptPaths);
|
|
355
357
|
case 'redirect':
|
|
356
358
|
return redirectTo(request, decision.location, decision.clearCookies);
|
|
357
359
|
case 'service_error':
|
|
@@ -363,11 +365,12 @@ async function executeDecision(request, decision, pathname, sessionPointer, sess
|
|
|
363
365
|
/** Paths that must never be RBAC-checked (they are RBAC redirect targets) */
|
|
364
366
|
const RBAC_EXEMPT_PATHS = ['/error', '/unauthorized', '/service-unavailable'];
|
|
365
367
|
/** Handle 'allow' decision - run RBAC if enabled */
|
|
366
|
-
async function handleAllow(request, pathname, sessionPointer, sessionStatus) {
|
|
368
|
+
async function handleAllow(request, pathname, sessionPointer, sessionStatus, rbacExemptPaths = []) {
|
|
367
369
|
const isPublic = (0, route_config_1.isUnauthenticatedRoute)(pathname);
|
|
368
370
|
if ((0, rbac_check_1.isRBACEnabled)() && !isPublic && sessionPointer.exists) {
|
|
369
|
-
// Skip RBAC for error/fallback pages
|
|
370
|
-
if (RBAC_EXEMPT_PATHS.some(p => pathname.startsWith(p))
|
|
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))) {
|
|
371
374
|
return server_1.NextResponse.next();
|
|
372
375
|
}
|
|
373
376
|
if (!sessionPointer.clientId) {
|
package/package.json
CHANGED
|
@@ -65,9 +65,9 @@ async function vibeServiceRequest<T = unknown>(
|
|
|
65
65
|
|
|
66
66
|
const proxyUrl = `${idpUrl}/api/vibe/proxy`;
|
|
67
67
|
|
|
68
|
-
// Get the
|
|
68
|
+
// Get the client slug from startup config for multi-client admin support
|
|
69
69
|
const idpConfig = getStartupIDPConfig();
|
|
70
|
-
const
|
|
70
|
+
const idpClientId = idpConfig?.clientSlug || idpConfig?.clientId;
|
|
71
71
|
|
|
72
72
|
try {
|
|
73
73
|
const res = await fetch(proxyUrl, {
|
|
@@ -77,7 +77,7 @@ async function vibeServiceRequest<T = unknown>(
|
|
|
77
77
|
'X-Vibe-Client-Id': clientId,
|
|
78
78
|
'X-Vibe-Timestamp': String(timestamp),
|
|
79
79
|
'X-Vibe-Signature': signature,
|
|
80
|
-
...(
|
|
80
|
+
...(idpClientId && { 'X-Client-Id': idpClientId }),
|
|
81
81
|
},
|
|
82
82
|
body: JSON.stringify({
|
|
83
83
|
endpoint,
|
|
@@ -64,9 +64,9 @@ async function vibeServiceRequest<T = unknown>(
|
|
|
64
64
|
|
|
65
65
|
const proxyUrl = `${idpUrl}/api/vibe/proxy`;
|
|
66
66
|
|
|
67
|
-
// Get the
|
|
67
|
+
// Get the client slug from startup config for multi-client admin support
|
|
68
68
|
const idpConfig = getStartupIDPConfig();
|
|
69
|
-
const
|
|
69
|
+
const idpClientId = idpConfig?.clientSlug || idpConfig?.clientId;
|
|
70
70
|
|
|
71
71
|
try {
|
|
72
72
|
const res = await fetch(proxyUrl, {
|
|
@@ -76,7 +76,7 @@ async function vibeServiceRequest<T = unknown>(
|
|
|
76
76
|
'X-Vibe-Client-Id': clientId,
|
|
77
77
|
'X-Vibe-Timestamp': String(timestamp),
|
|
78
78
|
'X-Vibe-Signature': signature,
|
|
79
|
-
...(
|
|
79
|
+
...(idpClientId && { 'X-Client-Id': idpClientId }),
|
|
80
80
|
},
|
|
81
81
|
body: JSON.stringify({
|
|
82
82
|
endpoint,
|
|
@@ -77,9 +77,9 @@ async function vibeServiceRequest<T = unknown>(
|
|
|
77
77
|
|
|
78
78
|
const proxyUrl = `${idpUrl}/api/vibe/proxy`;
|
|
79
79
|
|
|
80
|
-
// Get the
|
|
80
|
+
// Get the client slug from startup config for multi-client admin support
|
|
81
81
|
const idpConfig = getStartupIDPConfig();
|
|
82
|
-
const
|
|
82
|
+
const idpClientId = idpConfig?.clientSlug || idpConfig?.clientId;
|
|
83
83
|
|
|
84
84
|
try {
|
|
85
85
|
const res = await fetch(proxyUrl, {
|
|
@@ -89,7 +89,7 @@ async function vibeServiceRequest<T = unknown>(
|
|
|
89
89
|
'X-Vibe-Client-Id': clientId,
|
|
90
90
|
'X-Vibe-Timestamp': String(timestamp),
|
|
91
91
|
'X-Vibe-Signature': signature,
|
|
92
|
-
...(
|
|
92
|
+
...(idpClientId && { 'X-Client-Id': idpClientId }),
|
|
93
93
|
},
|
|
94
94
|
body: JSON.stringify({
|
|
95
95
|
endpoint,
|
|
@@ -64,9 +64,9 @@ async function vibeServiceRequest<T = unknown>(
|
|
|
64
64
|
|
|
65
65
|
const proxyUrl = `${idpUrl}/api/vibe/proxy`;
|
|
66
66
|
|
|
67
|
-
// Get the
|
|
67
|
+
// Get the client slug from startup config for multi-client admin support
|
|
68
68
|
const idpConfig = getStartupIDPConfig();
|
|
69
|
-
const
|
|
69
|
+
const idpClientId = idpConfig?.clientSlug || idpConfig?.clientId;
|
|
70
70
|
|
|
71
71
|
try {
|
|
72
72
|
const res = await fetch(proxyUrl, {
|
|
@@ -76,7 +76,7 @@ async function vibeServiceRequest<T = unknown>(
|
|
|
76
76
|
'X-Vibe-Client-Id': clientId,
|
|
77
77
|
'X-Vibe-Timestamp': String(timestamp),
|
|
78
78
|
'X-Vibe-Signature': signature,
|
|
79
|
-
...(
|
|
79
|
+
...(idpClientId && { 'X-Client-Id': idpClientId }),
|
|
80
80
|
},
|
|
81
81
|
body: JSON.stringify({
|
|
82
82
|
endpoint,
|
|
@@ -79,9 +79,9 @@ async function vibeServiceRequest<T = unknown>(
|
|
|
79
79
|
|
|
80
80
|
const proxyUrl = `${idpUrl}/api/vibe/proxy`;
|
|
81
81
|
|
|
82
|
-
// Get the
|
|
82
|
+
// Get the client slug from startup config for multi-client admin support
|
|
83
83
|
const idpConfig = getStartupIDPConfig();
|
|
84
|
-
const
|
|
84
|
+
const idpClientId = idpConfig?.clientSlug || idpConfig?.clientId;
|
|
85
85
|
|
|
86
86
|
try {
|
|
87
87
|
const res = await fetch(proxyUrl, {
|
|
@@ -92,7 +92,7 @@ async function vibeServiceRequest<T = unknown>(
|
|
|
92
92
|
'X-Vibe-Timestamp': String(timestamp),
|
|
93
93
|
'X-Vibe-Signature': signature,
|
|
94
94
|
// For multi-client admins: specify which client context to use
|
|
95
|
-
...(
|
|
95
|
+
...(idpClientId && { 'X-Client-Id': idpClientId }),
|
|
96
96
|
},
|
|
97
97
|
body: JSON.stringify({
|
|
98
98
|
endpoint,
|
|
@@ -56,7 +56,7 @@ export interface BrandingConfig {
|
|
|
56
56
|
}
|
|
57
57
|
|
|
58
58
|
export interface IDPClientConfig {
|
|
59
|
-
clientId:
|
|
59
|
+
clientId: string;
|
|
60
60
|
clientSlug: string;
|
|
61
61
|
nextAuthSecret: string;
|
|
62
62
|
configCacheTtlSeconds: number;
|
|
@@ -371,7 +371,7 @@ async function fetchConfigFromIDP(idpUrl: string, clientIdStr: string): Promise<
|
|
|
371
371
|
|
|
372
372
|
// Map response to our interface (IDP always returns snake_case)
|
|
373
373
|
const config: IDPClientConfig = {
|
|
374
|
-
clientId:
|
|
374
|
+
clientId: String(rawClientId),
|
|
375
375
|
clientSlug: configData.clientSlug ?? configData.client_slug ?? configData.slug ?? '',
|
|
376
376
|
nextAuthSecret: configData.nextAuthSecret ?? configData.next_auth_secret ?? '',
|
|
377
377
|
configCacheTtlSeconds: configData.configCacheTtlSeconds ?? configData.config_cache_ttl_seconds ?? 300,
|
|
@@ -420,7 +420,7 @@ async function fetchConfigFromIDP(idpUrl: string, clientIdStr: string): Promise<
|
|
|
420
420
|
|
|
421
421
|
// Validate we got what we need
|
|
422
422
|
if (!config.clientId) {
|
|
423
|
-
throw new Error('[IDP_CONFIG] FATAL: clientId is
|
|
423
|
+
throw new Error('[IDP_CONFIG] FATAL: clientId is empty or missing after parsing');
|
|
424
424
|
}
|
|
425
425
|
if (!config.nextAuthSecret) {
|
|
426
426
|
throw new Error('[IDP_CONFIG] FATAL: nextAuthSecret is empty after parsing');
|
|
@@ -32,10 +32,6 @@ export async function resolveNextAuthSecret(): Promise<string> {
|
|
|
32
32
|
const clientIdStr = process.env.CLIENT_ID;
|
|
33
33
|
if (!clientIdStr || clientIdStr.trim() === '') throw new Error('CLIENT_ID is required (e.g., "ideal_resume_website")');
|
|
34
34
|
|
|
35
|
-
// Determine if clientId is numeric or string
|
|
36
|
-
const isNumeric = /^[0-9]+$/.test(clientIdStr);
|
|
37
|
-
const clientId = isNumeric ? parseInt(clientIdStr, 10) : clientIdStr;
|
|
38
|
-
|
|
39
35
|
// Step 1: Request IDP to sign a client assertion (IDP has the keys, not us)
|
|
40
36
|
|
|
41
37
|
const signingUrl = new URL(`${base.replace(/\/$/, '')}/api/ExternalAuth/sign-client-assertion`);
|
|
@@ -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
|
});
|
|
@@ -474,6 +478,7 @@ interface ExecuteOptions {
|
|
|
474
478
|
circuitBreaker: CircuitBreakerProvider;
|
|
475
479
|
logger: MiddlewareLogger;
|
|
476
480
|
refreshEndpoint: string;
|
|
481
|
+
rbacExemptPaths: string[];
|
|
477
482
|
onRefreshSuccess?: () => void;
|
|
478
483
|
onRefreshFailure?: (status: number, isNetworkError: boolean) => void;
|
|
479
484
|
}
|
|
@@ -491,7 +496,7 @@ async function executeDecision(
|
|
|
491
496
|
|
|
492
497
|
switch (decision.type) {
|
|
493
498
|
case 'allow':
|
|
494
|
-
return handleAllow(request, pathname, sessionPointer, sessionStatus);
|
|
499
|
+
return handleAllow(request, pathname, sessionPointer, sessionStatus, opts.rbacExemptPaths);
|
|
495
500
|
|
|
496
501
|
case 'redirect':
|
|
497
502
|
return redirectTo(request, decision.location, decision.clearCookies);
|
|
@@ -512,13 +517,15 @@ async function handleAllow(
|
|
|
512
517
|
request: NextRequest,
|
|
513
518
|
pathname: string,
|
|
514
519
|
sessionPointer: SessionPointer,
|
|
515
|
-
sessionStatus: SessionStatus
|
|
520
|
+
sessionStatus: SessionStatus,
|
|
521
|
+
rbacExemptPaths: string[] = []
|
|
516
522
|
): Promise<NextResponse> {
|
|
517
523
|
const isPublic = isUnauthenticatedRoute(pathname);
|
|
518
524
|
|
|
519
525
|
if (isRBACEnabled() && !isPublic && sessionPointer.exists) {
|
|
520
|
-
// Skip RBAC for error/fallback pages
|
|
521
|
-
if (RBAC_EXEMPT_PATHS.some(p => pathname.startsWith(p))
|
|
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))) {
|
|
522
529
|
return NextResponse.next();
|
|
523
530
|
}
|
|
524
531
|
|