@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.
Files changed (181) hide show
  1. package/dist/api-handlers/account/profile.d.ts +38 -0
  2. package/dist/api-handlers/account/profile.js +73 -45
  3. package/dist/api-handlers/account/recovery/send-code.d.ts +33 -0
  4. package/dist/api-handlers/account/recovery/send-code.js +52 -18
  5. package/dist/api-handlers/account/recovery/verify-code.d.ts +33 -0
  6. package/dist/api-handlers/account/recovery/verify-code.js +52 -18
  7. package/dist/api-handlers/account/verify-email.js +15 -1
  8. package/dist/api-handlers/account/verify-sms.js +15 -1
  9. package/dist/api-handlers/auth/passkey-signin.d.ts +29 -0
  10. package/dist/api-handlers/auth/passkey-signin.js +97 -0
  11. package/dist/api-handlers/mfa/backup-code-verify.d.ts +73 -0
  12. package/dist/api-handlers/mfa/backup-code-verify.js +153 -0
  13. package/dist/api-handlers/mfa/backup-codes.d.ts +7 -0
  14. package/dist/api-handlers/mfa/backup-codes.js +40 -0
  15. package/dist/api-handlers/mfa/methods.d.ts +3 -0
  16. package/dist/api-handlers/mfa/methods.js +41 -0
  17. package/dist/api-handlers/mfa/totp-confirm.d.ts +8 -0
  18. package/dist/api-handlers/mfa/totp-confirm.js +41 -0
  19. package/dist/api-handlers/mfa/totp-enroll.d.ts +8 -0
  20. package/dist/api-handlers/mfa/totp-enroll.js +41 -0
  21. package/dist/api-handlers/mfa/totp-verify.d.ts +47 -0
  22. package/dist/api-handlers/mfa/totp-verify.js +134 -0
  23. package/dist/api-handlers/passkeys/list.d.ts +7 -0
  24. package/dist/api-handlers/passkeys/list.js +29 -0
  25. package/dist/api-handlers/passkeys/manage.d.ts +3 -0
  26. package/dist/api-handlers/passkeys/manage.js +54 -0
  27. package/dist/api-handlers/passkeys/register-options.d.ts +8 -0
  28. package/dist/api-handlers/passkeys/register-options.js +41 -0
  29. package/dist/api-handlers/passkeys/register-verify.d.ts +7 -0
  30. package/dist/api-handlers/passkeys/register-verify.js +40 -0
  31. package/dist/api-handlers/session/viability.js +19 -0
  32. package/dist/auth/better-auth.d.ts +68 -5
  33. package/dist/auth/better-auth.js +271 -45
  34. package/dist/auth/utils/idp-client.d.ts +13 -22
  35. package/dist/auth/utils/idp-client.js +12 -82
  36. package/dist/client/better-auth-client.d.ts +14 -2
  37. package/dist/lib/ensure-fresh-access-token.js +13 -2
  38. package/dist/lib/session-store.js +28 -21
  39. package/dist/lib/startup-init.js +19 -19
  40. package/dist/lib/token-lifecycle.d.ts +2 -0
  41. package/dist/lib/token-lifecycle.js +61 -7
  42. package/dist/models/SessionModel.d.ts +17 -0
  43. package/dist/pages/login/page.d.ts +10 -1
  44. package/dist/pages/login/page.js +69 -80
  45. package/dist/pages/verify-code/page.d.ts +10 -1
  46. package/dist/pages/verify-code/page.js +72 -4
  47. package/dist/routes/auth/passkey-signin.d.ts +13 -0
  48. package/dist/routes/auth/passkey-signin.js +18 -0
  49. package/dist/routes/auth/viability.d.ts +2 -0
  50. package/dist/routes/auth/viability.js +88 -0
  51. package/dist/routes/mfa/backup-code-verify.d.ts +1 -0
  52. package/dist/routes/mfa/backup-code-verify.js +5 -0
  53. package/dist/routes/mfa/backup-codes.d.ts +10 -0
  54. package/dist/routes/mfa/backup-codes.js +14 -0
  55. package/dist/routes/mfa/methods.d.ts +21 -0
  56. package/dist/routes/mfa/methods.js +10 -0
  57. package/dist/routes/mfa/totp-confirm.d.ts +10 -0
  58. package/dist/routes/mfa/totp-confirm.js +14 -0
  59. package/dist/routes/mfa/totp-enroll.d.ts +10 -0
  60. package/dist/routes/mfa/totp-enroll.js +14 -0
  61. package/dist/routes/mfa/totp-verify.d.ts +14 -0
  62. package/dist/routes/mfa/totp-verify.js +18 -0
  63. package/dist/routes/passkeys/list.d.ts +10 -0
  64. package/dist/routes/passkeys/list.js +14 -0
  65. package/dist/routes/passkeys/manage.d.ts +18 -0
  66. package/dist/routes/passkeys/manage.js +13 -0
  67. package/dist/routes/passkeys/register-options.d.ts +10 -0
  68. package/dist/routes/passkeys/register-options.js +14 -0
  69. package/dist/routes/passkeys/register-verify.d.ts +10 -0
  70. package/dist/routes/passkeys/register-verify.js +14 -0
  71. package/dist/server/auth.d.ts +2 -2
  72. package/dist/server/decode-session.js +37 -3
  73. package/package.json +958 -893
  74. package/scripts/check-internal-url-usage.sh +73 -73
  75. package/src/api/index.ts +17 -17
  76. package/src/api-handlers/account/masked-info.ts +45 -45
  77. package/src/api-handlers/account/profile.ts +99 -80
  78. package/src/api-handlers/account/recovery/initiate.ts +23 -23
  79. package/src/api-handlers/account/recovery/send-code.ts +73 -25
  80. package/src/api-handlers/account/recovery/verify-code.ts +73 -25
  81. package/src/api-handlers/account/reset-password.ts +23 -23
  82. package/src/api-handlers/account/send-code.ts +76 -76
  83. package/src/api-handlers/account/validate-password.ts +118 -118
  84. package/src/api-handlers/account/verify-email.ts +15 -1
  85. package/src/api-handlers/account/verify-sms.ts +15 -1
  86. package/src/api-handlers/admin/stats.ts +249 -249
  87. package/src/api-handlers/anon/preferences.ts +123 -123
  88. package/src/api-handlers/auth/jwks.ts +20 -20
  89. package/src/api-handlers/auth/login.ts +240 -240
  90. package/src/api-handlers/auth/passkey-signin.ts +98 -0
  91. package/src/api-handlers/auth/validate.ts +43 -43
  92. package/src/api-handlers/mfa/backup-code-verify.ts +175 -0
  93. package/src/api-handlers/mfa/backup-codes.ts +41 -0
  94. package/src/api-handlers/mfa/methods.ts +46 -0
  95. package/src/api-handlers/mfa/totp-confirm.ts +42 -0
  96. package/src/api-handlers/mfa/totp-enroll.ts +42 -0
  97. package/src/api-handlers/mfa/totp-verify.ts +156 -0
  98. package/src/api-handlers/passkeys/list.ts +31 -0
  99. package/src/api-handlers/passkeys/manage.ts +54 -0
  100. package/src/api-handlers/passkeys/register-options.ts +42 -0
  101. package/src/api-handlers/passkeys/register-verify.ts +41 -0
  102. package/src/api-handlers/session/viability.ts +161 -142
  103. package/src/auth/better-auth.ts +708 -455
  104. package/src/auth/unauthenticated-routes.ts +2 -2
  105. package/src/auth/utils/idp-client.ts +13 -95
  106. package/src/client/better-auth-client.ts +70 -70
  107. package/src/client/fetchWithSession.ts +21 -21
  108. package/src/client/index.ts +13 -13
  109. package/src/client/useAnonSession.ts +131 -131
  110. package/src/components/admin/VibeAdminContext.tsx +87 -87
  111. package/src/config/logging-config.ts +82 -82
  112. package/src/config/unauthenticated-routes.ts +19 -19
  113. package/src/edge/internal-api-url.ts +65 -65
  114. package/src/edge/middleware.ts +42 -42
  115. package/src/hooks/useProfile.ts +75 -75
  116. package/src/lib/anon-session.ts +213 -213
  117. package/src/lib/auth-secret.ts +137 -137
  118. package/src/lib/demo-mode.ts +17 -17
  119. package/src/lib/ensure-fresh-access-token.ts +13 -2
  120. package/src/lib/idp-client-config.ts +559 -559
  121. package/src/lib/idp-fetch.ts +126 -126
  122. package/src/lib/internal-api.ts +171 -171
  123. package/src/lib/jwt-decode-client.ts +45 -45
  124. package/src/lib/rate-limit-service.ts +9 -9
  125. package/src/lib/redis.ts +26 -26
  126. package/src/lib/refresh-token-validator.ts +64 -64
  127. package/src/lib/secret-validation.ts +8 -8
  128. package/src/lib/session-store.ts +702 -695
  129. package/src/lib/session.ts +34 -34
  130. package/src/lib/startup-init.ts +251 -251
  131. package/src/lib/token-expiry.ts +40 -40
  132. package/src/lib/token-lifecycle.ts +533 -470
  133. package/src/lib/types/api-responses.ts +336 -336
  134. package/src/middleware/auth-decision.ts +43 -43
  135. package/src/models/DecodedAccessToken.ts +16 -16
  136. package/src/models/SessionModel.ts +282 -264
  137. package/src/pages/admin-roles/index.ts +9 -9
  138. package/src/pages/login/page.tsx +123 -228
  139. package/src/pages/security/EnhancedSecurityPage.tsx +9 -9
  140. package/src/pages/verify-code/page.tsx +71 -5
  141. package/src/routes/account/index.ts +31 -31
  142. package/src/routes/account/masked-info.ts +36 -36
  143. package/src/routes/account/send-code.ts +39 -39
  144. package/src/routes/account/verify-email.ts +40 -40
  145. package/src/routes/account/verify-sms.ts +40 -40
  146. package/src/routes/auth/index.ts +22 -22
  147. package/src/routes/auth/passkey-signin.ts +13 -0
  148. package/src/routes/auth/refresh.ts +48 -48
  149. package/src/routes/auth/session.ts +166 -166
  150. package/src/routes/auth/viability.ts +89 -0
  151. package/src/routes/index.ts +17 -17
  152. package/src/routes/mfa/backup-code-verify.ts +1 -0
  153. package/src/routes/mfa/backup-codes.ts +10 -0
  154. package/src/routes/mfa/methods.ts +22 -0
  155. package/src/routes/mfa/totp-confirm.ts +10 -0
  156. package/src/routes/mfa/totp-enroll.ts +10 -0
  157. package/src/routes/mfa/totp-verify.ts +14 -0
  158. package/src/routes/passkeys/list.ts +10 -0
  159. package/src/routes/passkeys/manage.ts +23 -0
  160. package/src/routes/passkeys/register-options.ts +10 -0
  161. package/src/routes/passkeys/register-verify.ts +10 -0
  162. package/src/routes/session/index.ts +7 -7
  163. package/src/routes/session/refresh-viability.ts +17 -17
  164. package/src/server/auth.ts +373 -373
  165. package/src/server/decode-session.ts +235 -202
  166. package/src/services/signalrActivityService.ts +258 -258
  167. package/src/theme/ThemeProvider.tsx +39 -39
  168. package/src/theme/default.ts +33 -33
  169. package/src/theme/index.ts +31 -31
  170. package/src/theme/types.ts +69 -69
  171. package/src/theme/useTheme.ts +57 -57
  172. package/src/theme/utils.ts +40 -40
  173. package/src/types/api.ts +12 -12
  174. package/src/types/auth.d.ts +23 -23
  175. package/src/types/auth.ts +22 -22
  176. package/src/types/logging.ts +11 -11
  177. package/src/types/recovery.ts +54 -54
  178. package/src/utils/api.ts +353 -353
  179. package/src/utils/circuitBreaker.ts +40 -40
  180. package/src/utils/error-message.ts +108 -108
  181. package/src/utils/layout/reservedSpace.ts +124 -124
