@payez/next-mvp 4.1.9 → 4.1.10
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/README.md +792 -792
- package/dist/api-handlers/auth/refresh.js +38 -15
- package/dist/auth/better-auth.js +7 -18
- 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/ensure-fresh-access-token.js +30 -7
- package/dist/lib/idp-fetch.js +25 -9
- package/dist/lib/session-store.d.ts +1 -0
- package/dist/lib/session-store.js +12 -8
- package/dist/pages/login/page.js +28 -28
- package/dist/pages/settings/EnhancedSettingsPage.js +17 -17
- package/package.json +893 -893
- package/scripts/dev-broker.ps1 +35 -35
- package/scripts/dev-local.ps1 +45 -45
- package/src/api/auth-handler.ts +548 -548
- package/src/api-handlers/account/change-password.ts +142 -142
- package/src/api-handlers/account/update-phone.ts +79 -79
- package/src/api-handlers/account/verify-email.ts +125 -125
- package/src/api-handlers/account/verify-sms.ts +125 -125
- package/src/api-handlers/admin/analytics.ts +443 -443
- package/src/api-handlers/admin/audit.ts +223 -223
- package/src/api-handlers/admin/index.ts +63 -63
- package/src/api-handlers/admin/redis-sessions.ts +251 -251
- package/src/api-handlers/admin/sessions.ts +318 -318
- package/src/api-handlers/admin/site-logs.ts +365 -365
- package/src/api-handlers/admin/users.ts +242 -242
- package/src/api-handlers/admin/vibe-data.ts +324 -324
- package/src/api-handlers/auth/refresh.ts +705 -682
- package/src/api-handlers/auth/signout.ts +199 -199
- package/src/api-handlers/auth/status.ts +20 -20
- package/src/api-handlers/auth/update-session.ts +110 -110
- package/src/api-handlers/auth/verify-code.ts +112 -112
- package/src/api-handlers/session/refresh-viability.ts +36 -36
- package/src/api-handlers/test/force-expire.ts +60 -60
- package/src/auth/auth-decision.ts +229 -229
- package/src/auth/better-auth.ts +7 -18
- package/src/auth/route-config.ts +219 -219
- package/src/auth/types/auth-types.ts +555 -555
- package/src/auth/types/index.ts +7 -7
- package/src/auth/utils/idp-client.ts +445 -445
- package/src/auth/utils/index.ts +6 -6
- package/src/auth/utils/token-utils.ts +243 -243
- package/src/client/AuthContext.tsx +143 -143
- package/src/client/fetch-with-auth.ts +47 -47
- 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 +266 -266
- 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 +505 -505
- package/src/components/admin/LoggingSettingsTab.tsx +665 -665
- package/src/components/admin/SessionsTab.tsx +414 -414
- package/src/components/admin/StatsTab.tsx +379 -379
- package/src/components/admin/VibeAdminLayout.tsx +187 -187
- package/src/components/admin/index.ts +59 -59
- package/src/components/auth/FederatedAuthSection.tsx +95 -95
- package/src/components/auth/ModeAwareLoginPage.tsx +135 -135
- package/src/components/auth/ModeAwareSignupPage.tsx +267 -267
- package/src/components/auth/TraditionalAuthSection.tsx +99 -99
- package/src/components/recovery/CompleteStep.tsx +36 -36
- package/src/components/recovery/InitiateRecoveryStep.tsx +68 -68
- package/src/components/recovery/SelectMethodStep.tsx +73 -73
- package/src/components/recovery/SetPasswordStep.tsx +97 -97
- 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 +57 -57
- package/src/config/logger.ts +62 -62
- package/src/config/vibe-log-transport.ts +264 -264
- package/src/hooks/useAuth.ts +117 -117
- package/src/hooks/useAuthSettings.ts +97 -97
- package/src/hooks/useAvailableProviders.ts +120 -120
- package/src/hooks/usePasswordValidation.ts +127 -127
- package/src/hooks/usePublicAuthSettings.ts +149 -149
- package/src/hooks/useSessionExpiration.ts +101 -101
- package/src/hooks/useViabilitySession.ts +336 -336
- package/src/index.ts +60 -60
- package/src/lib/api-handler.ts +643 -643
- package/src/lib/app-slug.ts +178 -178
- package/src/lib/ensure-fresh-access-token.ts +358 -335
- package/src/lib/geolocation.ts +265 -265
- package/src/lib/idp-fetch.ts +26 -10
- package/src/lib/jwt-decode.ts +82 -82
- package/src/lib/roles.ts +177 -177
- package/src/lib/session-store.ts +4 -1
- package/src/lib/site-logger.ts +245 -245
- package/src/lib/standardized-client-api.ts +901 -901
- package/src/lib/test-aware-get-token.ts +41 -41
- 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 +663 -663
- package/src/middleware/rbac-check.ts +279 -279
- package/src/middleware/twofa-presets.ts +224 -224
- package/src/pages/admin-login/page.tsx +221 -221
- 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/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 +456 -456
- package/src/pages/profile/EnhancedProfilePage.tsx +485 -485
- package/src/pages/profile/index.ts +9 -9
- package/src/pages/profile/page.tsx +166 -166
- package/src/pages/recovery/page.tsx +234 -234
- 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 +565 -565
- package/src/pages/security/index.ts +9 -9
- 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 +180 -180
- package/src/pages/test-env/JwtInspectPage.tsx +456 -456
- package/src/pages/test-env/RefreshTokenPage.tsx +157 -157
- package/src/pages/test-env/TestEnvPage.tsx +118 -118
- package/src/pages/test-env/index.ts +25 -25
- package/src/pages/verify-code/page.tsx +652 -652
- package/src/routes/account/update-phone.ts +13 -13
- package/src/routes/auth/logout.ts +108 -108
- package/src/routes/auth/nextauth.ts +45 -45
- package/src/routes/auth/settings.ts +75 -75
- package/src/routes/auth/viability.ts +224 -224
- package/src/server/auth-guard.ts +170 -170
- package/src/server/slim-middleware.ts +117 -117
- package/src/server/with-auth.ts +93 -93
- package/src/stores/authStore.ts +1899 -1899
- package/src/templates/instrumentation.ts +41 -41
- package/src/utils/logout.ts +30 -30
- package/src/vibe/client.ts +590 -590
- package/src/vibe/enterprise-auth.ts +208 -208
- package/src/vibe/errors.ts +185 -185
- package/src/vibe/generic.ts +431 -431
- package/src/vibe/hooks/index.ts +367 -367
- package/src/vibe/index.ts +132 -132
- package/src/vibe/sessions.ts +551 -551
- package/src/vibe/types.ts +577 -577
|
@@ -1,456 +1,456 @@
|
|
|
1
|
-
'use client';
|
|
2
|
-
|
|
3
|
-
import { authClient } from '../../client/better-auth-client';
|
|
4
|
-
import { useState, useEffect } from 'react';
|
|
5
|
-
|
|
6
|
-
// Decode JWT header (contains kid, alg, typ)
|
|
7
|
-
function decodeJwtHeader(token: string): any {
|
|
8
|
-
try {
|
|
9
|
-
const parts = token.split('.');
|
|
10
|
-
if (parts.length !== 3) return null;
|
|
11
|
-
const header = parts[0].replace(/-/g, '+').replace(/_/g, '/');
|
|
12
|
-
const decoded = atob(header);
|
|
13
|
-
return JSON.parse(decoded);
|
|
14
|
-
} catch {
|
|
15
|
-
return null;
|
|
16
|
-
}
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
// Decode JWT payload (contains claims)
|
|
20
|
-
function decodeJwtPayload(token: string): any {
|
|
21
|
-
try {
|
|
22
|
-
const parts = token.split('.');
|
|
23
|
-
if (parts.length !== 3) return null;
|
|
24
|
-
const payload = parts[1].replace(/-/g, '+').replace(/_/g, '/');
|
|
25
|
-
const decoded = atob(payload);
|
|
26
|
-
return JSON.parse(decoded);
|
|
27
|
-
} catch {
|
|
28
|
-
return null;
|
|
29
|
-
}
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
/**
|
|
33
|
-
* Session Inspector Page
|
|
34
|
-
*
|
|
35
|
-
* Debug page for inspecting session data from Redis.
|
|
36
|
-
* Shows user info, roles, 2FA status, and tokens.
|
|
37
|
-
*
|
|
38
|
-
* Usage in consuming app:
|
|
39
|
-
* ```typescript
|
|
40
|
-
* // app/test-env/jwt-inspect/page.tsx
|
|
41
|
-
* export { JwtInspectPage as default } from '@payez/next-mvp/pages/test-env';
|
|
42
|
-
* ```
|
|
43
|
-
*/
|
|
44
|
-
export function JwtInspectPage() {
|
|
45
|
-
const { data: sessionData, isPending } = authClient.useSession();
|
|
46
|
-
const session = sessionData;
|
|
47
|
-
const status = isPending ? 'loading' : session ? 'authenticated' : 'unauthenticated';
|
|
48
|
-
const [copied, setCopied] = useState<string | null>(null);
|
|
49
|
-
const [isDarkMode, setIsDarkMode] = useState(false);
|
|
50
|
-
const [jwtHeader, setJwtHeader] = useState<any>(null);
|
|
51
|
-
const [jwtPayload, setJwtPayload] = useState<any>(null);
|
|
52
|
-
const [serverSession, setServerSession] = useState<any>(null);
|
|
53
|
-
|
|
54
|
-
// Detect dark mode
|
|
55
|
-
useEffect(() => {
|
|
56
|
-
const checkDarkMode = () => {
|
|
57
|
-
const isDark = document.documentElement.classList.contains('dark') ||
|
|
58
|
-
window.matchMedia('(prefers-color-scheme: dark)').matches;
|
|
59
|
-
setIsDarkMode(isDark);
|
|
60
|
-
};
|
|
61
|
-
checkDarkMode();
|
|
62
|
-
const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
|
|
63
|
-
mediaQuery.addEventListener('change', checkDarkMode);
|
|
64
|
-
return () => mediaQuery.removeEventListener('change', checkDarkMode);
|
|
65
|
-
}, []);
|
|
66
|
-
|
|
67
|
-
// Fetch enriched session from server (has IDP tokens, roles, userId)
|
|
68
|
-
useEffect(() => {
|
|
69
|
-
if (!session) return;
|
|
70
|
-
fetch('/api/internal/session-data')
|
|
71
|
-
.then(r => r.ok ? r.json() : null)
|
|
72
|
-
.then(d => {
|
|
73
|
-
if (d) setServerSession(d);
|
|
74
|
-
})
|
|
75
|
-
.catch(() => {});
|
|
76
|
-
}, [session]);
|
|
77
|
-
|
|
78
|
-
// Decode JWT header and payload from server session's IDP access token
|
|
79
|
-
useEffect(() => {
|
|
80
|
-
const token = serverSession?.idpAccessToken || (session as any)?.accessToken;
|
|
81
|
-
if (token) {
|
|
82
|
-
setJwtHeader(decodeJwtHeader(token));
|
|
83
|
-
setJwtPayload(decodeJwtPayload(token));
|
|
84
|
-
}
|
|
85
|
-
}, [serverSession, session]);
|
|
86
|
-
|
|
87
|
-
const copyToClipboard = (text: string, label: string) => {
|
|
88
|
-
navigator.clipboard.writeText(text);
|
|
89
|
-
setCopied(label);
|
|
90
|
-
setTimeout(() => setCopied(null), 2000);
|
|
91
|
-
};
|
|
92
|
-
|
|
93
|
-
if (status === 'loading') {
|
|
94
|
-
return (
|
|
95
|
-
<div className={`min-h-screen p-8 ${isDarkMode ? 'bg-slate-950 text-white' : 'bg-gray-50 text-gray-900'}`}>
|
|
96
|
-
Loading session...
|
|
97
|
-
</div>
|
|
98
|
-
);
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
if (status === 'unauthenticated') {
|
|
102
|
-
return (
|
|
103
|
-
<div className={`min-h-screen p-8 ${isDarkMode ? 'bg-slate-950 text-white' : 'bg-gray-50 text-gray-900'}`}>
|
|
104
|
-
<div className={`p-4 rounded-lg ${isDarkMode ? 'bg-red-900/30 border border-red-800' : 'bg-red-50 border border-red-200'}`}>
|
|
105
|
-
<h2 className="text-lg font-semibold mb-2">Not Authenticated</h2>
|
|
106
|
-
<p>Please log in to inspect session data.</p>
|
|
107
|
-
</div>
|
|
108
|
-
</div>
|
|
109
|
-
);
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
// Merge server-side enriched session with client session
|
|
113
|
-
const ss = serverSession || {};
|
|
114
|
-
const ext = session as any;
|
|
115
|
-
const user = {
|
|
116
|
-
id: ss.userId || ext?.user?.id,
|
|
117
|
-
email: ss.email || ext?.user?.email,
|
|
118
|
-
name: ss.name || ext?.user?.name,
|
|
119
|
-
roles: ss.roles || [],
|
|
120
|
-
oauthProvider: ss.oauthProvider,
|
|
121
|
-
idpClientId: ss.idpClientId,
|
|
122
|
-
merchantId: ss.merchantId,
|
|
123
|
-
mfaVerified: ss.mfaVerified,
|
|
124
|
-
requiresTwoFactor: false,
|
|
125
|
-
authenticationMethods: ss.authenticationMethods,
|
|
126
|
-
authenticationLevel: ss.authenticationLevel,
|
|
127
|
-
mfaCompletedAt: ss.mfaCompletedAt,
|
|
128
|
-
mfaExpiresAt: ss.mfaExpiresAt,
|
|
129
|
-
twoFactorSessionVerified: ss.mfaVerified,
|
|
130
|
-
};
|
|
131
|
-
// Token fields for display
|
|
132
|
-
const displayExt = {
|
|
133
|
-
...ext,
|
|
134
|
-
sessionToken: ss.sessionToken || ext?.sessionToken,
|
|
135
|
-
accessToken: ss.idpAccessToken || ext?.accessToken,
|
|
136
|
-
refreshToken: ss.idpRefreshToken || ext?.refreshToken,
|
|
137
|
-
accessTokenExpires: ss.idpAccessTokenExpires || ext?.accessTokenExpires,
|
|
138
|
-
};
|
|
139
|
-
|
|
140
|
-
// Card styling helpers
|
|
141
|
-
const cardClass = isDarkMode ? 'bg-slate-900 border border-slate-700' : 'bg-white border border-gray-200';
|
|
142
|
-
const labelClass = isDarkMode ? 'text-slate-400' : 'text-gray-500';
|
|
143
|
-
const valueClass = isDarkMode ? 'text-white' : 'text-gray-900';
|
|
144
|
-
|
|
145
|
-
return (
|
|
146
|
-
<div className={`min-h-screen p-8 ${isDarkMode ? 'bg-slate-950 text-white' : 'bg-gray-50 text-gray-900'}`}>
|
|
147
|
-
<div className="max-w-4xl mx-auto space-y-6">
|
|
148
|
-
<h1 className="text-2xl font-bold">Session Inspector</h1>
|
|
149
|
-
<p className={`text-sm ${labelClass}`}>
|
|
150
|
-
Better Auth session + IDP tokens from Redis
|
|
151
|
-
</p>
|
|
152
|
-
|
|
153
|
-
{/* User Identity */}
|
|
154
|
-
<div className={`p-4 rounded-lg ${cardClass}`}>
|
|
155
|
-
<h2 className="text-lg font-semibold mb-4">User Identity</h2>
|
|
156
|
-
<div className="grid grid-cols-1 md:grid-cols-2 gap-3 text-sm">
|
|
157
|
-
<InfoRow label="User ID" value={user.id} labelClass={labelClass} valueClass={valueClass} />
|
|
158
|
-
<InfoRow label="Email" value={user.email} labelClass={labelClass} valueClass={valueClass} />
|
|
159
|
-
<InfoRow label="Name" value={user.name} labelClass={labelClass} valueClass={valueClass} />
|
|
160
|
-
<InfoRow label="OAuth Provider" value={user.oauthProvider} labelClass={labelClass} valueClass={valueClass} />
|
|
161
|
-
<InfoRow label="IDP Client ID" value={user.idpClientId} labelClass={labelClass} valueClass={valueClass} />
|
|
162
|
-
<InfoRow label="Merchant ID" value={user.merchantId} labelClass={labelClass} valueClass={valueClass} />
|
|
163
|
-
</div>
|
|
164
|
-
</div>
|
|
165
|
-
|
|
166
|
-
{/* Roles */}
|
|
167
|
-
<div className={`p-4 rounded-lg ${isDarkMode ? 'bg-purple-900/30 border border-purple-700' : 'bg-purple-50 border border-purple-200'}`}>
|
|
168
|
-
<h2 className="text-lg font-semibold mb-4">Roles</h2>
|
|
169
|
-
{user.roles && user.roles.length > 0 ? (
|
|
170
|
-
<div className="flex flex-wrap gap-2">
|
|
171
|
-
{user.roles.map((role: string) => (
|
|
172
|
-
<span
|
|
173
|
-
key={role}
|
|
174
|
-
className={`px-3 py-1 rounded-full text-sm font-medium ${
|
|
175
|
-
isDarkMode ? 'bg-purple-800 text-purple-100' : 'bg-purple-200 text-purple-800'
|
|
176
|
-
}`}
|
|
177
|
-
>
|
|
178
|
-
{role}
|
|
179
|
-
</span>
|
|
180
|
-
))}
|
|
181
|
-
</div>
|
|
182
|
-
) : (
|
|
183
|
-
<p className={labelClass}>No roles assigned</p>
|
|
184
|
-
)}
|
|
185
|
-
</div>
|
|
186
|
-
|
|
187
|
-
{/* 2FA Status */}
|
|
188
|
-
<div className={`p-4 rounded-lg ${isDarkMode ? 'bg-yellow-900/30 border border-yellow-700' : 'bg-yellow-50 border border-yellow-200'}`}>
|
|
189
|
-
<h2 className="text-lg font-semibold mb-4">2FA Status</h2>
|
|
190
|
-
<div className="grid grid-cols-1 md:grid-cols-2 gap-3 text-sm">
|
|
191
|
-
<div>
|
|
192
|
-
<span className={labelClass}>2FA Verified:</span>{' '}
|
|
193
|
-
<StatusBadge
|
|
194
|
-
value={user.twoFactorSessionVerified}
|
|
195
|
-
trueText="Yes"
|
|
196
|
-
falseText="No"
|
|
197
|
-
isDarkMode={isDarkMode}
|
|
198
|
-
/>
|
|
199
|
-
</div>
|
|
200
|
-
<div>
|
|
201
|
-
<span className={labelClass}>Requires 2FA:</span>{' '}
|
|
202
|
-
<StatusBadge
|
|
203
|
-
value={user.requiresTwoFactor}
|
|
204
|
-
trueText="Yes"
|
|
205
|
-
falseText="No"
|
|
206
|
-
isDarkMode={isDarkMode}
|
|
207
|
-
invertColors
|
|
208
|
-
/>
|
|
209
|
-
</div>
|
|
210
|
-
<InfoRow
|
|
211
|
-
label="Auth Methods (AMR)"
|
|
212
|
-
value={user.authenticationMethods?.join(', ')}
|
|
213
|
-
labelClass={labelClass}
|
|
214
|
-
valueClass={valueClass}
|
|
215
|
-
/>
|
|
216
|
-
<InfoRow
|
|
217
|
-
label="Auth Level (ACR)"
|
|
218
|
-
value={user.authenticationLevel}
|
|
219
|
-
labelClass={labelClass}
|
|
220
|
-
valueClass={valueClass}
|
|
221
|
-
/>
|
|
222
|
-
<InfoRow
|
|
223
|
-
label="MFA Completed At"
|
|
224
|
-
value={user.mfaCompletedAt ? new Date(user.mfaCompletedAt).toISOString() : undefined}
|
|
225
|
-
labelClass={labelClass}
|
|
226
|
-
valueClass={valueClass}
|
|
227
|
-
/>
|
|
228
|
-
<InfoRow
|
|
229
|
-
label="MFA Expires At"
|
|
230
|
-
value={user.mfaExpiresAt ? new Date(user.mfaExpiresAt).toISOString() : undefined}
|
|
231
|
-
labelClass={labelClass}
|
|
232
|
-
valueClass={valueClass}
|
|
233
|
-
/>
|
|
234
|
-
</div>
|
|
235
|
-
</div>
|
|
236
|
-
|
|
237
|
-
{/* Tokens */}
|
|
238
|
-
<div className={`p-4 rounded-lg ${isDarkMode ? 'bg-blue-900/30 border border-blue-700' : 'bg-blue-50 border border-blue-200'}`}>
|
|
239
|
-
<h2 className="text-lg font-semibold mb-4">Tokens</h2>
|
|
240
|
-
<div className="space-y-4">
|
|
241
|
-
<div>
|
|
242
|
-
<div className="flex items-center justify-between mb-2">
|
|
243
|
-
<span className={labelClass}>Session Token (Redis Key):</span>
|
|
244
|
-
{displayExt.sessionToken && (
|
|
245
|
-
<CopyButton
|
|
246
|
-
onClick={() => copyToClipboard(displayExt.sessionToken, 'session')}
|
|
247
|
-
copied={copied === 'session'}
|
|
248
|
-
isDarkMode={isDarkMode}
|
|
249
|
-
/>
|
|
250
|
-
)}
|
|
251
|
-
</div>
|
|
252
|
-
<code className={`block p-2 rounded text-xs break-all ${isDarkMode ? 'bg-slate-800' : 'bg-gray-100'}`}>
|
|
253
|
-
{displayExt.sessionToken || 'N/A'}
|
|
254
|
-
</code>
|
|
255
|
-
</div>
|
|
256
|
-
|
|
257
|
-
<div>
|
|
258
|
-
<div className="flex items-center justify-between mb-2">
|
|
259
|
-
<span className={labelClass}>Access Token (IDP):</span>
|
|
260
|
-
{displayExt.accessToken && (
|
|
261
|
-
<CopyButton
|
|
262
|
-
onClick={() => copyToClipboard(displayExt.accessToken, 'access')}
|
|
263
|
-
copied={copied === 'access'}
|
|
264
|
-
isDarkMode={isDarkMode}
|
|
265
|
-
/>
|
|
266
|
-
)}
|
|
267
|
-
</div>
|
|
268
|
-
<code className={`block p-2 rounded text-xs break-all max-h-24 overflow-auto ${isDarkMode ? 'bg-slate-800' : 'bg-gray-100'}`}>
|
|
269
|
-
{displayExt.accessToken || 'N/A'}
|
|
270
|
-
</code>
|
|
271
|
-
</div>
|
|
272
|
-
|
|
273
|
-
<div className="grid grid-cols-1 md:grid-cols-2 gap-3 text-sm">
|
|
274
|
-
<div>
|
|
275
|
-
<span className={labelClass}>Has Refresh Token:</span>{' '}
|
|
276
|
-
<StatusBadge
|
|
277
|
-
value={!!displayExt.refreshToken}
|
|
278
|
-
trueText="Yes"
|
|
279
|
-
falseText="No"
|
|
280
|
-
isDarkMode={isDarkMode}
|
|
281
|
-
/>
|
|
282
|
-
</div>
|
|
283
|
-
<InfoRow
|
|
284
|
-
label="Access Token Expires"
|
|
285
|
-
value={displayExt.accessTokenExpires ? new Date(displayExt.accessTokenExpires).toISOString() : undefined}
|
|
286
|
-
labelClass={labelClass}
|
|
287
|
-
valueClass={valueClass}
|
|
288
|
-
/>
|
|
289
|
-
</div>
|
|
290
|
-
</div>
|
|
291
|
-
</div>
|
|
292
|
-
|
|
293
|
-
{/* JWT Header (contains kid for key lookup) */}
|
|
294
|
-
{jwtHeader && (
|
|
295
|
-
<div className={`p-4 rounded-lg ${isDarkMode ? 'bg-orange-900/30 border border-orange-700' : 'bg-orange-50 border border-orange-200'}`}>
|
|
296
|
-
<h2 className="text-lg font-semibold mb-4">JWT Header</h2>
|
|
297
|
-
<div className="grid grid-cols-1 md:grid-cols-2 gap-3 text-sm">
|
|
298
|
-
<InfoRow label="Algorithm (alg)" value={jwtHeader.alg} labelClass={labelClass} valueClass={valueClass} />
|
|
299
|
-
<InfoRow label="Type (typ)" value={jwtHeader.typ} labelClass={labelClass} valueClass={valueClass} />
|
|
300
|
-
<div>
|
|
301
|
-
<span className={labelClass}>Key ID (kid):</span>{' '}
|
|
302
|
-
<span className={`font-mono ${jwtHeader.kid ? (isDarkMode ? 'text-green-400' : 'text-green-600') : (isDarkMode ? 'text-red-400' : 'text-red-600')}`}>
|
|
303
|
-
{jwtHeader.kid || 'NOT PRESENT'}
|
|
304
|
-
</span>
|
|
305
|
-
</div>
|
|
306
|
-
{Object.entries(jwtHeader)
|
|
307
|
-
.filter(([key]) => !['alg', 'typ', 'kid'].includes(key))
|
|
308
|
-
.map(([key, value]) => (
|
|
309
|
-
<InfoRow
|
|
310
|
-
key={key}
|
|
311
|
-
label={key}
|
|
312
|
-
value={typeof value === 'object' ? JSON.stringify(value) : String(value)}
|
|
313
|
-
labelClass={labelClass}
|
|
314
|
-
valueClass={valueClass}
|
|
315
|
-
/>
|
|
316
|
-
))
|
|
317
|
-
}
|
|
318
|
-
</div>
|
|
319
|
-
</div>
|
|
320
|
-
)}
|
|
321
|
-
|
|
322
|
-
{/* JWT Payload Claims */}
|
|
323
|
-
{jwtPayload && (
|
|
324
|
-
<div className={`p-4 rounded-lg ${isDarkMode ? 'bg-green-900/30 border border-green-700' : 'bg-green-50 border border-green-200'}`}>
|
|
325
|
-
<h2 className="text-lg font-semibold mb-4">JWT Payload Claims</h2>
|
|
326
|
-
<div className="grid grid-cols-1 md:grid-cols-2 gap-3 text-sm">
|
|
327
|
-
<InfoRow label="Subject (sub)" value={jwtPayload.sub} labelClass={labelClass} valueClass={valueClass} />
|
|
328
|
-
<InfoRow label="Issuer (iss)" value={jwtPayload.iss} labelClass={labelClass} valueClass={valueClass} />
|
|
329
|
-
<InfoRow label="Audience (aud)" value={Array.isArray(jwtPayload.aud) ? jwtPayload.aud.join(', ') : jwtPayload.aud} labelClass={labelClass} valueClass={valueClass} />
|
|
330
|
-
<InfoRow label="Client ID" value={jwtPayload.client_id} labelClass={labelClass} valueClass={valueClass} />
|
|
331
|
-
<InfoRow
|
|
332
|
-
label="Expires (exp)"
|
|
333
|
-
value={jwtPayload.exp ? new Date(jwtPayload.exp * 1000).toISOString() : undefined}
|
|
334
|
-
labelClass={labelClass}
|
|
335
|
-
valueClass={valueClass}
|
|
336
|
-
/>
|
|
337
|
-
<InfoRow
|
|
338
|
-
label="Issued At (iat)"
|
|
339
|
-
value={jwtPayload.iat ? new Date(jwtPayload.iat * 1000).toISOString() : undefined}
|
|
340
|
-
labelClass={labelClass}
|
|
341
|
-
valueClass={valueClass}
|
|
342
|
-
/>
|
|
343
|
-
<InfoRow
|
|
344
|
-
label="AMR (Auth Methods)"
|
|
345
|
-
value={Array.isArray(jwtPayload.amr) ? jwtPayload.amr.join(', ') : jwtPayload.amr}
|
|
346
|
-
labelClass={labelClass}
|
|
347
|
-
valueClass={valueClass}
|
|
348
|
-
/>
|
|
349
|
-
<InfoRow label="ACR (Auth Context)" value={jwtPayload.acr} labelClass={labelClass} valueClass={valueClass} />
|
|
350
|
-
</div>
|
|
351
|
-
</div>
|
|
352
|
-
)}
|
|
353
|
-
|
|
354
|
-
{/* Session Expiry */}
|
|
355
|
-
<div className={`p-4 rounded-lg ${cardClass}`}>
|
|
356
|
-
<h2 className="text-lg font-semibold mb-4">Session Metadata</h2>
|
|
357
|
-
<div className="grid grid-cols-1 md:grid-cols-2 gap-3 text-sm">
|
|
358
|
-
<InfoRow
|
|
359
|
-
label="Session Expires"
|
|
360
|
-
value={(session as any)?.expires ? new Date((session as any).expires).toISOString() : undefined}
|
|
361
|
-
labelClass={labelClass}
|
|
362
|
-
valueClass={valueClass}
|
|
363
|
-
/>
|
|
364
|
-
<InfoRow
|
|
365
|
-
label="Error"
|
|
366
|
-
value={ext.error}
|
|
367
|
-
labelClass={labelClass}
|
|
368
|
-
valueClass={ext.error ? 'text-red-500' : valueClass}
|
|
369
|
-
/>
|
|
370
|
-
</div>
|
|
371
|
-
</div>
|
|
372
|
-
|
|
373
|
-
{/* Raw Session Data */}
|
|
374
|
-
<details className={`p-4 rounded-lg ${cardClass}`}>
|
|
375
|
-
<summary className="font-semibold cursor-pointer">Raw Session Data (Click to expand)</summary>
|
|
376
|
-
<pre className={`mt-4 text-xs overflow-auto p-3 rounded ${isDarkMode ? 'bg-slate-800' : 'bg-gray-100'}`}>
|
|
377
|
-
{JSON.stringify({ clientSession: session, serverSession: serverSession }, null, 2)}
|
|
378
|
-
</pre>
|
|
379
|
-
</details>
|
|
380
|
-
</div>
|
|
381
|
-
</div>
|
|
382
|
-
);
|
|
383
|
-
}
|
|
384
|
-
|
|
385
|
-
// Helper Components
|
|
386
|
-
|
|
387
|
-
function InfoRow({
|
|
388
|
-
label,
|
|
389
|
-
value,
|
|
390
|
-
labelClass,
|
|
391
|
-
valueClass,
|
|
392
|
-
}: {
|
|
393
|
-
label: string;
|
|
394
|
-
value?: string | null;
|
|
395
|
-
labelClass: string;
|
|
396
|
-
valueClass: string;
|
|
397
|
-
}) {
|
|
398
|
-
return (
|
|
399
|
-
<div>
|
|
400
|
-
<span className={labelClass}>{label}:</span>{' '}
|
|
401
|
-
<span className={valueClass}>{value || 'N/A'}</span>
|
|
402
|
-
</div>
|
|
403
|
-
);
|
|
404
|
-
}
|
|
405
|
-
|
|
406
|
-
function StatusBadge({
|
|
407
|
-
value,
|
|
408
|
-
trueText,
|
|
409
|
-
falseText,
|
|
410
|
-
isDarkMode,
|
|
411
|
-
invertColors = false,
|
|
412
|
-
}: {
|
|
413
|
-
value: boolean;
|
|
414
|
-
trueText: string;
|
|
415
|
-
falseText: string;
|
|
416
|
-
isDarkMode: boolean;
|
|
417
|
-
invertColors?: boolean;
|
|
418
|
-
}) {
|
|
419
|
-
const isPositive = invertColors ? !value : value;
|
|
420
|
-
const colorClass = isPositive
|
|
421
|
-
? isDarkMode ? 'bg-green-800 text-green-100' : 'bg-green-200 text-green-800'
|
|
422
|
-
: isDarkMode ? 'bg-red-800 text-red-100' : 'bg-red-200 text-red-800';
|
|
423
|
-
|
|
424
|
-
return (
|
|
425
|
-
<span className={`px-2 py-0.5 rounded text-xs font-medium ${colorClass}`}>
|
|
426
|
-
{value ? trueText : falseText}
|
|
427
|
-
</span>
|
|
428
|
-
);
|
|
429
|
-
}
|
|
430
|
-
|
|
431
|
-
function CopyButton({
|
|
432
|
-
onClick,
|
|
433
|
-
copied,
|
|
434
|
-
isDarkMode,
|
|
435
|
-
}: {
|
|
436
|
-
onClick: () => void;
|
|
437
|
-
copied: boolean;
|
|
438
|
-
isDarkMode: boolean;
|
|
439
|
-
}) {
|
|
440
|
-
return (
|
|
441
|
-
<button
|
|
442
|
-
onClick={onClick}
|
|
443
|
-
className={`px-2 py-1 rounded text-xs ${
|
|
444
|
-
copied
|
|
445
|
-
? 'bg-green-600 text-white'
|
|
446
|
-
: isDarkMode
|
|
447
|
-
? 'bg-slate-700 hover:bg-slate-600 text-white'
|
|
448
|
-
: 'bg-gray-200 hover:bg-gray-300 text-gray-700'
|
|
449
|
-
}`}
|
|
450
|
-
>
|
|
451
|
-
{copied ? 'Copied!' : 'Copy'}
|
|
452
|
-
</button>
|
|
453
|
-
);
|
|
454
|
-
}
|
|
455
|
-
|
|
456
|
-
export default JwtInspectPage;
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { authClient } from '../../client/better-auth-client';
|
|
4
|
+
import { useState, useEffect } from 'react';
|
|
5
|
+
|
|
6
|
+
// Decode JWT header (contains kid, alg, typ)
|
|
7
|
+
function decodeJwtHeader(token: string): any {
|
|
8
|
+
try {
|
|
9
|
+
const parts = token.split('.');
|
|
10
|
+
if (parts.length !== 3) return null;
|
|
11
|
+
const header = parts[0].replace(/-/g, '+').replace(/_/g, '/');
|
|
12
|
+
const decoded = atob(header);
|
|
13
|
+
return JSON.parse(decoded);
|
|
14
|
+
} catch {
|
|
15
|
+
return null;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// Decode JWT payload (contains claims)
|
|
20
|
+
function decodeJwtPayload(token: string): any {
|
|
21
|
+
try {
|
|
22
|
+
const parts = token.split('.');
|
|
23
|
+
if (parts.length !== 3) return null;
|
|
24
|
+
const payload = parts[1].replace(/-/g, '+').replace(/_/g, '/');
|
|
25
|
+
const decoded = atob(payload);
|
|
26
|
+
return JSON.parse(decoded);
|
|
27
|
+
} catch {
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Session Inspector Page
|
|
34
|
+
*
|
|
35
|
+
* Debug page for inspecting session data from Redis.
|
|
36
|
+
* Shows user info, roles, 2FA status, and tokens.
|
|
37
|
+
*
|
|
38
|
+
* Usage in consuming app:
|
|
39
|
+
* ```typescript
|
|
40
|
+
* // app/test-env/jwt-inspect/page.tsx
|
|
41
|
+
* export { JwtInspectPage as default } from '@payez/next-mvp/pages/test-env';
|
|
42
|
+
* ```
|
|
43
|
+
*/
|
|
44
|
+
export function JwtInspectPage() {
|
|
45
|
+
const { data: sessionData, isPending } = authClient.useSession();
|
|
46
|
+
const session = sessionData;
|
|
47
|
+
const status = isPending ? 'loading' : session ? 'authenticated' : 'unauthenticated';
|
|
48
|
+
const [copied, setCopied] = useState<string | null>(null);
|
|
49
|
+
const [isDarkMode, setIsDarkMode] = useState(false);
|
|
50
|
+
const [jwtHeader, setJwtHeader] = useState<any>(null);
|
|
51
|
+
const [jwtPayload, setJwtPayload] = useState<any>(null);
|
|
52
|
+
const [serverSession, setServerSession] = useState<any>(null);
|
|
53
|
+
|
|
54
|
+
// Detect dark mode
|
|
55
|
+
useEffect(() => {
|
|
56
|
+
const checkDarkMode = () => {
|
|
57
|
+
const isDark = document.documentElement.classList.contains('dark') ||
|
|
58
|
+
window.matchMedia('(prefers-color-scheme: dark)').matches;
|
|
59
|
+
setIsDarkMode(isDark);
|
|
60
|
+
};
|
|
61
|
+
checkDarkMode();
|
|
62
|
+
const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
|
|
63
|
+
mediaQuery.addEventListener('change', checkDarkMode);
|
|
64
|
+
return () => mediaQuery.removeEventListener('change', checkDarkMode);
|
|
65
|
+
}, []);
|
|
66
|
+
|
|
67
|
+
// Fetch enriched session from server (has IDP tokens, roles, userId)
|
|
68
|
+
useEffect(() => {
|
|
69
|
+
if (!session) return;
|
|
70
|
+
fetch('/api/internal/session-data')
|
|
71
|
+
.then(r => r.ok ? r.json() : null)
|
|
72
|
+
.then(d => {
|
|
73
|
+
if (d) setServerSession(d);
|
|
74
|
+
})
|
|
75
|
+
.catch(() => {});
|
|
76
|
+
}, [session]);
|
|
77
|
+
|
|
78
|
+
// Decode JWT header and payload from server session's IDP access token
|
|
79
|
+
useEffect(() => {
|
|
80
|
+
const token = serverSession?.idpAccessToken || (session as any)?.accessToken;
|
|
81
|
+
if (token) {
|
|
82
|
+
setJwtHeader(decodeJwtHeader(token));
|
|
83
|
+
setJwtPayload(decodeJwtPayload(token));
|
|
84
|
+
}
|
|
85
|
+
}, [serverSession, session]);
|
|
86
|
+
|
|
87
|
+
const copyToClipboard = (text: string, label: string) => {
|
|
88
|
+
navigator.clipboard.writeText(text);
|
|
89
|
+
setCopied(label);
|
|
90
|
+
setTimeout(() => setCopied(null), 2000);
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
if (status === 'loading') {
|
|
94
|
+
return (
|
|
95
|
+
<div className={`min-h-screen p-8 ${isDarkMode ? 'bg-slate-950 text-white' : 'bg-gray-50 text-gray-900'}`}>
|
|
96
|
+
Loading session...
|
|
97
|
+
</div>
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if (status === 'unauthenticated') {
|
|
102
|
+
return (
|
|
103
|
+
<div className={`min-h-screen p-8 ${isDarkMode ? 'bg-slate-950 text-white' : 'bg-gray-50 text-gray-900'}`}>
|
|
104
|
+
<div className={`p-4 rounded-lg ${isDarkMode ? 'bg-red-900/30 border border-red-800' : 'bg-red-50 border border-red-200'}`}>
|
|
105
|
+
<h2 className="text-lg font-semibold mb-2">Not Authenticated</h2>
|
|
106
|
+
<p>Please log in to inspect session data.</p>
|
|
107
|
+
</div>
|
|
108
|
+
</div>
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Merge server-side enriched session with client session
|
|
113
|
+
const ss = serverSession || {};
|
|
114
|
+
const ext = session as any;
|
|
115
|
+
const user = {
|
|
116
|
+
id: ss.userId || ext?.user?.id,
|
|
117
|
+
email: ss.email || ext?.user?.email,
|
|
118
|
+
name: ss.name || ext?.user?.name,
|
|
119
|
+
roles: ss.roles || [],
|
|
120
|
+
oauthProvider: ss.oauthProvider,
|
|
121
|
+
idpClientId: ss.idpClientId,
|
|
122
|
+
merchantId: ss.merchantId,
|
|
123
|
+
mfaVerified: ss.mfaVerified,
|
|
124
|
+
requiresTwoFactor: false,
|
|
125
|
+
authenticationMethods: ss.authenticationMethods,
|
|
126
|
+
authenticationLevel: ss.authenticationLevel,
|
|
127
|
+
mfaCompletedAt: ss.mfaCompletedAt,
|
|
128
|
+
mfaExpiresAt: ss.mfaExpiresAt,
|
|
129
|
+
twoFactorSessionVerified: ss.mfaVerified,
|
|
130
|
+
};
|
|
131
|
+
// Token fields for display
|
|
132
|
+
const displayExt = {
|
|
133
|
+
...ext,
|
|
134
|
+
sessionToken: ss.sessionToken || ext?.sessionToken,
|
|
135
|
+
accessToken: ss.idpAccessToken || ext?.accessToken,
|
|
136
|
+
refreshToken: ss.idpRefreshToken || ext?.refreshToken,
|
|
137
|
+
accessTokenExpires: ss.idpAccessTokenExpires || ext?.accessTokenExpires,
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
// Card styling helpers
|
|
141
|
+
const cardClass = isDarkMode ? 'bg-slate-900 border border-slate-700' : 'bg-white border border-gray-200';
|
|
142
|
+
const labelClass = isDarkMode ? 'text-slate-400' : 'text-gray-500';
|
|
143
|
+
const valueClass = isDarkMode ? 'text-white' : 'text-gray-900';
|
|
144
|
+
|
|
145
|
+
return (
|
|
146
|
+
<div className={`min-h-screen p-8 ${isDarkMode ? 'bg-slate-950 text-white' : 'bg-gray-50 text-gray-900'}`}>
|
|
147
|
+
<div className="max-w-4xl mx-auto space-y-6">
|
|
148
|
+
<h1 className="text-2xl font-bold">Session Inspector</h1>
|
|
149
|
+
<p className={`text-sm ${labelClass}`}>
|
|
150
|
+
Better Auth session + IDP tokens from Redis
|
|
151
|
+
</p>
|
|
152
|
+
|
|
153
|
+
{/* User Identity */}
|
|
154
|
+
<div className={`p-4 rounded-lg ${cardClass}`}>
|
|
155
|
+
<h2 className="text-lg font-semibold mb-4">User Identity</h2>
|
|
156
|
+
<div className="grid grid-cols-1 md:grid-cols-2 gap-3 text-sm">
|
|
157
|
+
<InfoRow label="User ID" value={user.id} labelClass={labelClass} valueClass={valueClass} />
|
|
158
|
+
<InfoRow label="Email" value={user.email} labelClass={labelClass} valueClass={valueClass} />
|
|
159
|
+
<InfoRow label="Name" value={user.name} labelClass={labelClass} valueClass={valueClass} />
|
|
160
|
+
<InfoRow label="OAuth Provider" value={user.oauthProvider} labelClass={labelClass} valueClass={valueClass} />
|
|
161
|
+
<InfoRow label="IDP Client ID" value={user.idpClientId} labelClass={labelClass} valueClass={valueClass} />
|
|
162
|
+
<InfoRow label="Merchant ID" value={user.merchantId} labelClass={labelClass} valueClass={valueClass} />
|
|
163
|
+
</div>
|
|
164
|
+
</div>
|
|
165
|
+
|
|
166
|
+
{/* Roles */}
|
|
167
|
+
<div className={`p-4 rounded-lg ${isDarkMode ? 'bg-purple-900/30 border border-purple-700' : 'bg-purple-50 border border-purple-200'}`}>
|
|
168
|
+
<h2 className="text-lg font-semibold mb-4">Roles</h2>
|
|
169
|
+
{user.roles && user.roles.length > 0 ? (
|
|
170
|
+
<div className="flex flex-wrap gap-2">
|
|
171
|
+
{user.roles.map((role: string) => (
|
|
172
|
+
<span
|
|
173
|
+
key={role}
|
|
174
|
+
className={`px-3 py-1 rounded-full text-sm font-medium ${
|
|
175
|
+
isDarkMode ? 'bg-purple-800 text-purple-100' : 'bg-purple-200 text-purple-800'
|
|
176
|
+
}`}
|
|
177
|
+
>
|
|
178
|
+
{role}
|
|
179
|
+
</span>
|
|
180
|
+
))}
|
|
181
|
+
</div>
|
|
182
|
+
) : (
|
|
183
|
+
<p className={labelClass}>No roles assigned</p>
|
|
184
|
+
)}
|
|
185
|
+
</div>
|
|
186
|
+
|
|
187
|
+
{/* 2FA Status */}
|
|
188
|
+
<div className={`p-4 rounded-lg ${isDarkMode ? 'bg-yellow-900/30 border border-yellow-700' : 'bg-yellow-50 border border-yellow-200'}`}>
|
|
189
|
+
<h2 className="text-lg font-semibold mb-4">2FA Status</h2>
|
|
190
|
+
<div className="grid grid-cols-1 md:grid-cols-2 gap-3 text-sm">
|
|
191
|
+
<div>
|
|
192
|
+
<span className={labelClass}>2FA Verified:</span>{' '}
|
|
193
|
+
<StatusBadge
|
|
194
|
+
value={user.twoFactorSessionVerified}
|
|
195
|
+
trueText="Yes"
|
|
196
|
+
falseText="No"
|
|
197
|
+
isDarkMode={isDarkMode}
|
|
198
|
+
/>
|
|
199
|
+
</div>
|
|
200
|
+
<div>
|
|
201
|
+
<span className={labelClass}>Requires 2FA:</span>{' '}
|
|
202
|
+
<StatusBadge
|
|
203
|
+
value={user.requiresTwoFactor}
|
|
204
|
+
trueText="Yes"
|
|
205
|
+
falseText="No"
|
|
206
|
+
isDarkMode={isDarkMode}
|
|
207
|
+
invertColors
|
|
208
|
+
/>
|
|
209
|
+
</div>
|
|
210
|
+
<InfoRow
|
|
211
|
+
label="Auth Methods (AMR)"
|
|
212
|
+
value={user.authenticationMethods?.join(', ')}
|
|
213
|
+
labelClass={labelClass}
|
|
214
|
+
valueClass={valueClass}
|
|
215
|
+
/>
|
|
216
|
+
<InfoRow
|
|
217
|
+
label="Auth Level (ACR)"
|
|
218
|
+
value={user.authenticationLevel}
|
|
219
|
+
labelClass={labelClass}
|
|
220
|
+
valueClass={valueClass}
|
|
221
|
+
/>
|
|
222
|
+
<InfoRow
|
|
223
|
+
label="MFA Completed At"
|
|
224
|
+
value={user.mfaCompletedAt ? new Date(user.mfaCompletedAt).toISOString() : undefined}
|
|
225
|
+
labelClass={labelClass}
|
|
226
|
+
valueClass={valueClass}
|
|
227
|
+
/>
|
|
228
|
+
<InfoRow
|
|
229
|
+
label="MFA Expires At"
|
|
230
|
+
value={user.mfaExpiresAt ? new Date(user.mfaExpiresAt).toISOString() : undefined}
|
|
231
|
+
labelClass={labelClass}
|
|
232
|
+
valueClass={valueClass}
|
|
233
|
+
/>
|
|
234
|
+
</div>
|
|
235
|
+
</div>
|
|
236
|
+
|
|
237
|
+
{/* Tokens */}
|
|
238
|
+
<div className={`p-4 rounded-lg ${isDarkMode ? 'bg-blue-900/30 border border-blue-700' : 'bg-blue-50 border border-blue-200'}`}>
|
|
239
|
+
<h2 className="text-lg font-semibold mb-4">Tokens</h2>
|
|
240
|
+
<div className="space-y-4">
|
|
241
|
+
<div>
|
|
242
|
+
<div className="flex items-center justify-between mb-2">
|
|
243
|
+
<span className={labelClass}>Session Token (Redis Key):</span>
|
|
244
|
+
{displayExt.sessionToken && (
|
|
245
|
+
<CopyButton
|
|
246
|
+
onClick={() => copyToClipboard(displayExt.sessionToken, 'session')}
|
|
247
|
+
copied={copied === 'session'}
|
|
248
|
+
isDarkMode={isDarkMode}
|
|
249
|
+
/>
|
|
250
|
+
)}
|
|
251
|
+
</div>
|
|
252
|
+
<code className={`block p-2 rounded text-xs break-all ${isDarkMode ? 'bg-slate-800' : 'bg-gray-100'}`}>
|
|
253
|
+
{displayExt.sessionToken || 'N/A'}
|
|
254
|
+
</code>
|
|
255
|
+
</div>
|
|
256
|
+
|
|
257
|
+
<div>
|
|
258
|
+
<div className="flex items-center justify-between mb-2">
|
|
259
|
+
<span className={labelClass}>Access Token (IDP):</span>
|
|
260
|
+
{displayExt.accessToken && (
|
|
261
|
+
<CopyButton
|
|
262
|
+
onClick={() => copyToClipboard(displayExt.accessToken, 'access')}
|
|
263
|
+
copied={copied === 'access'}
|
|
264
|
+
isDarkMode={isDarkMode}
|
|
265
|
+
/>
|
|
266
|
+
)}
|
|
267
|
+
</div>
|
|
268
|
+
<code className={`block p-2 rounded text-xs break-all max-h-24 overflow-auto ${isDarkMode ? 'bg-slate-800' : 'bg-gray-100'}`}>
|
|
269
|
+
{displayExt.accessToken || 'N/A'}
|
|
270
|
+
</code>
|
|
271
|
+
</div>
|
|
272
|
+
|
|
273
|
+
<div className="grid grid-cols-1 md:grid-cols-2 gap-3 text-sm">
|
|
274
|
+
<div>
|
|
275
|
+
<span className={labelClass}>Has Refresh Token:</span>{' '}
|
|
276
|
+
<StatusBadge
|
|
277
|
+
value={!!displayExt.refreshToken}
|
|
278
|
+
trueText="Yes"
|
|
279
|
+
falseText="No"
|
|
280
|
+
isDarkMode={isDarkMode}
|
|
281
|
+
/>
|
|
282
|
+
</div>
|
|
283
|
+
<InfoRow
|
|
284
|
+
label="Access Token Expires"
|
|
285
|
+
value={displayExt.accessTokenExpires ? new Date(displayExt.accessTokenExpires).toISOString() : undefined}
|
|
286
|
+
labelClass={labelClass}
|
|
287
|
+
valueClass={valueClass}
|
|
288
|
+
/>
|
|
289
|
+
</div>
|
|
290
|
+
</div>
|
|
291
|
+
</div>
|
|
292
|
+
|
|
293
|
+
{/* JWT Header (contains kid for key lookup) */}
|
|
294
|
+
{jwtHeader && (
|
|
295
|
+
<div className={`p-4 rounded-lg ${isDarkMode ? 'bg-orange-900/30 border border-orange-700' : 'bg-orange-50 border border-orange-200'}`}>
|
|
296
|
+
<h2 className="text-lg font-semibold mb-4">JWT Header</h2>
|
|
297
|
+
<div className="grid grid-cols-1 md:grid-cols-2 gap-3 text-sm">
|
|
298
|
+
<InfoRow label="Algorithm (alg)" value={jwtHeader.alg} labelClass={labelClass} valueClass={valueClass} />
|
|
299
|
+
<InfoRow label="Type (typ)" value={jwtHeader.typ} labelClass={labelClass} valueClass={valueClass} />
|
|
300
|
+
<div>
|
|
301
|
+
<span className={labelClass}>Key ID (kid):</span>{' '}
|
|
302
|
+
<span className={`font-mono ${jwtHeader.kid ? (isDarkMode ? 'text-green-400' : 'text-green-600') : (isDarkMode ? 'text-red-400' : 'text-red-600')}`}>
|
|
303
|
+
{jwtHeader.kid || 'NOT PRESENT'}
|
|
304
|
+
</span>
|
|
305
|
+
</div>
|
|
306
|
+
{Object.entries(jwtHeader)
|
|
307
|
+
.filter(([key]) => !['alg', 'typ', 'kid'].includes(key))
|
|
308
|
+
.map(([key, value]) => (
|
|
309
|
+
<InfoRow
|
|
310
|
+
key={key}
|
|
311
|
+
label={key}
|
|
312
|
+
value={typeof value === 'object' ? JSON.stringify(value) : String(value)}
|
|
313
|
+
labelClass={labelClass}
|
|
314
|
+
valueClass={valueClass}
|
|
315
|
+
/>
|
|
316
|
+
))
|
|
317
|
+
}
|
|
318
|
+
</div>
|
|
319
|
+
</div>
|
|
320
|
+
)}
|
|
321
|
+
|
|
322
|
+
{/* JWT Payload Claims */}
|
|
323
|
+
{jwtPayload && (
|
|
324
|
+
<div className={`p-4 rounded-lg ${isDarkMode ? 'bg-green-900/30 border border-green-700' : 'bg-green-50 border border-green-200'}`}>
|
|
325
|
+
<h2 className="text-lg font-semibold mb-4">JWT Payload Claims</h2>
|
|
326
|
+
<div className="grid grid-cols-1 md:grid-cols-2 gap-3 text-sm">
|
|
327
|
+
<InfoRow label="Subject (sub)" value={jwtPayload.sub} labelClass={labelClass} valueClass={valueClass} />
|
|
328
|
+
<InfoRow label="Issuer (iss)" value={jwtPayload.iss} labelClass={labelClass} valueClass={valueClass} />
|
|
329
|
+
<InfoRow label="Audience (aud)" value={Array.isArray(jwtPayload.aud) ? jwtPayload.aud.join(', ') : jwtPayload.aud} labelClass={labelClass} valueClass={valueClass} />
|
|
330
|
+
<InfoRow label="Client ID" value={jwtPayload.client_id} labelClass={labelClass} valueClass={valueClass} />
|
|
331
|
+
<InfoRow
|
|
332
|
+
label="Expires (exp)"
|
|
333
|
+
value={jwtPayload.exp ? new Date(jwtPayload.exp * 1000).toISOString() : undefined}
|
|
334
|
+
labelClass={labelClass}
|
|
335
|
+
valueClass={valueClass}
|
|
336
|
+
/>
|
|
337
|
+
<InfoRow
|
|
338
|
+
label="Issued At (iat)"
|
|
339
|
+
value={jwtPayload.iat ? new Date(jwtPayload.iat * 1000).toISOString() : undefined}
|
|
340
|
+
labelClass={labelClass}
|
|
341
|
+
valueClass={valueClass}
|
|
342
|
+
/>
|
|
343
|
+
<InfoRow
|
|
344
|
+
label="AMR (Auth Methods)"
|
|
345
|
+
value={Array.isArray(jwtPayload.amr) ? jwtPayload.amr.join(', ') : jwtPayload.amr}
|
|
346
|
+
labelClass={labelClass}
|
|
347
|
+
valueClass={valueClass}
|
|
348
|
+
/>
|
|
349
|
+
<InfoRow label="ACR (Auth Context)" value={jwtPayload.acr} labelClass={labelClass} valueClass={valueClass} />
|
|
350
|
+
</div>
|
|
351
|
+
</div>
|
|
352
|
+
)}
|
|
353
|
+
|
|
354
|
+
{/* Session Expiry */}
|
|
355
|
+
<div className={`p-4 rounded-lg ${cardClass}`}>
|
|
356
|
+
<h2 className="text-lg font-semibold mb-4">Session Metadata</h2>
|
|
357
|
+
<div className="grid grid-cols-1 md:grid-cols-2 gap-3 text-sm">
|
|
358
|
+
<InfoRow
|
|
359
|
+
label="Session Expires"
|
|
360
|
+
value={(session as any)?.expires ? new Date((session as any).expires).toISOString() : undefined}
|
|
361
|
+
labelClass={labelClass}
|
|
362
|
+
valueClass={valueClass}
|
|
363
|
+
/>
|
|
364
|
+
<InfoRow
|
|
365
|
+
label="Error"
|
|
366
|
+
value={ext.error}
|
|
367
|
+
labelClass={labelClass}
|
|
368
|
+
valueClass={ext.error ? 'text-red-500' : valueClass}
|
|
369
|
+
/>
|
|
370
|
+
</div>
|
|
371
|
+
</div>
|
|
372
|
+
|
|
373
|
+
{/* Raw Session Data */}
|
|
374
|
+
<details className={`p-4 rounded-lg ${cardClass}`}>
|
|
375
|
+
<summary className="font-semibold cursor-pointer">Raw Session Data (Click to expand)</summary>
|
|
376
|
+
<pre className={`mt-4 text-xs overflow-auto p-3 rounded ${isDarkMode ? 'bg-slate-800' : 'bg-gray-100'}`}>
|
|
377
|
+
{JSON.stringify({ clientSession: session, serverSession: serverSession }, null, 2)}
|
|
378
|
+
</pre>
|
|
379
|
+
</details>
|
|
380
|
+
</div>
|
|
381
|
+
</div>
|
|
382
|
+
);
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
// Helper Components
|
|
386
|
+
|
|
387
|
+
function InfoRow({
|
|
388
|
+
label,
|
|
389
|
+
value,
|
|
390
|
+
labelClass,
|
|
391
|
+
valueClass,
|
|
392
|
+
}: {
|
|
393
|
+
label: string;
|
|
394
|
+
value?: string | null;
|
|
395
|
+
labelClass: string;
|
|
396
|
+
valueClass: string;
|
|
397
|
+
}) {
|
|
398
|
+
return (
|
|
399
|
+
<div>
|
|
400
|
+
<span className={labelClass}>{label}:</span>{' '}
|
|
401
|
+
<span className={valueClass}>{value || 'N/A'}</span>
|
|
402
|
+
</div>
|
|
403
|
+
);
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
function StatusBadge({
|
|
407
|
+
value,
|
|
408
|
+
trueText,
|
|
409
|
+
falseText,
|
|
410
|
+
isDarkMode,
|
|
411
|
+
invertColors = false,
|
|
412
|
+
}: {
|
|
413
|
+
value: boolean;
|
|
414
|
+
trueText: string;
|
|
415
|
+
falseText: string;
|
|
416
|
+
isDarkMode: boolean;
|
|
417
|
+
invertColors?: boolean;
|
|
418
|
+
}) {
|
|
419
|
+
const isPositive = invertColors ? !value : value;
|
|
420
|
+
const colorClass = isPositive
|
|
421
|
+
? isDarkMode ? 'bg-green-800 text-green-100' : 'bg-green-200 text-green-800'
|
|
422
|
+
: isDarkMode ? 'bg-red-800 text-red-100' : 'bg-red-200 text-red-800';
|
|
423
|
+
|
|
424
|
+
return (
|
|
425
|
+
<span className={`px-2 py-0.5 rounded text-xs font-medium ${colorClass}`}>
|
|
426
|
+
{value ? trueText : falseText}
|
|
427
|
+
</span>
|
|
428
|
+
);
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
function CopyButton({
|
|
432
|
+
onClick,
|
|
433
|
+
copied,
|
|
434
|
+
isDarkMode,
|
|
435
|
+
}: {
|
|
436
|
+
onClick: () => void;
|
|
437
|
+
copied: boolean;
|
|
438
|
+
isDarkMode: boolean;
|
|
439
|
+
}) {
|
|
440
|
+
return (
|
|
441
|
+
<button
|
|
442
|
+
onClick={onClick}
|
|
443
|
+
className={`px-2 py-1 rounded text-xs ${
|
|
444
|
+
copied
|
|
445
|
+
? 'bg-green-600 text-white'
|
|
446
|
+
: isDarkMode
|
|
447
|
+
? 'bg-slate-700 hover:bg-slate-600 text-white'
|
|
448
|
+
: 'bg-gray-200 hover:bg-gray-300 text-gray-700'
|
|
449
|
+
}`}
|
|
450
|
+
>
|
|
451
|
+
{copied ? 'Copied!' : 'Copy'}
|
|
452
|
+
</button>
|
|
453
|
+
);
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
export default JwtInspectPage;
|