@payez/next-mvp 4.1.10 → 4.1.20
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/api-handlers/account/profile.d.ts +38 -0
- package/dist/api-handlers/account/profile.js +73 -45
- package/dist/api-handlers/account/recovery/send-code.d.ts +33 -0
- package/dist/api-handlers/account/recovery/send-code.js +52 -18
- package/dist/api-handlers/account/recovery/verify-code.d.ts +33 -0
- package/dist/api-handlers/account/recovery/verify-code.js +52 -18
- package/dist/api-handlers/account/verify-email.js +15 -1
- package/dist/api-handlers/account/verify-sms.js +15 -1
- package/dist/api-handlers/auth/passkey-signin.d.ts +29 -0
- package/dist/api-handlers/auth/passkey-signin.js +97 -0
- package/dist/api-handlers/mfa/backup-code-verify.d.ts +73 -0
- package/dist/api-handlers/mfa/backup-code-verify.js +153 -0
- package/dist/api-handlers/mfa/backup-codes.d.ts +7 -0
- package/dist/api-handlers/mfa/backup-codes.js +40 -0
- package/dist/api-handlers/mfa/methods.d.ts +3 -0
- package/dist/api-handlers/mfa/methods.js +41 -0
- package/dist/api-handlers/mfa/totp-confirm.d.ts +8 -0
- package/dist/api-handlers/mfa/totp-confirm.js +41 -0
- package/dist/api-handlers/mfa/totp-enroll.d.ts +8 -0
- package/dist/api-handlers/mfa/totp-enroll.js +41 -0
- package/dist/api-handlers/mfa/totp-verify.d.ts +47 -0
- package/dist/api-handlers/mfa/totp-verify.js +134 -0
- package/dist/api-handlers/passkeys/list.d.ts +7 -0
- package/dist/api-handlers/passkeys/list.js +29 -0
- package/dist/api-handlers/passkeys/manage.d.ts +3 -0
- package/dist/api-handlers/passkeys/manage.js +54 -0
- package/dist/api-handlers/passkeys/register-options.d.ts +8 -0
- package/dist/api-handlers/passkeys/register-options.js +41 -0
- package/dist/api-handlers/passkeys/register-verify.d.ts +7 -0
- package/dist/api-handlers/passkeys/register-verify.js +40 -0
- package/dist/api-handlers/session/viability.js +19 -0
- package/dist/auth/better-auth.d.ts +68 -5
- package/dist/auth/better-auth.js +271 -45
- package/dist/auth/utils/idp-client.d.ts +13 -22
- package/dist/auth/utils/idp-client.js +12 -82
- package/dist/client/better-auth-client.d.ts +14 -2
- package/dist/lib/ensure-fresh-access-token.js +13 -2
- package/dist/lib/session-store.js +28 -21
- package/dist/lib/startup-init.js +19 -19
- package/dist/lib/token-lifecycle.d.ts +2 -0
- package/dist/lib/token-lifecycle.js +61 -7
- package/dist/models/SessionModel.d.ts +17 -0
- package/dist/pages/login/page.d.ts +10 -1
- package/dist/pages/login/page.js +69 -80
- package/dist/pages/verify-code/page.d.ts +10 -1
- package/dist/pages/verify-code/page.js +72 -4
- package/dist/routes/auth/passkey-signin.d.ts +13 -0
- package/dist/routes/auth/passkey-signin.js +18 -0
- package/dist/routes/auth/viability.d.ts +2 -0
- package/dist/routes/auth/viability.js +88 -0
- package/dist/routes/mfa/backup-code-verify.d.ts +1 -0
- package/dist/routes/mfa/backup-code-verify.js +5 -0
- package/dist/routes/mfa/backup-codes.d.ts +10 -0
- package/dist/routes/mfa/backup-codes.js +14 -0
- package/dist/routes/mfa/methods.d.ts +21 -0
- package/dist/routes/mfa/methods.js +10 -0
- package/dist/routes/mfa/totp-confirm.d.ts +10 -0
- package/dist/routes/mfa/totp-confirm.js +14 -0
- package/dist/routes/mfa/totp-enroll.d.ts +10 -0
- package/dist/routes/mfa/totp-enroll.js +14 -0
- package/dist/routes/mfa/totp-verify.d.ts +14 -0
- package/dist/routes/mfa/totp-verify.js +18 -0
- package/dist/routes/passkeys/list.d.ts +10 -0
- package/dist/routes/passkeys/list.js +14 -0
- package/dist/routes/passkeys/manage.d.ts +18 -0
- package/dist/routes/passkeys/manage.js +13 -0
- package/dist/routes/passkeys/register-options.d.ts +10 -0
- package/dist/routes/passkeys/register-options.js +14 -0
- package/dist/routes/passkeys/register-verify.d.ts +10 -0
- package/dist/routes/passkeys/register-verify.js +14 -0
- package/dist/server/auth.d.ts +2 -2
- package/dist/server/decode-session.js +37 -3
- package/package.json +958 -893
- package/scripts/check-internal-url-usage.sh +73 -73
- package/src/api/index.ts +17 -17
- package/src/api-handlers/account/masked-info.ts +45 -45
- package/src/api-handlers/account/profile.ts +99 -80
- package/src/api-handlers/account/recovery/initiate.ts +23 -23
- package/src/api-handlers/account/recovery/send-code.ts +73 -25
- package/src/api-handlers/account/recovery/verify-code.ts +73 -25
- package/src/api-handlers/account/reset-password.ts +23 -23
- package/src/api-handlers/account/send-code.ts +76 -76
- package/src/api-handlers/account/validate-password.ts +118 -118
- package/src/api-handlers/account/verify-email.ts +15 -1
- package/src/api-handlers/account/verify-sms.ts +15 -1
- package/src/api-handlers/admin/stats.ts +249 -249
- package/src/api-handlers/anon/preferences.ts +123 -123
- package/src/api-handlers/auth/jwks.ts +20 -20
- package/src/api-handlers/auth/login.ts +240 -240
- package/src/api-handlers/auth/passkey-signin.ts +98 -0
- package/src/api-handlers/auth/validate.ts +43 -43
- package/src/api-handlers/mfa/backup-code-verify.ts +175 -0
- package/src/api-handlers/mfa/backup-codes.ts +41 -0
- package/src/api-handlers/mfa/methods.ts +46 -0
- package/src/api-handlers/mfa/totp-confirm.ts +42 -0
- package/src/api-handlers/mfa/totp-enroll.ts +42 -0
- package/src/api-handlers/mfa/totp-verify.ts +156 -0
- package/src/api-handlers/passkeys/list.ts +31 -0
- package/src/api-handlers/passkeys/manage.ts +54 -0
- package/src/api-handlers/passkeys/register-options.ts +42 -0
- package/src/api-handlers/passkeys/register-verify.ts +41 -0
- package/src/api-handlers/session/viability.ts +161 -142
- package/src/auth/better-auth.ts +708 -455
- package/src/auth/unauthenticated-routes.ts +2 -2
- package/src/auth/utils/idp-client.ts +13 -95
- package/src/client/better-auth-client.ts +70 -70
- package/src/client/fetchWithSession.ts +21 -21
- package/src/client/index.ts +13 -13
- package/src/client/useAnonSession.ts +131 -131
- package/src/components/admin/VibeAdminContext.tsx +87 -87
- package/src/config/logging-config.ts +82 -82
- package/src/config/unauthenticated-routes.ts +19 -19
- package/src/edge/internal-api-url.ts +65 -65
- package/src/edge/middleware.ts +42 -42
- package/src/hooks/useProfile.ts +75 -75
- package/src/lib/anon-session.ts +213 -213
- package/src/lib/auth-secret.ts +137 -137
- package/src/lib/demo-mode.ts +17 -17
- package/src/lib/ensure-fresh-access-token.ts +13 -2
- package/src/lib/idp-client-config.ts +559 -559
- package/src/lib/idp-fetch.ts +126 -126
- package/src/lib/internal-api.ts +171 -171
- package/src/lib/jwt-decode-client.ts +45 -45
- package/src/lib/rate-limit-service.ts +9 -9
- package/src/lib/redis.ts +26 -26
- package/src/lib/refresh-token-validator.ts +64 -64
- package/src/lib/secret-validation.ts +8 -8
- package/src/lib/session-store.ts +702 -695
- package/src/lib/session.ts +34 -34
- package/src/lib/startup-init.ts +251 -251
- package/src/lib/token-expiry.ts +40 -40
- package/src/lib/token-lifecycle.ts +533 -470
- package/src/lib/types/api-responses.ts +336 -336
- package/src/middleware/auth-decision.ts +43 -43
- package/src/models/DecodedAccessToken.ts +16 -16
- package/src/models/SessionModel.ts +282 -264
- package/src/pages/admin-roles/index.ts +9 -9
- package/src/pages/login/page.tsx +123 -228
- package/src/pages/security/EnhancedSecurityPage.tsx +9 -9
- package/src/pages/verify-code/page.tsx +71 -5
- 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/verify-email.ts +40 -40
- package/src/routes/account/verify-sms.ts +40 -40
- package/src/routes/auth/index.ts +22 -22
- package/src/routes/auth/passkey-signin.ts +13 -0
- package/src/routes/auth/refresh.ts +48 -48
- package/src/routes/auth/session.ts +166 -166
- package/src/routes/auth/viability.ts +89 -0
- package/src/routes/index.ts +17 -17
- package/src/routes/mfa/backup-code-verify.ts +1 -0
- package/src/routes/mfa/backup-codes.ts +10 -0
- package/src/routes/mfa/methods.ts +22 -0
- package/src/routes/mfa/totp-confirm.ts +10 -0
- package/src/routes/mfa/totp-enroll.ts +10 -0
- package/src/routes/mfa/totp-verify.ts +14 -0
- package/src/routes/passkeys/list.ts +10 -0
- package/src/routes/passkeys/manage.ts +23 -0
- package/src/routes/passkeys/register-options.ts +10 -0
- package/src/routes/passkeys/register-verify.ts +10 -0
- package/src/routes/session/index.ts +7 -7
- package/src/routes/session/refresh-viability.ts +17 -17
- package/src/server/auth.ts +373 -373
- package/src/server/decode-session.ts +235 -202
- package/src/services/signalrActivityService.ts +258 -258
- 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 +22 -22
- package/src/types/logging.ts +11 -11
- package/src/types/recovery.ts +54 -54
- package/src/utils/api.ts +353 -353
- 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/pages/login/page.tsx
CHANGED
|
@@ -30,10 +30,13 @@ import ReservedStatusBox from '../../components/reserved/ReservedStatusBox';
|
|
|
30
30
|
import ReservedRecoveryWarning from '../../components/reserved/ReservedRecoveryWarning';
|
|
31
31
|
import { useBranding, useColors, useLayout } from '../../theme/useTheme';
|
|
32
32
|
|
|
33
|
-
function LoginForm() {
|
|
33
|
+
function LoginForm({ altAuthSlot }: { altAuthSlot?: React.ReactNode }) {
|
|
34
34
|
const searchParams = useSearchParams();
|
|
35
35
|
const callbackUrl = searchParams?.get('callbackUrl') || '/dashboard';
|
|
36
36
|
const urlError = searchParams?.get('error');
|
|
37
|
+
// Better Auth mount redirects OAuth failures here as ?oauth_error={provider}
|
|
38
|
+
// (the failure-signal guard from the merge spec). Distinct from `error`.
|
|
39
|
+
const oauthError = searchParams?.get('oauth_error');
|
|
37
40
|
|
|
38
41
|
const { data: sessionData, isPending } = authClient.useSession();
|
|
39
42
|
const session = sessionData;
|
|
@@ -42,6 +45,53 @@ function LoginForm() {
|
|
|
42
45
|
const colors = useColors();
|
|
43
46
|
const layout = useLayout();
|
|
44
47
|
|
|
48
|
+
// Social sign-in. The button renders ONLY when the IDP client config has an
|
|
49
|
+
// enabled provider row — discovered from the public settings route (the same
|
|
50
|
+
// config the Better Auth engine builds providers from), never assumed.
|
|
51
|
+
// Absent row / fetch failure = no button. Fail quiet is correct here.
|
|
52
|
+
const [googleEnabled, setGoogleEnabled] = useState(false);
|
|
53
|
+
const [googleLoading, setGoogleLoading] = useState(false);
|
|
54
|
+
|
|
55
|
+
useEffect(() => {
|
|
56
|
+
let mounted = true;
|
|
57
|
+
fetch('/api/auth/settings', { cache: 'no-store' })
|
|
58
|
+
.then((r) => (r.ok ? r.json() : null))
|
|
59
|
+
.then((json) => {
|
|
60
|
+
if (!mounted || !json) return;
|
|
61
|
+
const providers: unknown = json?.data?.enabledProviders;
|
|
62
|
+
if (Array.isArray(providers) && providers.some((p) => typeof p === 'string' && p.toLowerCase() === 'google')) {
|
|
63
|
+
setGoogleEnabled(true);
|
|
64
|
+
}
|
|
65
|
+
})
|
|
66
|
+
.catch(() => undefined); // fail quiet: no button
|
|
67
|
+
return () => { mounted = false; };
|
|
68
|
+
}, []);
|
|
69
|
+
|
|
70
|
+
const handleGoogleSignIn = async () => {
|
|
71
|
+
setGoogleLoading(true);
|
|
72
|
+
setLoginError(null);
|
|
73
|
+
try {
|
|
74
|
+
// Pre-clean any stale session so account-switch works (idealvibe
|
|
75
|
+
// account-auth/login idiom: signOut before signIn.social).
|
|
76
|
+
await authClient.signOut().catch(() => undefined);
|
|
77
|
+
// The package's OWN SDK, not a raw fetch — Jon's ruling (QA 17400 /
|
|
78
|
+
// BAPert 17401): the page is the template consumers copy, so it teaches
|
|
79
|
+
// the same one-line idiom idealvibe uses (49cc95f, four call sites).
|
|
80
|
+
// The SDK hits POST /api/auth/sign-in/social itself, with its own
|
|
81
|
+
// credentials mode, baseURL and error contract, and navigates on the
|
|
82
|
+
// returned authorize URL.
|
|
83
|
+
await authClient.signIn.social({
|
|
84
|
+
provider: 'google',
|
|
85
|
+
// The social round trip lands on '/', not the form's /dashboard
|
|
86
|
+
// default: the post-callback session upgrade decides the destination.
|
|
87
|
+
callbackURL: searchParams?.get('callbackUrl') || '/',
|
|
88
|
+
});
|
|
89
|
+
} catch {
|
|
90
|
+
setGoogleLoading(false);
|
|
91
|
+
setLoginError('Google sign-in is unavailable right now. Please try again.');
|
|
92
|
+
}
|
|
93
|
+
};
|
|
94
|
+
|
|
45
95
|
// Helper to create lighter/darker shades for hover states
|
|
46
96
|
const adjustBrightness = (hex: string, percent: number) => {
|
|
47
97
|
const num = parseInt(hex.replace('#', ''), 16);
|
|
@@ -74,6 +124,14 @@ function LoginForm() {
|
|
|
74
124
|
// Force show form after timeout if stuck in loading
|
|
75
125
|
const [forceShowForm, setForceShowForm] = useState(false);
|
|
76
126
|
|
|
127
|
+
// Surface OAuth failures the Better Auth mount bounced back here.
|
|
128
|
+
useEffect(() => {
|
|
129
|
+
if (oauthError) {
|
|
130
|
+
const name = oauthError.charAt(0).toUpperCase() + oauthError.slice(1);
|
|
131
|
+
setLoginError(`${name} sign-in did not complete. Please try again.`);
|
|
132
|
+
}
|
|
133
|
+
}, [oauthError]);
|
|
134
|
+
|
|
77
135
|
// Handle URL errors with user-friendly messages
|
|
78
136
|
useEffect(() => {
|
|
79
137
|
if (urlError) {
|
|
@@ -119,71 +177,9 @@ function LoginForm() {
|
|
|
119
177
|
return () => clearTimeout(immediateTimeout);
|
|
120
178
|
}, [status, session]);
|
|
121
179
|
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
// Atomic state: set to submitting (clears error and success)
|
|
126
|
-
setIsSubmitting(true);
|
|
127
|
-
setLoading(true);
|
|
128
|
-
setLoginError(null);
|
|
129
|
-
setLoginSuccess(false);
|
|
130
|
-
|
|
131
|
-
try {
|
|
132
|
-
console.log('[LOGIN] Starting authentication...');
|
|
133
|
-
const result = await authClient.signIn.email({
|
|
134
|
-
email,
|
|
135
|
-
password,
|
|
136
|
-
callbackURL: callbackUrl,
|
|
137
|
-
});
|
|
138
|
-
|
|
139
|
-
if ((result as any)?.error) {
|
|
140
|
-
console.log('[LOGIN] Authentication failed:', (result as any).error);
|
|
141
|
-
|
|
142
|
-
setIsSubmitting(false);
|
|
143
|
-
setLoading(false);
|
|
144
|
-
setLoginSuccess(false);
|
|
145
|
-
|
|
146
|
-
const errorMsg = typeof (result as any).error === 'object'
|
|
147
|
-
? ((result as any).error as any).message || 'Authentication failed'
|
|
148
|
-
: String((result as any).error);
|
|
149
|
-
|
|
150
|
-
if (errorMsg.includes('password') || errorMsg.includes('Password')) {
|
|
151
|
-
console.log('[LOGIN] Password error detected - showing recovery options');
|
|
152
|
-
setShowRecoveryOptions(true);
|
|
153
|
-
setLoginError(errorMsg);
|
|
154
|
-
} else if (errorMsg.includes('Unable to connect')) {
|
|
155
|
-
setLoginError('The authentication service is currently unavailable. Please try again later.');
|
|
156
|
-
} else {
|
|
157
|
-
setLoginError(errorMsg);
|
|
158
|
-
}
|
|
159
|
-
return;
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
// Atomic state: set to success
|
|
163
|
-
console.log('[LOGIN] Authentication successful!');
|
|
164
|
-
setLoginSuccess(true);
|
|
165
|
-
setLoginError(null);
|
|
166
|
-
setIsSubmitting(false);
|
|
167
|
-
|
|
168
|
-
// Get updated session
|
|
169
|
-
const freshSession = await authClient.getSession();
|
|
170
|
-
console.log('[LOGIN] Fresh session obtained, redirecting to 2FA...');
|
|
171
|
-
|
|
172
|
-
// Redirect to verify-code for 2FA
|
|
173
|
-
const verifyUrl = `/account-auth/verify-code?callbackUrl=${encodeURIComponent(callbackUrl)}`;
|
|
174
|
-
console.log('[LOGIN] Redirecting to:', verifyUrl);
|
|
175
|
-
window.location.href = verifyUrl;
|
|
176
|
-
|
|
177
|
-
} catch (err) {
|
|
178
|
-
console.error('[LOGIN] Unexpected error:', err);
|
|
179
|
-
|
|
180
|
-
// Atomic state: set to error
|
|
181
|
-
setIsSubmitting(false);
|
|
182
|
-
setLoading(false);
|
|
183
|
-
setLoginSuccess(false);
|
|
184
|
-
setLoginError(err instanceof Error ? err.message : 'An error occurred during login');
|
|
185
|
-
}
|
|
186
|
-
};
|
|
180
|
+
// handleSubmit REMOVED with the email/password form it served: it called
|
|
181
|
+
// authClient.signIn.email, which better-auth always rejected because this
|
|
182
|
+
// package never configures emailAndPassword. See the note at the render site.
|
|
187
183
|
|
|
188
184
|
// Show loading spinner for initial loads
|
|
189
185
|
if (!forceShowForm && !loginError && (loading || (status === 'loading' && !session))) {
|
|
@@ -246,169 +242,62 @@ function LoginForm() {
|
|
|
246
242
|
</p>
|
|
247
243
|
</div>
|
|
248
244
|
|
|
249
|
-
{/*
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
autoFocus
|
|
261
|
-
autoComplete="email"
|
|
262
|
-
value={email}
|
|
263
|
-
onChange={(e) => setEmail(e.target.value)}
|
|
264
|
-
className={`themed-input w-full px-4 py-3 border border-gray-300 rounded-lg transition-colors`}
|
|
265
|
-
placeholder="you@example.com"
|
|
266
|
-
disabled={isSubmitting}
|
|
245
|
+
{/* Social sign-in — rendered ONLY from the IDP client config's
|
|
246
|
+
enabled providers (see the /api/auth/settings discovery above).
|
|
247
|
+
Above the form, divider idiom per the package's ModeAwareLoginPage. */}
|
|
248
|
+
{googleEnabled && (
|
|
249
|
+
<>
|
|
250
|
+
<button
|
|
251
|
+
type="button"
|
|
252
|
+
onClick={() => void handleGoogleSignIn()}
|
|
253
|
+
disabled={googleLoading || isSubmitting}
|
|
254
|
+
data-testid="google-sign-in"
|
|
255
|
+
className="w-full px-4 py-3 mb-6 rounded-lg border border-gray-300 font-semibold transition-colors flex items-center justify-center gap-2 disabled:opacity-50 disabled:cursor-not-allowed hover:bg-gray-50"
|
|
267
256
|
style={{ backgroundColor: 'white', color: '#1f2937' }}
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
<label htmlFor="password" className="block text-sm font-medium mb-2" style={{ color: 'inherit' }}>
|
|
274
|
-
Password
|
|
275
|
-
</label>
|
|
276
|
-
<div className="relative">
|
|
277
|
-
<input
|
|
278
|
-
id="password"
|
|
279
|
-
type={showPassword ? "text" : "password"}
|
|
280
|
-
required
|
|
281
|
-
autoComplete="current-password"
|
|
282
|
-
value={password}
|
|
283
|
-
onChange={(e) => setPassword(e.target.value)}
|
|
284
|
-
className={`themed-input w-full px-4 py-3 pr-12 border border-gray-300 rounded-lg transition-colors`}
|
|
285
|
-
placeholder="••••••••"
|
|
286
|
-
disabled={isSubmitting}
|
|
287
|
-
style={{ backgroundColor: 'white', color: '#1f2937' }}
|
|
288
|
-
/>
|
|
289
|
-
<button
|
|
290
|
-
type="button"
|
|
291
|
-
onClick={() => setShowPassword(!showPassword)}
|
|
292
|
-
className="absolute right-3 top-1/2 transform -translate-y-1/2 hover:opacity-70"
|
|
293
|
-
style={{ color: 'inherit', opacity: 0.7 }}
|
|
294
|
-
aria-label={showPassword ? "Hide password" : "Show password"}
|
|
295
|
-
>
|
|
296
|
-
{showPassword ? (
|
|
297
|
-
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" className="w-5 h-5">
|
|
298
|
-
<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" />
|
|
299
|
-
</svg>
|
|
300
|
-
) : (
|
|
301
|
-
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" className="w-5 h-5">
|
|
302
|
-
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
|
|
303
|
-
<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" />
|
|
304
|
-
</svg>
|
|
305
|
-
)}
|
|
306
|
-
</button>
|
|
307
|
-
</div>
|
|
308
|
-
</div>
|
|
309
|
-
|
|
310
|
-
{/* Atomic Status Display - Deterministic Reserved Space (zero layout shift) */}
|
|
311
|
-
<ReservedStatusBox
|
|
312
|
-
candidates={[
|
|
313
|
-
"Ready",
|
|
314
|
-
"Authenticating...",
|
|
315
|
-
"The authentication service is currently unavailable. Please try again later.",
|
|
316
|
-
"Invalid email or password. Please try again.",
|
|
317
|
-
"Login successful! Redirecting..."
|
|
318
|
-
]}
|
|
319
|
-
containerClass="p-3 text-sm leading-relaxed rounded-lg border"
|
|
320
|
-
iconSizePx={16}
|
|
321
|
-
>
|
|
322
|
-
{isSubmitting ? (
|
|
323
|
-
/* Themed: Submitting */
|
|
324
|
-
<div className="themed-status-submitting flex items-start space-x-2 p-3 rounded-lg">
|
|
325
|
-
<svg className="animate-spin w-4 h-4 mt-0.5 flex-shrink-0" fill="none" viewBox="0 0 24 24" style={{ color: buttonColor }}>
|
|
326
|
-
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
|
|
327
|
-
<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"></path>
|
|
257
|
+
>
|
|
258
|
+
{googleLoading ? (
|
|
259
|
+
<svg className="animate-spin w-5 h-5" fill="none" viewBox="0 0 24 24">
|
|
260
|
+
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" />
|
|
261
|
+
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8v4a4 4 0 00-4 4H4z" />
|
|
328
262
|
</svg>
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
<span className="text-red-600 text-sm leading-relaxed">{loginError}</span>
|
|
336
|
-
</div>
|
|
337
|
-
) : loginSuccess ? (
|
|
338
|
-
/* Green: Success */
|
|
339
|
-
<div className="flex items-start space-x-2 p-3 rounded-lg bg-green-500/10 border border-green-500/20">
|
|
340
|
-
<div className="w-4 h-4 bg-green-500 rounded-full mt-0.5 flex-shrink-0"></div>
|
|
341
|
-
<span className="text-green-600 text-sm leading-relaxed">Login successful! Redirecting...</span>
|
|
342
|
-
</div>
|
|
343
|
-
) : (
|
|
344
|
-
/* Ready State / Health Check Placeholder */
|
|
345
|
-
<div className="flex items-start space-x-2 p-3 rounded-lg bg-green-500/10 border border-green-500/20">
|
|
346
|
-
<div className="w-4 h-4 bg-green-500 rounded-full mt-0.5 flex-shrink-0"></div>
|
|
347
|
-
<span className="text-green-600 text-sm leading-relaxed">Ready</span>
|
|
348
|
-
{/*
|
|
349
|
-
CUSTOMIZATION: Replace above with your health check component
|
|
350
|
-
Example: <SignalRHealthCheck />
|
|
351
|
-
Or any real-time health status component
|
|
352
|
-
*/}
|
|
353
|
-
</div>
|
|
354
|
-
)}
|
|
355
|
-
</ReservedStatusBox>
|
|
356
|
-
|
|
357
|
-
{/* Account Recovery Options - Deterministic Reserved Space (zero layout shift) */}
|
|
358
|
-
<ReservedRecoveryWarning
|
|
359
|
-
show={showRecoveryOptions}
|
|
360
|
-
titleText="Account Lockout Warning"
|
|
361
|
-
bodyText="Your account will be locked after one more failed attempt. Need help?"
|
|
362
|
-
actionLabel="Start Account Recovery"
|
|
363
|
-
containerClass="p-4 bg-amber-50 border border-amber-200 rounded-lg"
|
|
364
|
-
titleClass="text-amber-900 font-medium mb-2"
|
|
365
|
-
bodyClass="text-amber-800 text-sm mb-3"
|
|
366
|
-
buttonClass="w-full bg-amber-600 hover:bg-amber-700 text-white font-medium py-2 px-4 rounded-md text-sm transition-colors"
|
|
367
|
-
iconSizePx={20}
|
|
368
|
-
>
|
|
369
|
-
{showRecoveryOptions && (
|
|
370
|
-
<div className="p-4 bg-amber-50 border border-amber-200 rounded-lg">
|
|
371
|
-
<div className="flex items-start space-x-3">
|
|
372
|
-
<svg className="w-5 h-5 text-amber-600 mt-0.5 flex-shrink-0" fill="currentColor" viewBox="0 0 20 20">
|
|
373
|
-
<path fillRule="evenodd" d="M8.257 3.099c.765-1.36 2.722-1.36 3.486 0l5.58 9.92c.75 1.334-.213 2.98-1.742 2.98H4.42c-1.53 0-2.493-1.646-1.743-2.98l5.58-9.92zM11 13a1 1 0 11-2 0 1 1 0 012 0zm-1-8a1 1 0 00-1 1v3a1 1 0 002 0V6a1 1 0 00-1-1z" clipRule="evenodd" />
|
|
374
|
-
</svg>
|
|
375
|
-
<div className="flex-1">
|
|
376
|
-
<h3 className="text-amber-900 font-medium mb-2">Account Lockout Warning</h3>
|
|
377
|
-
<p className="text-amber-800 text-sm mb-3">
|
|
378
|
-
Your account will be locked after one more failed attempt. Need help?
|
|
379
|
-
</p>
|
|
380
|
-
<button
|
|
381
|
-
type="button"
|
|
382
|
-
onClick={() => window.location.href = `/account-auth/recovery?email=${encodeURIComponent(email)}`}
|
|
383
|
-
className="w-full bg-amber-600 hover:bg-amber-700 text-white font-medium py-2 px-4 rounded-md text-sm transition-colors"
|
|
384
|
-
>
|
|
385
|
-
Start Account Recovery
|
|
386
|
-
</button>
|
|
387
|
-
</div>
|
|
388
|
-
</div>
|
|
389
|
-
</div>
|
|
390
|
-
)}
|
|
391
|
-
</ReservedRecoveryWarning>
|
|
392
|
-
|
|
393
|
-
{/* Submit Button */}
|
|
394
|
-
<button
|
|
395
|
-
type="submit"
|
|
396
|
-
disabled={isSubmitting}
|
|
397
|
-
className="themed-button w-full text-white font-semibold py-3 px-4 rounded-lg transition-colors disabled:opacity-50 disabled:cursor-not-allowed flex items-center justify-center"
|
|
398
|
-
>
|
|
399
|
-
{isSubmitting ? (
|
|
400
|
-
<>
|
|
401
|
-
<svg className="animate-spin -ml-1 mr-3 h-5 w-5 text-white" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
|
|
402
|
-
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
|
|
403
|
-
<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"></path>
|
|
263
|
+
) : (
|
|
264
|
+
<svg className="w-5 h-5" viewBox="0 0 24 24" fill="currentColor">
|
|
265
|
+
<path d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92c-.26 1.37-1.04 2.53-2.21 3.31v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.09z" fill="#4285F4"/>
|
|
266
|
+
<path d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z" fill="#34A853"/>
|
|
267
|
+
<path d="M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z" fill="#FBBC05"/>
|
|
268
|
+
<path d="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z" fill="#EA4335"/>
|
|
404
269
|
</svg>
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
270
|
+
)}
|
|
271
|
+
{googleLoading ? 'Redirecting to Google…' : 'Continue with Google'}
|
|
272
|
+
</button>
|
|
273
|
+
</>
|
|
274
|
+
)}
|
|
275
|
+
|
|
276
|
+
{/* EMAIL/PASSWORD FORM REMOVED — the package has NEVER implemented it.
|
|
277
|
+
`emailAndPassword` appears ZERO times in src/ and dist/: better-auth.ts
|
|
278
|
+
configures socialProviders only, so authClient.signIn.email always
|
|
279
|
+
returned "Email and password is not enabled". The form was a dead
|
|
280
|
+
control on the first screen of every consumer, and no consumer could
|
|
281
|
+
enable it without forking. Removed 2026-08-02 (Jon via BAPert 17586).
|
|
282
|
+
To ADD password auth later: set emailAndPassword in better-auth.ts and
|
|
283
|
+
restore this form together — one without the other is what created this. */}
|
|
284
|
+
|
|
285
|
+
{/* ALTERNATE SIGN-IN SLOT — the "B2 slot" (card 169071).
|
|
286
|
+
─────────────────────────────────────────────────────────────
|
|
287
|
+
Consumers with an extra sign-in route (today: passkey) rendered
|
|
288
|
+
it AFTER <LoginPage/> as a sibling, which put the control BELOW
|
|
289
|
+
THE FOLD, because this page owns `min-h-screen` centering. The
|
|
290
|
+
graft's own comment called that "knowingly ugly and knowingly
|
|
291
|
+
temporary" and named the fix: give LoginPage a slot.
|
|
292
|
+
|
|
293
|
+
It sits here — after the social row, before the footer links —
|
|
294
|
+
so an alternate method reads as a peer of "Continue with
|
|
295
|
+
Google" rather than as an afterthought below the card.
|
|
296
|
+
|
|
297
|
+
A SLOT, not a passkey prop: this page must not know what a
|
|
298
|
+
passkey is. The consumer owns the control and its ceremony;
|
|
299
|
+
the package owns only where it goes. */}
|
|
300
|
+
{altAuthSlot && <div className="mt-4">{altAuthSlot}</div>}
|
|
412
301
|
|
|
413
302
|
{/* Footer Links */}
|
|
414
303
|
<div className={`mt-6 text-center text-sm ${colors.muted}`}>
|
|
@@ -447,10 +336,16 @@ function LoginPageFallback() {
|
|
|
447
336
|
);
|
|
448
337
|
}
|
|
449
338
|
|
|
450
|
-
|
|
339
|
+
/**
|
|
340
|
+
* @param altAuthSlot an additional sign-in control (e.g. a passkey link)
|
|
341
|
+
* rendered INSIDE the card, after the social row. Optional: omitting it
|
|
342
|
+
* leaves the page byte-identical to before, so existing consumers are
|
|
343
|
+
* unaffected.
|
|
344
|
+
*/
|
|
345
|
+
export default function LoginPage({ altAuthSlot }: { altAuthSlot?: React.ReactNode } = {}) {
|
|
451
346
|
return (
|
|
452
347
|
<Suspense fallback={<LoginPageFallback />}>
|
|
453
|
-
<LoginForm />
|
|
348
|
+
<LoginForm altAuthSlot={altAuthSlot} />
|
|
454
349
|
</Suspense>
|
|
455
350
|
);
|
|
456
351
|
}
|
|
@@ -200,9 +200,9 @@ export default function EnhancedSecurityPage() {
|
|
|
200
200
|
const layout = useLayout();
|
|
201
201
|
const colors = useColors();
|
|
202
202
|
|
|
203
|
-
// Re-auth action types (moved for scope)
|
|
204
|
-
type ReAuthAction = 'enable_2fa' | 'disable_2fa' | 'export_data' | 'delete_account' | null;
|
|
205
|
-
|
|
203
|
+
// Re-auth action types (moved for scope)
|
|
204
|
+
type ReAuthAction = 'enable_2fa' | 'disable_2fa' | 'export_data' | 'delete_account' | null;
|
|
205
|
+
|
|
206
206
|
// Security data state (would come from API in production)
|
|
207
207
|
const [securityData, setSecurityData] = useState<SecurityData | null>(null);
|
|
208
208
|
const [loadingSecurityData, setLoadingSecurityData] = useState(true);
|
|
@@ -218,12 +218,12 @@ type ReAuthAction = 'enable_2fa' | 'disable_2fa' | 'export_data' | 'delete_accou
|
|
|
218
218
|
new_password: '',
|
|
219
219
|
confirm_password: '',
|
|
220
220
|
});
|
|
221
|
-
const [showPasswords, setShowPasswords] = useState({ current: false, new: false, confirm: false });
|
|
222
|
-
|
|
223
|
-
// Re-authentication modal state (Security Addendum)
|
|
224
|
-
const [reAuthModal, setReAuthModal] = useState<{
|
|
225
|
-
isOpen: boolean;
|
|
226
|
-
action: ReAuthAction;
|
|
221
|
+
const [showPasswords, setShowPasswords] = useState({ current: false, new: false, confirm: false });
|
|
222
|
+
|
|
223
|
+
// Re-authentication modal state (Security Addendum)
|
|
224
|
+
const [reAuthModal, setReAuthModal] = useState<{
|
|
225
|
+
isOpen: boolean;
|
|
226
|
+
action: ReAuthAction;
|
|
227
227
|
}>({ isOpen: false, action: null });
|
|
228
228
|
const [submitting, setSubmitting] = useState(false);
|
|
229
229
|
const [error, setError] = useState('');
|
|
@@ -49,7 +49,7 @@ interface MaskedInfo {
|
|
|
49
49
|
has_authenticator?: boolean;
|
|
50
50
|
}
|
|
51
51
|
|
|
52
|
-
function VerifyCodeForm() {
|
|
52
|
+
function VerifyCodeForm({ enrollHref }: { enrollHref: string }) {
|
|
53
53
|
const router = useRouter();
|
|
54
54
|
const searchParams = useSearchParams();
|
|
55
55
|
const callbackUrl = searchParams?.get('callbackUrl') || '/dashboard';
|
|
@@ -193,7 +193,36 @@ function VerifyCodeForm() {
|
|
|
193
193
|
const res = await fetch('/api/session/viability', {
|
|
194
194
|
credentials: 'include',
|
|
195
195
|
});
|
|
196
|
-
|
|
196
|
+
|
|
197
|
+
// ====================================================================
|
|
198
|
+
// THE ENROLMENT DEAD END (cards 170045 / 170047, was 141688 / 141741)
|
|
199
|
+
// ====================================================================
|
|
200
|
+
// A first-time social user arrives here with ZERO usable methods and is
|
|
201
|
+
// asked for a code from an authenticator that does not exist. There was
|
|
202
|
+
// no way out: not a missing feature, AN UNASKED QUESTION.
|
|
203
|
+
//
|
|
204
|
+
// The IDP already decides this and says so. The package already relays
|
|
205
|
+
// it — from BOTH viability exports, deliberately, so an app mounting
|
|
206
|
+
// either is not left blind. The graft already mounts the route. THIS
|
|
207
|
+
// PAGE ALREADY CALLS IT, four lines above. It simply never read the
|
|
208
|
+
// answer: the only branch was `res.status === 401`, so on a perfectly
|
|
209
|
+
// valid session the response was fetched and thrown away.
|
|
210
|
+
//
|
|
211
|
+
// 'enroll_mfa' no USABLE method — the user CANNOT complete a
|
|
212
|
+
// challenge, so keeping them on a code prompt is a trap
|
|
213
|
+
// 'mfa_verify' a usable method exists — the prompt is correct
|
|
214
|
+
// null nothing due
|
|
215
|
+
//
|
|
216
|
+
// Every link in that chain was built against these very cards. The
|
|
217
|
+
// last consumer just never asked.
|
|
218
|
+
if (res.ok) {
|
|
219
|
+
const data = await res.json().catch(() => ({}));
|
|
220
|
+
if (data?.nextStep === 'enroll_mfa') {
|
|
221
|
+
router.replace(enrollHref);
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
197
226
|
if (res.status === 401) {
|
|
198
227
|
const data = await res.json().catch(() => ({}));
|
|
199
228
|
|
|
@@ -223,7 +252,37 @@ function VerifyCodeForm() {
|
|
|
223
252
|
clearTimeout(initialTimeout);
|
|
224
253
|
clearInterval(interval);
|
|
225
254
|
};
|
|
226
|
-
}, [status, router]);
|
|
255
|
+
}, [status, router, enrollHref]);
|
|
256
|
+
|
|
257
|
+
// ==========================================================================
|
|
258
|
+
// Enrolment dead-end check — IMMEDIATE, and deliberately separate
|
|
259
|
+
// ==========================================================================
|
|
260
|
+
// The poller above first fires after 5 SECONDS. For expiry that is fine. For
|
|
261
|
+
// a user who can never complete this challenge it is five seconds of staring
|
|
262
|
+
// at an unanswerable prompt, which is the whole complaint in card 170045.
|
|
263
|
+
//
|
|
264
|
+
// This runs at once — but it is a SEPARATE effect rather than removing the
|
|
265
|
+
// 5s delay, because that delay guards the 401 path against racing session
|
|
266
|
+
// establishment immediately after sign-in. Firing the expiry logic instantly
|
|
267
|
+
// could sign a user out for having a session that is merely still settling.
|
|
268
|
+
// So: this reads ONLY the 200 body and ONLY routes on 'enroll_mfa'. It never
|
|
269
|
+
// signs anyone out, so it is safe to run with no delay.
|
|
270
|
+
useEffect(() => {
|
|
271
|
+
if (status !== 'authenticated') return;
|
|
272
|
+
let cancelled = false;
|
|
273
|
+
(async () => {
|
|
274
|
+
try {
|
|
275
|
+
const res = await fetch('/api/session/viability', { credentials: 'include' });
|
|
276
|
+
if (!res.ok || cancelled) return;
|
|
277
|
+
const data = await res.json().catch(() => ({}));
|
|
278
|
+
if (data?.nextStep === 'enroll_mfa') router.replace(enrollHref);
|
|
279
|
+
} catch {
|
|
280
|
+
// Silent: the poller and the next real API call both still cover this.
|
|
281
|
+
// A failed check must leave the user on the prompt, never redirect them.
|
|
282
|
+
}
|
|
283
|
+
})();
|
|
284
|
+
return () => { cancelled = true; };
|
|
285
|
+
}, [status, router, enrollHref]);
|
|
227
286
|
|
|
228
287
|
const handleSendCode = async (selectedMethod: 'email' | 'sms') => {
|
|
229
288
|
setSending(true);
|
|
@@ -643,10 +702,17 @@ function VerifyCodePageFallback() {
|
|
|
643
702
|
);
|
|
644
703
|
}
|
|
645
704
|
|
|
646
|
-
|
|
705
|
+
/**
|
|
706
|
+
* @param enrollHref where a user with NO usable 2FA method is sent to enrol.
|
|
707
|
+
* Defaults to `/account/security`, which is where the enrolment surface
|
|
708
|
+
* lives in the admin graft. It is a PROP rather than a constant because the
|
|
709
|
+
* mount path is the consumer's choice — the package must not assume another
|
|
710
|
+
* app's routing table.
|
|
711
|
+
*/
|
|
712
|
+
export default function VerifyCodePage({ enrollHref = '/account/security' }: { enrollHref?: string } = {}) {
|
|
647
713
|
return (
|
|
648
714
|
<Suspense fallback={<VerifyCodePageFallback />}>
|
|
649
|
-
<VerifyCodeForm />
|
|
715
|
+
<VerifyCodeForm enrollHref={enrollHref} />
|
|
650
716
|
</Suspense>
|
|
651
717
|
);
|
|
652
718
|
}
|
|
@@ -1,32 +1,32 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Account Route Exports
|
|
3
|
-
*
|
|
4
|
-
* Provides ready-to-use route exports for all 2FA/account management endpoints.
|
|
5
|
-
* These routes handle the complete 2FA flow with zero configuration.
|
|
6
|
-
*
|
|
7
|
-
* @example
|
|
8
|
-
* ```typescript
|
|
9
|
-
* // Import individual routes
|
|
10
|
-
* export { POST } from '@payez/next-mvp/routes/account/masked-info';
|
|
11
|
-
* export { POST } from '@payez/next-mvp/routes/account/send-code';
|
|
12
|
-
* export { POST } from '@payez/next-mvp/routes/account/verify-email';
|
|
13
|
-
* export { POST } from '@payez/next-mvp/routes/account/verify-sms';
|
|
14
|
-
* ```
|
|
15
|
-
*
|
|
16
|
-
* @version 2.3.0
|
|
17
|
-
* @since auth-ready-v2
|
|
18
|
-
*/
|
|
19
|
-
|
|
20
|
-
// Export individual route modules
|
|
21
|
-
export * as maskedInfo from './masked-info';
|
|
22
|
-
export * as sendCode from './send-code';
|
|
23
|
-
export * as verifyEmail from './verify-email';
|
|
24
|
-
export * as verifySms from './verify-sms';
|
|
25
|
-
export * as updatePhone from './update-phone';
|
|
26
|
-
|
|
27
|
-
// Re-export POST handlers for convenience (all 2FA endpoints use POST)
|
|
28
|
-
export { POST as maskedInfoPOST } from './masked-info';
|
|
29
|
-
export { POST as sendCodePOST } from './send-code';
|
|
30
|
-
export { POST as verifyEmailPOST } from './verify-email';
|
|
31
|
-
export { POST as verifySmsPOST } from './verify-sms';
|
|
1
|
+
/**
|
|
2
|
+
* Account Route Exports
|
|
3
|
+
*
|
|
4
|
+
* Provides ready-to-use route exports for all 2FA/account management endpoints.
|
|
5
|
+
* These routes handle the complete 2FA flow with zero configuration.
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* ```typescript
|
|
9
|
+
* // Import individual routes
|
|
10
|
+
* export { POST } from '@payez/next-mvp/routes/account/masked-info';
|
|
11
|
+
* export { POST } from '@payez/next-mvp/routes/account/send-code';
|
|
12
|
+
* export { POST } from '@payez/next-mvp/routes/account/verify-email';
|
|
13
|
+
* export { POST } from '@payez/next-mvp/routes/account/verify-sms';
|
|
14
|
+
* ```
|
|
15
|
+
*
|
|
16
|
+
* @version 2.3.0
|
|
17
|
+
* @since auth-ready-v2
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
// Export individual route modules
|
|
21
|
+
export * as maskedInfo from './masked-info';
|
|
22
|
+
export * as sendCode from './send-code';
|
|
23
|
+
export * as verifyEmail from './verify-email';
|
|
24
|
+
export * as verifySms from './verify-sms';
|
|
25
|
+
export * as updatePhone from './update-phone';
|
|
26
|
+
|
|
27
|
+
// Re-export POST handlers for convenience (all 2FA endpoints use POST)
|
|
28
|
+
export { POST as maskedInfoPOST } from './masked-info';
|
|
29
|
+
export { POST as sendCodePOST } from './send-code';
|
|
30
|
+
export { POST as verifyEmailPOST } from './verify-email';
|
|
31
|
+
export { POST as verifySmsPOST } from './verify-sms';
|
|
32
32
|
export { POST as updatePhonePOST } from './update-phone';
|