@@ -1,25 +1,73 @@
1
- import { NextRequest, NextResponse } from 'next/server';
2
-
3
- function getIdpUrl(): string {
4
- const url = process.env.IDP_URL;
5
- if (!url) throw new Error('[IDP_URL] FATAL: IDP_URL environment variable is REQUIRED.');
6
- return url;
7
- }
8
-
9
- export async function POST(req: NextRequest) {
10
- const IDP_URL = getIdpUrl();
11
- try {
12
- const auth = req.headers.get('authorization') || '';
13
- const token = auth.startsWith('Bearer ') ? auth.slice(7) : '';
14
- const { code, method } = await req.json();
15
- const upstream = await fetch(`${IDP_URL}/api/Account/recovery/verify-code`, {
16
- method: 'POST',
17
- headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${token}` },
18
- body: JSON.stringify({ code, method }),
19
- });
20
- const data = await upstream.json().catch(() => ({}));
21
- return NextResponse.json(data, { status: upstream.status });
22
- } catch {
23
- return NextResponse.json({ success: false, error: 'verify_code_failed' }, { status: 500 });
24
- }
25
- }
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 recovery — verify-code
7
+ *
8
+ * WHY THIS WAS REWRITTEN (2026-08-02, swept out of QAPert defect #7)
9
+ * This handler read the caller's bearer straight off the inbound request:
10
+ *
11
+ * const auth = req.headers.get('authorization') || '';
12
+ * const token = auth.startsWith('Bearer ') ? auth.slice(7) : '';
13
+ * ... headers: { 'Authorization': `Bearer ${token}` }
14
+ *
15
+ * Browsers send a session COOKIE and no Authorization header, so `token` was
16
+ * ALWAYS the empty string and this always called the IDP with a literal
17
+ * "Bearer " and nothing after it.
18
+ *
19
+ * THAT IS WORSE THAN THE profile.ts VERSION OF THIS BUG, in two ways:
20
+ *
21
+ * 1. profile.ts at least 401'd when the header was missing. This one has no
22
+ * check at all — `|| ''` swallows the absence — so it proceeds to call
23
+ * upstream with an empty credential and returns the IDP's rejection as an
24
+ * opaque failure. The user sees recovery "not working", not "not
25
+ * authenticated", and the log shows an upstream error rather than an auth
26
+ * one. The defect hides itself.
27
+ * 2. It is BOTH HALVES of account recovery — send-code and verify-code — so
28
+ * the entire recovery path is unreachable, not merely degraded. This is
29
+ * the flow a locked-out user reaches for, i.e. it fails exactly when it is
30
+ * the only thing left that could help them.
31
+ *
32
+ * Fixed the same way as profile.ts and masked-info.ts: idpFetchJSON resolves
33
+ * the caller's token from the server-side session, so the cookie works and the
34
+ * token is never handed to browser JavaScript.
35
+ *
36
+ * @package @payez/next-mvp
37
+ */
38
+
39
+ export async function POST(req: NextRequest) {
40
+ let payload: unknown = {};
41
+ try {
42
+ payload = await req.json();
43
+ } catch {
44
+ return NextResponse.json(
45
+ { success: false, error: 'invalid_request', message: 'A JSON body is required' },
46
+ { status: 400 }
47
+ );
48
+ }
49
+
50
+ const result = await idpFetchJSON(
51
+ req,
52
+ `${ENV_CONFIG.IDP_URL}/api/Account/recovery/verify-code`,
53
+ {
54
+ method: 'POST',
55
+ headers: { 'Content-Type': 'application/json' },
56
+ body: JSON.stringify(payload),
57
+ }
58
+ );
59
+
60
+ if (!result.ok) {
61
+ return NextResponse.json(
62
+ {
63
+ success: false,
64
+ error: 'verify_code_failed',
65
+ message: result.json?.message || 'Upstream error',
66
+ meta: { status: result.status, attemptedRefresh: result.attemptedRefresh },
67
+ },
68
+ { status: result.status }
69
+ );
70
+ }
71
+
72
+ return NextResponse.json(result.json, { status: 200 });
73
+ }
@@ -1,23 +1,23 @@
1
- import { NextRequest, NextResponse } from 'next/server';
2
-
3
- function getIdpUrl(): string {
4
- const url = process.env.IDP_URL;
5
- if (!url) throw new Error('[IDP_URL] FATAL: IDP_URL environment variable is REQUIRED.');
6
- return url;
7
- }
8
-
9
- export async function POST(req: NextRequest) {
10
- const IDP_URL = getIdpUrl();
11
- try {
12
- const payload = await req.json();
13
- const upstream = await fetch(`${IDP_URL}/api/Account/reset-password`, {
14
- method: 'POST',
15
- headers: { 'Content-Type': 'application/json' },
16
- body: JSON.stringify(payload),
17
- });
18
- const data = await upstream.json().catch(() => ({}));
19
- return NextResponse.json(data, { status: upstream.status });
20
- } catch {
21
- return NextResponse.json({ success: false, error: 'reset_password_failed' }, { status: 500 });
22
- }
23
- }
1
+ import { NextRequest, NextResponse } from 'next/server';
2
+
3
+ function getIdpUrl(): string {
4
+ const url = process.env.IDP_URL;
5
+ if (!url) throw new Error('[IDP_URL] FATAL: IDP_URL environment variable is REQUIRED.');
6
+ return url;
7
+ }
8
+
9
+ export async function POST(req: NextRequest) {
10
+ const IDP_URL = getIdpUrl();
11
+ try {
12
+ const payload = await req.json();
13
+ const upstream = await fetch(`${IDP_URL}/api/Account/reset-password`, {
14
+ method: 'POST',
15
+ headers: { 'Content-Type': 'application/json' },
16
+ body: JSON.stringify(payload),
17
+ });
18
+ const data = await upstream.json().catch(() => ({}));
19
+ return NextResponse.json(data, { status: upstream.status });
20
+ } catch {
21
+ return NextResponse.json({ success: false, error: 'reset_password_failed' }, { status: 500 });
22
+ }
23
+ }
@@ -1,76 +1,76 @@
1
- /**
2
- * Send 2FA Verification Code Handler
3
- *
4
- * Sends a verification code via email or SMS to the authenticated user.
5
- * Requires a provisional Bearer token (ACR=1) from initial login.
6
- *
7
- * @package @payez/next-mvp
8
- */
9
-
10
- import { NextRequest, NextResponse } from 'next/server';
11
- import { idpFetchJSON } from '../../lib/idp-fetch';
12
- import { ENV_CONFIG } from '../../config/env';
13
-
14
- export async function POST(req: NextRequest) {
15
- try {
16
- // Parse request body
17
- const body = await req.json();
18
- const method = String(body.method || '').toLowerCase();
19
-
20
- if (method !== 'sms' && method !== 'email') {
21
- return NextResponse.json(
22
- {
23
- success: false,
24
- error: 'Method must be either "sms" or "email"',
25
- code: 'INVALID_METHOD',
26
- },
27
- { status: 400 }
28
- );
29
- }
30
-
31
- // Build IDP endpoint URL
32
- const idpEndpoint = method === 'sms'
33
- ? '/api/ExternalAuth/twofa/sms/send'
34
- : '/api/ExternalAuth/twofa/email/send';
35
-
36
- // Send client_id in body (lower_snake_case per PayEz standards)
37
- const idpBody = JSON.stringify({ client_id: ENV_CONFIG.CLIENT_ID });
38
-
39
- // Call IDP using idpFetchJSON which auto-injects Bearer token from Redis session
40
- const result = await idpFetchJSON(req, `${ENV_CONFIG.IDP_URL}${idpEndpoint}`, {
41
- method: 'POST',
42
- headers: { 'Content-Type': 'application/json' },
43
- body: idpBody,
44
- });
45
-
46
- if (!result.ok) {
47
- return NextResponse.json(
48
- {
49
- success: false,
50
- error: result.json?.message || `Failed to send ${method} code`,
51
- code: result.json?.code || 'IDP_ERROR',
52
- meta: { attemptedRefresh: result.attemptedRefresh },
53
- },
54
- { status: result.status }
55
- );
56
- }
57
-
58
- return NextResponse.json(
59
- {
60
- success: true,
61
- message: `Verification code sent via ${method}`,
62
- },
63
- { status: 200 }
64
- );
65
- } catch (error) {
66
- console.error('[SEND_CODE] Error:', error);
67
- return NextResponse.json(
68
- {
69
- success: false,
70
- error: 'Failed to send verification code',
71
- code: 'INTERNAL_ERROR',
72
- },
73
- { status: 500 }
74
- );
75
- }
76
- }
1
+ /**
2
+ * Send 2FA Verification Code Handler
3
+ *
4
+ * Sends a verification code via email or SMS to the authenticated user.
5
+ * Requires a provisional Bearer token (ACR=1) from initial login.
6
+ *
7
+ * @package @payez/next-mvp
8
+ */
9
+
10
+ import { NextRequest, NextResponse } from 'next/server';
11
+ import { idpFetchJSON } from '../../lib/idp-fetch';
12
+ import { ENV_CONFIG } from '../../config/env';
13
+
14
+ export async function POST(req: NextRequest) {
15
+ try {
16
+ // Parse request body
17
+ const body = await req.json();
18
+ const method = String(body.method || '').toLowerCase();
19
+
20
+ if (method !== 'sms' && method !== 'email') {
21
+ return NextResponse.json(
22
+ {
23
+ success: false,
24
+ error: 'Method must be either "sms" or "email"',
25
+ code: 'INVALID_METHOD',
26
+ },
27
+ { status: 400 }
28
+ );
29
+ }
30
+
31
+ // Build IDP endpoint URL
32
+ const idpEndpoint = method === 'sms'
33
+ ? '/api/ExternalAuth/twofa/sms/send'
34
+ : '/api/ExternalAuth/twofa/email/send';
35
+
36
+ // Send client_id in body (lower_snake_case per PayEz standards)
37
+ const idpBody = JSON.stringify({ client_id: ENV_CONFIG.CLIENT_ID });
38
+
39
+ // Call IDP using idpFetchJSON which auto-injects Bearer token from Redis session
40
+ const result = await idpFetchJSON(req, `${ENV_CONFIG.IDP_URL}${idpEndpoint}`, {
41
+ method: 'POST',
42
+ headers: { 'Content-Type': 'application/json' },
43
+ body: idpBody,
44
+ });
45
+
46
+ if (!result.ok) {
47
+ return NextResponse.json(
48
+ {
49
+ success: false,
50
+ error: result.json?.message || `Failed to send ${method} code`,
51
+ code: result.json?.code || 'IDP_ERROR',
52
+ meta: { attemptedRefresh: result.attemptedRefresh },
53
+ },
54
+ { status: result.status }
55
+ );
56
+ }
57
+
58
+ return NextResponse.json(
59
+ {
60
+ success: true,
61
+ message: `Verification code sent via ${method}`,
62
+ },
63
+ { status: 200 }
64
+ );
65
+ } catch (error) {
66
+ console.error('[SEND_CODE] Error:', error);
67
+ return NextResponse.json(
68
+ {
69
+ success: false,
70
+ error: 'Failed to send verification code',
71
+ code: 'INTERNAL_ERROR',
72
+ },
73
+ { status: 500 }
74
+ );
75
+ }
76
+ }
@@ -1,118 +1,118 @@
1
- import { NextRequest, NextResponse } from 'next/server';
2
-
3
- interface ValidatePasswordRequest {
4
- password: string;
5
- }
6
-
7
- interface ValidatePasswordResponse {
8
- is_valid: boolean;
9
- score: number;
10
- failed_requirements: string[];
11
- tip?: string;
12
- policy?: {
13
- min_length?: number;
14
- require_uppercase?: boolean;
15
- require_lowercase?: boolean;
16
- require_digit?: boolean;
17
- require_special?: boolean;
18
- min_strength_score?: number;
19
- };
20
- }
21
-
22
- export async function POST(req: NextRequest) {
23
- try {
24
- const body = await req.json();
25
- const { password } = body as ValidatePasswordRequest;
26
- const requestId = req.headers.get('x-request-id') ?? crypto.randomUUID();
27
-
28
- // Validate input
29
- if (!password || typeof password !== 'string') {
30
- return NextResponse.json(
31
- {
32
- is_valid: false,
33
- score: 0,
34
- failed_requirements: ['Password is required'],
35
- } as ValidatePasswordResponse,
36
- {
37
- status: 200, // Return 200 even for validation errors to keep UI responsive
38
- headers: { 'Cache-Control': 'no-store' },
39
- }
40
- );
41
- }
42
-
43
- // Get IDP base URL and client ID from environment
44
- const idpBaseUrl = process.env.IDP_URL;
45
-
46
- const clientId = process.env.CLIENT_ID || process.env.NEXT_PUBLIC_CLIENT_ID;
47
-
48
- if (!idpBaseUrl) {
49
- console.error('[VALIDATE_PASSWORD] IDP_URL not configured');
50
- return NextResponse.json(
51
- {
52
- is_valid: false,
53
- score: 0,
54
- failed_requirements: ['Password validation service unavailable'],
55
- } as ValidatePasswordResponse,
56
- {
57
- status: 200,
58
- headers: { 'Cache-Control': 'no-store' },
59
- }
60
- );
61
- }
62
-
63
- // Proxy request to IDP
64
- const idpUrl = `${idpBaseUrl}/api/Account/validate-password`;
65
- const payload = {
66
- password,
67
- client_id: clientId,
68
- };
69
-
70
- const idpResponse = await fetch(idpUrl, {
71
- method: 'POST',
72
- headers: {
73
- 'Content-Type': 'application/json',
74
- 'x-request-id': requestId,
75
- },
76
- body: JSON.stringify(payload),
77
- });
78
-
79
- const responseData = await idpResponse.json().catch(() => ({}));
80
-
81
- if (!idpResponse.ok) {
82
- console.error('[VALIDATE_PASSWORD] IDP error:', {
83
- status: idpResponse.status,
84
- response: responseData,
85
- });
86
- return NextResponse.json(
87
- {
88
- is_valid: false,
89
- score: 0,
90
- failed_requirements: ['Password validation failed'],
91
- } as ValidatePasswordResponse,
92
- {
93
- status: 200,
94
- headers: { 'Cache-Control': 'no-store' },
95
- }
96
- );
97
- }
98
-
99
- // Return the IDP response with proper structure
100
- return NextResponse.json(responseData as ValidatePasswordResponse, {
101
- status: 200,
102
- headers: { 'Cache-Control': 'no-store' },
103
- });
104
- } catch (error) {
105
- console.error('[VALIDATE_PASSWORD] Error:', error);
106
- return NextResponse.json(
107
- {
108
- is_valid: false,
109
- score: 0,
110
- failed_requirements: ['Password validation failed'],
111
- } as ValidatePasswordResponse,
112
- {
113
- status: 200,
114
- headers: { 'Cache-Control': 'no-store' },
115
- }
116
- );
117
- }
118
- }
1
+ import { NextRequest, NextResponse } from 'next/server';
2
+
3
+ interface ValidatePasswordRequest {
4
+ password: string;
5
+ }
6
+
7
+ interface ValidatePasswordResponse {
8
+ is_valid: boolean;
9
+ score: number;
10
+ failed_requirements: string[];
11
+ tip?: string;
12
+ policy?: {
13
+ min_length?: number;
14
+ require_uppercase?: boolean;
15
+ require_lowercase?: boolean;
16
+ require_digit?: boolean;
17
+ require_special?: boolean;
18
+ min_strength_score?: number;
19
+ };
20
+ }
21
+
22
+ export async function POST(req: NextRequest) {
23
+ try {
24
+ const body = await req.json();
25
+ const { password } = body as ValidatePasswordRequest;
26
+ const requestId = req.headers.get('x-request-id') ?? crypto.randomUUID();
27
+
28
+ // Validate input
29
+ if (!password || typeof password !== 'string') {
30
+ return NextResponse.json(
31
+ {
32
+ is_valid: false,
33
+ score: 0,
34
+ failed_requirements: ['Password is required'],
35
+ } as ValidatePasswordResponse,
36
+ {
37
+ status: 200, // Return 200 even for validation errors to keep UI responsive
38
+ headers: { 'Cache-Control': 'no-store' },
39
+ }
40
+ );
41
+ }
42
+
43
+ // Get IDP base URL and client ID from environment
44
+ const idpBaseUrl = process.env.IDP_URL;
45
+
46
+ const clientId = process.env.CLIENT_ID || process.env.NEXT_PUBLIC_CLIENT_ID;
47
+
48
+ if (!idpBaseUrl) {
49
+ console.error('[VALIDATE_PASSWORD] IDP_URL not configured');
50
+ return NextResponse.json(
51
+ {
52
+ is_valid: false,
53
+ score: 0,
54
+ failed_requirements: ['Password validation service unavailable'],
55
+ } as ValidatePasswordResponse,
56
+ {
57
+ status: 200,
58
+ headers: { 'Cache-Control': 'no-store' },
59
+ }
60
+ );
61
+ }
62
+
63
+ // Proxy request to IDP
64
+ const idpUrl = `${idpBaseUrl}/api/Account/validate-password`;
65
+ const payload = {
66
+ password,
67
+ client_id: clientId,
68
+ };
69
+
70
+ const idpResponse = await fetch(idpUrl, {
71
+ method: 'POST',
72
+ headers: {
73
+ 'Content-Type': 'application/json',
74
+ 'x-request-id': requestId,
75
+ },
76
+ body: JSON.stringify(payload),
77
+ });
78
+
79
+ const responseData = await idpResponse.json().catch(() => ({}));
80
+
81
+ if (!idpResponse.ok) {
82
+ console.error('[VALIDATE_PASSWORD] IDP error:', {
83
+ status: idpResponse.status,
84
+ response: responseData,
85
+ });
86
+ return NextResponse.json(
87
+ {
88
+ is_valid: false,
89
+ score: 0,
90
+ failed_requirements: ['Password validation failed'],
91
+ } as ValidatePasswordResponse,
92
+ {
93
+ status: 200,
94
+ headers: { 'Cache-Control': 'no-store' },
95
+ }
96
+ );
97
+ }
98
+
99
+ // Return the IDP response with proper structure
100
+ return NextResponse.json(responseData as ValidatePasswordResponse, {
101
+ status: 200,
102
+ headers: { 'Cache-Control': 'no-store' },
103
+ });
104
+ } catch (error) {
105
+ console.error('[VALIDATE_PASSWORD] Error:', error);
106
+ return NextResponse.json(
107
+ {
108
+ is_valid: false,
109
+ score: 0,
110
+ failed_requirements: ['Password validation failed'],
111
+ } as ValidatePasswordResponse,
112
+ {
113
+ status: 200,
114
+ headers: { 'Cache-Control': 'no-store' },
115
+ }
116
+ );
117
+ }
118
+ }
@@ -54,6 +54,11 @@ export async function POST(req: NextRequest) {
54
54
  const unwrappedData = result.json?.data || result.json;
55
55
 
56
56
  // If we have new tokens, update the session to complete 2FA
57
+ // Reports whether the session ACTUALLY transitioned. Must not be assumed
58
+ // from a 2xx: the IDP can verify the code while we fail to transition
59
+ // (no tokens returned, or no resolvable session token).
60
+ let sessionTransitioned = false;
61
+
57
62
  if (unwrappedData.access_token && unwrappedData.refresh_token) {
58
63
  // Get session token from NextAuth
59
64
  // Support both field names: sessionToken (auth.ts JWT) and redisSessionId (legacy)
@@ -97,18 +102,27 @@ export async function POST(req: NextRequest) {
97
102
  'email' // Store 2FA method for refresh token flow
98
103
  );
99
104
 
105
+ sessionTransitioned = true;
106
+
100
107
  console.log('[VERIFY_EMAIL] Session updated successfully', {
101
108
  accessTokenExpires: new Date(accessTokenExpires).toISOString(),
102
109
  refreshTokenExpires: new Date(refreshTokenExpires).toISOString()
103
110
  });
111
+ } else {
112
+ console.warn('[VERIFY_EMAIL] IDP verified the code but no session token was resolvable — session NOT transitioned');
104
113
  }
114
+ } else {
115
+ console.warn('[VERIFY_EMAIL] IDP returned 2xx without tokens — session NOT transitioned');
105
116
  }
106
117
 
107
118
  // Return simplified success response (don't expose tokens to client)
108
119
  return NextResponse.json({
109
120
  success: true,
110
121
  verificationSuccessful: true,
111
- twoFactorSessionVerified: true,
122
+ // NOT a copy of `success` — reports what actually happened. A client that
123
+ // trusted a hardcoded true would show the user as MFA-verified while the
124
+ // session still says otherwise.
125
+ twoFactorSessionVerified: sessionTransitioned,
112
126
  message: unwrappedData.message || 'Email code verified successfully'
113
127
  }, { status: 200 });
114
128
  } catch (error) {
@@ -54,6 +54,11 @@ export async function POST(req: NextRequest) {
54
54
  const unwrappedData = result.json?.data || result.json;
55
55
 
56
56
  // If we have new tokens, update the session to complete 2FA
57
+ // Reports whether the session ACTUALLY transitioned. Must not be assumed
58
+ // from a 2xx: the IDP can verify the code while we fail to transition
59
+ // (no tokens returned, or no resolvable session token).
60
+ let sessionTransitioned = false;
61
+
57
62
  if (unwrappedData.access_token && unwrappedData.refresh_token) {
58
63
  // Get session token from NextAuth
59
64
  // Support both field names: sessionToken (auth.ts JWT) and redisSessionId (legacy)
@@ -97,18 +102,27 @@ export async function POST(req: NextRequest) {
97
102
  'sms' // Store 2FA method for refresh token flow
98
103
  );
99
104
 
105
+ sessionTransitioned = true;
106
+
100
107
  console.log('[VERIFY_SMS] Session updated successfully', {
101
108
  accessTokenExpires: new Date(accessTokenExpires).toISOString(),
102
109
  refreshTokenExpires: new Date(refreshTokenExpires).toISOString()
103
110
  });
111
+ } else {
112
+ console.warn('[VERIFY_SMS] IDP verified the code but no session token was resolvable — session NOT transitioned');
104
113
  }
114
+ } else {
115
+ console.warn('[VERIFY_SMS] IDP returned 2xx without tokens — session NOT transitioned');
105
116
  }
106
117
 
107
118
  // Return simplified success response (don't expose tokens to client)
108
119
  return NextResponse.json({
109
120
  success: true,
110
121
  verificationSuccessful: true,
111
- twoFactorSessionVerified: true,
122
+ // NOT a copy of `success` — reports what actually happened. A client that
123
+ // trusted a hardcoded true would show the user as MFA-verified while the
124
+ // session still says otherwise.
125
+ twoFactorSessionVerified: sessionTransitioned,
112
126
  message: unwrappedData.message || 'SMS code verified successfully'
113
127
  }, { status: 200 });
114
128
  } catch (error) {