@payez/next-mvp 4.8.0 → 4.8.2
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/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/middleware/create-middleware.js +6 -5
- package/dist/pages/login/page.js +28 -28
- package/dist/pages/settings/EnhancedSettingsPage.js +17 -17
- 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 +232 -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 +327 -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 +251 -251
- package/src/api-handlers/admin/vibe-data.ts +333 -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/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 +1139 -1138
- 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/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 +877 -877
- package/src/vibe/enterprise-auth.ts +208 -208
- package/src/vibe/errors.ts +209 -209
- package/src/vibe/generic.ts +339 -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
|
@@ -1,449 +1,449 @@
|
|
|
1
|
-
'use client';
|
|
2
|
-
|
|
3
|
-
import { authClient } from '../../client/better-auth-client';
|
|
4
|
-
import { useState, useEffect } from 'react';
|
|
5
|
-
import { maskToken, redactCredentials } from './redact-credentials';
|
|
6
|
-
|
|
7
|
-
// Decode JWT header (contains kid, alg, typ)
|
|
8
|
-
function decodeJwtHeader(token: string): any {
|
|
9
|
-
try {
|
|
10
|
-
const parts = token.split('.');
|
|
11
|
-
if (parts.length !== 3) return null;
|
|
12
|
-
const header = parts[0].replace(/-/g, '+').replace(/_/g, '/');
|
|
13
|
-
const decoded = atob(header);
|
|
14
|
-
return JSON.parse(decoded);
|
|
15
|
-
} catch {
|
|
16
|
-
return null;
|
|
17
|
-
}
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
// Decode JWT payload (contains claims)
|
|
21
|
-
function decodeJwtPayload(token: string): any {
|
|
22
|
-
try {
|
|
23
|
-
const parts = token.split('.');
|
|
24
|
-
if (parts.length !== 3) return null;
|
|
25
|
-
const payload = parts[1].replace(/-/g, '+').replace(/_/g, '/');
|
|
26
|
-
const decoded = atob(payload);
|
|
27
|
-
return JSON.parse(decoded);
|
|
28
|
-
} catch {
|
|
29
|
-
return null;
|
|
30
|
-
}
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
/**
|
|
34
|
-
* ⚠️ DEBUG PAGE THAT DISPLAYS AND COPIES LIVE CREDENTIALS — DO NOT MOUNT
|
|
35
|
-
* IN PRODUCTION. ⚠️
|
|
36
|
-
*
|
|
37
|
-
* Session Inspector Page. Debug page for inspecting session data from Redis:
|
|
38
|
-
* shows user info, roles, 2FA status, and TOKENS. It offers
|
|
39
|
-
* COPY-TO-CLIPBOARD buttons for the live SESSION TOKEN and ACCESS TOKEN, and
|
|
40
|
-
* reads them from the consuming app's `/api/internal/session-data` route.
|
|
41
|
-
*
|
|
42
|
-
* ────────────────────────── WHY THIS WARNING EXISTS (QAPert 35000 / 248991) ──────────────────────────
|
|
43
|
-
* THIS PAGE HAS NO ENV GATE OF ITS OWN. Unlike the force-expire handler
|
|
44
|
-
* (default-denied behind ENABLE_TEST_ROUTES) or the idp-token route (behind
|
|
45
|
-
* ENABLE_TOKEN_INSPECTOR), a mounted page is reachable exactly as far as the
|
|
46
|
-
* consuming app's own middleware allows. MOUNTING IT IS THE GATE. There is no
|
|
47
|
-
* second line of defence behind it.
|
|
48
|
-
*
|
|
49
|
-
* AND THE CREDENTIAL IT HANDS OVER IS THE PRECONDITION FOR A REAL ATTACK ON
|
|
50
|
-
* THIS SAME PACKAGE. The 246007 finding reduces to "a caller who KNOWS A
|
|
51
|
-
* SESSION TOKEN can act on a session it does not own". The purpose of this
|
|
52
|
-
* page is to put a live session token on the clipboard of whoever loads it. A
|
|
53
|
-
* screen-share, a shoulder-surfed screenshot or a support call is enough.
|
|
54
|
-
* Treat the rendered value as a live credential, because that is what it is.
|
|
55
|
-
*
|
|
56
|
-
* NOTE FOR MAINTAINERS: this warning must stay on THIS docstring, the one
|
|
57
|
-
* attached to the exported declaration. An identical block placed higher in
|
|
58
|
-
* the file does NOT reach the emitted .d.ts — measured, after I made exactly
|
|
59
|
-
* that mistake — so it would warn no consumer at all.
|
|
60
|
-
*
|
|
61
|
-
* ────────────────────────── IF YOU MOUNT IT, MOUNT IT LIKE THIS ──────────────────────────
|
|
62
|
-
* ```typescript
|
|
63
|
-
* // app/test-env/jwt-inspect/page.tsx — TEST/DEV ENVIRONMENTS ONLY
|
|
64
|
-
* export { JwtInspectPage as default } from '@payez/next-mvp/pages/test-env';
|
|
65
|
-
* ```
|
|
66
|
-
* Gate the whole /test-env segment in your middleware, per environment. If you
|
|
67
|
-
* are not actively debugging token contents, DO NOT MOUNT THIS AT ALL — the
|
|
68
|
-
* safest configuration is the absence of the route.
|
|
69
|
-
*/
|
|
70
|
-
export function JwtInspectPage() {
|
|
71
|
-
const { data: sessionData, isPending } = authClient.useSession();
|
|
72
|
-
const session = sessionData;
|
|
73
|
-
const status = isPending ? 'loading' : session ? 'authenticated' : 'unauthenticated';
|
|
74
|
-
const [isDarkMode, setIsDarkMode] = useState(false);
|
|
75
|
-
const [jwtHeader, setJwtHeader] = useState<any>(null);
|
|
76
|
-
const [jwtPayload, setJwtPayload] = useState<any>(null);
|
|
77
|
-
const [serverSession, setServerSession] = useState<any>(null);
|
|
78
|
-
|
|
79
|
-
// Detect dark mode
|
|
80
|
-
useEffect(() => {
|
|
81
|
-
const checkDarkMode = () => {
|
|
82
|
-
const isDark = document.documentElement.classList.contains('dark') ||
|
|
83
|
-
window.matchMedia('(prefers-color-scheme: dark)').matches;
|
|
84
|
-
setIsDarkMode(isDark);
|
|
85
|
-
};
|
|
86
|
-
checkDarkMode();
|
|
87
|
-
const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
|
|
88
|
-
mediaQuery.addEventListener('change', checkDarkMode);
|
|
89
|
-
return () => mediaQuery.removeEventListener('change', checkDarkMode);
|
|
90
|
-
}, []);
|
|
91
|
-
|
|
92
|
-
// Fetch enriched session from server (has IDP tokens, roles, userId)
|
|
93
|
-
useEffect(() => {
|
|
94
|
-
if (!session) return;
|
|
95
|
-
fetch('/api/internal/session-data')
|
|
96
|
-
.then(r => r.ok ? r.json() : null)
|
|
97
|
-
.then(d => {
|
|
98
|
-
if (d) setServerSession(d);
|
|
99
|
-
})
|
|
100
|
-
.catch(() => {});
|
|
101
|
-
}, [session]);
|
|
102
|
-
|
|
103
|
-
// Decode JWT header and payload from server session's IDP access token
|
|
104
|
-
useEffect(() => {
|
|
105
|
-
const token = serverSession?.idpAccessToken || (session as any)?.accessToken;
|
|
106
|
-
if (token) {
|
|
107
|
-
setJwtHeader(decodeJwtHeader(token));
|
|
108
|
-
setJwtPayload(decodeJwtPayload(token));
|
|
109
|
-
}
|
|
110
|
-
}, [serverSession, session]);
|
|
111
|
-
|
|
112
|
-
// PAY-1470 (BAPert rulings 59022, 59040): credentials are shown as tokenLogId + length ONLY. The Reveal toggles
|
|
113
|
-
// (per token and for the raw session JSON) and the Copy buttons are gone: "never renders token material"
|
|
114
|
-
// includes click-to-reveal, because a full bearer one click away on a prod-reachable page is the same defect.
|
|
115
|
-
// The redaction rules (key names, JWT / PEM / URL-password shapes) live in ./redact-credentials.
|
|
116
|
-
|
|
117
|
-
if (status === 'loading') {
|
|
118
|
-
return (
|
|
119
|
-
<div className={`min-h-screen p-8 ${isDarkMode ? 'bg-slate-950 text-white' : 'bg-gray-50 text-gray-900'}`}>
|
|
120
|
-
Loading session...
|
|
121
|
-
</div>
|
|
122
|
-
);
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
if (status === 'unauthenticated') {
|
|
126
|
-
return (
|
|
127
|
-
<div className={`min-h-screen p-8 ${isDarkMode ? 'bg-slate-950 text-white' : 'bg-gray-50 text-gray-900'}`}>
|
|
128
|
-
<div className={`p-4 rounded-lg ${isDarkMode ? 'bg-red-900/30 border border-red-800' : 'bg-red-50 border border-red-200'}`}>
|
|
129
|
-
<h2 className="text-lg font-semibold mb-2">Not Authenticated</h2>
|
|
130
|
-
<p>Please log in to inspect session data.</p>
|
|
131
|
-
</div>
|
|
132
|
-
</div>
|
|
133
|
-
);
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
// Merge server-side enriched session with client session
|
|
137
|
-
const ss = serverSession || {};
|
|
138
|
-
const ext = session as any;
|
|
139
|
-
const user = {
|
|
140
|
-
id: ss.userId || ext?.user?.id,
|
|
141
|
-
email: ss.email || ext?.user?.email,
|
|
142
|
-
name: ss.name || ext?.user?.name,
|
|
143
|
-
roles: ss.roles || [],
|
|
144
|
-
oauthProvider: ss.oauthProvider,
|
|
145
|
-
idpClientId: ss.idpClientId,
|
|
146
|
-
merchantId: ss.merchantId,
|
|
147
|
-
mfaVerified: ss.mfaVerified,
|
|
148
|
-
requiresTwoFactor: false,
|
|
149
|
-
authenticationMethods: ss.authenticationMethods,
|
|
150
|
-
authenticationLevel: ss.authenticationLevel,
|
|
151
|
-
mfaCompletedAt: ss.mfaCompletedAt,
|
|
152
|
-
mfaExpiresAt: ss.mfaExpiresAt,
|
|
153
|
-
twoFactorSessionVerified: ss.mfaVerified,
|
|
154
|
-
};
|
|
155
|
-
// Token fields for display
|
|
156
|
-
const displayExt = {
|
|
157
|
-
...ext,
|
|
158
|
-
sessionToken: ss.sessionToken || ext?.sessionToken,
|
|
159
|
-
accessToken: ss.idpAccessToken || ext?.accessToken,
|
|
160
|
-
refreshToken: ss.idpRefreshToken || ext?.refreshToken,
|
|
161
|
-
accessTokenExpires: ss.idpAccessTokenExpires || ext?.accessTokenExpires,
|
|
162
|
-
};
|
|
163
|
-
|
|
164
|
-
// Card styling helpers
|
|
165
|
-
const cardClass = isDarkMode ? 'bg-slate-900 border border-slate-700' : 'bg-white border border-gray-200';
|
|
166
|
-
const labelClass = isDarkMode ? 'text-slate-400' : 'text-gray-500';
|
|
167
|
-
const valueClass = isDarkMode ? 'text-white' : 'text-gray-900';
|
|
168
|
-
|
|
169
|
-
return (
|
|
170
|
-
<div className={`min-h-screen p-8 ${isDarkMode ? 'bg-slate-950 text-white' : 'bg-gray-50 text-gray-900'}`}>
|
|
171
|
-
<div className="max-w-4xl mx-auto space-y-6">
|
|
172
|
-
<h1 className="text-2xl font-bold">Session Inspector</h1>
|
|
173
|
-
<p className={`text-sm ${labelClass}`}>
|
|
174
|
-
Better Auth session + IDP tokens from Redis
|
|
175
|
-
</p>
|
|
176
|
-
|
|
177
|
-
{/* User Identity */}
|
|
178
|
-
<div className={`p-4 rounded-lg ${cardClass}`}>
|
|
179
|
-
<h2 className="text-lg font-semibold mb-4">User Identity</h2>
|
|
180
|
-
<div className="grid grid-cols-1 md:grid-cols-2 gap-3 text-sm">
|
|
181
|
-
<InfoRow label="User ID" value={user.id} labelClass={labelClass} valueClass={valueClass} />
|
|
182
|
-
<InfoRow label="Email" value={user.email} labelClass={labelClass} valueClass={valueClass} />
|
|
183
|
-
<InfoRow label="Name" value={user.name} labelClass={labelClass} valueClass={valueClass} />
|
|
184
|
-
<InfoRow label="OAuth Provider" value={user.oauthProvider} labelClass={labelClass} valueClass={valueClass} />
|
|
185
|
-
<InfoRow label="IDP Client ID" value={user.idpClientId} labelClass={labelClass} valueClass={valueClass} />
|
|
186
|
-
<InfoRow label="Merchant ID" value={user.merchantId} labelClass={labelClass} valueClass={valueClass} />
|
|
187
|
-
</div>
|
|
188
|
-
</div>
|
|
189
|
-
|
|
190
|
-
{/* Roles */}
|
|
191
|
-
<div className={`p-4 rounded-lg ${isDarkMode ? 'bg-purple-900/30 border border-purple-700' : 'bg-purple-50 border border-purple-200'}`}>
|
|
192
|
-
<h2 className="text-lg font-semibold mb-4">Roles</h2>
|
|
193
|
-
{user.roles && user.roles.length > 0 ? (
|
|
194
|
-
<div className="flex flex-wrap gap-2">
|
|
195
|
-
{user.roles.map((role: string) => (
|
|
196
|
-
<span
|
|
197
|
-
key={role}
|
|
198
|
-
className={`px-3 py-1 rounded-full text-sm font-medium ${
|
|
199
|
-
isDarkMode ? 'bg-purple-800 text-purple-100' : 'bg-purple-200 text-purple-800'
|
|
200
|
-
}`}
|
|
201
|
-
>
|
|
202
|
-
{role}
|
|
203
|
-
</span>
|
|
204
|
-
))}
|
|
205
|
-
</div>
|
|
206
|
-
) : (
|
|
207
|
-
<p className={labelClass}>No roles assigned</p>
|
|
208
|
-
)}
|
|
209
|
-
</div>
|
|
210
|
-
|
|
211
|
-
{/* 2FA Status */}
|
|
212
|
-
<div className={`p-4 rounded-lg ${isDarkMode ? 'bg-yellow-900/30 border border-yellow-700' : 'bg-yellow-50 border border-yellow-200'}`}>
|
|
213
|
-
<h2 className="text-lg font-semibold mb-4">2FA Status</h2>
|
|
214
|
-
<div className="grid grid-cols-1 md:grid-cols-2 gap-3 text-sm">
|
|
215
|
-
<div>
|
|
216
|
-
<span className={labelClass}>2FA Verified:</span>{' '}
|
|
217
|
-
<StatusBadge
|
|
218
|
-
value={user.twoFactorSessionVerified}
|
|
219
|
-
trueText="Yes"
|
|
220
|
-
falseText="No"
|
|
221
|
-
isDarkMode={isDarkMode}
|
|
222
|
-
/>
|
|
223
|
-
</div>
|
|
224
|
-
<div>
|
|
225
|
-
<span className={labelClass}>Requires 2FA:</span>{' '}
|
|
226
|
-
<StatusBadge
|
|
227
|
-
value={user.requiresTwoFactor}
|
|
228
|
-
trueText="Yes"
|
|
229
|
-
falseText="No"
|
|
230
|
-
isDarkMode={isDarkMode}
|
|
231
|
-
invertColors
|
|
232
|
-
/>
|
|
233
|
-
</div>
|
|
234
|
-
<InfoRow
|
|
235
|
-
label="Auth Methods (AMR)"
|
|
236
|
-
value={user.authenticationMethods?.join(', ')}
|
|
237
|
-
labelClass={labelClass}
|
|
238
|
-
valueClass={valueClass}
|
|
239
|
-
/>
|
|
240
|
-
<InfoRow
|
|
241
|
-
label="Auth Level (ACR)"
|
|
242
|
-
value={user.authenticationLevel}
|
|
243
|
-
labelClass={labelClass}
|
|
244
|
-
valueClass={valueClass}
|
|
245
|
-
/>
|
|
246
|
-
<InfoRow
|
|
247
|
-
label="MFA Completed At"
|
|
248
|
-
value={user.mfaCompletedAt ? new Date(user.mfaCompletedAt).toISOString() : undefined}
|
|
249
|
-
labelClass={labelClass}
|
|
250
|
-
valueClass={valueClass}
|
|
251
|
-
/>
|
|
252
|
-
<InfoRow
|
|
253
|
-
label="MFA Expires At"
|
|
254
|
-
value={user.mfaExpiresAt ? new Date(user.mfaExpiresAt).toISOString() : undefined}
|
|
255
|
-
labelClass={labelClass}
|
|
256
|
-
valueClass={valueClass}
|
|
257
|
-
/>
|
|
258
|
-
</div>
|
|
259
|
-
</div>
|
|
260
|
-
|
|
261
|
-
{/* Tokens */}
|
|
262
|
-
<div className={`p-4 rounded-lg ${isDarkMode ? 'bg-blue-900/30 border border-blue-700' : 'bg-blue-50 border border-blue-200'}`}>
|
|
263
|
-
<h2 className="text-lg font-semibold mb-4">Tokens</h2>
|
|
264
|
-
<div className="space-y-4">
|
|
265
|
-
<div>
|
|
266
|
-
<div className="flex items-center justify-between mb-2">
|
|
267
|
-
<span className={labelClass}>Session Token (Redis Key):</span>
|
|
268
|
-
</div>
|
|
269
|
-
<code className={`block p-2 rounded text-xs break-all ${isDarkMode ? 'bg-slate-800' : 'bg-gray-100'}`}>
|
|
270
|
-
{displayExt.sessionToken ? maskToken(displayExt.sessionToken) : 'N/A'}
|
|
271
|
-
</code>
|
|
272
|
-
</div>
|
|
273
|
-
|
|
274
|
-
<div>
|
|
275
|
-
<div className="flex items-center justify-between mb-2">
|
|
276
|
-
<span className={labelClass}>Access Token (IDP):</span>
|
|
277
|
-
</div>
|
|
278
|
-
<code className={`block p-2 rounded text-xs break-all max-h-24 overflow-auto ${isDarkMode ? 'bg-slate-800' : 'bg-gray-100'}`}>
|
|
279
|
-
{displayExt.accessToken ? maskToken(displayExt.accessToken) : 'N/A'}
|
|
280
|
-
</code>
|
|
281
|
-
</div>
|
|
282
|
-
|
|
283
|
-
<div className="grid grid-cols-1 md:grid-cols-2 gap-3 text-sm">
|
|
284
|
-
<div>
|
|
285
|
-
<span className={labelClass}>Has Refresh Token:</span>{' '}
|
|
286
|
-
<StatusBadge
|
|
287
|
-
value={!!displayExt.refreshToken}
|
|
288
|
-
trueText="Yes"
|
|
289
|
-
falseText="No"
|
|
290
|
-
isDarkMode={isDarkMode}
|
|
291
|
-
/>
|
|
292
|
-
</div>
|
|
293
|
-
<InfoRow
|
|
294
|
-
label="Access Token Expires"
|
|
295
|
-
value={displayExt.accessTokenExpires ? new Date(displayExt.accessTokenExpires).toISOString() : undefined}
|
|
296
|
-
labelClass={labelClass}
|
|
297
|
-
valueClass={valueClass}
|
|
298
|
-
/>
|
|
299
|
-
</div>
|
|
300
|
-
</div>
|
|
301
|
-
</div>
|
|
302
|
-
|
|
303
|
-
{/* JWT Header (contains kid for key lookup) */}
|
|
304
|
-
{jwtHeader && (
|
|
305
|
-
<div className={`p-4 rounded-lg ${isDarkMode ? 'bg-orange-900/30 border border-orange-700' : 'bg-orange-50 border border-orange-200'}`}>
|
|
306
|
-
<h2 className="text-lg font-semibold mb-4">JWT Header</h2>
|
|
307
|
-
<div className="grid grid-cols-1 md:grid-cols-2 gap-3 text-sm">
|
|
308
|
-
<InfoRow label="Algorithm (alg)" value={jwtHeader.alg} labelClass={labelClass} valueClass={valueClass} />
|
|
309
|
-
<InfoRow label="Type (typ)" value={jwtHeader.typ} labelClass={labelClass} valueClass={valueClass} />
|
|
310
|
-
<div>
|
|
311
|
-
<span className={labelClass}>Key ID (kid):</span>{' '}
|
|
312
|
-
<span className={`font-mono ${jwtHeader.kid ? (isDarkMode ? 'text-green-400' : 'text-green-600') : (isDarkMode ? 'text-red-400' : 'text-red-600')}`}>
|
|
313
|
-
{jwtHeader.kid || 'NOT PRESENT'}
|
|
314
|
-
</span>
|
|
315
|
-
</div>
|
|
316
|
-
{Object.entries(jwtHeader)
|
|
317
|
-
.filter(([key]) => !['alg', 'typ', 'kid'].includes(key))
|
|
318
|
-
.map(([key, value]) => (
|
|
319
|
-
<InfoRow
|
|
320
|
-
key={key}
|
|
321
|
-
label={key}
|
|
322
|
-
value={typeof value === 'object' ? JSON.stringify(redactCredentials(value)) : String(redactCredentials(value, key))}
|
|
323
|
-
labelClass={labelClass}
|
|
324
|
-
valueClass={valueClass}
|
|
325
|
-
/>
|
|
326
|
-
))
|
|
327
|
-
}
|
|
328
|
-
</div>
|
|
329
|
-
</div>
|
|
330
|
-
)}
|
|
331
|
-
|
|
332
|
-
{/* JWT Payload Claims */}
|
|
333
|
-
{jwtPayload && (
|
|
334
|
-
<div className={`p-4 rounded-lg ${isDarkMode ? 'bg-green-900/30 border border-green-700' : 'bg-green-50 border border-green-200'}`}>
|
|
335
|
-
<h2 className="text-lg font-semibold mb-4">JWT Payload Claims</h2>
|
|
336
|
-
<div className="grid grid-cols-1 md:grid-cols-2 gap-3 text-sm">
|
|
337
|
-
<InfoRow label="Subject (sub)" value={jwtPayload.sub} labelClass={labelClass} valueClass={valueClass} />
|
|
338
|
-
<InfoRow label="Issuer (iss)" value={jwtPayload.iss} labelClass={labelClass} valueClass={valueClass} />
|
|
339
|
-
<InfoRow label="Audience (aud)" value={Array.isArray(jwtPayload.aud) ? jwtPayload.aud.join(', ') : jwtPayload.aud} labelClass={labelClass} valueClass={valueClass} />
|
|
340
|
-
<InfoRow label="Client ID" value={jwtPayload.client_id} labelClass={labelClass} valueClass={valueClass} />
|
|
341
|
-
<InfoRow
|
|
342
|
-
label="Expires (exp)"
|
|
343
|
-
value={jwtPayload.exp ? new Date(jwtPayload.exp * 1000).toISOString() : undefined}
|
|
344
|
-
labelClass={labelClass}
|
|
345
|
-
valueClass={valueClass}
|
|
346
|
-
/>
|
|
347
|
-
<InfoRow
|
|
348
|
-
label="Issued At (iat)"
|
|
349
|
-
value={jwtPayload.iat ? new Date(jwtPayload.iat * 1000).toISOString() : undefined}
|
|
350
|
-
labelClass={labelClass}
|
|
351
|
-
valueClass={valueClass}
|
|
352
|
-
/>
|
|
353
|
-
<InfoRow
|
|
354
|
-
label="AMR (Auth Methods)"
|
|
355
|
-
value={Array.isArray(jwtPayload.amr) ? jwtPayload.amr.join(', ') : jwtPayload.amr}
|
|
356
|
-
labelClass={labelClass}
|
|
357
|
-
valueClass={valueClass}
|
|
358
|
-
/>
|
|
359
|
-
<InfoRow label="ACR (Auth Context)" value={jwtPayload.acr} labelClass={labelClass} valueClass={valueClass} />
|
|
360
|
-
</div>
|
|
361
|
-
</div>
|
|
362
|
-
)}
|
|
363
|
-
|
|
364
|
-
{/* Session Expiry */}
|
|
365
|
-
<div className={`p-4 rounded-lg ${cardClass}`}>
|
|
366
|
-
<h2 className="text-lg font-semibold mb-4">Session Metadata</h2>
|
|
367
|
-
<div className="grid grid-cols-1 md:grid-cols-2 gap-3 text-sm">
|
|
368
|
-
<InfoRow
|
|
369
|
-
label="Session Expires"
|
|
370
|
-
value={(session as any)?.expires ? new Date((session as any).expires).toISOString() : undefined}
|
|
371
|
-
labelClass={labelClass}
|
|
372
|
-
valueClass={valueClass}
|
|
373
|
-
/>
|
|
374
|
-
<InfoRow
|
|
375
|
-
label="Error"
|
|
376
|
-
value={ext.error}
|
|
377
|
-
labelClass={labelClass}
|
|
378
|
-
valueClass={ext.error ? 'text-red-500' : valueClass}
|
|
379
|
-
/>
|
|
380
|
-
</div>
|
|
381
|
-
</div>
|
|
382
|
-
|
|
383
|
-
{/* Raw Session Data - credentials redacted by default, see redactCredentials above */}
|
|
384
|
-
<details className={`p-4 rounded-lg ${cardClass}`}>
|
|
385
|
-
<summary className="font-semibold cursor-pointer">
|
|
386
|
-
Raw Session Data — credentials redacted (Click to expand)
|
|
387
|
-
</summary>
|
|
388
|
-
<div className="mt-3">
|
|
389
|
-
<span className={`text-xs ${labelClass}`}>
|
|
390
|
-
Token values are masked. Structure, claims and expiry fields are unchanged.
|
|
391
|
-
</span>
|
|
392
|
-
</div>
|
|
393
|
-
<pre className={`mt-4 text-xs overflow-auto p-3 rounded ${isDarkMode ? 'bg-slate-800' : 'bg-gray-100'}`}>
|
|
394
|
-
{JSON.stringify(redactCredentials({ clientSession: session, serverSession: serverSession }), null, 2)}
|
|
395
|
-
</pre>
|
|
396
|
-
</details>
|
|
397
|
-
</div>
|
|
398
|
-
</div>
|
|
399
|
-
);
|
|
400
|
-
}
|
|
401
|
-
|
|
402
|
-
// Helper Components
|
|
403
|
-
|
|
404
|
-
function InfoRow({
|
|
405
|
-
label,
|
|
406
|
-
value,
|
|
407
|
-
labelClass,
|
|
408
|
-
valueClass,
|
|
409
|
-
}: {
|
|
410
|
-
label: string;
|
|
411
|
-
value?: string | null;
|
|
412
|
-
labelClass: string;
|
|
413
|
-
valueClass: string;
|
|
414
|
-
}) {
|
|
415
|
-
return (
|
|
416
|
-
<div>
|
|
417
|
-
<span className={labelClass}>{label}:</span>{' '}
|
|
418
|
-
<span className={valueClass}>{value || 'N/A'}</span>
|
|
419
|
-
</div>
|
|
420
|
-
);
|
|
421
|
-
}
|
|
422
|
-
|
|
423
|
-
function StatusBadge({
|
|
424
|
-
value,
|
|
425
|
-
trueText,
|
|
426
|
-
falseText,
|
|
427
|
-
isDarkMode,
|
|
428
|
-
invertColors = false,
|
|
429
|
-
}: {
|
|
430
|
-
value: boolean;
|
|
431
|
-
trueText: string;
|
|
432
|
-
falseText: string;
|
|
433
|
-
isDarkMode: boolean;
|
|
434
|
-
invertColors?: boolean;
|
|
435
|
-
}) {
|
|
436
|
-
const isPositive = invertColors ? !value : value;
|
|
437
|
-
const colorClass = isPositive
|
|
438
|
-
? isDarkMode ? 'bg-green-800 text-green-100' : 'bg-green-200 text-green-800'
|
|
439
|
-
: isDarkMode ? 'bg-red-800 text-red-100' : 'bg-red-200 text-red-800';
|
|
440
|
-
|
|
441
|
-
return (
|
|
442
|
-
<span className={`px-2 py-0.5 rounded text-xs font-medium ${colorClass}`}>
|
|
443
|
-
{value ? trueText : falseText}
|
|
444
|
-
</span>
|
|
445
|
-
);
|
|
446
|
-
}
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
export default JwtInspectPage;
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { authClient } from '../../client/better-auth-client';
|
|
4
|
+
import { useState, useEffect } from 'react';
|
|
5
|
+
import { maskToken, redactCredentials } from './redact-credentials';
|
|
6
|
+
|
|
7
|
+
// Decode JWT header (contains kid, alg, typ)
|
|
8
|
+
function decodeJwtHeader(token: string): any {
|
|
9
|
+
try {
|
|
10
|
+
const parts = token.split('.');
|
|
11
|
+
if (parts.length !== 3) return null;
|
|
12
|
+
const header = parts[0].replace(/-/g, '+').replace(/_/g, '/');
|
|
13
|
+
const decoded = atob(header);
|
|
14
|
+
return JSON.parse(decoded);
|
|
15
|
+
} catch {
|
|
16
|
+
return null;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// Decode JWT payload (contains claims)
|
|
21
|
+
function decodeJwtPayload(token: string): any {
|
|
22
|
+
try {
|
|
23
|
+
const parts = token.split('.');
|
|
24
|
+
if (parts.length !== 3) return null;
|
|
25
|
+
const payload = parts[1].replace(/-/g, '+').replace(/_/g, '/');
|
|
26
|
+
const decoded = atob(payload);
|
|
27
|
+
return JSON.parse(decoded);
|
|
28
|
+
} catch {
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* ⚠️ DEBUG PAGE THAT DISPLAYS AND COPIES LIVE CREDENTIALS — DO NOT MOUNT
|
|
35
|
+
* IN PRODUCTION. ⚠️
|
|
36
|
+
*
|
|
37
|
+
* Session Inspector Page. Debug page for inspecting session data from Redis:
|
|
38
|
+
* shows user info, roles, 2FA status, and TOKENS. It offers
|
|
39
|
+
* COPY-TO-CLIPBOARD buttons for the live SESSION TOKEN and ACCESS TOKEN, and
|
|
40
|
+
* reads them from the consuming app's `/api/internal/session-data` route.
|
|
41
|
+
*
|
|
42
|
+
* ────────────────────────── WHY THIS WARNING EXISTS (QAPert 35000 / 248991) ──────────────────────────
|
|
43
|
+
* THIS PAGE HAS NO ENV GATE OF ITS OWN. Unlike the force-expire handler
|
|
44
|
+
* (default-denied behind ENABLE_TEST_ROUTES) or the idp-token route (behind
|
|
45
|
+
* ENABLE_TOKEN_INSPECTOR), a mounted page is reachable exactly as far as the
|
|
46
|
+
* consuming app's own middleware allows. MOUNTING IT IS THE GATE. There is no
|
|
47
|
+
* second line of defence behind it.
|
|
48
|
+
*
|
|
49
|
+
* AND THE CREDENTIAL IT HANDS OVER IS THE PRECONDITION FOR A REAL ATTACK ON
|
|
50
|
+
* THIS SAME PACKAGE. The 246007 finding reduces to "a caller who KNOWS A
|
|
51
|
+
* SESSION TOKEN can act on a session it does not own". The purpose of this
|
|
52
|
+
* page is to put a live session token on the clipboard of whoever loads it. A
|
|
53
|
+
* screen-share, a shoulder-surfed screenshot or a support call is enough.
|
|
54
|
+
* Treat the rendered value as a live credential, because that is what it is.
|
|
55
|
+
*
|
|
56
|
+
* NOTE FOR MAINTAINERS: this warning must stay on THIS docstring, the one
|
|
57
|
+
* attached to the exported declaration. An identical block placed higher in
|
|
58
|
+
* the file does NOT reach the emitted .d.ts — measured, after I made exactly
|
|
59
|
+
* that mistake — so it would warn no consumer at all.
|
|
60
|
+
*
|
|
61
|
+
* ────────────────────────── IF YOU MOUNT IT, MOUNT IT LIKE THIS ──────────────────────────
|
|
62
|
+
* ```typescript
|
|
63
|
+
* // app/test-env/jwt-inspect/page.tsx — TEST/DEV ENVIRONMENTS ONLY
|
|
64
|
+
* export { JwtInspectPage as default } from '@payez/next-mvp/pages/test-env';
|
|
65
|
+
* ```
|
|
66
|
+
* Gate the whole /test-env segment in your middleware, per environment. If you
|
|
67
|
+
* are not actively debugging token contents, DO NOT MOUNT THIS AT ALL — the
|
|
68
|
+
* safest configuration is the absence of the route.
|
|
69
|
+
*/
|
|
70
|
+
export function JwtInspectPage() {
|
|
71
|
+
const { data: sessionData, isPending } = authClient.useSession();
|
|
72
|
+
const session = sessionData;
|
|
73
|
+
const status = isPending ? 'loading' : session ? 'authenticated' : 'unauthenticated';
|
|
74
|
+
const [isDarkMode, setIsDarkMode] = useState(false);
|
|
75
|
+
const [jwtHeader, setJwtHeader] = useState<any>(null);
|
|
76
|
+
const [jwtPayload, setJwtPayload] = useState<any>(null);
|
|
77
|
+
const [serverSession, setServerSession] = useState<any>(null);
|
|
78
|
+
|
|
79
|
+
// Detect dark mode
|
|
80
|
+
useEffect(() => {
|
|
81
|
+
const checkDarkMode = () => {
|
|
82
|
+
const isDark = document.documentElement.classList.contains('dark') ||
|
|
83
|
+
window.matchMedia('(prefers-color-scheme: dark)').matches;
|
|
84
|
+
setIsDarkMode(isDark);
|
|
85
|
+
};
|
|
86
|
+
checkDarkMode();
|
|
87
|
+
const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
|
|
88
|
+
mediaQuery.addEventListener('change', checkDarkMode);
|
|
89
|
+
return () => mediaQuery.removeEventListener('change', checkDarkMode);
|
|
90
|
+
}, []);
|
|
91
|
+
|
|
92
|
+
// Fetch enriched session from server (has IDP tokens, roles, userId)
|
|
93
|
+
useEffect(() => {
|
|
94
|
+
if (!session) return;
|
|
95
|
+
fetch('/api/internal/session-data')
|
|
96
|
+
.then(r => r.ok ? r.json() : null)
|
|
97
|
+
.then(d => {
|
|
98
|
+
if (d) setServerSession(d);
|
|
99
|
+
})
|
|
100
|
+
.catch(() => {});
|
|
101
|
+
}, [session]);
|
|
102
|
+
|
|
103
|
+
// Decode JWT header and payload from server session's IDP access token
|
|
104
|
+
useEffect(() => {
|
|
105
|
+
const token = serverSession?.idpAccessToken || (session as any)?.accessToken;
|
|
106
|
+
if (token) {
|
|
107
|
+
setJwtHeader(decodeJwtHeader(token));
|
|
108
|
+
setJwtPayload(decodeJwtPayload(token));
|
|
109
|
+
}
|
|
110
|
+
}, [serverSession, session]);
|
|
111
|
+
|
|
112
|
+
// PAY-1470 (BAPert rulings 59022, 59040): credentials are shown as tokenLogId + length ONLY. The Reveal toggles
|
|
113
|
+
// (per token and for the raw session JSON) and the Copy buttons are gone: "never renders token material"
|
|
114
|
+
// includes click-to-reveal, because a full bearer one click away on a prod-reachable page is the same defect.
|
|
115
|
+
// The redaction rules (key names, JWT / PEM / URL-password shapes) live in ./redact-credentials.
|
|
116
|
+
|
|
117
|
+
if (status === 'loading') {
|
|
118
|
+
return (
|
|
119
|
+
<div className={`min-h-screen p-8 ${isDarkMode ? 'bg-slate-950 text-white' : 'bg-gray-50 text-gray-900'}`}>
|
|
120
|
+
Loading session...
|
|
121
|
+
</div>
|
|
122
|
+
);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
if (status === 'unauthenticated') {
|
|
126
|
+
return (
|
|
127
|
+
<div className={`min-h-screen p-8 ${isDarkMode ? 'bg-slate-950 text-white' : 'bg-gray-50 text-gray-900'}`}>
|
|
128
|
+
<div className={`p-4 rounded-lg ${isDarkMode ? 'bg-red-900/30 border border-red-800' : 'bg-red-50 border border-red-200'}`}>
|
|
129
|
+
<h2 className="text-lg font-semibold mb-2">Not Authenticated</h2>
|
|
130
|
+
<p>Please log in to inspect session data.</p>
|
|
131
|
+
</div>
|
|
132
|
+
</div>
|
|
133
|
+
);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Merge server-side enriched session with client session
|
|
137
|
+
const ss = serverSession || {};
|
|
138
|
+
const ext = session as any;
|
|
139
|
+
const user = {
|
|
140
|
+
id: ss.userId || ext?.user?.id,
|
|
141
|
+
email: ss.email || ext?.user?.email,
|
|
142
|
+
name: ss.name || ext?.user?.name,
|
|
143
|
+
roles: ss.roles || [],
|
|
144
|
+
oauthProvider: ss.oauthProvider,
|
|
145
|
+
idpClientId: ss.idpClientId,
|
|
146
|
+
merchantId: ss.merchantId,
|
|
147
|
+
mfaVerified: ss.mfaVerified,
|
|
148
|
+
requiresTwoFactor: false,
|
|
149
|
+
authenticationMethods: ss.authenticationMethods,
|
|
150
|
+
authenticationLevel: ss.authenticationLevel,
|
|
151
|
+
mfaCompletedAt: ss.mfaCompletedAt,
|
|
152
|
+
mfaExpiresAt: ss.mfaExpiresAt,
|
|
153
|
+
twoFactorSessionVerified: ss.mfaVerified,
|
|
154
|
+
};
|
|
155
|
+
// Token fields for display
|
|
156
|
+
const displayExt = {
|
|
157
|
+
...ext,
|
|
158
|
+
sessionToken: ss.sessionToken || ext?.sessionToken,
|
|
159
|
+
accessToken: ss.idpAccessToken || ext?.accessToken,
|
|
160
|
+
refreshToken: ss.idpRefreshToken || ext?.refreshToken,
|
|
161
|
+
accessTokenExpires: ss.idpAccessTokenExpires || ext?.accessTokenExpires,
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
// Card styling helpers
|
|
165
|
+
const cardClass = isDarkMode ? 'bg-slate-900 border border-slate-700' : 'bg-white border border-gray-200';
|
|
166
|
+
const labelClass = isDarkMode ? 'text-slate-400' : 'text-gray-500';
|
|
167
|
+
const valueClass = isDarkMode ? 'text-white' : 'text-gray-900';
|
|
168
|
+
|
|
169
|
+
return (
|
|
170
|
+
<div className={`min-h-screen p-8 ${isDarkMode ? 'bg-slate-950 text-white' : 'bg-gray-50 text-gray-900'}`}>
|
|
171
|
+
<div className="max-w-4xl mx-auto space-y-6">
|
|
172
|
+
<h1 className="text-2xl font-bold">Session Inspector</h1>
|
|
173
|
+
<p className={`text-sm ${labelClass}`}>
|
|
174
|
+
Better Auth session + IDP tokens from Redis
|
|
175
|
+
</p>
|
|
176
|
+
|
|
177
|
+
{/* User Identity */}
|
|
178
|
+
<div className={`p-4 rounded-lg ${cardClass}`}>
|
|
179
|
+
<h2 className="text-lg font-semibold mb-4">User Identity</h2>
|
|
180
|
+
<div className="grid grid-cols-1 md:grid-cols-2 gap-3 text-sm">
|
|
181
|
+
<InfoRow label="User ID" value={user.id} labelClass={labelClass} valueClass={valueClass} />
|
|
182
|
+
<InfoRow label="Email" value={user.email} labelClass={labelClass} valueClass={valueClass} />
|
|
183
|
+
<InfoRow label="Name" value={user.name} labelClass={labelClass} valueClass={valueClass} />
|
|
184
|
+
<InfoRow label="OAuth Provider" value={user.oauthProvider} labelClass={labelClass} valueClass={valueClass} />
|
|
185
|
+
<InfoRow label="IDP Client ID" value={user.idpClientId} labelClass={labelClass} valueClass={valueClass} />
|
|
186
|
+
<InfoRow label="Merchant ID" value={user.merchantId} labelClass={labelClass} valueClass={valueClass} />
|
|
187
|
+
</div>
|
|
188
|
+
</div>
|
|
189
|
+
|
|
190
|
+
{/* Roles */}
|
|
191
|
+
<div className={`p-4 rounded-lg ${isDarkMode ? 'bg-purple-900/30 border border-purple-700' : 'bg-purple-50 border border-purple-200'}`}>
|
|
192
|
+
<h2 className="text-lg font-semibold mb-4">Roles</h2>
|
|
193
|
+
{user.roles && user.roles.length > 0 ? (
|
|
194
|
+
<div className="flex flex-wrap gap-2">
|
|
195
|
+
{user.roles.map((role: string) => (
|
|
196
|
+
<span
|
|
197
|
+
key={role}
|
|
198
|
+
className={`px-3 py-1 rounded-full text-sm font-medium ${
|
|
199
|
+
isDarkMode ? 'bg-purple-800 text-purple-100' : 'bg-purple-200 text-purple-800'
|
|
200
|
+
}`}
|
|
201
|
+
>
|
|
202
|
+
{role}
|
|
203
|
+
</span>
|
|
204
|
+
))}
|
|
205
|
+
</div>
|
|
206
|
+
) : (
|
|
207
|
+
<p className={labelClass}>No roles assigned</p>
|
|
208
|
+
)}
|
|
209
|
+
</div>
|
|
210
|
+
|
|
211
|
+
{/* 2FA Status */}
|
|
212
|
+
<div className={`p-4 rounded-lg ${isDarkMode ? 'bg-yellow-900/30 border border-yellow-700' : 'bg-yellow-50 border border-yellow-200'}`}>
|
|
213
|
+
<h2 className="text-lg font-semibold mb-4">2FA Status</h2>
|
|
214
|
+
<div className="grid grid-cols-1 md:grid-cols-2 gap-3 text-sm">
|
|
215
|
+
<div>
|
|
216
|
+
<span className={labelClass}>2FA Verified:</span>{' '}
|
|
217
|
+
<StatusBadge
|
|
218
|
+
value={user.twoFactorSessionVerified}
|
|
219
|
+
trueText="Yes"
|
|
220
|
+
falseText="No"
|
|
221
|
+
isDarkMode={isDarkMode}
|
|
222
|
+
/>
|
|
223
|
+
</div>
|
|
224
|
+
<div>
|
|
225
|
+
<span className={labelClass}>Requires 2FA:</span>{' '}
|
|
226
|
+
<StatusBadge
|
|
227
|
+
value={user.requiresTwoFactor}
|
|
228
|
+
trueText="Yes"
|
|
229
|
+
falseText="No"
|
|
230
|
+
isDarkMode={isDarkMode}
|
|
231
|
+
invertColors
|
|
232
|
+
/>
|
|
233
|
+
</div>
|
|
234
|
+
<InfoRow
|
|
235
|
+
label="Auth Methods (AMR)"
|
|
236
|
+
value={user.authenticationMethods?.join(', ')}
|
|
237
|
+
labelClass={labelClass}
|
|
238
|
+
valueClass={valueClass}
|
|
239
|
+
/>
|
|
240
|
+
<InfoRow
|
|
241
|
+
label="Auth Level (ACR)"
|
|
242
|
+
value={user.authenticationLevel}
|
|
243
|
+
labelClass={labelClass}
|
|
244
|
+
valueClass={valueClass}
|
|
245
|
+
/>
|
|
246
|
+
<InfoRow
|
|
247
|
+
label="MFA Completed At"
|
|
248
|
+
value={user.mfaCompletedAt ? new Date(user.mfaCompletedAt).toISOString() : undefined}
|
|
249
|
+
labelClass={labelClass}
|
|
250
|
+
valueClass={valueClass}
|
|
251
|
+
/>
|
|
252
|
+
<InfoRow
|
|
253
|
+
label="MFA Expires At"
|
|
254
|
+
value={user.mfaExpiresAt ? new Date(user.mfaExpiresAt).toISOString() : undefined}
|
|
255
|
+
labelClass={labelClass}
|
|
256
|
+
valueClass={valueClass}
|
|
257
|
+
/>
|
|
258
|
+
</div>
|
|
259
|
+
</div>
|
|
260
|
+
|
|
261
|
+
{/* Tokens */}
|
|
262
|
+
<div className={`p-4 rounded-lg ${isDarkMode ? 'bg-blue-900/30 border border-blue-700' : 'bg-blue-50 border border-blue-200'}`}>
|
|
263
|
+
<h2 className="text-lg font-semibold mb-4">Tokens</h2>
|
|
264
|
+
<div className="space-y-4">
|
|
265
|
+
<div>
|
|
266
|
+
<div className="flex items-center justify-between mb-2">
|
|
267
|
+
<span className={labelClass}>Session Token (Redis Key):</span>
|
|
268
|
+
</div>
|
|
269
|
+
<code className={`block p-2 rounded text-xs break-all ${isDarkMode ? 'bg-slate-800' : 'bg-gray-100'}`}>
|
|
270
|
+
{displayExt.sessionToken ? maskToken(displayExt.sessionToken) : 'N/A'}
|
|
271
|
+
</code>
|
|
272
|
+
</div>
|
|
273
|
+
|
|
274
|
+
<div>
|
|
275
|
+
<div className="flex items-center justify-between mb-2">
|
|
276
|
+
<span className={labelClass}>Access Token (IDP):</span>
|
|
277
|
+
</div>
|
|
278
|
+
<code className={`block p-2 rounded text-xs break-all max-h-24 overflow-auto ${isDarkMode ? 'bg-slate-800' : 'bg-gray-100'}`}>
|
|
279
|
+
{displayExt.accessToken ? maskToken(displayExt.accessToken) : 'N/A'}
|
|
280
|
+
</code>
|
|
281
|
+
</div>
|
|
282
|
+
|
|
283
|
+
<div className="grid grid-cols-1 md:grid-cols-2 gap-3 text-sm">
|
|
284
|
+
<div>
|
|
285
|
+
<span className={labelClass}>Has Refresh Token:</span>{' '}
|
|
286
|
+
<StatusBadge
|
|
287
|
+
value={!!displayExt.refreshToken}
|
|
288
|
+
trueText="Yes"
|
|
289
|
+
falseText="No"
|
|
290
|
+
isDarkMode={isDarkMode}
|
|
291
|
+
/>
|
|
292
|
+
</div>
|
|
293
|
+
<InfoRow
|
|
294
|
+
label="Access Token Expires"
|
|
295
|
+
value={displayExt.accessTokenExpires ? new Date(displayExt.accessTokenExpires).toISOString() : undefined}
|
|
296
|
+
labelClass={labelClass}
|
|
297
|
+
valueClass={valueClass}
|
|
298
|
+
/>
|
|
299
|
+
</div>
|
|
300
|
+
</div>
|
|
301
|
+
</div>
|
|
302
|
+
|
|
303
|
+
{/* JWT Header (contains kid for key lookup) */}
|
|
304
|
+
{jwtHeader && (
|
|
305
|
+
<div className={`p-4 rounded-lg ${isDarkMode ? 'bg-orange-900/30 border border-orange-700' : 'bg-orange-50 border border-orange-200'}`}>
|
|
306
|
+
<h2 className="text-lg font-semibold mb-4">JWT Header</h2>
|
|
307
|
+
<div className="grid grid-cols-1 md:grid-cols-2 gap-3 text-sm">
|
|
308
|
+
<InfoRow label="Algorithm (alg)" value={jwtHeader.alg} labelClass={labelClass} valueClass={valueClass} />
|
|
309
|
+
<InfoRow label="Type (typ)" value={jwtHeader.typ} labelClass={labelClass} valueClass={valueClass} />
|
|
310
|
+
<div>
|
|
311
|
+
<span className={labelClass}>Key ID (kid):</span>{' '}
|
|
312
|
+
<span className={`font-mono ${jwtHeader.kid ? (isDarkMode ? 'text-green-400' : 'text-green-600') : (isDarkMode ? 'text-red-400' : 'text-red-600')}`}>
|
|
313
|
+
{jwtHeader.kid || 'NOT PRESENT'}
|
|
314
|
+
</span>
|
|
315
|
+
</div>
|
|
316
|
+
{Object.entries(jwtHeader)
|
|
317
|
+
.filter(([key]) => !['alg', 'typ', 'kid'].includes(key))
|
|
318
|
+
.map(([key, value]) => (
|
|
319
|
+
<InfoRow
|
|
320
|
+
key={key}
|
|
321
|
+
label={key}
|
|
322
|
+
value={typeof value === 'object' ? JSON.stringify(redactCredentials(value)) : String(redactCredentials(value, key))}
|
|
323
|
+
labelClass={labelClass}
|
|
324
|
+
valueClass={valueClass}
|
|
325
|
+
/>
|
|
326
|
+
))
|
|
327
|
+
}
|
|
328
|
+
</div>
|
|
329
|
+
</div>
|
|
330
|
+
)}
|
|
331
|
+
|
|
332
|
+
{/* JWT Payload Claims */}
|
|
333
|
+
{jwtPayload && (
|
|
334
|
+
<div className={`p-4 rounded-lg ${isDarkMode ? 'bg-green-900/30 border border-green-700' : 'bg-green-50 border border-green-200'}`}>
|
|
335
|
+
<h2 className="text-lg font-semibold mb-4">JWT Payload Claims</h2>
|
|
336
|
+
<div className="grid grid-cols-1 md:grid-cols-2 gap-3 text-sm">
|
|
337
|
+
<InfoRow label="Subject (sub)" value={jwtPayload.sub} labelClass={labelClass} valueClass={valueClass} />
|
|
338
|
+
<InfoRow label="Issuer (iss)" value={jwtPayload.iss} labelClass={labelClass} valueClass={valueClass} />
|
|
339
|
+
<InfoRow label="Audience (aud)" value={Array.isArray(jwtPayload.aud) ? jwtPayload.aud.join(', ') : jwtPayload.aud} labelClass={labelClass} valueClass={valueClass} />
|
|
340
|
+
<InfoRow label="Client ID" value={jwtPayload.client_id} labelClass={labelClass} valueClass={valueClass} />
|
|
341
|
+
<InfoRow
|
|
342
|
+
label="Expires (exp)"
|
|
343
|
+
value={jwtPayload.exp ? new Date(jwtPayload.exp * 1000).toISOString() : undefined}
|
|
344
|
+
labelClass={labelClass}
|
|
345
|
+
valueClass={valueClass}
|
|
346
|
+
/>
|
|
347
|
+
<InfoRow
|
|
348
|
+
label="Issued At (iat)"
|
|
349
|
+
value={jwtPayload.iat ? new Date(jwtPayload.iat * 1000).toISOString() : undefined}
|
|
350
|
+
labelClass={labelClass}
|
|
351
|
+
valueClass={valueClass}
|
|
352
|
+
/>
|
|
353
|
+
<InfoRow
|
|
354
|
+
label="AMR (Auth Methods)"
|
|
355
|
+
value={Array.isArray(jwtPayload.amr) ? jwtPayload.amr.join(', ') : jwtPayload.amr}
|
|
356
|
+
labelClass={labelClass}
|
|
357
|
+
valueClass={valueClass}
|
|
358
|
+
/>
|
|
359
|
+
<InfoRow label="ACR (Auth Context)" value={jwtPayload.acr} labelClass={labelClass} valueClass={valueClass} />
|
|
360
|
+
</div>
|
|
361
|
+
</div>
|
|
362
|
+
)}
|
|
363
|
+
|
|
364
|
+
{/* Session Expiry */}
|
|
365
|
+
<div className={`p-4 rounded-lg ${cardClass}`}>
|
|
366
|
+
<h2 className="text-lg font-semibold mb-4">Session Metadata</h2>
|
|
367
|
+
<div className="grid grid-cols-1 md:grid-cols-2 gap-3 text-sm">
|
|
368
|
+
<InfoRow
|
|
369
|
+
label="Session Expires"
|
|
370
|
+
value={(session as any)?.expires ? new Date((session as any).expires).toISOString() : undefined}
|
|
371
|
+
labelClass={labelClass}
|
|
372
|
+
valueClass={valueClass}
|
|
373
|
+
/>
|
|
374
|
+
<InfoRow
|
|
375
|
+
label="Error"
|
|
376
|
+
value={ext.error}
|
|
377
|
+
labelClass={labelClass}
|
|
378
|
+
valueClass={ext.error ? 'text-red-500' : valueClass}
|
|
379
|
+
/>
|
|
380
|
+
</div>
|
|
381
|
+
</div>
|
|
382
|
+
|
|
383
|
+
{/* Raw Session Data - credentials redacted by default, see redactCredentials above */}
|
|
384
|
+
<details className={`p-4 rounded-lg ${cardClass}`}>
|
|
385
|
+
<summary className="font-semibold cursor-pointer">
|
|
386
|
+
Raw Session Data — credentials redacted (Click to expand)
|
|
387
|
+
</summary>
|
|
388
|
+
<div className="mt-3">
|
|
389
|
+
<span className={`text-xs ${labelClass}`}>
|
|
390
|
+
Token values are masked. Structure, claims and expiry fields are unchanged.
|
|
391
|
+
</span>
|
|
392
|
+
</div>
|
|
393
|
+
<pre className={`mt-4 text-xs overflow-auto p-3 rounded ${isDarkMode ? 'bg-slate-800' : 'bg-gray-100'}`}>
|
|
394
|
+
{JSON.stringify(redactCredentials({ clientSession: session, serverSession: serverSession }), null, 2)}
|
|
395
|
+
</pre>
|
|
396
|
+
</details>
|
|
397
|
+
</div>
|
|
398
|
+
</div>
|
|
399
|
+
);
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
// Helper Components
|
|
403
|
+
|
|
404
|
+
function InfoRow({
|
|
405
|
+
label,
|
|
406
|
+
value,
|
|
407
|
+
labelClass,
|
|
408
|
+
valueClass,
|
|
409
|
+
}: {
|
|
410
|
+
label: string;
|
|
411
|
+
value?: string | null;
|
|
412
|
+
labelClass: string;
|
|
413
|
+
valueClass: string;
|
|
414
|
+
}) {
|
|
415
|
+
return (
|
|
416
|
+
<div>
|
|
417
|
+
<span className={labelClass}>{label}:</span>{' '}
|
|
418
|
+
<span className={valueClass}>{value || 'N/A'}</span>
|
|
419
|
+
</div>
|
|
420
|
+
);
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
function StatusBadge({
|
|
424
|
+
value,
|
|
425
|
+
trueText,
|
|
426
|
+
falseText,
|
|
427
|
+
isDarkMode,
|
|
428
|
+
invertColors = false,
|
|
429
|
+
}: {
|
|
430
|
+
value: boolean;
|
|
431
|
+
trueText: string;
|
|
432
|
+
falseText: string;
|
|
433
|
+
isDarkMode: boolean;
|
|
434
|
+
invertColors?: boolean;
|
|
435
|
+
}) {
|
|
436
|
+
const isPositive = invertColors ? !value : value;
|
|
437
|
+
const colorClass = isPositive
|
|
438
|
+
? isDarkMode ? 'bg-green-800 text-green-100' : 'bg-green-200 text-green-800'
|
|
439
|
+
: isDarkMode ? 'bg-red-800 text-red-100' : 'bg-red-200 text-red-800';
|
|
440
|
+
|
|
441
|
+
return (
|
|
442
|
+
<span className={`px-2 py-0.5 rounded text-xs font-medium ${colorClass}`}>
|
|
443
|
+
{value ? trueText : falseText}
|
|
444
|
+
</span>
|
|
445
|
+
);
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
|
|
449
|
+
export default JwtInspectPage;
|