@payez/next-mvp 4.8.1 → 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/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/revocation-fields.ts +16 -16
- 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 +1141 -1141
- 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/service-unavailable/page.tsx +63 -63
- 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,99 +1,99 @@
|
|
|
1
|
-
import { NextRequest, NextResponse } from 'next/server';
|
|
2
|
-
import { idpFetchJSON } from '../../lib/idp-fetch';
|
|
3
|
-
import { ENV_CONFIG } from '../../config/env';
|
|
4
|
-
|
|
5
|
-
/**
|
|
6
|
-
* Account Profile API Handler
|
|
7
|
-
*
|
|
8
|
-
* GET /api/account/profile - Get user profile
|
|
9
|
-
* PUT /api/account/profile - Update user profile
|
|
10
|
-
*
|
|
11
|
-
* WHY THIS WAS REWRITTEN (2026-08-02, QAPert defect #7)
|
|
12
|
-
* Reported as "/api/account/profile 401s on the same cookie masked-info
|
|
13
|
-
* accepts (200)". Two endpoints disagreeing about one cookie meant two auth
|
|
14
|
-
* implementations, and they were:
|
|
15
|
-
*
|
|
16
|
-
* masked-info.ts idpFetchJSON(req, ...) resolves the caller's token
|
|
17
|
-
* from the server-side session
|
|
18
|
-
* profile.ts req.headers.get('authorization') required an INBOUND bearer
|
|
19
|
-
*
|
|
20
|
-
* A browser sends a session COOKIE and no Authorization header, so the old
|
|
21
|
-
* profile.ts 401'd EVERY browser request by construction. Nothing was revoked
|
|
22
|
-
* and no token was stale — the handler demanded a credential the app does not
|
|
23
|
-
* use.
|
|
24
|
-
*
|
|
25
|
-
* THE OLD AUTH CHECK WAS A VENEER. It tested that the header was PRESENT, never
|
|
26
|
-
* that it was valid, then forwarded it upstream unexamined. Any string at all
|
|
27
|
-
* got past it and the IDP did the real work. It looked like enforcement and
|
|
28
|
-
* enforced nothing.
|
|
29
|
-
*
|
|
30
|
-
* WHAT THE WORKAROUND COST US: consumers could not call this with a cookie, so
|
|
31
|
-
* the graft's useProfile hook fetched the raw IDP access token from
|
|
32
|
-
* /api/session/idp-token INTO BROWSER JAVASCRIPT and re-sent it as a bearer.
|
|
33
|
-
* That defeats the reason idpFetchJSON exists — it keeps the token server-side
|
|
34
|
-
* precisely so client-side script can never hold it. The bearer-only handler
|
|
35
|
-
* did not merely fail; it pushed its callers into exposing the token.
|
|
36
|
-
*
|
|
37
|
-
* Switching to idpFetchJSON makes the cookie work, so the hook's token round
|
|
38
|
-
* trip can be deleted and /api/session/idp-token drops to a single deliberate
|
|
39
|
-
* caller: the jwt-inspect debug page.
|
|
40
|
-
*
|
|
41
|
-
* @package @payez/next-mvp
|
|
42
|
-
*/
|
|
43
|
-
|
|
44
|
-
export async function GET(req: NextRequest) {
|
|
45
|
-
const result = await idpFetchJSON(req, `${ENV_CONFIG.IDP_URL}/api/Account/profile`, {
|
|
46
|
-
method: 'GET',
|
|
47
|
-
headers: { 'Content-Type': 'application/json' },
|
|
48
|
-
});
|
|
49
|
-
|
|
50
|
-
if (!result.ok) {
|
|
51
|
-
return NextResponse.json(
|
|
52
|
-
{
|
|
53
|
-
success: false,
|
|
54
|
-
error: {
|
|
55
|
-
code: 'UPSTREAM_SERVICE_ERROR',
|
|
56
|
-
message: result.json?.message || 'Failed to fetch profile',
|
|
57
|
-
status: result.status,
|
|
58
|
-
},
|
|
59
|
-
meta: { attemptedRefresh: result.attemptedRefresh },
|
|
60
|
-
},
|
|
61
|
-
{ status: result.status }
|
|
62
|
-
);
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
return NextResponse.json(result.json, { status: 200 });
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
export async function PUT(req: NextRequest) {
|
|
69
|
-
let body = '{}';
|
|
70
|
-
try {
|
|
71
|
-
const raw = await req.text();
|
|
72
|
-
if (raw && raw.trim().length > 0) body = raw;
|
|
73
|
-
} catch {
|
|
74
|
-
// Empty body is valid here; the IDP validates the payload.
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
const result = await idpFetchJSON(req, `${ENV_CONFIG.IDP_URL}/api/Account/profile`, {
|
|
78
|
-
method: 'PUT',
|
|
79
|
-
headers: { 'Content-Type': 'application/json' },
|
|
80
|
-
body,
|
|
81
|
-
});
|
|
82
|
-
|
|
83
|
-
if (!result.ok) {
|
|
84
|
-
return NextResponse.json(
|
|
85
|
-
{
|
|
86
|
-
success: false,
|
|
87
|
-
error: {
|
|
88
|
-
code: 'UPSTREAM_SERVICE_ERROR',
|
|
89
|
-
message: result.json?.message || 'Failed to update profile',
|
|
90
|
-
status: result.status,
|
|
91
|
-
},
|
|
92
|
-
meta: { attemptedRefresh: result.attemptedRefresh },
|
|
93
|
-
},
|
|
94
|
-
{ status: result.status }
|
|
95
|
-
);
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
return NextResponse.json(result.json, { status: 200 });
|
|
99
|
-
}
|
|
1
|
+
import { NextRequest, NextResponse } from 'next/server';
|
|
2
|
+
import { idpFetchJSON } from '../../lib/idp-fetch';
|
|
3
|
+
import { ENV_CONFIG } from '../../config/env';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Account Profile API Handler
|
|
7
|
+
*
|
|
8
|
+
* GET /api/account/profile - Get user profile
|
|
9
|
+
* PUT /api/account/profile - Update user profile
|
|
10
|
+
*
|
|
11
|
+
* WHY THIS WAS REWRITTEN (2026-08-02, QAPert defect #7)
|
|
12
|
+
* Reported as "/api/account/profile 401s on the same cookie masked-info
|
|
13
|
+
* accepts (200)". Two endpoints disagreeing about one cookie meant two auth
|
|
14
|
+
* implementations, and they were:
|
|
15
|
+
*
|
|
16
|
+
* masked-info.ts idpFetchJSON(req, ...) resolves the caller's token
|
|
17
|
+
* from the server-side session
|
|
18
|
+
* profile.ts req.headers.get('authorization') required an INBOUND bearer
|
|
19
|
+
*
|
|
20
|
+
* A browser sends a session COOKIE and no Authorization header, so the old
|
|
21
|
+
* profile.ts 401'd EVERY browser request by construction. Nothing was revoked
|
|
22
|
+
* and no token was stale — the handler demanded a credential the app does not
|
|
23
|
+
* use.
|
|
24
|
+
*
|
|
25
|
+
* THE OLD AUTH CHECK WAS A VENEER. It tested that the header was PRESENT, never
|
|
26
|
+
* that it was valid, then forwarded it upstream unexamined. Any string at all
|
|
27
|
+
* got past it and the IDP did the real work. It looked like enforcement and
|
|
28
|
+
* enforced nothing.
|
|
29
|
+
*
|
|
30
|
+
* WHAT THE WORKAROUND COST US: consumers could not call this with a cookie, so
|
|
31
|
+
* the graft's useProfile hook fetched the raw IDP access token from
|
|
32
|
+
* /api/session/idp-token INTO BROWSER JAVASCRIPT and re-sent it as a bearer.
|
|
33
|
+
* That defeats the reason idpFetchJSON exists — it keeps the token server-side
|
|
34
|
+
* precisely so client-side script can never hold it. The bearer-only handler
|
|
35
|
+
* did not merely fail; it pushed its callers into exposing the token.
|
|
36
|
+
*
|
|
37
|
+
* Switching to idpFetchJSON makes the cookie work, so the hook's token round
|
|
38
|
+
* trip can be deleted and /api/session/idp-token drops to a single deliberate
|
|
39
|
+
* caller: the jwt-inspect debug page.
|
|
40
|
+
*
|
|
41
|
+
* @package @payez/next-mvp
|
|
42
|
+
*/
|
|
43
|
+
|
|
44
|
+
export async function GET(req: NextRequest) {
|
|
45
|
+
const result = await idpFetchJSON(req, `${ENV_CONFIG.IDP_URL}/api/Account/profile`, {
|
|
46
|
+
method: 'GET',
|
|
47
|
+
headers: { 'Content-Type': 'application/json' },
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
if (!result.ok) {
|
|
51
|
+
return NextResponse.json(
|
|
52
|
+
{
|
|
53
|
+
success: false,
|
|
54
|
+
error: {
|
|
55
|
+
code: 'UPSTREAM_SERVICE_ERROR',
|
|
56
|
+
message: result.json?.message || 'Failed to fetch profile',
|
|
57
|
+
status: result.status,
|
|
58
|
+
},
|
|
59
|
+
meta: { attemptedRefresh: result.attemptedRefresh },
|
|
60
|
+
},
|
|
61
|
+
{ status: result.status }
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return NextResponse.json(result.json, { status: 200 });
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export async function PUT(req: NextRequest) {
|
|
69
|
+
let body = '{}';
|
|
70
|
+
try {
|
|
71
|
+
const raw = await req.text();
|
|
72
|
+
if (raw && raw.trim().length > 0) body = raw;
|
|
73
|
+
} catch {
|
|
74
|
+
// Empty body is valid here; the IDP validates the payload.
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const result = await idpFetchJSON(req, `${ENV_CONFIG.IDP_URL}/api/Account/profile`, {
|
|
78
|
+
method: 'PUT',
|
|
79
|
+
headers: { 'Content-Type': 'application/json' },
|
|
80
|
+
body,
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
if (!result.ok) {
|
|
84
|
+
return NextResponse.json(
|
|
85
|
+
{
|
|
86
|
+
success: false,
|
|
87
|
+
error: {
|
|
88
|
+
code: 'UPSTREAM_SERVICE_ERROR',
|
|
89
|
+
message: result.json?.message || 'Failed to update profile',
|
|
90
|
+
status: result.status,
|
|
91
|
+
},
|
|
92
|
+
meta: { attemptedRefresh: result.attemptedRefresh },
|
|
93
|
+
},
|
|
94
|
+
{ status: result.status }
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return NextResponse.json(result.json, { status: 200 });
|
|
99
|
+
}
|
|
@@ -1,117 +1,117 @@
|
|
|
1
|
-
import { NextRequest, NextResponse } from 'next/server';
|
|
2
|
-
import { ENV_CONFIG } from '../../../config/env';
|
|
3
|
-
import { getIDPClientConfig } from '../../../lib/idp-client-config';
|
|
4
|
-
import { resolveClientKey } from '../../../lib/client-key';
|
|
5
|
-
import { idpErrorEnvelope } from '../../../lib/safe-error-response';
|
|
6
|
-
|
|
7
|
-
/**
|
|
8
|
-
* Account recovery - the ONE forwarder behind /recovery/send-code and /recovery/verify-code (PAY-1588).
|
|
9
|
-
*
|
|
10
|
-
* THE DEFECT (QAPert, W1 walk on :4797 -> dev-93 IdP 90f766bd0, 2026-09-19): portal /account-auth/recovery,
|
|
11
|
-
* initiate -> 200, send-code -> 401 "Upstream error". The browser sends `Authorization: Bearer <recovery session
|
|
12
|
-
* token>` (the token `initiate` minted); these handlers went through idpFetchJSON, which sets Authorization ONLY from
|
|
13
|
-
* the server-side SESSION and never forwards the inbound header, so the IdP got no Bearer at all
|
|
14
|
-
* (AccountController.cs:872-885 = 401 MISSING_TOKEN). Positive control direct to the IdP: without Authorization 401,
|
|
15
|
-
* with it 200 and the email is sent.
|
|
16
|
-
*
|
|
17
|
-
* WHY THE 2026-08-02 REWRITE GOT IT WRONG. It reasoned "browsers send a session COOKIE and no Authorization header"
|
|
18
|
-
* and moved both handlers onto idpFetchJSON. That is true of every OTHER authenticated route. It is false of this
|
|
19
|
-
* flow: the caller is LOCKED OUT, so there is no session to resolve - the recovery session token IS the credential,
|
|
20
|
-
* and the browser is the only party that holds it. Resolving a token from a session that does not exist is why the
|
|
21
|
-
* entire recovery path, both halves, has been dead: it fails exactly when it is the only thing left that could help.
|
|
22
|
-
*
|
|
23
|
-
* WHAT THIS DOES INSTEAD
|
|
24
|
-
* - Forwards the inbound `Authorization: Bearer <token>` to the IdP and nothing else from the caller's headers.
|
|
25
|
-
* - Uses a direct fetch, NOT idpFetchJSON: a stray session belonging to someone else on the same browser must not
|
|
26
|
-
* be able to override the recovery credential with its own bearer (idpFetchJSON's session bearer wins over any
|
|
27
|
-
* caller-supplied Authorization).
|
|
28
|
-
* - FAILS LOUD when there is no usable bearer: 401 `missing_recovery_token`, without calling upstream. The old
|
|
29
|
-
* behaviour returned the IdP's opaque rejection, so the user saw recovery "not working" and the log showed an
|
|
30
|
-
* upstream error rather than the auth fault it was.
|
|
31
|
-
* - Shape-checks the token (a bearer scheme, one opaque token, sane length) before forwarding, so the header cannot
|
|
32
|
-
* be used to smuggle anything but a credential to the IdP. It is never logged.
|
|
33
|
-
* - Anonymous route (PAY-1438): on failure only the whitelisted envelope, never the IdP body.
|
|
34
|
-
*
|
|
35
|
-
* SERVER VALUE WINS for client_id (cards 268012 / 268410): applied AFTER the caller's payload spread, because these are
|
|
36
|
-
* unauthenticated endpoints and a browser-supplied client_id would be a tenant-targeting lever on a password-reset path.
|
|
37
|
-
*
|
|
38
|
-
* @package @payez/next-mvp
|
|
39
|
-
*/
|
|
40
|
-
|
|
41
|
-
/** One opaque token: JWT / base64url / opaque-hex alphabet, no whitespace or control characters, bounded length. */
|
|
42
|
-
const TOKEN_SHAPE = /^[A-Za-z0-9\-._~+/]+=*$/;
|
|
43
|
-
|
|
44
|
-
/** The recovery bearer from the inbound request, or null when absent / malformed. Never throws. */
|
|
45
|
-
export function inboundRecoveryBearer(req: Pick<NextRequest, 'headers'>): string | null {
|
|
46
|
-
const raw = req.headers.get('authorization');
|
|
47
|
-
if (!raw) return null;
|
|
48
|
-
const m = /^Bearer[ ]+(\S+)$/i.exec(raw.trim());
|
|
49
|
-
if (!m) return null;
|
|
50
|
-
const token = m[1];
|
|
51
|
-
if (token.length < 16 || token.length > 4096) return null;
|
|
52
|
-
return TOKEN_SHAPE.test(token) ? token : null;
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
async function recoveryClientKey(where: string): Promise<string | null> {
|
|
56
|
-
try {
|
|
57
|
-
const config = await getIDPClientConfig();
|
|
58
|
-
return resolveClientKey(config, where).key;
|
|
59
|
-
} catch (e) {
|
|
60
|
-
console.error(`[${where}] could not resolve IDP client config for client_id:`, e);
|
|
61
|
-
return null;
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
export interface RecoveryForward {
|
|
66
|
-
/** IdP path after the host, e.g. /api/Account/recovery/send-code */
|
|
67
|
-
upstreamPath: string;
|
|
68
|
-
/** log/route label used for the client-key resolver */
|
|
69
|
-
where: string;
|
|
70
|
-
/** stable `error` code for an upstream that could not be reached */
|
|
71
|
-
failureCode: string;
|
|
72
|
-
/** user-facing fallback when the IdP gave no message */
|
|
73
|
-
fallbackMessage: string;
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
export async function forwardRecoveryCall(req: NextRequest, spec: RecoveryForward): Promise<NextResponse> {
|
|
77
|
-
let payload: unknown = {};
|
|
78
|
-
try {
|
|
79
|
-
payload = await req.json();
|
|
80
|
-
} catch {
|
|
81
|
-
return NextResponse.json({ success: false, error: 'invalid_request', message: 'A JSON body is required' }, { status: 400 });
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
const bearer = inboundRecoveryBearer(req);
|
|
85
|
-
if (!bearer) {
|
|
86
|
-
return NextResponse.json(
|
|
87
|
-
{
|
|
88
|
-
success: false,
|
|
89
|
-
error: 'missing_recovery_token',
|
|
90
|
-
message: 'Your recovery session could not be found. Start the recovery again from the beginning.',
|
|
91
|
-
},
|
|
92
|
-
{ status: 401 },
|
|
93
|
-
);
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
const clientKey = await recoveryClientKey(spec.where);
|
|
97
|
-
|
|
98
|
-
let upstream: Response;
|
|
99
|
-
try {
|
|
100
|
-
upstream = await fetch(`${ENV_CONFIG.IDP_URL}${spec.upstreamPath}`, {
|
|
101
|
-
method: 'POST',
|
|
102
|
-
headers: {
|
|
103
|
-
'Content-Type': 'application/json',
|
|
104
|
-
Accept: 'application/json',
|
|
105
|
-
Authorization: `Bearer ${bearer}`,
|
|
106
|
-
...(clientKey ? { 'X-Client-Id': clientKey } : {}),
|
|
107
|
-
},
|
|
108
|
-
body: JSON.stringify({ ...(payload as Record<string, unknown>), ...(clientKey ? { client_id: clientKey } : {}) }),
|
|
109
|
-
});
|
|
110
|
-
} catch {
|
|
111
|
-
return NextResponse.json({ success: false, error: spec.failureCode }, { status: 502 });
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
const data = await upstream.json().catch(() => ({}));
|
|
115
|
-
if (!upstream.ok) return NextResponse.json(idpErrorEnvelope(data, spec.fallbackMessage), { status: upstream.status });
|
|
116
|
-
return NextResponse.json(data, { status: 200 });
|
|
117
|
-
}
|
|
1
|
+
import { NextRequest, NextResponse } from 'next/server';
|
|
2
|
+
import { ENV_CONFIG } from '../../../config/env';
|
|
3
|
+
import { getIDPClientConfig } from '../../../lib/idp-client-config';
|
|
4
|
+
import { resolveClientKey } from '../../../lib/client-key';
|
|
5
|
+
import { idpErrorEnvelope } from '../../../lib/safe-error-response';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Account recovery - the ONE forwarder behind /recovery/send-code and /recovery/verify-code (PAY-1588).
|
|
9
|
+
*
|
|
10
|
+
* THE DEFECT (QAPert, W1 walk on :4797 -> dev-93 IdP 90f766bd0, 2026-09-19): portal /account-auth/recovery,
|
|
11
|
+
* initiate -> 200, send-code -> 401 "Upstream error". The browser sends `Authorization: Bearer <recovery session
|
|
12
|
+
* token>` (the token `initiate` minted); these handlers went through idpFetchJSON, which sets Authorization ONLY from
|
|
13
|
+
* the server-side SESSION and never forwards the inbound header, so the IdP got no Bearer at all
|
|
14
|
+
* (AccountController.cs:872-885 = 401 MISSING_TOKEN). Positive control direct to the IdP: without Authorization 401,
|
|
15
|
+
* with it 200 and the email is sent.
|
|
16
|
+
*
|
|
17
|
+
* WHY THE 2026-08-02 REWRITE GOT IT WRONG. It reasoned "browsers send a session COOKIE and no Authorization header"
|
|
18
|
+
* and moved both handlers onto idpFetchJSON. That is true of every OTHER authenticated route. It is false of this
|
|
19
|
+
* flow: the caller is LOCKED OUT, so there is no session to resolve - the recovery session token IS the credential,
|
|
20
|
+
* and the browser is the only party that holds it. Resolving a token from a session that does not exist is why the
|
|
21
|
+
* entire recovery path, both halves, has been dead: it fails exactly when it is the only thing left that could help.
|
|
22
|
+
*
|
|
23
|
+
* WHAT THIS DOES INSTEAD
|
|
24
|
+
* - Forwards the inbound `Authorization: Bearer <token>` to the IdP and nothing else from the caller's headers.
|
|
25
|
+
* - Uses a direct fetch, NOT idpFetchJSON: a stray session belonging to someone else on the same browser must not
|
|
26
|
+
* be able to override the recovery credential with its own bearer (idpFetchJSON's session bearer wins over any
|
|
27
|
+
* caller-supplied Authorization).
|
|
28
|
+
* - FAILS LOUD when there is no usable bearer: 401 `missing_recovery_token`, without calling upstream. The old
|
|
29
|
+
* behaviour returned the IdP's opaque rejection, so the user saw recovery "not working" and the log showed an
|
|
30
|
+
* upstream error rather than the auth fault it was.
|
|
31
|
+
* - Shape-checks the token (a bearer scheme, one opaque token, sane length) before forwarding, so the header cannot
|
|
32
|
+
* be used to smuggle anything but a credential to the IdP. It is never logged.
|
|
33
|
+
* - Anonymous route (PAY-1438): on failure only the whitelisted envelope, never the IdP body.
|
|
34
|
+
*
|
|
35
|
+
* SERVER VALUE WINS for client_id (cards 268012 / 268410): applied AFTER the caller's payload spread, because these are
|
|
36
|
+
* unauthenticated endpoints and a browser-supplied client_id would be a tenant-targeting lever on a password-reset path.
|
|
37
|
+
*
|
|
38
|
+
* @package @payez/next-mvp
|
|
39
|
+
*/
|
|
40
|
+
|
|
41
|
+
/** One opaque token: JWT / base64url / opaque-hex alphabet, no whitespace or control characters, bounded length. */
|
|
42
|
+
const TOKEN_SHAPE = /^[A-Za-z0-9\-._~+/]+=*$/;
|
|
43
|
+
|
|
44
|
+
/** The recovery bearer from the inbound request, or null when absent / malformed. Never throws. */
|
|
45
|
+
export function inboundRecoveryBearer(req: Pick<NextRequest, 'headers'>): string | null {
|
|
46
|
+
const raw = req.headers.get('authorization');
|
|
47
|
+
if (!raw) return null;
|
|
48
|
+
const m = /^Bearer[ ]+(\S+)$/i.exec(raw.trim());
|
|
49
|
+
if (!m) return null;
|
|
50
|
+
const token = m[1];
|
|
51
|
+
if (token.length < 16 || token.length > 4096) return null;
|
|
52
|
+
return TOKEN_SHAPE.test(token) ? token : null;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
async function recoveryClientKey(where: string): Promise<string | null> {
|
|
56
|
+
try {
|
|
57
|
+
const config = await getIDPClientConfig();
|
|
58
|
+
return resolveClientKey(config, where).key;
|
|
59
|
+
} catch (e) {
|
|
60
|
+
console.error(`[${where}] could not resolve IDP client config for client_id:`, e);
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export interface RecoveryForward {
|
|
66
|
+
/** IdP path after the host, e.g. /api/Account/recovery/send-code */
|
|
67
|
+
upstreamPath: string;
|
|
68
|
+
/** log/route label used for the client-key resolver */
|
|
69
|
+
where: string;
|
|
70
|
+
/** stable `error` code for an upstream that could not be reached */
|
|
71
|
+
failureCode: string;
|
|
72
|
+
/** user-facing fallback when the IdP gave no message */
|
|
73
|
+
fallbackMessage: string;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export async function forwardRecoveryCall(req: NextRequest, spec: RecoveryForward): Promise<NextResponse> {
|
|
77
|
+
let payload: unknown = {};
|
|
78
|
+
try {
|
|
79
|
+
payload = await req.json();
|
|
80
|
+
} catch {
|
|
81
|
+
return NextResponse.json({ success: false, error: 'invalid_request', message: 'A JSON body is required' }, { status: 400 });
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const bearer = inboundRecoveryBearer(req);
|
|
85
|
+
if (!bearer) {
|
|
86
|
+
return NextResponse.json(
|
|
87
|
+
{
|
|
88
|
+
success: false,
|
|
89
|
+
error: 'missing_recovery_token',
|
|
90
|
+
message: 'Your recovery session could not be found. Start the recovery again from the beginning.',
|
|
91
|
+
},
|
|
92
|
+
{ status: 401 },
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const clientKey = await recoveryClientKey(spec.where);
|
|
97
|
+
|
|
98
|
+
let upstream: Response;
|
|
99
|
+
try {
|
|
100
|
+
upstream = await fetch(`${ENV_CONFIG.IDP_URL}${spec.upstreamPath}`, {
|
|
101
|
+
method: 'POST',
|
|
102
|
+
headers: {
|
|
103
|
+
'Content-Type': 'application/json',
|
|
104
|
+
Accept: 'application/json',
|
|
105
|
+
Authorization: `Bearer ${bearer}`,
|
|
106
|
+
...(clientKey ? { 'X-Client-Id': clientKey } : {}),
|
|
107
|
+
},
|
|
108
|
+
body: JSON.stringify({ ...(payload as Record<string, unknown>), ...(clientKey ? { client_id: clientKey } : {}) }),
|
|
109
|
+
});
|
|
110
|
+
} catch {
|
|
111
|
+
return NextResponse.json({ success: false, error: spec.failureCode }, { status: 502 });
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const data = await upstream.json().catch(() => ({}));
|
|
115
|
+
if (!upstream.ok) return NextResponse.json(idpErrorEnvelope(data, spec.fallbackMessage), { status: upstream.status });
|
|
116
|
+
return NextResponse.json(data, { status: 200 });
|
|
117
|
+
}
|
|
@@ -1,59 +1,59 @@
|
|
|
1
|
-
import { NextRequest, NextResponse } from 'next/server';
|
|
2
|
-
import { getIDPClientConfig } from '../../../lib/idp-client-config';
|
|
3
|
-
import { resolveClientKey } from '../../../lib/client-key';
|
|
4
|
-
import { idpErrorEnvelope } from '../../../lib/safe-error-response';
|
|
5
|
-
|
|
6
|
-
function getIdpUrl(): string {
|
|
7
|
-
const url = process.env.IDP_URL;
|
|
8
|
-
if (!url) throw new Error('[IDP_URL] FATAL: IDP_URL environment variable is REQUIRED.');
|
|
9
|
-
return url;
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
/**
|
|
13
|
-
* RECOVERY CLIENT KEY — cards 268012 (S12) and 268410.
|
|
14
|
-
*
|
|
15
|
-
* These four proxies populated NONE of PayEzClientIdResolver's six slots, so
|
|
16
|
-
* RequirePayEzUnAuthorized 404'd the request BEFORE the action ran and no
|
|
17
|
-
* recovery token was ever minted. That is why S12 failed at step 1.
|
|
18
|
-
*
|
|
19
|
-
* NOT `ENV_CONFIG.CLIENT_ID`, which is what card 268012 originally prescribed.
|
|
20
|
-
* That variable is the ENVIRONMENT-SPECIFIC NUMERIC id, and the IDP accepts a
|
|
21
|
-
* numeric with NO existence check (RequirePayEzUnAuthorizedAttribute.cs:31-33)
|
|
22
|
-
* while it VALIDATES a slug (:41-47). Hardcoding the numeric here would have
|
|
23
|
-
* added four more sites to the 268410 family and made a stale id answer for
|
|
24
|
-
* the WRONG TENANT on a password-reset path, silently. Slug-preferring, with
|
|
25
|
-
* the numeric fallback logged, is the same resolver the other nine sites use.
|
|
26
|
-
*
|
|
27
|
-
* SERVER VALUE WINS: client_id is applied AFTER the caller's payload spread.
|
|
28
|
-
* These are unauthenticated recovery endpoints, so a browser-supplied
|
|
29
|
-
* client_id would be a tenant-targeting lever. The caller does not get to
|
|
30
|
-
* choose which tenant their password reset is addressed to.
|
|
31
|
-
*/
|
|
32
|
-
async function recoveryClientKey(): Promise<string | null> {
|
|
33
|
-
try {
|
|
34
|
-
const config = await getIDPClientConfig();
|
|
35
|
-
return resolveClientKey(config, 'account/recovery/initiate').key;
|
|
36
|
-
} catch (e) {
|
|
37
|
-
console.error('[account/recovery/initiate] could not resolve IDP client config for client_id:', e);
|
|
38
|
-
return null;
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
export async function POST(req: NextRequest) {
|
|
43
|
-
const IDP_URL = getIdpUrl();
|
|
44
|
-
try {
|
|
45
|
-
const { email } = await req.json();
|
|
46
|
-
const clientKey = await recoveryClientKey();
|
|
47
|
-
const upstream = await fetch(`${IDP_URL}/api/Account/recovery/initiate`, {
|
|
48
|
-
method: 'POST',
|
|
49
|
-
headers: { 'Content-Type': 'application/json', ...(clientKey ? { 'X-Client-Id': clientKey } : {}) },
|
|
50
|
-
body: JSON.stringify({ email, ...(clientKey ? { client_id: clientKey } : {}) }),
|
|
51
|
-
});
|
|
52
|
-
const data = await upstream.json().catch(() => ({}));
|
|
53
|
-
// PAY-1438: anonymous route - on failure only the whitelisted envelope, never the IDP body.
|
|
54
|
-
if (!upstream.ok) return NextResponse.json(idpErrorEnvelope(data, 'Failed to initiate recovery'), { status: upstream.status });
|
|
55
|
-
return NextResponse.json(data, { status: upstream.status });
|
|
56
|
-
} catch (e) {
|
|
57
|
-
return NextResponse.json({ success: false, error: 'initiate_failed' }, { status: 500 });
|
|
58
|
-
}
|
|
59
|
-
}
|
|
1
|
+
import { NextRequest, NextResponse } from 'next/server';
|
|
2
|
+
import { getIDPClientConfig } from '../../../lib/idp-client-config';
|
|
3
|
+
import { resolveClientKey } from '../../../lib/client-key';
|
|
4
|
+
import { idpErrorEnvelope } from '../../../lib/safe-error-response';
|
|
5
|
+
|
|
6
|
+
function getIdpUrl(): string {
|
|
7
|
+
const url = process.env.IDP_URL;
|
|
8
|
+
if (!url) throw new Error('[IDP_URL] FATAL: IDP_URL environment variable is REQUIRED.');
|
|
9
|
+
return url;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* RECOVERY CLIENT KEY — cards 268012 (S12) and 268410.
|
|
14
|
+
*
|
|
15
|
+
* These four proxies populated NONE of PayEzClientIdResolver's six slots, so
|
|
16
|
+
* RequirePayEzUnAuthorized 404'd the request BEFORE the action ran and no
|
|
17
|
+
* recovery token was ever minted. That is why S12 failed at step 1.
|
|
18
|
+
*
|
|
19
|
+
* NOT `ENV_CONFIG.CLIENT_ID`, which is what card 268012 originally prescribed.
|
|
20
|
+
* That variable is the ENVIRONMENT-SPECIFIC NUMERIC id, and the IDP accepts a
|
|
21
|
+
* numeric with NO existence check (RequirePayEzUnAuthorizedAttribute.cs:31-33)
|
|
22
|
+
* while it VALIDATES a slug (:41-47). Hardcoding the numeric here would have
|
|
23
|
+
* added four more sites to the 268410 family and made a stale id answer for
|
|
24
|
+
* the WRONG TENANT on a password-reset path, silently. Slug-preferring, with
|
|
25
|
+
* the numeric fallback logged, is the same resolver the other nine sites use.
|
|
26
|
+
*
|
|
27
|
+
* SERVER VALUE WINS: client_id is applied AFTER the caller's payload spread.
|
|
28
|
+
* These are unauthenticated recovery endpoints, so a browser-supplied
|
|
29
|
+
* client_id would be a tenant-targeting lever. The caller does not get to
|
|
30
|
+
* choose which tenant their password reset is addressed to.
|
|
31
|
+
*/
|
|
32
|
+
async function recoveryClientKey(): Promise<string | null> {
|
|
33
|
+
try {
|
|
34
|
+
const config = await getIDPClientConfig();
|
|
35
|
+
return resolveClientKey(config, 'account/recovery/initiate').key;
|
|
36
|
+
} catch (e) {
|
|
37
|
+
console.error('[account/recovery/initiate] could not resolve IDP client config for client_id:', e);
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export async function POST(req: NextRequest) {
|
|
43
|
+
const IDP_URL = getIdpUrl();
|
|
44
|
+
try {
|
|
45
|
+
const { email } = await req.json();
|
|
46
|
+
const clientKey = await recoveryClientKey();
|
|
47
|
+
const upstream = await fetch(`${IDP_URL}/api/Account/recovery/initiate`, {
|
|
48
|
+
method: 'POST',
|
|
49
|
+
headers: { 'Content-Type': 'application/json', ...(clientKey ? { 'X-Client-Id': clientKey } : {}) },
|
|
50
|
+
body: JSON.stringify({ email, ...(clientKey ? { client_id: clientKey } : {}) }),
|
|
51
|
+
});
|
|
52
|
+
const data = await upstream.json().catch(() => ({}));
|
|
53
|
+
// PAY-1438: anonymous route - on failure only the whitelisted envelope, never the IDP body.
|
|
54
|
+
if (!upstream.ok) return NextResponse.json(idpErrorEnvelope(data, 'Failed to initiate recovery'), { status: upstream.status });
|
|
55
|
+
return NextResponse.json(data, { status: upstream.status });
|
|
56
|
+
} catch (e) {
|
|
57
|
+
return NextResponse.json({ success: false, error: 'initiate_failed' }, { status: 500 });
|
|
58
|
+
}
|
|
59
|
+
}
|
|
@@ -1,21 +1,21 @@
|
|
|
1
|
-
import { NextRequest } from 'next/server';
|
|
2
|
-
import { forwardRecoveryCall } from './forward';
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* Account recovery - send-code.
|
|
6
|
-
*
|
|
7
|
-
* PAY-1588: the recovery session token minted by `initiate` is the caller's only credential (they are locked out; there is
|
|
8
|
-
* no session), so this forwards the inbound `Authorization: Bearer <token>` to the IdP and fails loud (401
|
|
9
|
-
* `missing_recovery_token`) when it is absent. The whole reasoning, and why the 2026-08-02 idpFetchJSON rewrite killed
|
|
10
|
-
* this flow, is in ./forward.ts. send-code and verify-code share that ONE forwarder so they cannot drift apart again.
|
|
11
|
-
*
|
|
12
|
-
* @package @payez/next-mvp
|
|
13
|
-
*/
|
|
14
|
-
export async function POST(req: NextRequest) {
|
|
15
|
-
return forwardRecoveryCall(req, {
|
|
16
|
-
upstreamPath: '/api/Account/recovery/send-code',
|
|
17
|
-
where: 'account/recovery/send-code',
|
|
18
|
-
failureCode: 'send_code_failed',
|
|
19
|
-
fallbackMessage: 'Failed to send the verification code',
|
|
20
|
-
});
|
|
21
|
-
}
|
|
1
|
+
import { NextRequest } from 'next/server';
|
|
2
|
+
import { forwardRecoveryCall } from './forward';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Account recovery - send-code.
|
|
6
|
+
*
|
|
7
|
+
* PAY-1588: the recovery session token minted by `initiate` is the caller's only credential (they are locked out; there is
|
|
8
|
+
* no session), so this forwards the inbound `Authorization: Bearer <token>` to the IdP and fails loud (401
|
|
9
|
+
* `missing_recovery_token`) when it is absent. The whole reasoning, and why the 2026-08-02 idpFetchJSON rewrite killed
|
|
10
|
+
* this flow, is in ./forward.ts. send-code and verify-code share that ONE forwarder so they cannot drift apart again.
|
|
11
|
+
*
|
|
12
|
+
* @package @payez/next-mvp
|
|
13
|
+
*/
|
|
14
|
+
export async function POST(req: NextRequest) {
|
|
15
|
+
return forwardRecoveryCall(req, {
|
|
16
|
+
upstreamPath: '/api/Account/recovery/send-code',
|
|
17
|
+
where: 'account/recovery/send-code',
|
|
18
|
+
failureCode: 'send_code_failed',
|
|
19
|
+
fallbackMessage: 'Failed to send the verification code',
|
|
20
|
+
});
|
|
21
|
+
}
|
|
@@ -1,20 +1,20 @@
|
|
|
1
|
-
import { NextRequest } from 'next/server';
|
|
2
|
-
import { forwardRecoveryCall } from './forward';
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* Account recovery - verify-code.
|
|
6
|
-
*
|
|
7
|
-
* PAY-1588: forwards the inbound recovery `Authorization: Bearer <token>` to the IdP and fails loud (401
|
|
8
|
-
* `missing_recovery_token`) when it is absent. See ./forward.ts for the full reasoning; send-code and verify-code share
|
|
9
|
-
* that ONE forwarder so they cannot drift apart again.
|
|
10
|
-
*
|
|
11
|
-
* @package @payez/next-mvp
|
|
12
|
-
*/
|
|
13
|
-
export async function POST(req: NextRequest) {
|
|
14
|
-
return forwardRecoveryCall(req, {
|
|
15
|
-
upstreamPath: '/api/Account/recovery/verify-code',
|
|
16
|
-
where: 'account/recovery/verify-code',
|
|
17
|
-
failureCode: 'verify_code_failed',
|
|
18
|
-
fallbackMessage: 'Failed to verify the code',
|
|
19
|
-
});
|
|
20
|
-
}
|
|
1
|
+
import { NextRequest } from 'next/server';
|
|
2
|
+
import { forwardRecoveryCall } from './forward';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Account recovery - verify-code.
|
|
6
|
+
*
|
|
7
|
+
* PAY-1588: forwards the inbound recovery `Authorization: Bearer <token>` to the IdP and fails loud (401
|
|
8
|
+
* `missing_recovery_token`) when it is absent. See ./forward.ts for the full reasoning; send-code and verify-code share
|
|
9
|
+
* that ONE forwarder so they cannot drift apart again.
|
|
10
|
+
*
|
|
11
|
+
* @package @payez/next-mvp
|
|
12
|
+
*/
|
|
13
|
+
export async function POST(req: NextRequest) {
|
|
14
|
+
return forwardRecoveryCall(req, {
|
|
15
|
+
upstreamPath: '/api/Account/recovery/verify-code',
|
|
16
|
+
where: 'account/recovery/verify-code',
|
|
17
|
+
failureCode: 'verify_code_failed',
|
|
18
|
+
fallbackMessage: 'Failed to verify the code',
|
|
19
|
+
});
|
|
20
|
+
}
|