@payez/next-mvp 4.1.10 → 4.1.12

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 (89) hide show
  1. package/dist/api-handlers/auth/refresh.js +15 -38
  2. package/dist/auth/better-auth.js +9 -18
  3. package/dist/client/better-auth-client.d.ts +14 -2
  4. package/dist/lib/ensure-fresh-access-token.js +7 -30
  5. package/dist/lib/session-store.d.ts +0 -1
  6. package/dist/lib/session-store.js +29 -33
  7. package/dist/lib/startup-init.js +19 -19
  8. package/package.json +4 -3
  9. package/scripts/check-internal-url-usage.sh +73 -73
  10. package/src/api/index.ts +17 -17
  11. package/src/api-handlers/account/masked-info.ts +45 -45
  12. package/src/api-handlers/account/profile.ts +80 -80
  13. package/src/api-handlers/account/recovery/initiate.ts +23 -23
  14. package/src/api-handlers/account/recovery/send-code.ts +25 -25
  15. package/src/api-handlers/account/recovery/verify-code.ts +25 -25
  16. package/src/api-handlers/account/reset-password.ts +23 -23
  17. package/src/api-handlers/account/send-code.ts +76 -76
  18. package/src/api-handlers/account/validate-password.ts +118 -118
  19. package/src/api-handlers/admin/stats.ts +249 -249
  20. package/src/api-handlers/anon/preferences.ts +123 -123
  21. package/src/api-handlers/auth/jwks.ts +20 -20
  22. package/src/api-handlers/auth/login.ts +240 -240
  23. package/src/api-handlers/auth/refresh.ts +15 -38
  24. package/src/api-handlers/auth/validate.ts +43 -43
  25. package/src/api-handlers/session/viability.ts +142 -142
  26. package/src/auth/better-auth.ts +9 -14
  27. package/src/auth/unauthenticated-routes.ts +2 -2
  28. package/src/client/better-auth-client.ts +70 -70
  29. package/src/client/fetchWithSession.ts +21 -21
  30. package/src/client/index.ts +13 -13
  31. package/src/client/useAnonSession.ts +131 -131
  32. package/src/components/admin/VibeAdminContext.tsx +87 -87
  33. package/src/config/logging-config.ts +82 -82
  34. package/src/config/unauthenticated-routes.ts +19 -19
  35. package/src/edge/internal-api-url.ts +65 -65
  36. package/src/edge/middleware.ts +42 -42
  37. package/src/hooks/useProfile.ts +75 -75
  38. package/src/lib/anon-session.ts +213 -213
  39. package/src/lib/auth-secret.ts +137 -137
  40. package/src/lib/demo-mode.ts +17 -17
  41. package/src/lib/ensure-fresh-access-token.ts +7 -30
  42. package/src/lib/idp-client-config.ts +559 -559
  43. package/src/lib/idp-fetch.ts +126 -126
  44. package/src/lib/internal-api.ts +171 -171
  45. package/src/lib/jwt-decode-client.ts +45 -45
  46. package/src/lib/rate-limit-service.ts +9 -9
  47. package/src/lib/redis.ts +26 -26
  48. package/src/lib/refresh-token-validator.ts +64 -64
  49. package/src/lib/secret-validation.ts +8 -8
  50. package/src/lib/session-store.ts +692 -695
  51. package/src/lib/session.ts +34 -34
  52. package/src/lib/startup-init.ts +251 -251
  53. package/src/lib/token-expiry.ts +40 -40
  54. package/src/lib/token-lifecycle.ts +470 -470
  55. package/src/lib/types/api-responses.ts +336 -336
  56. package/src/middleware/auth-decision.ts +43 -43
  57. package/src/models/DecodedAccessToken.ts +16 -16
  58. package/src/models/SessionModel.ts +264 -264
  59. package/src/pages/admin-roles/index.ts +9 -9
  60. package/src/pages/security/EnhancedSecurityPage.tsx +9 -9
  61. package/src/routes/account/index.ts +31 -31
  62. package/src/routes/account/masked-info.ts +36 -36
  63. package/src/routes/account/send-code.ts +39 -39
  64. package/src/routes/account/verify-email.ts +40 -40
  65. package/src/routes/account/verify-sms.ts +40 -40
  66. package/src/routes/auth/index.ts +22 -22
  67. package/src/routes/auth/refresh.ts +48 -48
  68. package/src/routes/auth/session.ts +166 -166
  69. package/src/routes/index.ts +17 -17
  70. package/src/routes/session/index.ts +7 -7
  71. package/src/routes/session/refresh-viability.ts +17 -17
  72. package/src/server/auth.ts +373 -373
  73. package/src/server/decode-session.ts +202 -202
  74. package/src/services/signalrActivityService.ts +258 -258
  75. package/src/theme/ThemeProvider.tsx +39 -39
  76. package/src/theme/default.ts +33 -33
  77. package/src/theme/index.ts +31 -31
  78. package/src/theme/types.ts +69 -69
  79. package/src/theme/useTheme.ts +57 -57
  80. package/src/theme/utils.ts +40 -40
  81. package/src/types/api.ts +12 -12
  82. package/src/types/auth.d.ts +23 -23
  83. package/src/types/auth.ts +22 -22
  84. package/src/types/logging.ts +11 -11
  85. package/src/types/recovery.ts +54 -54
  86. package/src/utils/api.ts +353 -353
  87. package/src/utils/circuitBreaker.ts +40 -40
  88. package/src/utils/error-message.ts +108 -108
  89. package/src/utils/layout/reservedSpace.ts +124 -124
