@payez/next-mvp 4.1.21 → 4.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/POST-INSTALL.md +408 -0
- package/README.md +7 -0
- package/dist/api-handlers/account/recovery/initiate.js +35 -2
- package/dist/api-handlers/account/recovery/send-code.d.ts +0 -33
- package/dist/api-handlers/account/recovery/send-code.js +35 -2
- package/dist/api-handlers/account/recovery/verify-code.d.ts +0 -33
- package/dist/api-handlers/account/recovery/verify-code.js +35 -2
- package/dist/api-handlers/account/reset-password.js +35 -2
- package/dist/api-handlers/account/validate-password.js +21 -3
- package/dist/api-handlers/admin/analytics.js +2 -1
- package/dist/api-handlers/admin/audit.js +2 -1
- package/dist/api-handlers/admin/sessions.js +2 -1
- package/dist/api-handlers/admin/stats.js +2 -1
- package/dist/api-handlers/admin/users.js +2 -1
- package/dist/api-handlers/admin/vibe-data.js +2 -1
- package/dist/api-handlers/auth/jwks.js +10 -0
- package/dist/api-handlers/auth/login.js +5 -2
- package/dist/api-handlers/auth/passkey-signin.js +16 -1
- package/dist/api-handlers/auth/refresh.d.ts +23 -3
- package/dist/api-handlers/auth/refresh.js +107 -16
- package/dist/api-handlers/auth/signout.js +3 -2
- package/dist/api-handlers/auth/verify-code.js +5 -2
- package/dist/api-handlers/session/viability.js +27 -1
- package/dist/api-handlers/test/force-expire.d.ts +29 -15
- package/dist/api-handlers/test/force-expire.js +109 -14
- package/dist/auth/better-auth.d.ts +263 -6
- package/dist/auth/better-auth.js +437 -59
- package/dist/auth/utils/idp-client.js +1 -1
- package/dist/client/AuthContext.js +49 -17
- package/dist/client/better-auth-client.d.ts +62 -62
- package/dist/components/account/UserAvatarMenu.js +12 -0
- package/dist/components/auth/ModeAwareLoginPage.js +10 -8
- package/dist/components/auth/ModeAwareSignupPage.js +5 -2
- package/dist/components/auth/TraditionalAuthSection.js +5 -1
- package/dist/hooks/useAuthSettings.js +14 -2
- package/dist/hooks/usePublicAuthSettings.d.ts +2 -2
- package/dist/hooks/usePublicAuthSettings.js +28 -14
- package/dist/lib/client-key.d.ts +61 -0
- package/dist/lib/client-key.js +72 -0
- package/dist/lib/demo-mode.js +2 -1
- package/dist/lib/ensure-fresh-access-token.js +5 -1
- package/dist/lib/idp-client-config.d.ts +25 -2
- package/dist/lib/idp-client-config.js +381 -36
- package/dist/lib/idp-fetch.d.ts +2 -0
- package/dist/lib/idp-fetch.js +31 -1
- package/dist/lib/idp-url-guard.d.ts +29 -0
- package/dist/lib/idp-url-guard.js +38 -0
- package/dist/lib/redis.js +77 -5
- package/dist/lib/session-cookie-shape.d.ts +69 -0
- package/dist/lib/session-cookie-shape.js +126 -0
- package/dist/lib/session-store.d.ts +35 -2
- package/dist/lib/session-store.js +135 -4
- package/dist/lib/single-flight.d.ts +35 -0
- package/dist/lib/single-flight.js +30 -0
- package/dist/lib/token-lifecycle.js +12 -0
- package/dist/middleware/create-middleware.js +12 -0
- package/dist/models/SessionModel.d.ts +41 -0
- package/dist/models/SessionModel.js +9 -0
- package/dist/pages/admin-login/page.d.ts +1 -1
- package/dist/pages/admin-login/page.js +5 -41
- package/dist/pages/security/EnhancedSecurityPage.d.ts +35 -1
- package/dist/pages/security/EnhancedSecurityPage.js +42 -2
- package/dist/pages/security/index.d.ts +1 -0
- package/dist/pages/security/index.js +3 -0
- package/dist/pages/test-env/EmergencyLogoutPage.d.ts +17 -5
- package/dist/pages/test-env/EmergencyLogoutPage.js +17 -5
- package/dist/pages/test-env/JwtInspectPage.d.ts +30 -5
- package/dist/pages/test-env/JwtInspectPage.js +166 -7
- package/dist/pages/test-env/RefreshTokenPage.d.ts +22 -6
- package/dist/pages/test-env/RefreshTokenPage.js +22 -6
- package/dist/pages/test-env/TestEnvPage.d.ts +16 -4
- package/dist/pages/test-env/TestEnvPage.js +16 -4
- package/dist/pages/test-env/guarded.d.ts +4 -0
- package/dist/pages/test-env/guarded.js +86 -0
- package/dist/pages/test-env/index.d.ts +50 -7
- package/dist/pages/test-env/index.js +58 -12
- package/dist/routes/auth/logout.js +3 -2
- package/dist/routes/auth/refresh.js +27 -1
- package/dist/routes/auth/session.d.ts +12 -3
- package/dist/routes/auth/session.js +92 -9
- package/dist/routes/auth/settings.d.ts +34 -4
- package/dist/routes/auth/settings.js +87 -33
- package/dist/routes/auth/viability.d.ts +1 -1
- package/dist/routes/auth/viability.js +122 -11
- package/dist/server/auth-guard.js +22 -15
- package/dist/server/auth.d.ts +177 -20
- package/dist/server/auth.js +34 -75
- package/dist/server/decode-session.js +84 -10
- package/dist/types/auth.d.ts +18 -0
- package/package.json +969 -958
- package/scripts/check-internal-url-usage.sh +73 -73
- package/scripts/check-publish-health.mjs +120 -0
- package/scripts/dev-broker.ps1 +71 -6
- package/scripts/print-client-config.mjs +162 -0
- package/scripts/server-only-stub.cjs +4 -0
- package/src/api-handlers/account/recovery/initiate.ts +35 -2
- package/src/api-handlers/account/recovery/send-code.ts +36 -2
- package/src/api-handlers/account/recovery/verify-code.ts +36 -2
- package/src/api-handlers/account/reset-password.ts +35 -2
- package/src/api-handlers/account/validate-password.ts +22 -3
- package/src/api-handlers/admin/analytics.ts +2 -1
- package/src/api-handlers/admin/audit.ts +2 -1
- package/src/api-handlers/admin/sessions.ts +2 -1
- package/src/api-handlers/admin/stats.ts +2 -1
- package/src/api-handlers/admin/users.ts +2 -1
- package/src/api-handlers/admin/vibe-data.ts +2 -1
- package/src/api-handlers/auth/jwks.ts +10 -0
- package/src/api-handlers/auth/login.ts +5 -2
- package/src/api-handlers/auth/passkey-signin.ts +112 -98
- package/src/api-handlers/auth/refresh.ts +112 -15
- package/src/api-handlers/auth/signout.ts +4 -3
- package/src/api-handlers/auth/verify-code.ts +5 -2
- package/src/api-handlers/mfa/backup-code-verify.ts +175 -175
- package/src/api-handlers/mfa/backup-codes.ts +41 -41
- package/src/api-handlers/mfa/methods.ts +46 -46
- package/src/api-handlers/mfa/totp-confirm.ts +42 -42
- package/src/api-handlers/mfa/totp-enroll.ts +42 -42
- package/src/api-handlers/passkeys/list.ts +31 -31
- package/src/api-handlers/passkeys/manage.ts +54 -54
- package/src/api-handlers/passkeys/register-options.ts +42 -42
- package/src/api-handlers/passkeys/register-verify.ts +41 -41
- package/src/api-handlers/session/viability.ts +29 -1
- package/src/api-handlers/test/force-expire.ts +118 -13
- package/src/auth/better-auth.ts +486 -51
- package/src/auth/utils/idp-client.ts +1 -1
- package/src/client/AuthContext.tsx +55 -19
- package/src/components/account/UserAvatarMenu.tsx +11 -0
- package/src/components/auth/ModeAwareLoginPage.tsx +40 -49
- package/src/components/auth/ModeAwareSignupPage.tsx +5 -2
- package/src/components/auth/TraditionalAuthSection.tsx +5 -1
- package/src/hooks/useAuthSettings.ts +14 -2
- package/src/hooks/usePublicAuthSettings.ts +38 -16
- package/src/lib/client-key.ts +88 -0
- package/src/lib/demo-mode.ts +2 -1
- package/src/lib/ensure-fresh-access-token.ts +5 -1
- package/src/lib/idp-client-config.ts +399 -36
- package/src/lib/idp-fetch.ts +36 -1
- package/src/lib/idp-url-guard.ts +37 -0
- package/src/lib/redis.ts +84 -6
- package/src/lib/session-cookie-shape.ts +184 -0
- package/src/lib/session-store.ts +152 -6
- package/src/lib/single-flight.ts +64 -0
- package/src/lib/token-lifecycle.ts +12 -0
- package/src/middleware/create-middleware.ts +11 -0
- package/src/models/SessionModel.ts +49 -0
- package/src/pages/admin-login/page.tsx +17 -127
- package/src/pages/security/EnhancedSecurityPage.tsx +95 -9
- package/src/pages/security/index.ts +3 -0
- package/src/pages/test-env/EmergencyLogoutPage.tsx +17 -5
- package/src/pages/test-env/JwtInspectPage.tsx +235 -20
- package/src/pages/test-env/RefreshTokenPage.tsx +22 -6
- package/src/pages/test-env/TestEnvPage.tsx +16 -4
- package/src/pages/test-env/guarded.tsx +81 -0
- package/src/pages/test-env/index.ts +59 -7
- package/src/routes/auth/logout.ts +4 -3
- package/src/routes/auth/passkey-signin.ts +13 -13
- package/src/routes/auth/refresh.ts +30 -1
- package/src/routes/auth/session.ts +98 -9
- package/src/routes/auth/settings.ts +144 -42
- package/src/routes/auth/viability.ts +129 -11
- 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/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/server/auth-guard.ts +24 -17
- package/src/server/auth.ts +37 -73
- package/src/server/decode-session.ts +95 -11
- package/src/templates/instrumentation.ts +21 -2
- package/src/types/auth.ts +20 -0
- package/dist/lib/auth-secret.d.ts +0 -17
- package/dist/lib/auth-secret.js +0 -116
- package/src/lib/auth-secret.ts +0 -137
|
@@ -77,6 +77,46 @@ export interface SessionData {
|
|
|
77
77
|
/** Whether MFA has been verified for this session */
|
|
78
78
|
mfaVerified: boolean;
|
|
79
79
|
|
|
80
|
+
/**
|
|
81
|
+
* *** R807 (BAPert, card 362550). DID THE PRODUCER ACTUALLY SAY ANYTHING ABOUT MFA? ***
|
|
82
|
+
*
|
|
83
|
+
* `mfaVerified` above is deliberately FAIL-CLOSED: every construction site
|
|
84
|
+
* collapses an absent value to `false` (`?? false`), so by the time anyone
|
|
85
|
+
* reads it, "the producer said false" and "the producer said nothing" are
|
|
86
|
+
* indistinguishable. That collapse is CORRECT and must not be removed - a
|
|
87
|
+
* security consumer treating absence as "verified" is an AUTH BYPASS.
|
|
88
|
+
*
|
|
89
|
+
* But `routes/auth/viability.ts` asks a genuinely different question: "did the
|
|
90
|
+
* OAuth callback AFFIRMATIVELY set false, or did we simply never know?" It
|
|
91
|
+
* cannot ask that of a field whose absence has already been collapsed. This
|
|
92
|
+
* field carries that one bit, so the collapse can stay.
|
|
93
|
+
*
|
|
94
|
+
* true -> the producer emitted an explicit mfaVerified; `mfaVerified` is a measurement.
|
|
95
|
+
* false -> the producer emitted nothing; `mfaVerified`'s `false` is a DEFAULT, not a signal.
|
|
96
|
+
* undefined -> LEGACY SESSION, minted before this field existed. Same meaning as `false`
|
|
97
|
+
* for viability (do NOT force a re-challenge), and it is why this is
|
|
98
|
+
* optional rather than defaulted: `?? false` here would be the very
|
|
99
|
+
* absence-destroying move this field exists to undo.
|
|
100
|
+
*
|
|
101
|
+
* *** CONFINED, BY RULING: THIS FIELD MUST NEVER REACH AN AUTH DECISION. ***
|
|
102
|
+
* It exists to make viability LESS aggressive (suppressing a mass re-challenge of
|
|
103
|
+
* the pre-existing session population). Letting it influence an auth path would
|
|
104
|
+
* turn a permissive read into a fail-open, and R807 is void if it does. Gate on
|
|
105
|
+
* `mfaVerified` for anything that grants access; gate on this only to decide
|
|
106
|
+
* whether `mfaVerified === false` was a real statement.
|
|
107
|
+
*
|
|
108
|
+
* *** DECLARING IT HERE IS LOAD-BEARING FOR MORE THAN DOCUMENTATION. ***
|
|
109
|
+
* `SessionData` carries `[key: string]: any` (the "backward compatibility"
|
|
110
|
+
* member at the END of this interface, added for migration back-compat — it
|
|
111
|
+
* was line 147 before this block was inserted above it, which is the number
|
|
112
|
+
* quoted in R807/362550; adding these lines moved it down, so find it by name,
|
|
113
|
+
* not by number). Any field NOT declared explicitly is absorbed by that index
|
|
114
|
+
* signature and becomes `any` - so an undeclared `mfaSignalPresent` would let a
|
|
115
|
+
* typo at the consumer compile clean and read `undefined` forever, silently
|
|
116
|
+
* disabling the viability override. Measured, not assumed: see the card.
|
|
117
|
+
*/
|
|
118
|
+
mfaSignalPresent?: boolean;
|
|
119
|
+
|
|
80
120
|
/**
|
|
81
121
|
* The IDP's own routing decision for a 2FA-pending login (cards 141688 /
|
|
82
122
|
* 141741, IDP WO 113023). Wire name is snake — `next_step`.
|
|
@@ -175,6 +215,8 @@ export class SessionModel {
|
|
|
175
215
|
|
|
176
216
|
// MFA State
|
|
177
217
|
mfaVerified: boolean;
|
|
218
|
+
/** R807: see SessionData.mfaSignalPresent. Propagated verbatim, never defaulted. */
|
|
219
|
+
mfaSignalPresent?: boolean;
|
|
178
220
|
mfaMethod?: 'email' | 'sms' | 'totp';
|
|
179
221
|
mfaCompletedAt?: number;
|
|
180
222
|
mfaExpiresAt?: number;
|
|
@@ -209,6 +251,10 @@ export class SessionModel {
|
|
|
209
251
|
|
|
210
252
|
// MFA State
|
|
211
253
|
this.mfaVerified = data.mfaVerified ?? false;
|
|
254
|
+
// *** R807: PROPAGATE, DO NOT DEFAULT. *** `?? false` here would destroy the
|
|
255
|
+
// legacy/never-knew distinction on every path through this class - invisibly,
|
|
256
|
+
// because callers construct a SessionModel without restating the field.
|
|
257
|
+
this.mfaSignalPresent = data.mfaSignalPresent;
|
|
212
258
|
this.mfaMethod = data.mfaMethod;
|
|
213
259
|
this.mfaCompletedAt = data.mfaCompletedAt;
|
|
214
260
|
this.mfaExpiresAt = data.mfaExpiresAt;
|
|
@@ -266,6 +312,9 @@ export class SessionModel {
|
|
|
266
312
|
decodedAccessToken: this.decodedAccessToken,
|
|
267
313
|
bearerKeyId: this.bearerKeyId,
|
|
268
314
|
mfaVerified: this.mfaVerified,
|
|
315
|
+
// R807: must survive the Redis round-trip, or the session is re-read as
|
|
316
|
+
// `legacy` one hop later and viability silently stops firing.
|
|
317
|
+
mfaSignalPresent: this.mfaSignalPresent,
|
|
269
318
|
mfaMethod: this.mfaMethod,
|
|
270
319
|
mfaCompletedAt: this.mfaCompletedAt,
|
|
271
320
|
mfaExpiresAt: this.mfaExpiresAt,
|
|
@@ -16,9 +16,7 @@
|
|
|
16
16
|
|
|
17
17
|
'use client';
|
|
18
18
|
|
|
19
|
-
import React
|
|
20
|
-
import { authClient } from '../../client/better-auth-client';
|
|
21
|
-
import { useSearchParams } from 'next/navigation';
|
|
19
|
+
import React from 'react';
|
|
22
20
|
import { Suspense } from 'react';
|
|
23
21
|
import { useBranding, useColors } from '../../theme/useTheme';
|
|
24
22
|
|
|
@@ -36,48 +34,12 @@ interface AdminLoginFormProps {
|
|
|
36
34
|
function AdminLoginForm({
|
|
37
35
|
title = 'Admin Login',
|
|
38
36
|
subtitle = 'Authorized personnel only',
|
|
39
|
-
callbackUrl: propCallbackUrl,
|
|
40
37
|
logo,
|
|
41
38
|
}: AdminLoginFormProps) {
|
|
42
|
-
const searchParams = useSearchParams();
|
|
43
|
-
const callbackUrl = propCallbackUrl || searchParams?.get('callbackUrl') || '/dashboard';
|
|
44
|
-
|
|
45
39
|
const branding = useBranding();
|
|
46
40
|
const colors = useColors();
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
const [password, setPassword] = useState('');
|
|
50
|
-
const [showPassword, setShowPassword] = useState(false);
|
|
51
|
-
const [isLoading, setIsLoading] = useState(false);
|
|
52
|
-
const [error, setError] = useState<string | null>(null);
|
|
53
|
-
|
|
54
|
-
const handleSubmit = async (e: React.FormEvent) => {
|
|
55
|
-
e.preventDefault();
|
|
56
|
-
setIsLoading(true);
|
|
57
|
-
setError(null);
|
|
58
|
-
|
|
59
|
-
try {
|
|
60
|
-
const result = await authClient.signIn.email({
|
|
61
|
-
email,
|
|
62
|
-
password,
|
|
63
|
-
callbackURL: callbackUrl,
|
|
64
|
-
});
|
|
65
|
-
|
|
66
|
-
if ((result as any)?.error) {
|
|
67
|
-
const errorMsg = typeof (result as any).error === 'object'
|
|
68
|
-
? ((result as any).error as any).message || 'Invalid credentials'
|
|
69
|
-
: String((result as any).error);
|
|
70
|
-
setError(errorMsg);
|
|
71
|
-
} else if (result?.data) {
|
|
72
|
-
// Redirect to verify-code for 2FA or directly to callback
|
|
73
|
-
window.location.href = `/account-auth/verify-code?callbackUrl=${encodeURIComponent(callbackUrl)}`;
|
|
74
|
-
}
|
|
75
|
-
} catch (err) {
|
|
76
|
-
setError('An unexpected error occurred');
|
|
77
|
-
} finally {
|
|
78
|
-
setIsLoading(false);
|
|
79
|
-
}
|
|
80
|
-
};
|
|
41
|
+
void branding;
|
|
42
|
+
void colors;
|
|
81
43
|
|
|
82
44
|
return (
|
|
83
45
|
<div className="min-h-screen flex items-center justify-center bg-gradient-to-br from-slate-700 via-slate-800 to-slate-900 p-4">
|
|
@@ -97,92 +59,20 @@ function AdminLoginForm({
|
|
|
97
59
|
{subtitle}
|
|
98
60
|
</p>
|
|
99
61
|
|
|
100
|
-
{/*
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
Email
|
|
115
|
-
</label>
|
|
116
|
-
<input
|
|
117
|
-
id="admin-email"
|
|
118
|
-
type="email"
|
|
119
|
-
value={email}
|
|
120
|
-
onChange={(e) => setEmail(e.target.value)}
|
|
121
|
-
required
|
|
122
|
-
disabled={isLoading}
|
|
123
|
-
autoComplete="email"
|
|
124
|
-
className="w-full px-4 py-3 rounded-lg border border-slate-300 bg-white text-slate-900 placeholder-slate-400 focus:outline-none focus:ring-2 focus:ring-slate-500 focus:border-transparent disabled:opacity-50 disabled:cursor-not-allowed"
|
|
125
|
-
placeholder="admin@example.com"
|
|
126
|
-
/>
|
|
127
|
-
</div>
|
|
128
|
-
|
|
129
|
-
<div>
|
|
130
|
-
<label
|
|
131
|
-
htmlFor="admin-password"
|
|
132
|
-
className="block text-sm font-medium mb-2 text-slate-700"
|
|
133
|
-
>
|
|
134
|
-
Password
|
|
135
|
-
</label>
|
|
136
|
-
<div className="relative">
|
|
137
|
-
<input
|
|
138
|
-
id="admin-password"
|
|
139
|
-
type={showPassword ? 'text' : 'password'}
|
|
140
|
-
value={password}
|
|
141
|
-
onChange={(e) => setPassword(e.target.value)}
|
|
142
|
-
required
|
|
143
|
-
disabled={isLoading}
|
|
144
|
-
autoComplete="current-password"
|
|
145
|
-
className="w-full px-4 py-3 pr-12 rounded-lg border border-slate-300 bg-white text-slate-900 placeholder-slate-400 focus:outline-none focus:ring-2 focus:ring-slate-500 focus:border-transparent disabled:opacity-50 disabled:cursor-not-allowed"
|
|
146
|
-
placeholder="Enter your password"
|
|
147
|
-
/>
|
|
148
|
-
<button
|
|
149
|
-
type="button"
|
|
150
|
-
onClick={() => setShowPassword(!showPassword)}
|
|
151
|
-
className="absolute right-3 top-1/2 transform -translate-y-1/2 text-slate-500 hover:text-slate-700"
|
|
152
|
-
aria-label={showPassword ? 'Hide password' : 'Show password'}
|
|
153
|
-
>
|
|
154
|
-
{showPassword ? (
|
|
155
|
-
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" className="w-5 h-5">
|
|
156
|
-
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13.875 18.825A10.05 10.05 0 0112 19c-4.478 0-8.268-2.943-9.543-7a9.97 9.97 0 011.563-3.029m5.858.908a3 3 0 114.243 4.243M9.878 9.878l4.242 4.242M9.878 9.878L6.464 6.464m7.535 7.535l3.415 3.414M3 3l3.464 3.464M21 21l-3.415-3.414" />
|
|
157
|
-
</svg>
|
|
158
|
-
) : (
|
|
159
|
-
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" className="w-5 h-5">
|
|
160
|
-
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
|
|
161
|
-
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z" />
|
|
162
|
-
</svg>
|
|
163
|
-
)}
|
|
164
|
-
</button>
|
|
165
|
-
</div>
|
|
166
|
-
</div>
|
|
167
|
-
|
|
168
|
-
<button
|
|
169
|
-
type="submit"
|
|
170
|
-
disabled={isLoading}
|
|
171
|
-
className="w-full py-3 px-4 rounded-lg font-semibold text-white transition-colors bg-slate-700 hover:bg-slate-800 disabled:bg-slate-400 disabled:cursor-not-allowed"
|
|
172
|
-
>
|
|
173
|
-
{isLoading ? (
|
|
174
|
-
<span className="flex items-center justify-center">
|
|
175
|
-
<svg className="animate-spin -ml-1 mr-3 h-5 w-5 text-white" fill="none" viewBox="0 0 24 24">
|
|
176
|
-
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" />
|
|
177
|
-
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z" />
|
|
178
|
-
</svg>
|
|
179
|
-
Signing in...
|
|
180
|
-
</span>
|
|
181
|
-
) : (
|
|
182
|
-
'Sign In'
|
|
183
|
-
)}
|
|
184
|
-
</button>
|
|
185
|
-
</form>
|
|
62
|
+
{/* EMAIL/PASSWORD FORM REMOVED — same root cause as pages/login (see that
|
|
63
|
+
file's identical note): `emailAndPassword` appears ZERO times in
|
|
64
|
+
better-auth.ts, so authClient.signIn.email always returned "Email and
|
|
65
|
+
password is not enabled". Unlike pages/login, THIS page's form was never
|
|
66
|
+
removed when that happened — it kept accepting input and 400ing on every
|
|
67
|
+
submit, real credentials included (QAPert 32622, live-verified against a
|
|
68
|
+
seeded TOTP fixture 2026-08-22). The 222363 accessibility work made the
|
|
69
|
+
error STATE correct; it never made the endpoint real — a dead door that
|
|
70
|
+
looks alive is worse than an absent one (BAPert 32626). Removed 4.1.31.
|
|
71
|
+
To ADD real admin password auth: wire this page to the IDP's password
|
|
72
|
+
endpoint directly (the same one the graft's auth-flow BFF used to call,
|
|
73
|
+
or its package-pattern successor per Jon's B ruling, 32627) — NOT
|
|
74
|
+
authClient.signIn.email, which has never worked and was never wired to
|
|
75
|
+
anything. Restore a form only together with that wiring. */}
|
|
186
76
|
|
|
187
77
|
{/* Security notice */}
|
|
188
78
|
<p className="mt-6 text-center text-xs text-slate-500">
|
|
@@ -10,6 +10,13 @@
|
|
|
10
10
|
* - Danger zone (data export, account deletion)
|
|
11
11
|
*
|
|
12
12
|
* @see docs/specs/MEMBER_SELF_SERVICE_SPEC.md
|
|
13
|
+
*
|
|
14
|
+
* @deprecated RETIRED (BAPert ruling, card 221074, 2026-08-18). The canonical,
|
|
15
|
+
* maintained copy is graft-local:
|
|
16
|
+
* idp_admin_ui_graft/src/app/account/security/EnhancedSecurityPage.tsx.
|
|
17
|
+
* What stage serves is what was scored against Clerk; this package copy was
|
|
18
|
+
* imported by nothing in the graft and receives NO further fixes. Do not
|
|
19
|
+
* import it into new work.
|
|
13
20
|
*/
|
|
14
21
|
|
|
15
22
|
'use client';
|
|
@@ -195,10 +202,53 @@ function formatRelativeTime(dateString?: string): string {
|
|
|
195
202
|
} catch { return dateString; }
|
|
196
203
|
}
|
|
197
204
|
|
|
198
|
-
export
|
|
205
|
+
export interface EnhancedSecurityPageProps {
|
|
206
|
+
/**
|
|
207
|
+
* 152745. Where "Enable 2FA" navigates to start enrollment. Deliberately a
|
|
208
|
+
* PROP, not a hardcoded route: this component ships in a shared package
|
|
209
|
+
* consumed by apps whose enrollment routes differ (idp_admin_ui_graft has
|
|
210
|
+
* one at /user-authentication/multi-factor; idealresume — per QAPert's
|
|
211
|
+
* 170047 gate — has NONE at all). Hardcoding either app's route into the
|
|
212
|
+
* package would be wrong for the other, and would silently reproduce the
|
|
213
|
+
* exact "advertises 2FA, nothing reachable" defect this card exists to
|
|
214
|
+
* fix. Undefined -> the button renders honestly disabled (see below),
|
|
215
|
+
* never a fake-live control pointing nowhere.
|
|
216
|
+
*/
|
|
217
|
+
mfaEnrollHref?: string;
|
|
218
|
+
/**
|
|
219
|
+
* 152745. Called when "Disable 2FA" is clicked. Left to the consumer for
|
|
220
|
+
* the same reason as mfaEnrollHref: disabling 2FA is a real, consequential
|
|
221
|
+
* write (DELETE /api/Account/mfa/methods/{id} per cluster card 170059) and
|
|
222
|
+
* this package has no opinion on which method id, confirmation flow, or
|
|
223
|
+
* step-up policy a given app requires. Undefined -> honestly disabled.
|
|
224
|
+
*/
|
|
225
|
+
onDisable2FA?: () => Promise<void> | void;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* @deprecated RETIRED (BAPert ruling, card 221074, 2026-08-18). Canonical copy:
|
|
230
|
+
* idp_admin_ui_graft/src/app/account/security/EnhancedSecurityPage.tsx.
|
|
231
|
+
* This copy receives NO further fixes. Do not import.
|
|
232
|
+
*/
|
|
233
|
+
export default function EnhancedSecurityPage({ mfaEnrollHref, onDisable2FA }: EnhancedSecurityPageProps = {}) {
|
|
199
234
|
const { data: profileData, isLoading: isProfileLoading } = useProfile();
|
|
200
235
|
const layout = useLayout();
|
|
201
236
|
const colors = useColors();
|
|
237
|
+
const [disabling2FA, setDisabling2FA] = useState(false);
|
|
238
|
+
const [disable2FAError, setDisable2FAError] = useState('');
|
|
239
|
+
|
|
240
|
+
const handleDisable2FA = async () => {
|
|
241
|
+
if (!onDisable2FA || disabling2FA) return;
|
|
242
|
+
setDisable2FAError('');
|
|
243
|
+
try {
|
|
244
|
+
setDisabling2FA(true);
|
|
245
|
+
await onDisable2FA();
|
|
246
|
+
} catch (err: any) {
|
|
247
|
+
setDisable2FAError(err?.message || 'Failed to disable two-factor authentication');
|
|
248
|
+
} finally {
|
|
249
|
+
setDisabling2FA(false);
|
|
250
|
+
}
|
|
251
|
+
};
|
|
202
252
|
|
|
203
253
|
// Re-auth action types (moved for scope)
|
|
204
254
|
type ReAuthAction = 'enable_2fa' | 'disable_2fa' | 'export_data' | 'delete_account' | null;
|
|
@@ -437,16 +487,52 @@ type ReAuthAction = 'enable_2fa' | 'disable_2fa' | 'export_data' | 'delete_accou
|
|
|
437
487
|
<StatusBadge enabled={twoFactorEnabled} isDark={isDark} />
|
|
438
488
|
</div>
|
|
439
489
|
{twoFactorEnabled ? (
|
|
440
|
-
<div className="flex gap-2">
|
|
441
|
-
<
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
490
|
+
<div className="flex flex-col gap-2">
|
|
491
|
+
<div className="flex gap-2">
|
|
492
|
+
<button className={`px-3 py-1.5 text-sm rounded border ${isDark ? 'border-slate-600 hover:bg-slate-700' : 'border-gray-300 hover:bg-gray-50'} ${textSecondary}`}>
|
|
493
|
+
Change Method
|
|
494
|
+
</button>
|
|
495
|
+
{/* 152745: was a dead styled button (no onClick/type/href) that
|
|
496
|
+
advertised a working control. Disabling a real 2FA method
|
|
497
|
+
is consequential (DELETE .../mfa/methods/{id}, cluster
|
|
498
|
+
170059), so this package has no default action — only a
|
|
499
|
+
consumer-supplied onDisable2FA makes it live. Unwired, it
|
|
500
|
+
renders honestly disabled rather than clickable-looking. */}
|
|
501
|
+
<button
|
|
502
|
+
type="button"
|
|
503
|
+
onClick={handleDisable2FA}
|
|
504
|
+
disabled={!onDisable2FA || disabling2FA}
|
|
505
|
+
title={!onDisable2FA ? 'Disabling two-factor authentication is not available here' : undefined}
|
|
506
|
+
className="px-3 py-1.5 text-sm rounded border border-red-500 text-red-500 hover:bg-red-500/10 disabled:opacity-40 disabled:cursor-not-allowed disabled:hover:bg-transparent"
|
|
507
|
+
>
|
|
508
|
+
{disabling2FA ? 'Disabling...' : 'Disable 2FA'}
|
|
509
|
+
</button>
|
|
510
|
+
</div>
|
|
511
|
+
{disable2FAError && <p className="text-xs text-red-400">{disable2FAError}</p>}
|
|
447
512
|
</div>
|
|
513
|
+
) : mfaEnrollHref ? (
|
|
514
|
+
// 152745: real navigation to the consumer's own enrollment
|
|
515
|
+
// route (a plain <a>, matching "Back to Profile" below — this
|
|
516
|
+
// package has no opinion on which router the host app uses).
|
|
517
|
+
<a
|
|
518
|
+
href={mfaEnrollHref}
|
|
519
|
+
className="inline-block px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700"
|
|
520
|
+
>
|
|
521
|
+
Enable 2FA
|
|
522
|
+
</a>
|
|
448
523
|
) : (
|
|
449
|
-
|
|
524
|
+
// 152745: no enrollment destination configured. This is the
|
|
525
|
+
// case QAPert's 170047 gate found live on idealresume.online —
|
|
526
|
+
// rendering an active-looking button here would be the exact
|
|
527
|
+
// "advertises 2FA, nothing reachable" defect this card exists
|
|
528
|
+
// to close, just moved from "no onClick" to "onClick navigates
|
|
529
|
+
// to a 404". Honestly disabled instead.
|
|
530
|
+
<button
|
|
531
|
+
type="button"
|
|
532
|
+
disabled
|
|
533
|
+
title="Two-factor authentication enrollment is not available here"
|
|
534
|
+
className="px-4 py-2 bg-blue-600 text-white rounded-md opacity-40 cursor-not-allowed"
|
|
535
|
+
>
|
|
450
536
|
Enable 2FA
|
|
451
537
|
</button>
|
|
452
538
|
)}
|
|
@@ -6,4 +6,7 @@
|
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
export { default as SecurityPage } from './page';
|
|
9
|
+
// RETIRED (BAPert ruling, card 221074, 2026-08-18): canonical copy is
|
|
10
|
+
// graft-local at idp_admin_ui_graft/src/app/account/security/EnhancedSecurityPage.tsx.
|
|
11
|
+
/** @deprecated use the graft-local canonical copy (card 221074) */
|
|
9
12
|
export { default as EnhancedSecurityPage } from './EnhancedSecurityPage';
|
|
@@ -4,14 +4,26 @@ import { authClient } from '../../client/better-auth-client';
|
|
|
4
4
|
import { useState, useEffect } from 'react';
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
|
-
*
|
|
7
|
+
* ⚠️ DESTRUCTIVE DEBUG PAGE — NOT INTENDED FOR PRODUCTION. ⚠️
|
|
8
8
|
*
|
|
9
|
-
* Nuclear option for clearing auth state when things go
|
|
10
|
-
*
|
|
9
|
+
* Emergency Logout Page. Nuclear option for clearing auth state when things go
|
|
10
|
+
* wrong: clears cookies, clears localStorage, and forces a signout.
|
|
11
11
|
*
|
|
12
|
-
*
|
|
12
|
+
* ────────────────────────── SCOPE, STATED HONESTLY (QAPert 35000 sweep) ──────────────────────────
|
|
13
|
+
* This is the MILDEST of the four test-env pages and this warning is
|
|
14
|
+
* deliberately weaker than its siblings'. The damage is SELF-INFLICTED and
|
|
15
|
+
* recoverable: a visitor can only destroy their OWN client-side auth state,
|
|
16
|
+
* and the remedy is to sign in again. It exposes no credential and mutates no
|
|
17
|
+
* other user's state. Do not read the banner above as equivalent to the JWT
|
|
18
|
+
* inspector's — inflating a mild finding is how real ones get discounted.
|
|
19
|
+
*
|
|
20
|
+
* It is flagged only because it ships with no env gate of its own, and because
|
|
21
|
+
* clearing localStorage wholesale can discard unrelated application state that
|
|
22
|
+
* a consuming app kept there. Mounting it is the gate.
|
|
23
|
+
*
|
|
24
|
+
* ────────────────────────── IF YOU MOUNT IT, MOUNT IT LIKE THIS ──────────────────────────
|
|
13
25
|
* ```typescript
|
|
14
|
-
* // app/test-env/emergency-logout/page.tsx
|
|
26
|
+
* // app/test-env/emergency-logout/page.tsx — TEST/DEV ENVIRONMENTS ONLY
|
|
15
27
|
* export { EmergencyLogoutPage as default } from '@payez/next-mvp/pages/test-env';
|
|
16
28
|
* ```
|
|
17
29
|
*/
|