@payez/next-mvp 4.1.10 → 4.1.20
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/api-handlers/account/profile.d.ts +38 -0
- package/dist/api-handlers/account/profile.js +73 -45
- package/dist/api-handlers/account/recovery/send-code.d.ts +33 -0
- package/dist/api-handlers/account/recovery/send-code.js +52 -18
- package/dist/api-handlers/account/recovery/verify-code.d.ts +33 -0
- package/dist/api-handlers/account/recovery/verify-code.js +52 -18
- package/dist/api-handlers/account/verify-email.js +15 -1
- package/dist/api-handlers/account/verify-sms.js +15 -1
- package/dist/api-handlers/auth/passkey-signin.d.ts +29 -0
- package/dist/api-handlers/auth/passkey-signin.js +97 -0
- package/dist/api-handlers/mfa/backup-code-verify.d.ts +73 -0
- package/dist/api-handlers/mfa/backup-code-verify.js +153 -0
- package/dist/api-handlers/mfa/backup-codes.d.ts +7 -0
- package/dist/api-handlers/mfa/backup-codes.js +40 -0
- package/dist/api-handlers/mfa/methods.d.ts +3 -0
- package/dist/api-handlers/mfa/methods.js +41 -0
- package/dist/api-handlers/mfa/totp-confirm.d.ts +8 -0
- package/dist/api-handlers/mfa/totp-confirm.js +41 -0
- package/dist/api-handlers/mfa/totp-enroll.d.ts +8 -0
- package/dist/api-handlers/mfa/totp-enroll.js +41 -0
- package/dist/api-handlers/mfa/totp-verify.d.ts +47 -0
- package/dist/api-handlers/mfa/totp-verify.js +134 -0
- package/dist/api-handlers/passkeys/list.d.ts +7 -0
- package/dist/api-handlers/passkeys/list.js +29 -0
- package/dist/api-handlers/passkeys/manage.d.ts +3 -0
- package/dist/api-handlers/passkeys/manage.js +54 -0
- package/dist/api-handlers/passkeys/register-options.d.ts +8 -0
- package/dist/api-handlers/passkeys/register-options.js +41 -0
- package/dist/api-handlers/passkeys/register-verify.d.ts +7 -0
- package/dist/api-handlers/passkeys/register-verify.js +40 -0
- package/dist/api-handlers/session/viability.js +19 -0
- package/dist/auth/better-auth.d.ts +68 -5
- package/dist/auth/better-auth.js +271 -45
- package/dist/auth/utils/idp-client.d.ts +13 -22
- package/dist/auth/utils/idp-client.js +12 -82
- package/dist/client/better-auth-client.d.ts +14 -2
- package/dist/lib/ensure-fresh-access-token.js +13 -2
- package/dist/lib/session-store.js +28 -21
- package/dist/lib/startup-init.js +19 -19
- package/dist/lib/token-lifecycle.d.ts +2 -0
- package/dist/lib/token-lifecycle.js +61 -7
- package/dist/models/SessionModel.d.ts +17 -0
- package/dist/pages/login/page.d.ts +10 -1
- package/dist/pages/login/page.js +69 -80
- package/dist/pages/verify-code/page.d.ts +10 -1
- package/dist/pages/verify-code/page.js +72 -4
- package/dist/routes/auth/passkey-signin.d.ts +13 -0
- package/dist/routes/auth/passkey-signin.js +18 -0
- package/dist/routes/auth/viability.d.ts +2 -0
- package/dist/routes/auth/viability.js +88 -0
- package/dist/routes/mfa/backup-code-verify.d.ts +1 -0
- package/dist/routes/mfa/backup-code-verify.js +5 -0
- package/dist/routes/mfa/backup-codes.d.ts +10 -0
- package/dist/routes/mfa/backup-codes.js +14 -0
- package/dist/routes/mfa/methods.d.ts +21 -0
- package/dist/routes/mfa/methods.js +10 -0
- package/dist/routes/mfa/totp-confirm.d.ts +10 -0
- package/dist/routes/mfa/totp-confirm.js +14 -0
- package/dist/routes/mfa/totp-enroll.d.ts +10 -0
- package/dist/routes/mfa/totp-enroll.js +14 -0
- package/dist/routes/mfa/totp-verify.d.ts +14 -0
- package/dist/routes/mfa/totp-verify.js +18 -0
- package/dist/routes/passkeys/list.d.ts +10 -0
- package/dist/routes/passkeys/list.js +14 -0
- package/dist/routes/passkeys/manage.d.ts +18 -0
- package/dist/routes/passkeys/manage.js +13 -0
- package/dist/routes/passkeys/register-options.d.ts +10 -0
- package/dist/routes/passkeys/register-options.js +14 -0
- package/dist/routes/passkeys/register-verify.d.ts +10 -0
- package/dist/routes/passkeys/register-verify.js +14 -0
- package/dist/server/auth.d.ts +2 -2
- package/dist/server/decode-session.js +37 -3
- package/package.json +958 -893
- package/scripts/check-internal-url-usage.sh +73 -73
- package/src/api/index.ts +17 -17
- package/src/api-handlers/account/masked-info.ts +45 -45
- package/src/api-handlers/account/profile.ts +99 -80
- package/src/api-handlers/account/recovery/initiate.ts +23 -23
- package/src/api-handlers/account/recovery/send-code.ts +73 -25
- package/src/api-handlers/account/recovery/verify-code.ts +73 -25
- package/src/api-handlers/account/reset-password.ts +23 -23
- package/src/api-handlers/account/send-code.ts +76 -76
- package/src/api-handlers/account/validate-password.ts +118 -118
- package/src/api-handlers/account/verify-email.ts +15 -1
- package/src/api-handlers/account/verify-sms.ts +15 -1
- package/src/api-handlers/admin/stats.ts +249 -249
- package/src/api-handlers/anon/preferences.ts +123 -123
- package/src/api-handlers/auth/jwks.ts +20 -20
- package/src/api-handlers/auth/login.ts +240 -240
- package/src/api-handlers/auth/passkey-signin.ts +98 -0
- package/src/api-handlers/auth/validate.ts +43 -43
- package/src/api-handlers/mfa/backup-code-verify.ts +175 -0
- package/src/api-handlers/mfa/backup-codes.ts +41 -0
- package/src/api-handlers/mfa/methods.ts +46 -0
- package/src/api-handlers/mfa/totp-confirm.ts +42 -0
- package/src/api-handlers/mfa/totp-enroll.ts +42 -0
- package/src/api-handlers/mfa/totp-verify.ts +156 -0
- package/src/api-handlers/passkeys/list.ts +31 -0
- package/src/api-handlers/passkeys/manage.ts +54 -0
- package/src/api-handlers/passkeys/register-options.ts +42 -0
- package/src/api-handlers/passkeys/register-verify.ts +41 -0
- package/src/api-handlers/session/viability.ts +161 -142
- package/src/auth/better-auth.ts +708 -455
- package/src/auth/unauthenticated-routes.ts +2 -2
- package/src/auth/utils/idp-client.ts +13 -95
- package/src/client/better-auth-client.ts +70 -70
- package/src/client/fetchWithSession.ts +21 -21
- package/src/client/index.ts +13 -13
- package/src/client/useAnonSession.ts +131 -131
- package/src/components/admin/VibeAdminContext.tsx +87 -87
- package/src/config/logging-config.ts +82 -82
- package/src/config/unauthenticated-routes.ts +19 -19
- package/src/edge/internal-api-url.ts +65 -65
- package/src/edge/middleware.ts +42 -42
- package/src/hooks/useProfile.ts +75 -75
- package/src/lib/anon-session.ts +213 -213
- package/src/lib/auth-secret.ts +137 -137
- package/src/lib/demo-mode.ts +17 -17
- package/src/lib/ensure-fresh-access-token.ts +13 -2
- package/src/lib/idp-client-config.ts +559 -559
- package/src/lib/idp-fetch.ts +126 -126
- package/src/lib/internal-api.ts +171 -171
- package/src/lib/jwt-decode-client.ts +45 -45
- package/src/lib/rate-limit-service.ts +9 -9
- package/src/lib/redis.ts +26 -26
- package/src/lib/refresh-token-validator.ts +64 -64
- package/src/lib/secret-validation.ts +8 -8
- package/src/lib/session-store.ts +702 -695
- package/src/lib/session.ts +34 -34
- package/src/lib/startup-init.ts +251 -251
- package/src/lib/token-expiry.ts +40 -40
- package/src/lib/token-lifecycle.ts +533 -470
- package/src/lib/types/api-responses.ts +336 -336
- package/src/middleware/auth-decision.ts +43 -43
- package/src/models/DecodedAccessToken.ts +16 -16
- package/src/models/SessionModel.ts +282 -264
- package/src/pages/admin-roles/index.ts +9 -9
- package/src/pages/login/page.tsx +123 -228
- package/src/pages/security/EnhancedSecurityPage.tsx +9 -9
- package/src/pages/verify-code/page.tsx +71 -5
- package/src/routes/account/index.ts +31 -31
- package/src/routes/account/masked-info.ts +36 -36
- package/src/routes/account/send-code.ts +39 -39
- package/src/routes/account/verify-email.ts +40 -40
- package/src/routes/account/verify-sms.ts +40 -40
- package/src/routes/auth/index.ts +22 -22
- package/src/routes/auth/passkey-signin.ts +13 -0
- package/src/routes/auth/refresh.ts +48 -48
- package/src/routes/auth/session.ts +166 -166
- package/src/routes/auth/viability.ts +89 -0
- package/src/routes/index.ts +17 -17
- package/src/routes/mfa/backup-code-verify.ts +1 -0
- package/src/routes/mfa/backup-codes.ts +10 -0
- package/src/routes/mfa/methods.ts +22 -0
- package/src/routes/mfa/totp-confirm.ts +10 -0
- package/src/routes/mfa/totp-enroll.ts +10 -0
- package/src/routes/mfa/totp-verify.ts +14 -0
- package/src/routes/passkeys/list.ts +10 -0
- package/src/routes/passkeys/manage.ts +23 -0
- package/src/routes/passkeys/register-options.ts +10 -0
- package/src/routes/passkeys/register-verify.ts +10 -0
- package/src/routes/session/index.ts +7 -7
- package/src/routes/session/refresh-viability.ts +17 -17
- package/src/server/auth.ts +373 -373
- package/src/server/decode-session.ts +235 -202
- package/src/services/signalrActivityService.ts +258 -258
- package/src/theme/ThemeProvider.tsx +39 -39
- package/src/theme/default.ts +33 -33
- package/src/theme/index.ts +31 -31
- package/src/theme/types.ts +69 -69
- package/src/theme/useTheme.ts +57 -57
- package/src/theme/utils.ts +40 -40
- package/src/types/api.ts +12 -12
- package/src/types/auth.d.ts +23 -23
- package/src/types/auth.ts +22 -22
- package/src/types/logging.ts +11 -11
- package/src/types/recovery.ts +54 -54
- package/src/utils/api.ts +353 -353
- package/src/utils/circuitBreaker.ts +40 -40
- package/src/utils/error-message.ts +108 -108
- package/src/utils/layout/reservedSpace.ts +124 -124
|
@@ -1,25 +1,73 @@
|
|
|
1
|
-
import { NextRequest, NextResponse } from 'next/server';
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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) {
|