@@ -1,45 +1,45 @@
1
- import { NextRequest, NextResponse } from 'next/server';
2
- import { idpFetchJSON } from '../../lib/idp-fetch';
3
- import { ENV_CONFIG } from '../../config/env';
4
-
5
- // IDP masked-info is POST and uses capital 'A' in /api/Account
6
- export async function POST(req: NextRequest) {
7
- const url = `${ENV_CONFIG.IDP_URL}/api/Account/masked-info`;
8
-
9
- // Forward request body if present; IDP often accepts empty object
10
- let body = '{}';
11
- try {
12
- const raw = await req.text();
13
- if (raw && raw.trim().length > 0) body = raw;
14
- } catch {}
15
-
16
- const result = await idpFetchJSON(req, url, {
17
- method: 'POST',
18
- headers: { 'Content-Type': 'application/json' },
19
- body
20
- });
21
-
22
- if (!result.ok) {
23
- return NextResponse.json(
24
- {
25
- success: false,
26
- message: 'Upstream error',
27
- error: { code: 'UPSTREAM_SERVICE_ERROR', status: result.status, details: result.json },
28
- meta: { attemptedRefresh: result.attemptedRefresh },
29
- },
30
- { status: result.status }
31
- );
32
- }
33
-
34
- const bodyJson: any = result.json;
35
- // Unwrap if IDP returns envelope { success, data }
36
- if (bodyJson && typeof bodyJson === 'object' && 'success' in bodyJson && 'data' in bodyJson) {
37
- if ((bodyJson as any).success === true) {
38
- return NextResponse.json((bodyJson as any).data, { status: 200 });
39
- }
40
- return NextResponse.json(bodyJson, { status: 200 });
41
- }
42
-
43
- // Passthrough otherwise
44
- return NextResponse.json(bodyJson ?? {}, { status: 200 });
45
- }
1
+ import { NextRequest, NextResponse } from 'next/server';
2
+ import { idpFetchJSON } from '../../lib/idp-fetch';
3
+ import { ENV_CONFIG } from '../../config/env';
4
+
5
+ // IDP masked-info is POST and uses capital 'A' in /api/Account
6
+ export async function POST(req: NextRequest) {
7
+ const url = `${ENV_CONFIG.IDP_URL}/api/Account/masked-info`;
8
+
9
+ // Forward request body if present; IDP often accepts empty object
10
+ let body = '{}';
11
+ try {
12
+ const raw = await req.text();
13
+ if (raw && raw.trim().length > 0) body = raw;
14
+ } catch {}
15
+
16
+ const result = await idpFetchJSON(req, url, {
17
+ method: 'POST',
18
+ headers: { 'Content-Type': 'application/json' },
19
+ body
20
+ });
21
+
22
+ if (!result.ok) {
23
+ return NextResponse.json(
24
+ {
25
+ success: false,
26
+ message: 'Upstream error',
27
+ error: { code: 'UPSTREAM_SERVICE_ERROR', status: result.status, details: result.json },
28
+ meta: { attemptedRefresh: result.attemptedRefresh },
29
+ },
30
+ { status: result.status }
31
+ );
32
+ }
33
+
34
+ const bodyJson: any = result.json;
35
+ // Unwrap if IDP returns envelope { success, data }
36
+ if (bodyJson && typeof bodyJson === 'object' && 'success' in bodyJson && 'data' in bodyJson) {
37
+ if ((bodyJson as any).success === true) {
38
+ return NextResponse.json((bodyJson as any).data, { status: 200 });
39
+ }
40
+ return NextResponse.json(bodyJson, { status: 200 });
41
+ }
42
+
43
+ // Passthrough otherwise
44
+ return NextResponse.json(bodyJson ?? {}, { status: 200 });
45
+ }
@@ -1,80 +1,80 @@
1
- import { NextRequest, NextResponse } from 'next/server';
2
-
3
- /**
4
- * Account Profile API Handler
5
- * Simple proxy to IDP profile endpoint
6
- *
7
- * GET /api/account/profile - Get user profile
8
- * PUT /api/account/profile - Update user profile
9
- */
10
-
11
- function getIdpUrl(): string {
12
- const url = process.env.IDP_URL;
13
- if (!url) {
14
- throw new Error('[IDP_URL] FATAL: IDP_URL environment variable is REQUIRED.');
15
- }
16
- return url;
17
- }
18
-
19
- export async function GET(req: NextRequest) {
20
- const IDP_URL = getIdpUrl();
21
- const authHeader = req.headers.get('authorization');
22
-
23
- if (!authHeader) {
24
- return NextResponse.json(
25
- { success: false, error: { code: 'UNAUTHORIZED', message: 'Authentication required' } },
26
- { status: 401 }
27
- );
28
- }
29
-
30
- try {
31
- const upstream = await fetch(`${IDP_URL}/api/Account/profile`, {
32
- method: 'GET',
33
- headers: {
34
- 'Content-Type': 'application/json',
35
- 'Authorization': authHeader,
36
- },
37
- });
38
-
39
- const data = await upstream.json().catch(() => ({}));
40
- return NextResponse.json(data, { status: upstream.status });
41
- } catch (error) {
42
- return NextResponse.json(
43
- { success: false, error: { code: 'UPSTREAM_ERROR', message: 'Failed to fetch profile' } },
44
- { status: 500 }
45
- );
46
- }
47
- }
48
-
49
- export async function PUT(req: NextRequest) {
50
- const IDP_URL = getIdpUrl();
51
- const authHeader = req.headers.get('authorization');
52
-
53
- if (!authHeader) {
54
- return NextResponse.json(
55
- { success: false, error: { code: 'UNAUTHORIZED', message: 'Authentication required' } },
56
- { status: 401 }
57
- );
58
- }
59
-
60
- try {
61
- const body = await req.text();
62
-
63
- const upstream = await fetch(`${IDP_URL}/api/Account/profile`, {
64
- method: 'PUT',
65
- headers: {
66
- 'Content-Type': 'application/json',
67
- 'Authorization': authHeader,
68
- },
69
- body,
70
- });
71
-
72
- const data = await upstream.json().catch(() => ({}));
73
- return NextResponse.json(data, { status: upstream.status });
74
- } catch (error) {
75
- return NextResponse.json(
76
- { success: false, error: { code: 'UPSTREAM_ERROR', message: 'Failed to update profile' } },
77
- { status: 500 }
78
- );
79
- }
80
- }
1
+ import { NextRequest, NextResponse } from 'next/server';
2
+
3
+ /**
4
+ * Account Profile API Handler
5
+ * Simple proxy to IDP profile endpoint
6
+ *
7
+ * GET /api/account/profile - Get user profile
8
+ * PUT /api/account/profile - Update user profile
9
+ */
10
+
11
+ function getIdpUrl(): string {
12
+ const url = process.env.IDP_URL;
13
+ if (!url) {
14
+ throw new Error('[IDP_URL] FATAL: IDP_URL environment variable is REQUIRED.');
15
+ }
16
+ return url;
17
+ }
18
+
19
+ export async function GET(req: NextRequest) {
20
+ const IDP_URL = getIdpUrl();
21
+ const authHeader = req.headers.get('authorization');
22
+
23
+ if (!authHeader) {
24
+ return NextResponse.json(
25
+ { success: false, error: { code: 'UNAUTHORIZED', message: 'Authentication required' } },
26
+ { status: 401 }
27
+ );
28
+ }
29
+
30
+ try {
31
+ const upstream = await fetch(`${IDP_URL}/api/Account/profile`, {
32
+ method: 'GET',
33
+ headers: {
34
+ 'Content-Type': 'application/json',
35
+ 'Authorization': authHeader,
36
+ },
37
+ });
38
+
39
+ const data = await upstream.json().catch(() => ({}));
40
+ return NextResponse.json(data, { status: upstream.status });
41
+ } catch (error) {
42
+ return NextResponse.json(
43
+ { success: false, error: { code: 'UPSTREAM_ERROR', message: 'Failed to fetch profile' } },
44
+ { status: 500 }
45
+ );
46
+ }
47
+ }
48
+
49
+ export async function PUT(req: NextRequest) {
50
+ const IDP_URL = getIdpUrl();
51
+ const authHeader = req.headers.get('authorization');
52
+
53
+ if (!authHeader) {
54
+ return NextResponse.json(
55
+ { success: false, error: { code: 'UNAUTHORIZED', message: 'Authentication required' } },
56
+ { status: 401 }
57
+ );
58
+ }
59
+
60
+ try {
61
+ const body = await req.text();
62
+
63
+ const upstream = await fetch(`${IDP_URL}/api/Account/profile`, {
64
+ method: 'PUT',
65
+ headers: {
66
+ 'Content-Type': 'application/json',
67
+ 'Authorization': authHeader,
68
+ },
69
+ body,
70
+ });
71
+
72
+ const data = await upstream.json().catch(() => ({}));
73
+ return NextResponse.json(data, { status: upstream.status });
74
+ } catch (error) {
75
+ return NextResponse.json(
76
+ { success: false, error: { code: 'UPSTREAM_ERROR', message: 'Failed to update profile' } },
77
+ { status: 500 }
78
+ );
79
+ }
80
+ }
@@ -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 { email } = await req.json();
13
- const upstream = await fetch(`${IDP_URL}/api/Account/recovery/initiate`, {
14
- method: 'POST',
15
- headers: { 'Content-Type': 'application/json' },
16
- body: JSON.stringify({ email }),
17
- });
18
- const data = await upstream.json().catch(() => ({}));
19
- return NextResponse.json(data, { status: upstream.status });
20
- } catch (e) {
21
- return NextResponse.json({ success: false, error: 'initiate_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 { email } = await req.json();
13
+ const upstream = await fetch(`${IDP_URL}/api/Account/recovery/initiate`, {
14
+ method: 'POST',
15
+ headers: { 'Content-Type': 'application/json' },
16
+ body: JSON.stringify({ email }),
17
+ });
18
+ const data = await upstream.json().catch(() => ({}));
19
+ return NextResponse.json(data, { status: upstream.status });
20
+ } catch (e) {
21
+ return NextResponse.json({ success: false, error: 'initiate_failed' }, { status: 500 });
22
+ }
23
+ }
@@ -1,25 +1,25 @@
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 { method } = await req.json();
15
- const upstream = await fetch(`${IDP_URL}/api/Account/recovery/send-code`, {
16
- method: 'POST',
17
- headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${token}` },
18
- body: JSON.stringify({ 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: 'send_code_failed' }, { status: 500 });
24
- }
25
- }
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 { method } = await req.json();
15
+ const upstream = await fetch(`${IDP_URL}/api/Account/recovery/send-code`, {
16
+ method: 'POST',
17
+ headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${token}` },
18
+ body: JSON.stringify({ 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: 'send_code_failed' }, { status: 500 });
24
+ }
25
+ }
@@ -1,25 +1,25 @@
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
+
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,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
+ }