@payez/next-mvp 4.8.0 → 4.8.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/POST-INSTALL.md +408 -408
- package/README.md +859 -859
- package/dist/components/account/MobileNavDrawer.js +18 -18
- package/dist/components/account/UserAvatarMenu.js +1 -1
- package/dist/components/admin/AlertSettingsTab.js +40 -40
- package/dist/components/admin/LoggingSettingsTab.js +28 -28
- package/dist/lib/session-store.js +21 -21
- package/dist/lib/startup-init.js +19 -19
- package/dist/middleware/create-middleware.js +6 -5
- package/dist/pages/login/page.js +28 -28
- package/dist/pages/settings/EnhancedSettingsPage.js +17 -17
- package/dist/vibe/sessions.js +28 -3
- package/package.json +2 -2
- package/scripts/check-publish-eol.mjs +33 -15
- package/src/api/auth-handler.ts +550 -550
- package/src/api/index.ts +17 -17
- package/src/api-handlers/account/change-password.ts +170 -170
- package/src/api-handlers/account/masked-info.ts +46 -46
- package/src/api-handlers/account/profile.ts +99 -99
- package/src/api-handlers/account/recovery/forward.ts +117 -117
- package/src/api-handlers/account/recovery/initiate.ts +59 -59
- package/src/api-handlers/account/recovery/send-code.ts +21 -21
- package/src/api-handlers/account/recovery/verify-code.ts +20 -20
- package/src/api-handlers/account/reset-password.ts +87 -87
- package/src/api-handlers/account/send-code.ts +70 -70
- package/src/api-handlers/account/update-phone.ts +84 -84
- package/src/api-handlers/account/validate-password.ts +139 -139
- package/src/api-handlers/account/verify-email.ts +134 -134
- package/src/api-handlers/account/verify-sms.ts +134 -134
- package/src/api-handlers/admin/analytics.ts +452 -452
- package/src/api-handlers/admin/audit.ts +232 -232
- package/src/api-handlers/admin/index.ts +63 -63
- package/src/api-handlers/admin/redis-sessions.ts +259 -259
- package/src/api-handlers/admin/sessions.ts +327 -327
- package/src/api-handlers/admin/site-logs.ts +388 -388
- package/src/api-handlers/admin/stats.ts +260 -260
- package/src/api-handlers/admin/users.ts +251 -251
- package/src/api-handlers/admin/vibe-data.ts +333 -333
- package/src/api-handlers/anon/preferences.ts +123 -123
- package/src/api-handlers/auth/credential-login-email-code.ts +92 -92
- package/src/api-handlers/auth/credential-login.ts +948 -948
- package/src/api-handlers/auth/jwks.ts +31 -31
- package/src/api-handlers/auth/login.ts +252 -252
- package/src/api-handlers/auth/passkey-signin.ts +116 -116
- package/src/api-handlers/auth/refresh.ts +829 -829
- package/src/api-handlers/auth/signout.ts +201 -201
- package/src/api-handlers/auth/status.ts +23 -23
- package/src/api-handlers/auth/update-session.ts +144 -144
- package/src/api-handlers/auth/validate.ts +45 -45
- package/src/api-handlers/auth/verify-code.ts +116 -116
- package/src/api-handlers/mfa/backup-code-verify.ts +170 -170
- package/src/api-handlers/mfa/backup-codes.ts +42 -42
- package/src/api-handlers/mfa/methods.ts +47 -47
- package/src/api-handlers/mfa/totp-confirm.ts +43 -43
- package/src/api-handlers/mfa/totp-enroll.ts +43 -43
- package/src/api-handlers/mfa/totp-verify.ts +172 -172
- package/src/api-handlers/oauth/resume.ts +182 -182
- package/src/api-handlers/onboarding/send-email-code.ts +18 -18
- package/src/api-handlers/onboarding/set-password.ts +21 -21
- package/src/api-handlers/onboarding/verify-code.ts +20 -20
- package/src/api-handlers/onboarding/verify-welcome-email.ts +17 -17
- package/src/api-handlers/passkeys/list.ts +32 -32
- package/src/api-handlers/passkeys/manage.ts +55 -55
- package/src/api-handlers/passkeys/register-options.ts +43 -43
- package/src/api-handlers/passkeys/register-verify.ts +42 -42
- package/src/api-handlers/session/refresh-viability.ts +56 -56
- package/src/api-handlers/session/viability.ts +238 -238
- package/src/api-handlers/test/force-expire.ts +167 -167
- package/src/auth/auth-decision.ts +292 -292
- package/src/auth/better-auth.ts +1688 -1688
- package/src/auth/idp-oauth.ts +310 -310
- package/src/auth/password-reset-bridge.ts +98 -98
- package/src/auth/route-config.ts +236 -236
- package/src/auth/safe-callback.ts +199 -199
- package/src/auth/session-cookie-clear.ts +30 -30
- package/src/auth/session-reset-link.ts +159 -159
- package/src/auth/session-response-sanitizer.ts +151 -151
- package/src/auth/types/auth-types.ts +555 -555
- package/src/auth/types/index.ts +7 -7
- package/src/auth/unauthenticated-routes.ts +2 -2
- package/src/auth/utils/idp-client.ts +364 -364
- package/src/auth/utils/index.ts +6 -6
- package/src/auth/utils/token-utils.ts +243 -243
- package/src/client/AuthContext.tsx +192 -192
- package/src/client/better-auth-client.ts +70 -70
- package/src/client/fetch-with-auth.ts +47 -47
- package/src/client/fetchWithSession.ts +21 -21
- package/src/client/index.ts +13 -13
- package/src/client/useAnonSession.ts +131 -131
- package/src/components/SessionSync.tsx +138 -138
- package/src/components/SignalRHealthCheck.tsx +131 -131
- package/src/components/account/MobileNavDrawer.tsx +305 -305
- package/src/components/account/UserAvatarMenu.tsx +277 -277
- package/src/components/account/index.ts +13 -13
- package/src/components/admin/AlertSettingsTab.tsx +728 -728
- package/src/components/admin/AnalyticsTab.tsx +703 -703
- package/src/components/admin/DataBrowserTab.tsx +508 -508
- package/src/components/admin/LoggingSettingsTab.tsx +665 -665
- package/src/components/admin/SessionsTab.tsx +421 -421
- package/src/components/admin/StatsTab.tsx +379 -379
- package/src/components/admin/VibeAdminContext.tsx +87 -87
- package/src/components/admin/VibeAdminLayout.tsx +187 -187
- package/src/components/admin/index.ts +59 -59
- package/src/components/auth/CredentialSignInForm.tsx +240 -240
- package/src/components/auth/FederatedAuthSection.tsx +129 -129
- package/src/components/auth/ModeAwareLoginPage.tsx +146 -146
- package/src/components/auth/ModeAwareSignupPage.tsx +200 -200
- package/src/components/auth/PasswordResetBridge.tsx +30 -30
- package/src/components/auth/TraditionalAuthSection.tsx +103 -103
- package/src/components/recovery/CompleteStep.tsx +36 -36
- package/src/components/recovery/InitiateRecoveryStep.tsx +68 -68
- package/src/components/recovery/RequestedStep.tsx +30 -30
- package/src/components/recovery/SelectMethodStep.tsx +73 -73
- package/src/components/recovery/SetPasswordStep.tsx +118 -118
- package/src/components/recovery/VerifyCodeStep.tsx +90 -90
- package/src/components/reserved/ReservedRecoveryWarning.tsx +160 -160
- package/src/components/reserved/ReservedStatusBox.tsx +118 -118
- package/src/components/ui/BetaBadge.tsx +58 -58
- package/src/components/ui/Footer.tsx +93 -93
- package/src/config/env.ts +55 -55
- package/src/config/logger.ts +62 -62
- package/src/config/logging-config.ts +82 -82
- package/src/config/unauthenticated-routes.ts +19 -19
- package/src/config/vibe-log-transport.ts +264 -264
- package/src/edge/internal-api-url.ts +65 -65
- package/src/edge/middleware.ts +42 -42
- package/src/hooks/useAuth.ts +117 -117
- package/src/hooks/useAuthSettings.ts +109 -109
- package/src/hooks/useAvailableProviders.ts +143 -143
- package/src/hooks/usePasswordValidation.ts +127 -127
- package/src/hooks/useProfile.ts +82 -82
- package/src/hooks/usePublicAuthSettings.ts +171 -171
- package/src/hooks/useSessionExpiration.ts +101 -101
- package/src/hooks/useViabilitySession.ts +337 -337
- package/src/index.ts +64 -64
- package/src/lib/anon-session.ts +213 -213
- package/src/lib/api-handler.ts +643 -643
- package/src/lib/app-slug.ts +228 -228
- package/src/lib/authorize-resume.ts +172 -172
- package/src/lib/bearer-fetch.ts +31 -31
- package/src/lib/client-key.ts +88 -88
- package/src/lib/code-step-copy.ts +83 -83
- package/src/lib/credential-sign-in-client.ts +101 -101
- package/src/lib/demo-mode.ts +18 -18
- package/src/lib/device-id.ts +67 -67
- package/src/lib/ensure-fresh-access-token.ts +509 -509
- package/src/lib/geolocation.ts +265 -265
- package/src/lib/idp-client-config.ts +1005 -1005
- package/src/lib/idp-fetch.ts +169 -169
- package/src/lib/idp-url-guard.ts +37 -37
- package/src/lib/internal-api.ts +171 -171
- package/src/lib/jwt-decode-client.ts +45 -45
- package/src/lib/jwt-decode.ts +82 -82
- package/src/lib/lock-window.ts +49 -49
- package/src/lib/login-error-message.ts +41 -41
- package/src/lib/never-in-response.ts +64 -64
- package/src/lib/nextauth-secret.ts +168 -168
- package/src/lib/onboarding-proxy.ts +57 -57
- package/src/lib/password-rules.ts +50 -50
- package/src/lib/rate-limit-service.ts +9 -9
- package/src/lib/redis.ts +105 -105
- package/src/lib/refresh-token-validator.ts +64 -64
- package/src/lib/roles.ts +177 -177
- package/src/lib/safe-error-response.ts +149 -149
- package/src/lib/secret-validation.ts +8 -8
- package/src/lib/session-cookie-shape.ts +185 -185
- package/src/lib/session-store.ts +1092 -1092
- package/src/lib/session.ts +34 -34
- package/src/lib/single-flight.ts +64 -64
- package/src/lib/site-logger.ts +245 -245
- package/src/lib/standardized-client-api.ts +901 -901
- package/src/lib/startup-init.ts +276 -276
- package/src/lib/test-aware-get-token.ts +41 -41
- package/src/lib/token-expiry.ts +40 -40
- package/src/lib/token-lifecycle.ts +546 -546
- package/src/lib/token-log-id.ts +69 -69
- package/src/lib/twofa-rejection.ts +91 -91
- package/src/lib/types/api-responses.ts +336 -336
- package/src/lib/user-agent-parser.ts +252 -252
- package/src/logging/api/admin-analytics.ts +51 -51
- package/src/logging/api/audit-log.ts +53 -53
- package/src/logging/components/AdminAnalyticsLayout.tsx +49 -49
- package/src/logging/components/AuditLogViewer.tsx +125 -125
- package/src/logging/components/ErrorMetricsCard.tsx +98 -98
- package/src/logging/components/HealthMetricsCard.tsx +70 -70
- package/src/logging/hooks/useAdminAnalytics.ts +22 -22
- package/src/logging/hooks/useAuditLog.ts +24 -24
- package/src/logging/hooks/useErrorMetrics.ts +40 -40
- package/src/logging/hooks/useHealthMetrics.ts +44 -44
- package/src/logging/index.ts +18 -18
- package/src/logging/types/analytics.ts +81 -81
- package/src/logging/types/audit.ts +31 -31
- package/src/logging/types/index.ts +3 -3
- package/src/middleware/create-middleware.ts +1139 -1138
- package/src/middleware/rbac-check.ts +279 -279
- package/src/middleware/twofa-presets.ts +224 -224
- package/src/models/DecodedAccessToken.ts +16 -16
- package/src/models/SessionModel.ts +331 -331
- package/src/pages/admin-login/page.tsx +111 -111
- package/src/pages/admin-page-permissions/PagePermissionsAdminPage.tsx +527 -527
- package/src/pages/admin-page-permissions/index.ts +7 -7
- package/src/pages/admin-roles/RolesAdminPage.tsx +533 -533
- package/src/pages/admin-roles/index.ts +9 -9
- package/src/pages/admin-roles/modals.tsx +469 -469
- package/src/pages/client-admin/ClientSiteAdminPage.tsx +382 -382
- package/src/pages/client-admin/index.ts +33 -33
- package/src/pages/coming-soon/page.tsx +83 -83
- package/src/pages/login/page.tsx +338 -338
- package/src/pages/onboarding/index.ts +11 -11
- package/src/pages/onboarding/page.tsx +212 -212
- package/src/pages/profile/EnhancedProfilePage.tsx +514 -514
- package/src/pages/profile/index.ts +9 -9
- package/src/pages/profile/page.tsx +166 -166
- package/src/pages/recovery/page.tsx +267 -267
- package/src/pages/roles/MyRolesPage.tsx +211 -211
- package/src/pages/roles/components.tsx +294 -294
- package/src/pages/roles/index.ts +17 -17
- package/src/pages/security/EnhancedSecurityPage.tsx +660 -660
- package/src/pages/security/index.ts +12 -12
- package/src/pages/security/page.tsx +507 -507
- package/src/pages/settings/EnhancedSettingsPage.tsx +642 -642
- package/src/pages/settings/index.ts +9 -9
- package/src/pages/settings/page.tsx +47 -47
- package/src/pages/showcase/ShowcasePage.tsx +532 -532
- package/src/pages/showcase/index.ts +13 -13
- package/src/pages/test-env/EmergencyLogoutPage.tsx +192 -192
- package/src/pages/test-env/JwtInspectPage.tsx +449 -449
- package/src/pages/test-env/RefreshTokenPage.tsx +178 -178
- package/src/pages/test-env/TestEnvPage.tsx +130 -130
- package/src/pages/test-env/guarded.tsx +81 -81
- package/src/pages/test-env/index.ts +77 -77
- package/src/pages/test-env/redact-credentials.ts +96 -96
- package/src/pages/verify-code/page.tsx +746 -746
- package/src/routes/account/index.ts +31 -31
- package/src/routes/account/masked-info.ts +36 -36
- package/src/routes/account/send-code.ts +39 -39
- package/src/routes/account/update-phone.ts +13 -13
- package/src/routes/account/verify-email.ts +40 -40
- package/src/routes/account/verify-sms.ts +40 -40
- package/src/routes/auth/credential-login-email-code.ts +6 -6
- package/src/routes/auth/credential-login.ts +6 -6
- package/src/routes/auth/index.ts +22 -22
- package/src/routes/auth/logout.ts +111 -111
- package/src/routes/auth/nextauth.ts +45 -45
- package/src/routes/auth/passkey-signin.ts +13 -13
- package/src/routes/auth/refresh.ts +77 -77
- package/src/routes/auth/session-reset.ts +37 -37
- package/src/routes/auth/session.ts +261 -261
- package/src/routes/auth/settings.ts +181 -181
- package/src/routes/auth/viability.ts +519 -519
- package/src/routes/index.ts +17 -17
- package/src/routes/mfa/backup-code-verify.ts +1 -1
- package/src/routes/mfa/backup-codes.ts +10 -10
- package/src/routes/mfa/methods.ts +22 -22
- package/src/routes/mfa/totp-confirm.ts +10 -10
- package/src/routes/mfa/totp-enroll.ts +10 -10
- package/src/routes/mfa/totp-verify.ts +14 -14
- package/src/routes/onboarding/index.ts +15 -15
- package/src/routes/onboarding/send-email-code.ts +2 -2
- package/src/routes/onboarding/set-password.ts +2 -2
- package/src/routes/onboarding/verify-code.ts +2 -2
- package/src/routes/onboarding/verify-welcome-email.ts +2 -2
- package/src/routes/passkeys/list.ts +10 -10
- package/src/routes/passkeys/manage.ts +23 -23
- package/src/routes/passkeys/register-options.ts +10 -10
- package/src/routes/passkeys/register-verify.ts +10 -10
- package/src/routes/session/index.ts +7 -7
- package/src/routes/session/refresh-viability.ts +17 -17
- package/src/server/auth-guard.ts +203 -203
- package/src/server/auth.ts +486 -486
- package/src/server/decode-session.ts +309 -309
- package/src/server/slim-middleware.ts +117 -117
- package/src/server/verified-session-token.ts +103 -103
- package/src/server/with-auth.ts +93 -93
- package/src/services/signalrActivityService.ts +258 -258
- package/src/stores/authStore.ts +1900 -1900
- package/src/templates/instrumentation.ts +60 -60
- package/src/theme/ThemeProvider.tsx +39 -39
- package/src/theme/default.ts +33 -33
- package/src/theme/index.ts +31 -31
- package/src/theme/types.ts +69 -69
- package/src/theme/useTheme.ts +57 -57
- package/src/theme/utils.ts +40 -40
- package/src/types/api.ts +12 -12
- package/src/types/auth.d.ts +23 -23
- package/src/types/auth.ts +48 -48
- package/src/types/logging.ts +11 -11
- package/src/types/recovery.ts +54 -54
- package/src/utils/api.ts +401 -401
- package/src/utils/circuitBreaker.ts +40 -40
- package/src/utils/error-message.ts +108 -108
- package/src/utils/layout/reservedSpace.ts +124 -124
- package/src/utils/logout.ts +30 -30
- package/src/vibe/client.ts +877 -877
- package/src/vibe/enterprise-auth.ts +208 -208
- package/src/vibe/errors.ts +209 -209
- package/src/vibe/generic.ts +339 -339
- package/src/vibe/hooks/index.ts +367 -367
- package/src/vibe/index.ts +133 -133
- package/src/vibe/sessions.ts +925 -899
- package/src/vibe/types.ts +577 -577
|
@@ -1,267 +1,267 @@
|
|
|
1
|
-
'use client';
|
|
2
|
-
|
|
3
|
-
import { useState, useEffect, Suspense } from 'react';
|
|
4
|
-
import { useRouter, useSearchParams } from 'next/navigation';
|
|
5
|
-
import { RecoveryStep, RecoverySession, PasswordResetToken } from '../../types/recovery';
|
|
6
|
-
import { accountApi } from '../../utils/api';
|
|
7
|
-
import { recoveryEntry, resetSubmitTarget } from '../../auth/password-reset-bridge';
|
|
8
|
-
import { InitiateRecoveryStep } from '../../components/recovery/InitiateRecoveryStep';
|
|
9
|
-
import { SelectMethodStep } from '../../components/recovery/SelectMethodStep';
|
|
10
|
-
import { VerifyCodeStep } from '../../components/recovery/VerifyCodeStep';
|
|
11
|
-
import { SetPasswordStep } from '../../components/recovery/SetPasswordStep';
|
|
12
|
-
import { CompleteStep } from '../../components/recovery/CompleteStep';
|
|
13
|
-
import { RequestedStep } from '../../components/recovery/RequestedStep';
|
|
14
|
-
import { useColors } from '../../theme/useTheme';
|
|
15
|
-
|
|
16
|
-
function RecoveryContent() {
|
|
17
|
-
const router = useRouter();
|
|
18
|
-
const searchParams = useSearchParams();
|
|
19
|
-
// PAY-2121 (BAPert 70576): a VERIFIED passwordless user arrives here with a real
|
|
20
|
-
// reset token (?token=) from the IdP's post-verify fragment, via the bridge. In
|
|
21
|
-
// that case skip the request-a-code step and go straight to set-password - the
|
|
22
|
-
// token is the credential; asking for a code would throw it away.
|
|
23
|
-
const entry = recoveryEntry(searchParams?.get('token'), searchParams?.get('email'));
|
|
24
|
-
const colors = useColors();
|
|
25
|
-
|
|
26
|
-
const [currentStep, setCurrentStep] = useState<RecoveryStep>(entry.step);
|
|
27
|
-
const [email, setEmail] = useState(entry.email);
|
|
28
|
-
const [recoverySession, setRecoverySession] = useState<RecoverySession | null>(null);
|
|
29
|
-
const [selectedMethod, setSelectedMethod] = useState<'email' | 'sms' | 'authenticator' | null>(null);
|
|
30
|
-
const [verificationCode, setVerificationCode] = useState('');
|
|
31
|
-
const [passwordResetToken, setPasswordResetToken] = useState<PasswordResetToken | null>(
|
|
32
|
-
entry.resetToken ? { token: entry.resetToken, expiresAt: '' } : null,
|
|
33
|
-
);
|
|
34
|
-
const [error, setError] = useState<string | null>(null);
|
|
35
|
-
// PAY-1591: true when the reset failed because the recovery itself is dead; the error box then offers a way back.
|
|
36
|
-
const [recoveryExpired, setRecoveryExpired] = useState(false);
|
|
37
|
-
const [loading, setLoading] = useState(false);
|
|
38
|
-
// PAY-1588: true only after the IdP accepted the new password. The 'complete' step is ALSO reached from initiate()
|
|
39
|
-
// when no recovery session comes back, and that path must not tell the user a password was set.
|
|
40
|
-
const [resetSucceeded, setResetSucceeded] = useState(false);
|
|
41
|
-
|
|
42
|
-
const handleInitiateRecovery = async () => {
|
|
43
|
-
setLoading(true);
|
|
44
|
-
setError(null);
|
|
45
|
-
|
|
46
|
-
try {
|
|
47
|
-
const response = await accountApi.initiateRecovery(email);
|
|
48
|
-
|
|
49
|
-
if (response.success && response.data?.recovery_session_token) {
|
|
50
|
-
setRecoverySession({
|
|
51
|
-
recoveryToken: response.data.recovery_session_token,
|
|
52
|
-
email: email,
|
|
53
|
-
maskedEmail: response.data.masked_email,
|
|
54
|
-
maskedPhone: response.data.masked_phone,
|
|
55
|
-
hasAuthenticator: response.data.has_authenticator,
|
|
56
|
-
availableMethods: response.data.available_methods || [],
|
|
57
|
-
expiresAt: response.data.expires_at || ''
|
|
58
|
-
});
|
|
59
|
-
setCurrentStep('select-method');
|
|
60
|
-
} else {
|
|
61
|
-
// No recovery session (unknown / locked / throttled): the ONE neutral step, never CompleteStep (rigpert 60711).
|
|
62
|
-
setCurrentStep('requested');
|
|
63
|
-
}
|
|
64
|
-
} catch (err) {
|
|
65
|
-
setError(err instanceof Error ? err.message : 'Failed to initiate recovery. Please try again.');
|
|
66
|
-
} finally {
|
|
67
|
-
setLoading(false);
|
|
68
|
-
}
|
|
69
|
-
};
|
|
70
|
-
|
|
71
|
-
const handleSendCode = async (method: 'email' | 'sms' | 'authenticator') => {
|
|
72
|
-
if (!recoverySession) return;
|
|
73
|
-
|
|
74
|
-
setLoading(true);
|
|
75
|
-
setError(null);
|
|
76
|
-
setSelectedMethod(method);
|
|
77
|
-
|
|
78
|
-
try {
|
|
79
|
-
const response = await accountApi.sendRecoveryCode(recoverySession.recoveryToken, method);
|
|
80
|
-
|
|
81
|
-
if (response.success) {
|
|
82
|
-
setCurrentStep('verify-code');
|
|
83
|
-
} else {
|
|
84
|
-
setError('Failed to send verification code. Please try again.');
|
|
85
|
-
}
|
|
86
|
-
} catch (err) {
|
|
87
|
-
setError(err instanceof Error ? err.message : 'Failed to send verification code.');
|
|
88
|
-
} finally {
|
|
89
|
-
setLoading(false);
|
|
90
|
-
}
|
|
91
|
-
};
|
|
92
|
-
|
|
93
|
-
const handleVerifyCode = async () => {
|
|
94
|
-
if (!recoverySession || !selectedMethod) return;
|
|
95
|
-
|
|
96
|
-
setLoading(true);
|
|
97
|
-
setError(null);
|
|
98
|
-
|
|
99
|
-
try {
|
|
100
|
-
const response = await accountApi.verifyRecoveryCode(
|
|
101
|
-
recoverySession.recoveryToken,
|
|
102
|
-
verificationCode,
|
|
103
|
-
selectedMethod
|
|
104
|
-
);
|
|
105
|
-
|
|
106
|
-
if (response.success && response.data) {
|
|
107
|
-
setPasswordResetToken({
|
|
108
|
-
token: response.data.password_reset_token,
|
|
109
|
-
expiresAt: response.data.expires_at
|
|
110
|
-
});
|
|
111
|
-
setCurrentStep('set-password');
|
|
112
|
-
} else {
|
|
113
|
-
setError(response.error?.message || 'Invalid verification code');
|
|
114
|
-
}
|
|
115
|
-
} catch (err) {
|
|
116
|
-
setError(err instanceof Error ? err.message : 'Failed to verify code.');
|
|
117
|
-
} finally {
|
|
118
|
-
setLoading(false);
|
|
119
|
-
}
|
|
120
|
-
};
|
|
121
|
-
|
|
122
|
-
const handleResetPassword = async (password: string, confirmPassword: string) => {
|
|
123
|
-
// PAY-2121 (BAPert 70582): the ONE place that decides whether a submit may proceed.
|
|
124
|
-
// No token (or no email anywhere) -> refuse BEFORE any request; a guard-less submit
|
|
125
|
-
// would otherwise call the API with an empty token.
|
|
126
|
-
const target = resetSubmitTarget(passwordResetToken?.token, recoverySession?.email, email);
|
|
127
|
-
if (!target) {
|
|
128
|
-
setError(passwordResetToken ? 'We could not tell which account to reset. Start recovery again.' : 'This reset link is missing its token. Start recovery again.');
|
|
129
|
-
return;
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
setLoading(true);
|
|
133
|
-
setError(null);
|
|
134
|
-
setRecoveryExpired(false);
|
|
135
|
-
|
|
136
|
-
try {
|
|
137
|
-
const response = await accountApi.resetPasswordWithToken(
|
|
138
|
-
target.email,
|
|
139
|
-
target.token,
|
|
140
|
-
password,
|
|
141
|
-
confirmPassword
|
|
142
|
-
);
|
|
143
|
-
|
|
144
|
-
if (response.success) {
|
|
145
|
-
setResetSucceeded(true);
|
|
146
|
-
setCurrentStep('complete');
|
|
147
|
-
} else {
|
|
148
|
-
setError('Failed to reset password. Please try again.');
|
|
149
|
-
}
|
|
150
|
-
} catch (err) {
|
|
151
|
-
setError(err instanceof Error ? err.message : 'Failed to reset password.');
|
|
152
|
-
setRecoveryExpired(!!err && (err as { code?: string }).code === 'RECOVERY_EXPIRED');
|
|
153
|
-
} finally {
|
|
154
|
-
setLoading(false);
|
|
155
|
-
}
|
|
156
|
-
};
|
|
157
|
-
|
|
158
|
-
const handleGoBack = () => {
|
|
159
|
-
if (currentStep === 'select-method') setCurrentStep('initiate');
|
|
160
|
-
else if (currentStep === 'verify-code') setCurrentStep('select-method');
|
|
161
|
-
// PAY-2121: a deep-linked set-password has no verify-code to go back to; return to
|
|
162
|
-
// the request-code step so a bad token still leaves a way forward.
|
|
163
|
-
else if (currentStep === 'set-password') setCurrentStep(recoverySession ? 'verify-code' : 'initiate');
|
|
164
|
-
};
|
|
165
|
-
|
|
166
|
-
return (
|
|
167
|
-
<div className="w-full flex items-center justify-center p-6" style={{ background: 'var(--bg-default)' }}>
|
|
168
|
-
<div className="max-w-md w-full">
|
|
169
|
-
{error && (
|
|
170
|
-
<div className="mb-4 bg-red-50 border border-red-200 rounded-2xl p-3">
|
|
171
|
-
<p className="text-red-700 text-sm">{error}</p>
|
|
172
|
-
{recoveryExpired && (
|
|
173
|
-
<a href="/account-auth/recovery" className="text-sm font-medium underline">
|
|
174
|
-
Start password recovery again
|
|
175
|
-
</a>
|
|
176
|
-
)}
|
|
177
|
-
</div>
|
|
178
|
-
)}
|
|
179
|
-
|
|
180
|
-
{loading && (
|
|
181
|
-
<div className="mb-4 rounded-2xl p-3" style={{ background: 'var(--bg-card)', borderColor: 'var(--border-default)' }}>
|
|
182
|
-
<p className="text-sm" style={{ color: 'var(--text-secondary)' }}>Processing...</p>
|
|
183
|
-
</div>
|
|
184
|
-
)}
|
|
185
|
-
|
|
186
|
-
{currentStep === 'initiate' && (
|
|
187
|
-
<InitiateRecoveryStep
|
|
188
|
-
email={email}
|
|
189
|
-
setEmail={setEmail}
|
|
190
|
-
onSubmit={handleInitiateRecovery}
|
|
191
|
-
loading={loading}
|
|
192
|
-
/>
|
|
193
|
-
)}
|
|
194
|
-
|
|
195
|
-
{currentStep === 'select-method' && recoverySession && (
|
|
196
|
-
<SelectMethodStep
|
|
197
|
-
session={recoverySession}
|
|
198
|
-
onSelectMethod={handleSendCode}
|
|
199
|
-
loading={loading}
|
|
200
|
-
/>
|
|
201
|
-
)}
|
|
202
|
-
|
|
203
|
-
{currentStep === 'verify-code' && (
|
|
204
|
-
<VerifyCodeStep
|
|
205
|
-
code={verificationCode}
|
|
206
|
-
setCode={setVerificationCode}
|
|
207
|
-
onSubmit={handleVerifyCode}
|
|
208
|
-
onResend={() => selectedMethod && handleSendCode(selectedMethod)}
|
|
209
|
-
loading={loading}
|
|
210
|
-
maskedDestination={
|
|
211
|
-
selectedMethod === 'sms'
|
|
212
|
-
? recoverySession?.maskedPhone
|
|
213
|
-
: recoverySession?.maskedEmail
|
|
214
|
-
}
|
|
215
|
-
/>
|
|
216
|
-
)}
|
|
217
|
-
|
|
218
|
-
{currentStep === 'set-password' && (
|
|
219
|
-
<SetPasswordStep
|
|
220
|
-
onSubmit={handleResetPassword}
|
|
221
|
-
loading={loading}
|
|
222
|
-
/>
|
|
223
|
-
)}
|
|
224
|
-
|
|
225
|
-
{currentStep === 'requested' && <RequestedStep onGoToLogin={() => router.push('/account-auth/login')} />}
|
|
226
|
-
|
|
227
|
-
{currentStep === 'complete' && (
|
|
228
|
-
<CompleteStep
|
|
229
|
-
onGoToLogin={() => router.push(resetSucceeded ? '/account-auth/login?notice=password-set' : '/account-auth/login')}
|
|
230
|
-
/>
|
|
231
|
-
)}
|
|
232
|
-
|
|
233
|
-
{currentStep !== 'complete' && currentStep !== 'requested' && currentStep !== 'initiate' && !loading && (
|
|
234
|
-
<div className="mt-6 text-center">
|
|
235
|
-
<button
|
|
236
|
-
type="button"
|
|
237
|
-
onClick={handleGoBack}
|
|
238
|
-
className="text-sm hover:underline font-medium"
|
|
239
|
-
style={{ color: 'var(--text-primary)' }}
|
|
240
|
-
>
|
|
241
|
-
← Go Back
|
|
242
|
-
</button>
|
|
243
|
-
</div>
|
|
244
|
-
)}
|
|
245
|
-
</div>
|
|
246
|
-
</div>
|
|
247
|
-
);
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
function LoadingFallback() {
|
|
251
|
-
const colors = useColors();
|
|
252
|
-
return (
|
|
253
|
-
<div className="w-full flex items-center justify-center" style={{ background: 'var(--bg-default)' }}>
|
|
254
|
-
<div className="border rounded-2xl p-8" style={{ background: 'var(--bg-card)', borderColor: 'var(--border-default)' }}>
|
|
255
|
-
<p style={{ color: 'var(--text-muted)' }}>Loading...</p>
|
|
256
|
-
</div>
|
|
257
|
-
</div>
|
|
258
|
-
);
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
export default function RecoveryPage() {
|
|
262
|
-
return (
|
|
263
|
-
<Suspense fallback={<LoadingFallback />}>
|
|
264
|
-
<RecoveryContent />
|
|
265
|
-
</Suspense>
|
|
266
|
-
);
|
|
267
|
-
}
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useState, useEffect, Suspense } from 'react';
|
|
4
|
+
import { useRouter, useSearchParams } from 'next/navigation';
|
|
5
|
+
import { RecoveryStep, RecoverySession, PasswordResetToken } from '../../types/recovery';
|
|
6
|
+
import { accountApi } from '../../utils/api';
|
|
7
|
+
import { recoveryEntry, resetSubmitTarget } from '../../auth/password-reset-bridge';
|
|
8
|
+
import { InitiateRecoveryStep } from '../../components/recovery/InitiateRecoveryStep';
|
|
9
|
+
import { SelectMethodStep } from '../../components/recovery/SelectMethodStep';
|
|
10
|
+
import { VerifyCodeStep } from '../../components/recovery/VerifyCodeStep';
|
|
11
|
+
import { SetPasswordStep } from '../../components/recovery/SetPasswordStep';
|
|
12
|
+
import { CompleteStep } from '../../components/recovery/CompleteStep';
|
|
13
|
+
import { RequestedStep } from '../../components/recovery/RequestedStep';
|
|
14
|
+
import { useColors } from '../../theme/useTheme';
|
|
15
|
+
|
|
16
|
+
function RecoveryContent() {
|
|
17
|
+
const router = useRouter();
|
|
18
|
+
const searchParams = useSearchParams();
|
|
19
|
+
// PAY-2121 (BAPert 70576): a VERIFIED passwordless user arrives here with a real
|
|
20
|
+
// reset token (?token=) from the IdP's post-verify fragment, via the bridge. In
|
|
21
|
+
// that case skip the request-a-code step and go straight to set-password - the
|
|
22
|
+
// token is the credential; asking for a code would throw it away.
|
|
23
|
+
const entry = recoveryEntry(searchParams?.get('token'), searchParams?.get('email'));
|
|
24
|
+
const colors = useColors();
|
|
25
|
+
|
|
26
|
+
const [currentStep, setCurrentStep] = useState<RecoveryStep>(entry.step);
|
|
27
|
+
const [email, setEmail] = useState(entry.email);
|
|
28
|
+
const [recoverySession, setRecoverySession] = useState<RecoverySession | null>(null);
|
|
29
|
+
const [selectedMethod, setSelectedMethod] = useState<'email' | 'sms' | 'authenticator' | null>(null);
|
|
30
|
+
const [verificationCode, setVerificationCode] = useState('');
|
|
31
|
+
const [passwordResetToken, setPasswordResetToken] = useState<PasswordResetToken | null>(
|
|
32
|
+
entry.resetToken ? { token: entry.resetToken, expiresAt: '' } : null,
|
|
33
|
+
);
|
|
34
|
+
const [error, setError] = useState<string | null>(null);
|
|
35
|
+
// PAY-1591: true when the reset failed because the recovery itself is dead; the error box then offers a way back.
|
|
36
|
+
const [recoveryExpired, setRecoveryExpired] = useState(false);
|
|
37
|
+
const [loading, setLoading] = useState(false);
|
|
38
|
+
// PAY-1588: true only after the IdP accepted the new password. The 'complete' step is ALSO reached from initiate()
|
|
39
|
+
// when no recovery session comes back, and that path must not tell the user a password was set.
|
|
40
|
+
const [resetSucceeded, setResetSucceeded] = useState(false);
|
|
41
|
+
|
|
42
|
+
const handleInitiateRecovery = async () => {
|
|
43
|
+
setLoading(true);
|
|
44
|
+
setError(null);
|
|
45
|
+
|
|
46
|
+
try {
|
|
47
|
+
const response = await accountApi.initiateRecovery(email);
|
|
48
|
+
|
|
49
|
+
if (response.success && response.data?.recovery_session_token) {
|
|
50
|
+
setRecoverySession({
|
|
51
|
+
recoveryToken: response.data.recovery_session_token,
|
|
52
|
+
email: email,
|
|
53
|
+
maskedEmail: response.data.masked_email,
|
|
54
|
+
maskedPhone: response.data.masked_phone,
|
|
55
|
+
hasAuthenticator: response.data.has_authenticator,
|
|
56
|
+
availableMethods: response.data.available_methods || [],
|
|
57
|
+
expiresAt: response.data.expires_at || ''
|
|
58
|
+
});
|
|
59
|
+
setCurrentStep('select-method');
|
|
60
|
+
} else {
|
|
61
|
+
// No recovery session (unknown / locked / throttled): the ONE neutral step, never CompleteStep (rigpert 60711).
|
|
62
|
+
setCurrentStep('requested');
|
|
63
|
+
}
|
|
64
|
+
} catch (err) {
|
|
65
|
+
setError(err instanceof Error ? err.message : 'Failed to initiate recovery. Please try again.');
|
|
66
|
+
} finally {
|
|
67
|
+
setLoading(false);
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
const handleSendCode = async (method: 'email' | 'sms' | 'authenticator') => {
|
|
72
|
+
if (!recoverySession) return;
|
|
73
|
+
|
|
74
|
+
setLoading(true);
|
|
75
|
+
setError(null);
|
|
76
|
+
setSelectedMethod(method);
|
|
77
|
+
|
|
78
|
+
try {
|
|
79
|
+
const response = await accountApi.sendRecoveryCode(recoverySession.recoveryToken, method);
|
|
80
|
+
|
|
81
|
+
if (response.success) {
|
|
82
|
+
setCurrentStep('verify-code');
|
|
83
|
+
} else {
|
|
84
|
+
setError('Failed to send verification code. Please try again.');
|
|
85
|
+
}
|
|
86
|
+
} catch (err) {
|
|
87
|
+
setError(err instanceof Error ? err.message : 'Failed to send verification code.');
|
|
88
|
+
} finally {
|
|
89
|
+
setLoading(false);
|
|
90
|
+
}
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
const handleVerifyCode = async () => {
|
|
94
|
+
if (!recoverySession || !selectedMethod) return;
|
|
95
|
+
|
|
96
|
+
setLoading(true);
|
|
97
|
+
setError(null);
|
|
98
|
+
|
|
99
|
+
try {
|
|
100
|
+
const response = await accountApi.verifyRecoveryCode(
|
|
101
|
+
recoverySession.recoveryToken,
|
|
102
|
+
verificationCode,
|
|
103
|
+
selectedMethod
|
|
104
|
+
);
|
|
105
|
+
|
|
106
|
+
if (response.success && response.data) {
|
|
107
|
+
setPasswordResetToken({
|
|
108
|
+
token: response.data.password_reset_token,
|
|
109
|
+
expiresAt: response.data.expires_at
|
|
110
|
+
});
|
|
111
|
+
setCurrentStep('set-password');
|
|
112
|
+
} else {
|
|
113
|
+
setError(response.error?.message || 'Invalid verification code');
|
|
114
|
+
}
|
|
115
|
+
} catch (err) {
|
|
116
|
+
setError(err instanceof Error ? err.message : 'Failed to verify code.');
|
|
117
|
+
} finally {
|
|
118
|
+
setLoading(false);
|
|
119
|
+
}
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
const handleResetPassword = async (password: string, confirmPassword: string) => {
|
|
123
|
+
// PAY-2121 (BAPert 70582): the ONE place that decides whether a submit may proceed.
|
|
124
|
+
// No token (or no email anywhere) -> refuse BEFORE any request; a guard-less submit
|
|
125
|
+
// would otherwise call the API with an empty token.
|
|
126
|
+
const target = resetSubmitTarget(passwordResetToken?.token, recoverySession?.email, email);
|
|
127
|
+
if (!target) {
|
|
128
|
+
setError(passwordResetToken ? 'We could not tell which account to reset. Start recovery again.' : 'This reset link is missing its token. Start recovery again.');
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
setLoading(true);
|
|
133
|
+
setError(null);
|
|
134
|
+
setRecoveryExpired(false);
|
|
135
|
+
|
|
136
|
+
try {
|
|
137
|
+
const response = await accountApi.resetPasswordWithToken(
|
|
138
|
+
target.email,
|
|
139
|
+
target.token,
|
|
140
|
+
password,
|
|
141
|
+
confirmPassword
|
|
142
|
+
);
|
|
143
|
+
|
|
144
|
+
if (response.success) {
|
|
145
|
+
setResetSucceeded(true);
|
|
146
|
+
setCurrentStep('complete');
|
|
147
|
+
} else {
|
|
148
|
+
setError('Failed to reset password. Please try again.');
|
|
149
|
+
}
|
|
150
|
+
} catch (err) {
|
|
151
|
+
setError(err instanceof Error ? err.message : 'Failed to reset password.');
|
|
152
|
+
setRecoveryExpired(!!err && (err as { code?: string }).code === 'RECOVERY_EXPIRED');
|
|
153
|
+
} finally {
|
|
154
|
+
setLoading(false);
|
|
155
|
+
}
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
const handleGoBack = () => {
|
|
159
|
+
if (currentStep === 'select-method') setCurrentStep('initiate');
|
|
160
|
+
else if (currentStep === 'verify-code') setCurrentStep('select-method');
|
|
161
|
+
// PAY-2121: a deep-linked set-password has no verify-code to go back to; return to
|
|
162
|
+
// the request-code step so a bad token still leaves a way forward.
|
|
163
|
+
else if (currentStep === 'set-password') setCurrentStep(recoverySession ? 'verify-code' : 'initiate');
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
return (
|
|
167
|
+
<div className="w-full flex items-center justify-center p-6" style={{ background: 'var(--bg-default)' }}>
|
|
168
|
+
<div className="max-w-md w-full">
|
|
169
|
+
{error && (
|
|
170
|
+
<div className="mb-4 bg-red-50 border border-red-200 rounded-2xl p-3">
|
|
171
|
+
<p className="text-red-700 text-sm">{error}</p>
|
|
172
|
+
{recoveryExpired && (
|
|
173
|
+
<a href="/account-auth/recovery" className="text-sm font-medium underline">
|
|
174
|
+
Start password recovery again
|
|
175
|
+
</a>
|
|
176
|
+
)}
|
|
177
|
+
</div>
|
|
178
|
+
)}
|
|
179
|
+
|
|
180
|
+
{loading && (
|
|
181
|
+
<div className="mb-4 rounded-2xl p-3" style={{ background: 'var(--bg-card)', borderColor: 'var(--border-default)' }}>
|
|
182
|
+
<p className="text-sm" style={{ color: 'var(--text-secondary)' }}>Processing...</p>
|
|
183
|
+
</div>
|
|
184
|
+
)}
|
|
185
|
+
|
|
186
|
+
{currentStep === 'initiate' && (
|
|
187
|
+
<InitiateRecoveryStep
|
|
188
|
+
email={email}
|
|
189
|
+
setEmail={setEmail}
|
|
190
|
+
onSubmit={handleInitiateRecovery}
|
|
191
|
+
loading={loading}
|
|
192
|
+
/>
|
|
193
|
+
)}
|
|
194
|
+
|
|
195
|
+
{currentStep === 'select-method' && recoverySession && (
|
|
196
|
+
<SelectMethodStep
|
|
197
|
+
session={recoverySession}
|
|
198
|
+
onSelectMethod={handleSendCode}
|
|
199
|
+
loading={loading}
|
|
200
|
+
/>
|
|
201
|
+
)}
|
|
202
|
+
|
|
203
|
+
{currentStep === 'verify-code' && (
|
|
204
|
+
<VerifyCodeStep
|
|
205
|
+
code={verificationCode}
|
|
206
|
+
setCode={setVerificationCode}
|
|
207
|
+
onSubmit={handleVerifyCode}
|
|
208
|
+
onResend={() => selectedMethod && handleSendCode(selectedMethod)}
|
|
209
|
+
loading={loading}
|
|
210
|
+
maskedDestination={
|
|
211
|
+
selectedMethod === 'sms'
|
|
212
|
+
? recoverySession?.maskedPhone
|
|
213
|
+
: recoverySession?.maskedEmail
|
|
214
|
+
}
|
|
215
|
+
/>
|
|
216
|
+
)}
|
|
217
|
+
|
|
218
|
+
{currentStep === 'set-password' && (
|
|
219
|
+
<SetPasswordStep
|
|
220
|
+
onSubmit={handleResetPassword}
|
|
221
|
+
loading={loading}
|
|
222
|
+
/>
|
|
223
|
+
)}
|
|
224
|
+
|
|
225
|
+
{currentStep === 'requested' && <RequestedStep onGoToLogin={() => router.push('/account-auth/login')} />}
|
|
226
|
+
|
|
227
|
+
{currentStep === 'complete' && (
|
|
228
|
+
<CompleteStep
|
|
229
|
+
onGoToLogin={() => router.push(resetSucceeded ? '/account-auth/login?notice=password-set' : '/account-auth/login')}
|
|
230
|
+
/>
|
|
231
|
+
)}
|
|
232
|
+
|
|
233
|
+
{currentStep !== 'complete' && currentStep !== 'requested' && currentStep !== 'initiate' && !loading && (
|
|
234
|
+
<div className="mt-6 text-center">
|
|
235
|
+
<button
|
|
236
|
+
type="button"
|
|
237
|
+
onClick={handleGoBack}
|
|
238
|
+
className="text-sm hover:underline font-medium"
|
|
239
|
+
style={{ color: 'var(--text-primary)' }}
|
|
240
|
+
>
|
|
241
|
+
← Go Back
|
|
242
|
+
</button>
|
|
243
|
+
</div>
|
|
244
|
+
)}
|
|
245
|
+
</div>
|
|
246
|
+
</div>
|
|
247
|
+
);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
function LoadingFallback() {
|
|
251
|
+
const colors = useColors();
|
|
252
|
+
return (
|
|
253
|
+
<div className="w-full flex items-center justify-center" style={{ background: 'var(--bg-default)' }}>
|
|
254
|
+
<div className="border rounded-2xl p-8" style={{ background: 'var(--bg-card)', borderColor: 'var(--border-default)' }}>
|
|
255
|
+
<p style={{ color: 'var(--text-muted)' }}>Loading...</p>
|
|
256
|
+
</div>
|
|
257
|
+
</div>
|
|
258
|
+
);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
export default function RecoveryPage() {
|
|
262
|
+
return (
|
|
263
|
+
<Suspense fallback={<LoadingFallback />}>
|
|
264
|
+
<RecoveryContent />
|
|
265
|
+
</Suspense>
|
|
266
|
+
);
|
|
267
|
+
}
|