@payez/next-mvp 4.8.1 → 4.8.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.
- package/POST-INSTALL.md +408 -408
- package/README.md +859 -859
- package/dist/api-handlers/admin/audit.js +13 -21
- package/dist/api-handlers/admin/sessions.js +59 -45
- package/dist/api-handlers/admin/stats.js +1 -1
- package/dist/api-handlers/admin/users.js +15 -23
- package/dist/api-handlers/admin/vibe-data.d.ts +5 -0
- package/dist/api-handlers/admin/vibe-data.js +5 -0
- package/dist/components/account/MobileNavDrawer.js +18 -18
- package/dist/components/account/UserAvatarMenu.js +1 -1
- package/dist/components/admin/AlertSettingsTab.js +40 -40
- package/dist/components/admin/LoggingSettingsTab.js +28 -28
- package/dist/lib/session-store.js +21 -21
- package/dist/lib/startup-init.js +19 -19
- package/dist/pages/login/page.js +28 -28
- package/dist/pages/settings/EnhancedSettingsPage.js +17 -17
- package/dist/vibe/client.d.ts +1 -1
- package/dist/vibe/client.js +2 -1
- package/dist/vibe/filters.d.ts +36 -0
- package/dist/vibe/filters.js +47 -0
- package/dist/vibe/generic.js +2 -1
- package/dist/vibe/sessions.js +28 -3
- package/package.json +2 -2
- package/scripts/check-publish-eol.mjs +33 -15
- package/src/api/auth-handler.ts +550 -550
- package/src/api/index.ts +17 -17
- package/src/api-handlers/account/change-password.ts +170 -170
- package/src/api-handlers/account/masked-info.ts +46 -46
- package/src/api-handlers/account/profile.ts +99 -99
- package/src/api-handlers/account/recovery/forward.ts +117 -117
- package/src/api-handlers/account/recovery/initiate.ts +59 -59
- package/src/api-handlers/account/recovery/send-code.ts +21 -21
- package/src/api-handlers/account/recovery/verify-code.ts +20 -20
- package/src/api-handlers/account/reset-password.ts +87 -87
- package/src/api-handlers/account/send-code.ts +70 -70
- package/src/api-handlers/account/update-phone.ts +84 -84
- package/src/api-handlers/account/validate-password.ts +139 -139
- package/src/api-handlers/account/verify-email.ts +134 -134
- package/src/api-handlers/account/verify-sms.ts +134 -134
- package/src/api-handlers/admin/analytics.ts +452 -452
- package/src/api-handlers/admin/audit.ts +223 -232
- package/src/api-handlers/admin/index.ts +63 -63
- package/src/api-handlers/admin/redis-sessions.ts +259 -259
- package/src/api-handlers/admin/sessions.ts +341 -327
- package/src/api-handlers/admin/site-logs.ts +388 -388
- package/src/api-handlers/admin/stats.ts +260 -260
- package/src/api-handlers/admin/users.ts +240 -251
- package/src/api-handlers/admin/vibe-data.ts +338 -333
- package/src/api-handlers/anon/preferences.ts +123 -123
- package/src/api-handlers/auth/credential-login-email-code.ts +92 -92
- package/src/api-handlers/auth/credential-login.ts +948 -948
- package/src/api-handlers/auth/jwks.ts +31 -31
- package/src/api-handlers/auth/login.ts +252 -252
- package/src/api-handlers/auth/passkey-signin.ts +116 -116
- package/src/api-handlers/auth/refresh.ts +829 -829
- package/src/api-handlers/auth/signout.ts +201 -201
- package/src/api-handlers/auth/status.ts +23 -23
- package/src/api-handlers/auth/update-session.ts +144 -144
- package/src/api-handlers/auth/validate.ts +45 -45
- package/src/api-handlers/auth/verify-code.ts +116 -116
- package/src/api-handlers/mfa/backup-code-verify.ts +170 -170
- package/src/api-handlers/mfa/backup-codes.ts +42 -42
- package/src/api-handlers/mfa/methods.ts +47 -47
- package/src/api-handlers/mfa/totp-confirm.ts +43 -43
- package/src/api-handlers/mfa/totp-enroll.ts +43 -43
- package/src/api-handlers/mfa/totp-verify.ts +172 -172
- package/src/api-handlers/oauth/resume.ts +182 -182
- package/src/api-handlers/onboarding/send-email-code.ts +18 -18
- package/src/api-handlers/onboarding/set-password.ts +21 -21
- package/src/api-handlers/onboarding/verify-code.ts +20 -20
- package/src/api-handlers/onboarding/verify-welcome-email.ts +17 -17
- package/src/api-handlers/passkeys/list.ts +32 -32
- package/src/api-handlers/passkeys/manage.ts +55 -55
- package/src/api-handlers/passkeys/register-options.ts +43 -43
- package/src/api-handlers/passkeys/register-verify.ts +42 -42
- package/src/api-handlers/session/refresh-viability.ts +56 -56
- package/src/api-handlers/session/viability.ts +238 -238
- package/src/api-handlers/test/force-expire.ts +167 -167
- package/src/auth/auth-decision.ts +292 -292
- package/src/auth/better-auth.ts +1688 -1688
- package/src/auth/idp-oauth.ts +310 -310
- package/src/auth/password-reset-bridge.ts +98 -98
- package/src/auth/revocation-fields.ts +16 -16
- package/src/auth/route-config.ts +236 -236
- package/src/auth/safe-callback.ts +199 -199
- package/src/auth/session-cookie-clear.ts +30 -30
- package/src/auth/session-reset-link.ts +159 -159
- package/src/auth/session-response-sanitizer.ts +151 -151
- package/src/auth/types/auth-types.ts +555 -555
- package/src/auth/types/index.ts +7 -7
- package/src/auth/unauthenticated-routes.ts +2 -2
- package/src/auth/utils/idp-client.ts +364 -364
- package/src/auth/utils/index.ts +6 -6
- package/src/auth/utils/token-utils.ts +243 -243
- package/src/client/AuthContext.tsx +192 -192
- package/src/client/better-auth-client.ts +70 -70
- package/src/client/fetch-with-auth.ts +47 -47
- package/src/client/fetchWithSession.ts +21 -21
- package/src/client/index.ts +13 -13
- package/src/client/useAnonSession.ts +131 -131
- package/src/components/SessionSync.tsx +138 -138
- package/src/components/SignalRHealthCheck.tsx +131 -131
- package/src/components/account/MobileNavDrawer.tsx +305 -305
- package/src/components/account/UserAvatarMenu.tsx +277 -277
- package/src/components/account/index.ts +13 -13
- package/src/components/admin/AlertSettingsTab.tsx +728 -728
- package/src/components/admin/AnalyticsTab.tsx +703 -703
- package/src/components/admin/DataBrowserTab.tsx +508 -508
- package/src/components/admin/LoggingSettingsTab.tsx +665 -665
- package/src/components/admin/SessionsTab.tsx +421 -421
- package/src/components/admin/StatsTab.tsx +379 -379
- package/src/components/admin/VibeAdminContext.tsx +87 -87
- package/src/components/admin/VibeAdminLayout.tsx +187 -187
- package/src/components/admin/index.ts +59 -59
- package/src/components/auth/CredentialSignInForm.tsx +240 -240
- package/src/components/auth/FederatedAuthSection.tsx +129 -129
- package/src/components/auth/ModeAwareLoginPage.tsx +146 -146
- package/src/components/auth/ModeAwareSignupPage.tsx +200 -200
- package/src/components/auth/PasswordResetBridge.tsx +30 -30
- package/src/components/auth/TraditionalAuthSection.tsx +103 -103
- package/src/components/recovery/CompleteStep.tsx +36 -36
- package/src/components/recovery/InitiateRecoveryStep.tsx +68 -68
- package/src/components/recovery/RequestedStep.tsx +30 -30
- package/src/components/recovery/SelectMethodStep.tsx +73 -73
- package/src/components/recovery/SetPasswordStep.tsx +118 -118
- package/src/components/recovery/VerifyCodeStep.tsx +90 -90
- package/src/components/reserved/ReservedRecoveryWarning.tsx +160 -160
- package/src/components/reserved/ReservedStatusBox.tsx +118 -118
- package/src/components/ui/BetaBadge.tsx +58 -58
- package/src/components/ui/Footer.tsx +93 -93
- package/src/config/env.ts +55 -55
- package/src/config/logger.ts +62 -62
- package/src/config/logging-config.ts +82 -82
- package/src/config/unauthenticated-routes.ts +19 -19
- package/src/config/vibe-log-transport.ts +264 -264
- package/src/edge/internal-api-url.ts +65 -65
- package/src/edge/middleware.ts +42 -42
- package/src/hooks/useAuth.ts +117 -117
- package/src/hooks/useAuthSettings.ts +109 -109
- package/src/hooks/useAvailableProviders.ts +143 -143
- package/src/hooks/usePasswordValidation.ts +127 -127
- package/src/hooks/useProfile.ts +82 -82
- package/src/hooks/usePublicAuthSettings.ts +171 -171
- package/src/hooks/useSessionExpiration.ts +101 -101
- package/src/hooks/useViabilitySession.ts +337 -337
- package/src/index.ts +64 -64
- package/src/lib/anon-session.ts +213 -213
- package/src/lib/api-handler.ts +643 -643
- package/src/lib/app-slug.ts +228 -228
- package/src/lib/authorize-resume.ts +172 -172
- package/src/lib/bearer-fetch.ts +31 -31
- package/src/lib/client-key.ts +88 -88
- package/src/lib/code-step-copy.ts +83 -83
- package/src/lib/credential-sign-in-client.ts +101 -101
- package/src/lib/demo-mode.ts +18 -18
- package/src/lib/device-id.ts +67 -67
- package/src/lib/ensure-fresh-access-token.ts +509 -509
- package/src/lib/geolocation.ts +265 -265
- package/src/lib/idp-client-config.ts +1005 -1005
- package/src/lib/idp-fetch.ts +169 -169
- package/src/lib/idp-url-guard.ts +37 -37
- package/src/lib/internal-api.ts +171 -171
- package/src/lib/jwt-decode-client.ts +45 -45
- package/src/lib/jwt-decode.ts +82 -82
- package/src/lib/lock-window.ts +49 -49
- package/src/lib/login-error-message.ts +41 -41
- package/src/lib/never-in-response.ts +64 -64
- package/src/lib/nextauth-secret.ts +168 -168
- package/src/lib/onboarding-proxy.ts +57 -57
- package/src/lib/password-rules.ts +50 -50
- package/src/lib/rate-limit-service.ts +9 -9
- package/src/lib/redis.ts +105 -105
- package/src/lib/refresh-token-validator.ts +64 -64
- package/src/lib/roles.ts +177 -177
- package/src/lib/safe-error-response.ts +149 -149
- package/src/lib/secret-validation.ts +8 -8
- package/src/lib/session-cookie-shape.ts +185 -185
- package/src/lib/session-store.ts +1092 -1092
- package/src/lib/session.ts +34 -34
- package/src/lib/single-flight.ts +64 -64
- package/src/lib/site-logger.ts +245 -245
- package/src/lib/standardized-client-api.ts +901 -901
- package/src/lib/startup-init.ts +276 -276
- package/src/lib/test-aware-get-token.ts +41 -41
- package/src/lib/token-expiry.ts +40 -40
- package/src/lib/token-lifecycle.ts +546 -546
- package/src/lib/token-log-id.ts +69 -69
- package/src/lib/twofa-rejection.ts +91 -91
- package/src/lib/types/api-responses.ts +336 -336
- package/src/lib/user-agent-parser.ts +252 -252
- package/src/logging/api/admin-analytics.ts +51 -51
- package/src/logging/api/audit-log.ts +53 -53
- package/src/logging/components/AdminAnalyticsLayout.tsx +49 -49
- package/src/logging/components/AuditLogViewer.tsx +125 -125
- package/src/logging/components/ErrorMetricsCard.tsx +98 -98
- package/src/logging/components/HealthMetricsCard.tsx +70 -70
- package/src/logging/hooks/useAdminAnalytics.ts +22 -22
- package/src/logging/hooks/useAuditLog.ts +24 -24
- package/src/logging/hooks/useErrorMetrics.ts +40 -40
- package/src/logging/hooks/useHealthMetrics.ts +44 -44
- package/src/logging/index.ts +18 -18
- package/src/logging/types/analytics.ts +81 -81
- package/src/logging/types/audit.ts +31 -31
- package/src/logging/types/index.ts +3 -3
- package/src/middleware/create-middleware.ts +1141 -1141
- package/src/middleware/rbac-check.ts +279 -279
- package/src/middleware/twofa-presets.ts +224 -224
- package/src/models/DecodedAccessToken.ts +16 -16
- package/src/models/SessionModel.ts +331 -331
- package/src/pages/admin-login/page.tsx +111 -111
- package/src/pages/admin-page-permissions/PagePermissionsAdminPage.tsx +527 -527
- package/src/pages/admin-page-permissions/index.ts +7 -7
- package/src/pages/admin-roles/RolesAdminPage.tsx +533 -533
- package/src/pages/admin-roles/index.ts +9 -9
- package/src/pages/admin-roles/modals.tsx +469 -469
- package/src/pages/client-admin/ClientSiteAdminPage.tsx +382 -382
- package/src/pages/client-admin/index.ts +33 -33
- package/src/pages/coming-soon/page.tsx +83 -83
- package/src/pages/login/page.tsx +338 -338
- package/src/pages/onboarding/index.ts +11 -11
- package/src/pages/onboarding/page.tsx +212 -212
- package/src/pages/profile/EnhancedProfilePage.tsx +514 -514
- package/src/pages/profile/index.ts +9 -9
- package/src/pages/profile/page.tsx +166 -166
- package/src/pages/recovery/page.tsx +267 -267
- package/src/pages/roles/MyRolesPage.tsx +211 -211
- package/src/pages/roles/components.tsx +294 -294
- package/src/pages/roles/index.ts +17 -17
- package/src/pages/security/EnhancedSecurityPage.tsx +660 -660
- package/src/pages/security/index.ts +12 -12
- package/src/pages/security/page.tsx +507 -507
- package/src/pages/service-unavailable/page.tsx +63 -63
- package/src/pages/settings/EnhancedSettingsPage.tsx +642 -642
- package/src/pages/settings/index.ts +9 -9
- package/src/pages/settings/page.tsx +47 -47
- package/src/pages/showcase/ShowcasePage.tsx +532 -532
- package/src/pages/showcase/index.ts +13 -13
- package/src/pages/test-env/EmergencyLogoutPage.tsx +192 -192
- package/src/pages/test-env/JwtInspectPage.tsx +449 -449
- package/src/pages/test-env/RefreshTokenPage.tsx +178 -178
- package/src/pages/test-env/TestEnvPage.tsx +130 -130
- package/src/pages/test-env/guarded.tsx +81 -81
- package/src/pages/test-env/index.ts +77 -77
- package/src/pages/test-env/redact-credentials.ts +96 -96
- package/src/pages/verify-code/page.tsx +746 -746
- package/src/routes/account/index.ts +31 -31
- package/src/routes/account/masked-info.ts +36 -36
- package/src/routes/account/send-code.ts +39 -39
- package/src/routes/account/update-phone.ts +13 -13
- package/src/routes/account/verify-email.ts +40 -40
- package/src/routes/account/verify-sms.ts +40 -40
- package/src/routes/auth/credential-login-email-code.ts +6 -6
- package/src/routes/auth/credential-login.ts +6 -6
- package/src/routes/auth/index.ts +22 -22
- package/src/routes/auth/logout.ts +111 -111
- package/src/routes/auth/nextauth.ts +45 -45
- package/src/routes/auth/passkey-signin.ts +13 -13
- package/src/routes/auth/refresh.ts +77 -77
- package/src/routes/auth/session-reset.ts +37 -37
- package/src/routes/auth/session.ts +261 -261
- package/src/routes/auth/settings.ts +181 -181
- package/src/routes/auth/viability.ts +519 -519
- package/src/routes/index.ts +17 -17
- package/src/routes/mfa/backup-code-verify.ts +1 -1
- package/src/routes/mfa/backup-codes.ts +10 -10
- package/src/routes/mfa/methods.ts +22 -22
- package/src/routes/mfa/totp-confirm.ts +10 -10
- package/src/routes/mfa/totp-enroll.ts +10 -10
- package/src/routes/mfa/totp-verify.ts +14 -14
- package/src/routes/onboarding/index.ts +15 -15
- package/src/routes/onboarding/send-email-code.ts +2 -2
- package/src/routes/onboarding/set-password.ts +2 -2
- package/src/routes/onboarding/verify-code.ts +2 -2
- package/src/routes/onboarding/verify-welcome-email.ts +2 -2
- package/src/routes/passkeys/list.ts +10 -10
- package/src/routes/passkeys/manage.ts +23 -23
- package/src/routes/passkeys/register-options.ts +10 -10
- package/src/routes/passkeys/register-verify.ts +10 -10
- package/src/routes/session/index.ts +7 -7
- package/src/routes/session/refresh-viability.ts +17 -17
- package/src/server/auth-guard.ts +203 -203
- package/src/server/auth.ts +486 -486
- package/src/server/decode-session.ts +309 -309
- package/src/server/slim-middleware.ts +117 -117
- package/src/server/verified-session-token.ts +103 -103
- package/src/server/with-auth.ts +93 -93
- package/src/services/signalrActivityService.ts +258 -258
- package/src/stores/authStore.ts +1900 -1900
- package/src/templates/instrumentation.ts +60 -60
- package/src/theme/ThemeProvider.tsx +39 -39
- package/src/theme/default.ts +33 -33
- package/src/theme/index.ts +31 -31
- package/src/theme/types.ts +69 -69
- package/src/theme/useTheme.ts +57 -57
- package/src/theme/utils.ts +40 -40
- package/src/types/api.ts +12 -12
- package/src/types/auth.d.ts +23 -23
- package/src/types/auth.ts +48 -48
- package/src/types/logging.ts +11 -11
- package/src/types/recovery.ts +54 -54
- package/src/utils/api.ts +401 -401
- package/src/utils/circuitBreaker.ts +40 -40
- package/src/utils/error-message.ts +108 -108
- package/src/utils/layout/reservedSpace.ts +124 -124
- package/src/utils/logout.ts +30 -30
- package/src/vibe/client.ts +878 -877
- package/src/vibe/enterprise-auth.ts +208 -208
- package/src/vibe/errors.ts +209 -209
- package/src/vibe/filters.ts +52 -0
- package/src/vibe/generic.ts +340 -339
- package/src/vibe/hooks/index.ts +367 -367
- package/src/vibe/index.ts +133 -133
- package/src/vibe/sessions.ts +925 -899
- package/src/vibe/types.ts +577 -577
package/src/server/auth-guard.ts
CHANGED
|
@@ -1,203 +1,203 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Server-Side Auth Guard for Layouts
|
|
3
|
-
*
|
|
4
|
-
* Replaces middleware's self-fetch auth checks with direct Redis/function calls.
|
|
5
|
-
* Call from server-component layouts to protect routes.
|
|
6
|
-
*
|
|
7
|
-
* Zero HTTP self-fetches. ~8ms total (Redis + in-memory checks).
|
|
8
|
-
*/
|
|
9
|
-
|
|
10
|
-
import 'server-only';
|
|
11
|
-
import { redirect } from 'next/navigation';
|
|
12
|
-
import { headers } from 'next/headers';
|
|
13
|
-
import { decodeSession } from './decode-session';
|
|
14
|
-
import { getIDPClientConfig } from '../lib/idp-client-config';
|
|
15
|
-
import { getSessionCookieName, getSecureSessionCookieName } from '../lib/app-slug';
|
|
16
|
-
import type { SessionData } from '../lib/session-store';
|
|
17
|
-
|
|
18
|
-
// =============================================================================
|
|
19
|
-
// TYPES
|
|
20
|
-
// =============================================================================
|
|
21
|
-
|
|
22
|
-
export interface AuthGuardOptions {
|
|
23
|
-
/** Custom checks to run after standard auth validation */
|
|
24
|
-
checks?: AuthCheck[];
|
|
25
|
-
/** Override login redirect URL (default: /account-auth/login) */
|
|
26
|
-
loginUrl?: string;
|
|
27
|
-
/** Override 2FA redirect URL (default: /account-auth/verify-code) */
|
|
28
|
-
verifyCodeUrl?: string;
|
|
29
|
-
/** Override service unavailable URL (default: /service-unavailable) */
|
|
30
|
-
serviceUnavailableUrl?: string;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
export interface AuthCheck {
|
|
34
|
-
/** Name for logging */
|
|
35
|
-
name: string;
|
|
36
|
-
/** Returns redirect URL if check fails, null if passes */
|
|
37
|
-
check: (session: SessionData, pathname: string) => Promise<string | null>;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
export interface AuthGuardResult {
|
|
41
|
-
userId: string;
|
|
42
|
-
email: string;
|
|
43
|
-
roles: string[];
|
|
44
|
-
sessionData: SessionData;
|
|
45
|
-
accessToken?: string;
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
// =============================================================================
|
|
49
|
-
// CONSTANTS
|
|
50
|
-
// =============================================================================
|
|
51
|
-
|
|
52
|
-
const LOGIN_PAGE = '/account-auth/login';
|
|
53
|
-
const VERIFY_CODE_PAGE = '/account-auth/verify-code';
|
|
54
|
-
const SERVICE_UNAVAILABLE_PAGE = '/service-unavailable';
|
|
55
|
-
|
|
56
|
-
// =============================================================================
|
|
57
|
-
// MAIN
|
|
58
|
-
// =============================================================================
|
|
59
|
-
|
|
60
|
-
/**
|
|
61
|
-
* Server-side auth guard. Call from async server layouts.
|
|
62
|
-
*
|
|
63
|
-
* Redirects (via next/navigation redirect()) if:
|
|
64
|
-
* - No session cookie / invalid JWT
|
|
65
|
-
* - Session not in Redis (stale)
|
|
66
|
-
* - Session force-invalidated
|
|
67
|
-
* - 2FA required but not completed / expired
|
|
68
|
-
* - Any custom check fails
|
|
69
|
-
*
|
|
70
|
-
* Returns the authenticated user's session data on success.
|
|
71
|
-
*/
|
|
72
|
-
export async function authGuard(options?: AuthGuardOptions): Promise<AuthGuardResult> {
|
|
73
|
-
const loginUrl = options?.loginUrl || LOGIN_PAGE;
|
|
74
|
-
const verifyCodeUrl = options?.verifyCodeUrl || VERIFY_CODE_PAGE;
|
|
75
|
-
const serviceUnavailableUrl = options?.serviceUnavailableUrl || SERVICE_UNAVAILABLE_PAGE;
|
|
76
|
-
|
|
77
|
-
// Get current pathname from headers (set by Next.js)
|
|
78
|
-
const headerStore = await headers();
|
|
79
|
-
const pathname = headerStore.get('x-next-pathname') ||
|
|
80
|
-
headerStore.get('x-invoke-path') ||
|
|
81
|
-
headerStore.get('x-matched-path') ||
|
|
82
|
-
'/';
|
|
83
|
-
|
|
84
|
-
const callbackUrl = encodeURIComponent(pathname);
|
|
85
|
-
|
|
86
|
-
// --- Decode session (cookie → JWT → Redis) ---
|
|
87
|
-
let decoded: Awaited<ReturnType<typeof decodeSession>>;
|
|
88
|
-
try {
|
|
89
|
-
decoded = await decodeSession();
|
|
90
|
-
} catch (error) {
|
|
91
|
-
// Redis unreachable or startup failure → fail closed
|
|
92
|
-
console.error('[AUTH-GUARD] Session decode failed (service error):', error instanceof Error ? error.message : String(error));
|
|
93
|
-
redirect(serviceUnavailableUrl);
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
// No session at all → redirect to login
|
|
97
|
-
if (!decoded) {
|
|
98
|
-
redirect(`${loginUrl}?callbackUrl=${callbackUrl}`);
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
const { sessionData } = decoded;
|
|
102
|
-
|
|
103
|
-
// --- Force-invalidated session (admin action, password change) ---
|
|
104
|
-
if (sessionData.forceInvalidated) {
|
|
105
|
-
console.warn('[AUTH-GUARD] Session force-invalidated', {
|
|
106
|
-
userId: sessionData.userId,
|
|
107
|
-
pathname,
|
|
108
|
-
});
|
|
109
|
-
redirect(`${loginUrl}?callbackUrl=${callbackUrl}&reason=invalidated`);
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
// --- 2FA check ---
|
|
113
|
-
// This try wraps ONLY the config fetch, because that is the only thing here
|
|
114
|
-
// that can genuinely fail. Do NOT widen it to cover the redirect below:
|
|
115
|
-
// next/navigation's redirect() signals by THROWING a NEXT_REDIRECT digest, so
|
|
116
|
-
// a redirect inside this try is caught by the catch and rewritten into a
|
|
117
|
-
// service-unavailable page - denying a 2FA user the one page that would let
|
|
118
|
-
// them satisfy the challenge, and reporting it as an outage. See card 312242.
|
|
119
|
-
let requires2FA: boolean;
|
|
120
|
-
try {
|
|
121
|
-
const config = await getIDPClientConfig();
|
|
122
|
-
requires2FA = config.authSettings?.require2FA ?? true;
|
|
123
|
-
} catch (error) {
|
|
124
|
-
// If we can't check 2FA config, fail closed
|
|
125
|
-
console.error('[AUTH-GUARD] 2FA config check failed:', error instanceof Error ? error.message : String(error));
|
|
126
|
-
redirect(serviceUnavailableUrl);
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
if (requires2FA) {
|
|
130
|
-
// 369858: the legacy `?? (sessionData as any).twoFactorComplete` carry is REMOVED.
|
|
131
|
-
// This is THE access gate - the only one of the 8 carry sites that grants access
|
|
132
|
-
// rather than reporting status - so it reads exactly one source of truth.
|
|
133
|
-
//
|
|
134
|
-
// Removing it is fail-closed. No current writer emits the legacy field alone:
|
|
135
|
-
// session-store.ts:444-445 writes both as a pair from a server-derived stepped-up
|
|
136
|
-
// token, auth/refresh.ts:376 writes it as a MIRROR of mfaVerified, and
|
|
137
|
-
// routes/auth/session.ts:149-157 refuses both on PRESENCE, so a caller cannot
|
|
138
|
-
// assert either. The carry therefore only ever served sessions minted before
|
|
139
|
-
// `mfaVerified` existed, and for those the correct behaviour is re-verify, not
|
|
140
|
-
// grandfather. Cost is one forced re-challenge for a stale legacy session.
|
|
141
|
-
//
|
|
142
|
-
// Severity note, so nobody re-inflates this later: the operator was `??`
|
|
143
|
-
// (nullish), NOT `||`. `false ?? x` is `false`, so an explicitly DENIED session
|
|
144
|
-
// could never be upgraded by a stale flag. This was legacy grandfathering, not a
|
|
145
|
-
// live 2FA bypass. Had it been `||`, it would have been the bypass.
|
|
146
|
-
const mfaVerified = sessionData.mfaVerified ?? false;
|
|
147
|
-
|
|
148
|
-
// 339563 / 369858 §3: THIS IS STILL A FAIL-OPEN AND IS DELIBERATELY LEFT VISIBLE.
|
|
149
|
-
// An absent mfaExpiresAt collapses to 0, so `mfaExpired` is false and the MFA
|
|
150
|
-
// verdict NEVER EXPIRES. That needs no legacy-session hypothesis: it fires on a
|
|
151
|
-
// current `mfaVerified: true` session whose token carried no `mfa_expires` claim -
|
|
152
|
-
// the passwordless/primary-credential lane, named on the write path in the
|
|
153
|
-
// source's own voice at session-store.ts:464-467 ("it IS the case the consumers
|
|
154
|
-
// currently fail OPEN on"). Removing the carry above does NOT touch this.
|
|
155
|
-
// Tracked separately by BAPert; not fixed here because failing closed on absent
|
|
156
|
-
// expiry would deny the passwordless lane outright and that is a product call.
|
|
157
|
-
const mfaExpiresAt = sessionData.mfaExpiresAt || 0;
|
|
158
|
-
const mfaExpired = mfaExpiresAt > 0 && mfaExpiresAt < Date.now();
|
|
159
|
-
|
|
160
|
-
if (!mfaVerified || mfaExpired) {
|
|
161
|
-
console.log('[AUTH-GUARD] 2FA required', {
|
|
162
|
-
mfaVerified,
|
|
163
|
-
mfaExpired,
|
|
164
|
-
userId: sessionData.userId,
|
|
165
|
-
pathname,
|
|
166
|
-
});
|
|
167
|
-
redirect(`${verifyCodeUrl}?callbackUrl=${callbackUrl}`);
|
|
168
|
-
}
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
// --- Custom checks (beta, admin, etc.) ---
|
|
172
|
-
if (options?.checks) {
|
|
173
|
-
for (const check of options.checks) {
|
|
174
|
-
try {
|
|
175
|
-
const redirectUrl = await check.check(sessionData, pathname);
|
|
176
|
-
if (redirectUrl) {
|
|
177
|
-
console.log(`[AUTH-GUARD] Custom check "${check.name}" failed`, {
|
|
178
|
-
userId: sessionData.userId,
|
|
179
|
-
pathname,
|
|
180
|
-
redirectUrl,
|
|
181
|
-
});
|
|
182
|
-
redirect(redirectUrl);
|
|
183
|
-
}
|
|
184
|
-
} catch (error) {
|
|
185
|
-
// If the error is a redirect (from next/navigation), re-throw it
|
|
186
|
-
if (error && typeof error === 'object' && 'digest' in error) {
|
|
187
|
-
throw error;
|
|
188
|
-
}
|
|
189
|
-
console.error(`[AUTH-GUARD] Custom check "${check.name}" error:`, error instanceof Error ? error.message : String(error));
|
|
190
|
-
redirect(serviceUnavailableUrl);
|
|
191
|
-
}
|
|
192
|
-
}
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
// --- All checks passed ---
|
|
196
|
-
return {
|
|
197
|
-
userId: sessionData.userId,
|
|
198
|
-
email: sessionData.email,
|
|
199
|
-
roles: sessionData.roles || [],
|
|
200
|
-
sessionData,
|
|
201
|
-
accessToken: sessionData.idpAccessToken,
|
|
202
|
-
};
|
|
203
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* Server-Side Auth Guard for Layouts
|
|
3
|
+
*
|
|
4
|
+
* Replaces middleware's self-fetch auth checks with direct Redis/function calls.
|
|
5
|
+
* Call from server-component layouts to protect routes.
|
|
6
|
+
*
|
|
7
|
+
* Zero HTTP self-fetches. ~8ms total (Redis + in-memory checks).
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import 'server-only';
|
|
11
|
+
import { redirect } from 'next/navigation';
|
|
12
|
+
import { headers } from 'next/headers';
|
|
13
|
+
import { decodeSession } from './decode-session';
|
|
14
|
+
import { getIDPClientConfig } from '../lib/idp-client-config';
|
|
15
|
+
import { getSessionCookieName, getSecureSessionCookieName } from '../lib/app-slug';
|
|
16
|
+
import type { SessionData } from '../lib/session-store';
|
|
17
|
+
|
|
18
|
+
// =============================================================================
|
|
19
|
+
// TYPES
|
|
20
|
+
// =============================================================================
|
|
21
|
+
|
|
22
|
+
export interface AuthGuardOptions {
|
|
23
|
+
/** Custom checks to run after standard auth validation */
|
|
24
|
+
checks?: AuthCheck[];
|
|
25
|
+
/** Override login redirect URL (default: /account-auth/login) */
|
|
26
|
+
loginUrl?: string;
|
|
27
|
+
/** Override 2FA redirect URL (default: /account-auth/verify-code) */
|
|
28
|
+
verifyCodeUrl?: string;
|
|
29
|
+
/** Override service unavailable URL (default: /service-unavailable) */
|
|
30
|
+
serviceUnavailableUrl?: string;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface AuthCheck {
|
|
34
|
+
/** Name for logging */
|
|
35
|
+
name: string;
|
|
36
|
+
/** Returns redirect URL if check fails, null if passes */
|
|
37
|
+
check: (session: SessionData, pathname: string) => Promise<string | null>;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export interface AuthGuardResult {
|
|
41
|
+
userId: string;
|
|
42
|
+
email: string;
|
|
43
|
+
roles: string[];
|
|
44
|
+
sessionData: SessionData;
|
|
45
|
+
accessToken?: string;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// =============================================================================
|
|
49
|
+
// CONSTANTS
|
|
50
|
+
// =============================================================================
|
|
51
|
+
|
|
52
|
+
const LOGIN_PAGE = '/account-auth/login';
|
|
53
|
+
const VERIFY_CODE_PAGE = '/account-auth/verify-code';
|
|
54
|
+
const SERVICE_UNAVAILABLE_PAGE = '/service-unavailable';
|
|
55
|
+
|
|
56
|
+
// =============================================================================
|
|
57
|
+
// MAIN
|
|
58
|
+
// =============================================================================
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Server-side auth guard. Call from async server layouts.
|
|
62
|
+
*
|
|
63
|
+
* Redirects (via next/navigation redirect()) if:
|
|
64
|
+
* - No session cookie / invalid JWT
|
|
65
|
+
* - Session not in Redis (stale)
|
|
66
|
+
* - Session force-invalidated
|
|
67
|
+
* - 2FA required but not completed / expired
|
|
68
|
+
* - Any custom check fails
|
|
69
|
+
*
|
|
70
|
+
* Returns the authenticated user's session data on success.
|
|
71
|
+
*/
|
|
72
|
+
export async function authGuard(options?: AuthGuardOptions): Promise<AuthGuardResult> {
|
|
73
|
+
const loginUrl = options?.loginUrl || LOGIN_PAGE;
|
|
74
|
+
const verifyCodeUrl = options?.verifyCodeUrl || VERIFY_CODE_PAGE;
|
|
75
|
+
const serviceUnavailableUrl = options?.serviceUnavailableUrl || SERVICE_UNAVAILABLE_PAGE;
|
|
76
|
+
|
|
77
|
+
// Get current pathname from headers (set by Next.js)
|
|
78
|
+
const headerStore = await headers();
|
|
79
|
+
const pathname = headerStore.get('x-next-pathname') ||
|
|
80
|
+
headerStore.get('x-invoke-path') ||
|
|
81
|
+
headerStore.get('x-matched-path') ||
|
|
82
|
+
'/';
|
|
83
|
+
|
|
84
|
+
const callbackUrl = encodeURIComponent(pathname);
|
|
85
|
+
|
|
86
|
+
// --- Decode session (cookie → JWT → Redis) ---
|
|
87
|
+
let decoded: Awaited<ReturnType<typeof decodeSession>>;
|
|
88
|
+
try {
|
|
89
|
+
decoded = await decodeSession();
|
|
90
|
+
} catch (error) {
|
|
91
|
+
// Redis unreachable or startup failure → fail closed
|
|
92
|
+
console.error('[AUTH-GUARD] Session decode failed (service error):', error instanceof Error ? error.message : String(error));
|
|
93
|
+
redirect(serviceUnavailableUrl);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// No session at all → redirect to login
|
|
97
|
+
if (!decoded) {
|
|
98
|
+
redirect(`${loginUrl}?callbackUrl=${callbackUrl}`);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const { sessionData } = decoded;
|
|
102
|
+
|
|
103
|
+
// --- Force-invalidated session (admin action, password change) ---
|
|
104
|
+
if (sessionData.forceInvalidated) {
|
|
105
|
+
console.warn('[AUTH-GUARD] Session force-invalidated', {
|
|
106
|
+
userId: sessionData.userId,
|
|
107
|
+
pathname,
|
|
108
|
+
});
|
|
109
|
+
redirect(`${loginUrl}?callbackUrl=${callbackUrl}&reason=invalidated`);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// --- 2FA check ---
|
|
113
|
+
// This try wraps ONLY the config fetch, because that is the only thing here
|
|
114
|
+
// that can genuinely fail. Do NOT widen it to cover the redirect below:
|
|
115
|
+
// next/navigation's redirect() signals by THROWING a NEXT_REDIRECT digest, so
|
|
116
|
+
// a redirect inside this try is caught by the catch and rewritten into a
|
|
117
|
+
// service-unavailable page - denying a 2FA user the one page that would let
|
|
118
|
+
// them satisfy the challenge, and reporting it as an outage. See card 312242.
|
|
119
|
+
let requires2FA: boolean;
|
|
120
|
+
try {
|
|
121
|
+
const config = await getIDPClientConfig();
|
|
122
|
+
requires2FA = config.authSettings?.require2FA ?? true;
|
|
123
|
+
} catch (error) {
|
|
124
|
+
// If we can't check 2FA config, fail closed
|
|
125
|
+
console.error('[AUTH-GUARD] 2FA config check failed:', error instanceof Error ? error.message : String(error));
|
|
126
|
+
redirect(serviceUnavailableUrl);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
if (requires2FA) {
|
|
130
|
+
// 369858: the legacy `?? (sessionData as any).twoFactorComplete` carry is REMOVED.
|
|
131
|
+
// This is THE access gate - the only one of the 8 carry sites that grants access
|
|
132
|
+
// rather than reporting status - so it reads exactly one source of truth.
|
|
133
|
+
//
|
|
134
|
+
// Removing it is fail-closed. No current writer emits the legacy field alone:
|
|
135
|
+
// session-store.ts:444-445 writes both as a pair from a server-derived stepped-up
|
|
136
|
+
// token, auth/refresh.ts:376 writes it as a MIRROR of mfaVerified, and
|
|
137
|
+
// routes/auth/session.ts:149-157 refuses both on PRESENCE, so a caller cannot
|
|
138
|
+
// assert either. The carry therefore only ever served sessions minted before
|
|
139
|
+
// `mfaVerified` existed, and for those the correct behaviour is re-verify, not
|
|
140
|
+
// grandfather. Cost is one forced re-challenge for a stale legacy session.
|
|
141
|
+
//
|
|
142
|
+
// Severity note, so nobody re-inflates this later: the operator was `??`
|
|
143
|
+
// (nullish), NOT `||`. `false ?? x` is `false`, so an explicitly DENIED session
|
|
144
|
+
// could never be upgraded by a stale flag. This was legacy grandfathering, not a
|
|
145
|
+
// live 2FA bypass. Had it been `||`, it would have been the bypass.
|
|
146
|
+
const mfaVerified = sessionData.mfaVerified ?? false;
|
|
147
|
+
|
|
148
|
+
// 339563 / 369858 §3: THIS IS STILL A FAIL-OPEN AND IS DELIBERATELY LEFT VISIBLE.
|
|
149
|
+
// An absent mfaExpiresAt collapses to 0, so `mfaExpired` is false and the MFA
|
|
150
|
+
// verdict NEVER EXPIRES. That needs no legacy-session hypothesis: it fires on a
|
|
151
|
+
// current `mfaVerified: true` session whose token carried no `mfa_expires` claim -
|
|
152
|
+
// the passwordless/primary-credential lane, named on the write path in the
|
|
153
|
+
// source's own voice at session-store.ts:464-467 ("it IS the case the consumers
|
|
154
|
+
// currently fail OPEN on"). Removing the carry above does NOT touch this.
|
|
155
|
+
// Tracked separately by BAPert; not fixed here because failing closed on absent
|
|
156
|
+
// expiry would deny the passwordless lane outright and that is a product call.
|
|
157
|
+
const mfaExpiresAt = sessionData.mfaExpiresAt || 0;
|
|
158
|
+
const mfaExpired = mfaExpiresAt > 0 && mfaExpiresAt < Date.now();
|
|
159
|
+
|
|
160
|
+
if (!mfaVerified || mfaExpired) {
|
|
161
|
+
console.log('[AUTH-GUARD] 2FA required', {
|
|
162
|
+
mfaVerified,
|
|
163
|
+
mfaExpired,
|
|
164
|
+
userId: sessionData.userId,
|
|
165
|
+
pathname,
|
|
166
|
+
});
|
|
167
|
+
redirect(`${verifyCodeUrl}?callbackUrl=${callbackUrl}`);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// --- Custom checks (beta, admin, etc.) ---
|
|
172
|
+
if (options?.checks) {
|
|
173
|
+
for (const check of options.checks) {
|
|
174
|
+
try {
|
|
175
|
+
const redirectUrl = await check.check(sessionData, pathname);
|
|
176
|
+
if (redirectUrl) {
|
|
177
|
+
console.log(`[AUTH-GUARD] Custom check "${check.name}" failed`, {
|
|
178
|
+
userId: sessionData.userId,
|
|
179
|
+
pathname,
|
|
180
|
+
redirectUrl,
|
|
181
|
+
});
|
|
182
|
+
redirect(redirectUrl);
|
|
183
|
+
}
|
|
184
|
+
} catch (error) {
|
|
185
|
+
// If the error is a redirect (from next/navigation), re-throw it
|
|
186
|
+
if (error && typeof error === 'object' && 'digest' in error) {
|
|
187
|
+
throw error;
|
|
188
|
+
}
|
|
189
|
+
console.error(`[AUTH-GUARD] Custom check "${check.name}" error:`, error instanceof Error ? error.message : String(error));
|
|
190
|
+
redirect(serviceUnavailableUrl);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// --- All checks passed ---
|
|
196
|
+
return {
|
|
197
|
+
userId: sessionData.userId,
|
|
198
|
+
email: sessionData.email,
|
|
199
|
+
roles: sessionData.roles || [],
|
|
200
|
+
sessionData,
|
|
201
|
+
accessToken: sessionData.idpAccessToken,
|
|
202
|
+
};
|
|
203
|
+
}
|