@payez/next-mvp 4.1.7 → 4.1.9
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/README.md +792 -792
- package/dist/auth/better-auth.js +15 -3
- 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/pages/login/page.js +28 -28
- package/dist/pages/settings/EnhancedSettingsPage.js +17 -17
- package/package.json +1 -1
- package/scripts/dev-broker.ps1 +35 -35
- package/scripts/dev-local.ps1 +45 -45
- package/src/api/auth-handler.ts +548 -548
- package/src/api-handlers/account/change-password.ts +142 -142
- package/src/api-handlers/account/update-phone.ts +79 -79
- package/src/api-handlers/account/verify-email.ts +125 -125
- package/src/api-handlers/account/verify-sms.ts +125 -125
- package/src/api-handlers/admin/analytics.ts +443 -443
- package/src/api-handlers/admin/audit.ts +223 -223
- package/src/api-handlers/admin/index.ts +63 -63
- package/src/api-handlers/admin/redis-sessions.ts +251 -251
- package/src/api-handlers/admin/sessions.ts +318 -318
- package/src/api-handlers/admin/site-logs.ts +365 -365
- package/src/api-handlers/admin/users.ts +242 -242
- package/src/api-handlers/admin/vibe-data.ts +324 -324
- package/src/api-handlers/auth/refresh.ts +682 -682
- package/src/api-handlers/auth/signout.ts +199 -199
- package/src/api-handlers/auth/status.ts +20 -20
- package/src/api-handlers/auth/update-session.ts +110 -110
- package/src/api-handlers/auth/verify-code.ts +112 -112
- package/src/api-handlers/session/refresh-viability.ts +36 -36
- package/src/api-handlers/test/force-expire.ts +60 -60
- package/src/auth/auth-decision.ts +229 -229
- package/src/auth/better-auth.ts +15 -3
- package/src/auth/route-config.ts +219 -219
- package/src/auth/types/auth-types.ts +555 -555
- package/src/auth/types/index.ts +7 -7
- package/src/auth/utils/idp-client.ts +445 -445
- package/src/auth/utils/index.ts +6 -6
- package/src/auth/utils/token-utils.ts +243 -243
- package/src/client/AuthContext.tsx +143 -143
- package/src/client/fetch-with-auth.ts +47 -47
- 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 +266 -266
- 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 +505 -505
- package/src/components/admin/LoggingSettingsTab.tsx +665 -665
- package/src/components/admin/SessionsTab.tsx +414 -414
- package/src/components/admin/StatsTab.tsx +379 -379
- package/src/components/admin/VibeAdminLayout.tsx +187 -187
- package/src/components/admin/index.ts +59 -59
- package/src/components/auth/FederatedAuthSection.tsx +95 -95
- package/src/components/auth/ModeAwareLoginPage.tsx +135 -135
- package/src/components/auth/ModeAwareSignupPage.tsx +267 -267
- package/src/components/auth/TraditionalAuthSection.tsx +99 -99
- package/src/components/recovery/CompleteStep.tsx +36 -36
- package/src/components/recovery/InitiateRecoveryStep.tsx +68 -68
- package/src/components/recovery/SelectMethodStep.tsx +73 -73
- package/src/components/recovery/SetPasswordStep.tsx +97 -97
- 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 +57 -57
- package/src/config/logger.ts +62 -62
- package/src/config/vibe-log-transport.ts +264 -264
- package/src/hooks/useAuth.ts +117 -117
- package/src/hooks/useAuthSettings.ts +97 -97
- package/src/hooks/useAvailableProviders.ts +120 -120
- package/src/hooks/usePasswordValidation.ts +127 -127
- package/src/hooks/usePublicAuthSettings.ts +149 -149
- package/src/hooks/useSessionExpiration.ts +101 -101
- package/src/hooks/useViabilitySession.ts +336 -336
- package/src/index.ts +60 -60
- package/src/lib/api-handler.ts +643 -643
- package/src/lib/app-slug.ts +178 -178
- package/src/lib/geolocation.ts +265 -265
- package/src/lib/idp-fetch.ts +110 -110
- package/src/lib/jwt-decode.ts +82 -82
- package/src/lib/roles.ts +177 -177
- package/src/lib/site-logger.ts +245 -245
- package/src/lib/standardized-client-api.ts +901 -901
- package/src/lib/test-aware-get-token.ts +41 -41
- 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 +663 -663
- package/src/middleware/rbac-check.ts +279 -279
- package/src/middleware/twofa-presets.ts +224 -224
- package/src/pages/admin-login/page.tsx +221 -221
- 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/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 +456 -456
- package/src/pages/profile/EnhancedProfilePage.tsx +485 -485
- package/src/pages/profile/index.ts +9 -9
- package/src/pages/profile/page.tsx +166 -166
- package/src/pages/recovery/page.tsx +234 -234
- 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 +565 -565
- package/src/pages/security/index.ts +9 -9
- package/src/pages/security/page.tsx +507 -507
- package/src/pages/settings/EnhancedSettingsPage.tsx +642 -642
- package/src/pages/settings/index.ts +9 -9
- package/src/pages/settings/page.tsx +47 -47
- package/src/pages/showcase/ShowcasePage.tsx +532 -532
- package/src/pages/showcase/index.ts +13 -13
- package/src/pages/test-env/EmergencyLogoutPage.tsx +180 -180
- package/src/pages/test-env/JwtInspectPage.tsx +456 -456
- package/src/pages/test-env/RefreshTokenPage.tsx +157 -157
- package/src/pages/test-env/TestEnvPage.tsx +118 -118
- package/src/pages/test-env/index.ts +25 -25
- package/src/pages/verify-code/page.tsx +652 -652
- package/src/routes/account/update-phone.ts +13 -13
- package/src/routes/auth/logout.ts +108 -108
- package/src/routes/auth/nextauth.ts +45 -45
- package/src/routes/auth/settings.ts +75 -75
- package/src/routes/auth/viability.ts +224 -224
- package/src/server/auth-guard.ts +170 -170
- package/src/server/slim-middleware.ts +117 -117
- package/src/server/with-auth.ts +93 -93
- package/src/stores/authStore.ts +1899 -1899
- package/src/templates/instrumentation.ts +41 -41
- package/src/utils/logout.ts +30 -30
- package/src/vibe/client.ts +590 -590
- package/src/vibe/enterprise-auth.ts +208 -208
- package/src/vibe/errors.ts +185 -185
- package/src/vibe/generic.ts +431 -431
- package/src/vibe/hooks/index.ts +367 -367
- package/src/vibe/index.ts +132 -132
- package/src/vibe/sessions.ts +551 -551
- package/src/vibe/types.ts +577 -577
|
@@ -1,125 +1,125 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Verify Email 2FA Code Handler
|
|
3
|
-
*
|
|
4
|
-
* Verifies the 2FA email verification code and completes the 2FA flow.
|
|
5
|
-
* Updates the session with new tokens upon successful verification.
|
|
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
|
-
import { getTokenTestAware } from '../../lib/test-aware-get-token';
|
|
14
|
-
import { transitionTo2FASession } from '../../lib/session-store';
|
|
15
|
-
import { jwtDecode } from '../../lib/jwt-decode';
|
|
16
|
-
|
|
17
|
-
export async function POST(req: NextRequest) {
|
|
18
|
-
try {
|
|
19
|
-
// Parse request body
|
|
20
|
-
const body = await req.json();
|
|
21
|
-
const verificationCode = body.verificationCode || body.verification_code;
|
|
22
|
-
|
|
23
|
-
if (!verificationCode) {
|
|
24
|
-
return NextResponse.json(
|
|
25
|
-
{
|
|
26
|
-
success: false,
|
|
27
|
-
error: 'Verification code is required',
|
|
28
|
-
code: 'INVALID_REQUEST',
|
|
29
|
-
},
|
|
30
|
-
{ status: 400 }
|
|
31
|
-
);
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
// Call IDP using idpFetchJSON which auto-injects Bearer token from Redis session
|
|
35
|
-
const result = await idpFetchJSON(req, `${ENV_CONFIG.IDP_URL}/api/ExternalAuth/twofa/email/verify`, {
|
|
36
|
-
method: 'POST',
|
|
37
|
-
headers: { 'Content-Type': 'application/json' },
|
|
38
|
-
body: JSON.stringify({ verification_code: verificationCode }),
|
|
39
|
-
});
|
|
40
|
-
|
|
41
|
-
if (!result.ok) {
|
|
42
|
-
return NextResponse.json(
|
|
43
|
-
{
|
|
44
|
-
success: false,
|
|
45
|
-
error: result.json?.message || 'Verification failed',
|
|
46
|
-
code: result.json?.code || 'IDP_ERROR',
|
|
47
|
-
meta: { attemptedRefresh: result.attemptedRefresh },
|
|
48
|
-
},
|
|
49
|
-
{ status: result.status }
|
|
50
|
-
);
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
// Unwrap IDP envelope
|
|
54
|
-
const unwrappedData = result.json?.data || result.json;
|
|
55
|
-
|
|
56
|
-
// If we have new tokens, update the session to complete 2FA
|
|
57
|
-
if (unwrappedData.access_token && unwrappedData.refresh_token) {
|
|
58
|
-
// Get session token from NextAuth
|
|
59
|
-
// Support both field names: sessionToken (auth.ts JWT) and redisSessionId (legacy)
|
|
60
|
-
const token = await getTokenTestAware(req);
|
|
61
|
-
const sessionToken = (token?.sessionToken || token?.redisSessionId) as string | undefined;
|
|
62
|
-
|
|
63
|
-
if (sessionToken) {
|
|
64
|
-
console.log('[VERIFY_EMAIL] Updating session with new tokens');
|
|
65
|
-
|
|
66
|
-
// Decode access token to get actual expiration
|
|
67
|
-
let accessTokenExpires = Date.now() + (15 * 60 * 1000); // Default: 15 minutes
|
|
68
|
-
try {
|
|
69
|
-
const decoded = jwtDecode<{ exp?: number }>(unwrappedData.access_token);
|
|
70
|
-
if (decoded?.exp) {
|
|
71
|
-
accessTokenExpires = decoded.exp * 1000; // Convert to milliseconds
|
|
72
|
-
}
|
|
73
|
-
} catch (err) {
|
|
74
|
-
console.warn('[VERIFY_EMAIL] Could not decode access token, using default expiration');
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
// Decode refresh token to get actual expiration (optional)
|
|
78
|
-
let refreshTokenExpires = Date.now() + (3 * 24 * 60 * 60 * 1000); // Default: 3 days
|
|
79
|
-
try {
|
|
80
|
-
const decoded = jwtDecode<{ exp?: number }>(unwrappedData.refresh_token);
|
|
81
|
-
if (decoded?.exp) {
|
|
82
|
-
refreshTokenExpires = decoded.exp * 1000;
|
|
83
|
-
}
|
|
84
|
-
} catch {
|
|
85
|
-
// Refresh token may not have exp claim, use default
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
// Update session with new tokens and mark 2FA complete
|
|
89
|
-
await transitionTo2FASession(
|
|
90
|
-
sessionToken,
|
|
91
|
-
{
|
|
92
|
-
accessToken: unwrappedData.access_token,
|
|
93
|
-
refreshToken: unwrappedData.refresh_token,
|
|
94
|
-
accessTokenExpires,
|
|
95
|
-
refreshTokenExpires
|
|
96
|
-
},
|
|
97
|
-
'email' // Store 2FA method for refresh token flow
|
|
98
|
-
);
|
|
99
|
-
|
|
100
|
-
console.log('[VERIFY_EMAIL] Session updated successfully', {
|
|
101
|
-
accessTokenExpires: new Date(accessTokenExpires).toISOString(),
|
|
102
|
-
refreshTokenExpires: new Date(refreshTokenExpires).toISOString()
|
|
103
|
-
});
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
// Return simplified success response (don't expose tokens to client)
|
|
108
|
-
return NextResponse.json({
|
|
109
|
-
success: true,
|
|
110
|
-
verificationSuccessful: true,
|
|
111
|
-
twoFactorSessionVerified: true,
|
|
112
|
-
message: unwrappedData.message || 'Email code verified successfully'
|
|
113
|
-
}, { status: 200 });
|
|
114
|
-
} catch (error) {
|
|
115
|
-
console.error('[VERIFY_EMAIL] Error:', error);
|
|
116
|
-
return NextResponse.json(
|
|
117
|
-
{
|
|
118
|
-
success: false,
|
|
119
|
-
error: 'Failed to verify code',
|
|
120
|
-
code: 'INTERNAL_ERROR',
|
|
121
|
-
},
|
|
122
|
-
{ status: 500 }
|
|
123
|
-
);
|
|
124
|
-
}
|
|
125
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* Verify Email 2FA Code Handler
|
|
3
|
+
*
|
|
4
|
+
* Verifies the 2FA email verification code and completes the 2FA flow.
|
|
5
|
+
* Updates the session with new tokens upon successful verification.
|
|
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
|
+
import { getTokenTestAware } from '../../lib/test-aware-get-token';
|
|
14
|
+
import { transitionTo2FASession } from '../../lib/session-store';
|
|
15
|
+
import { jwtDecode } from '../../lib/jwt-decode';
|
|
16
|
+
|
|
17
|
+
export async function POST(req: NextRequest) {
|
|
18
|
+
try {
|
|
19
|
+
// Parse request body
|
|
20
|
+
const body = await req.json();
|
|
21
|
+
const verificationCode = body.verificationCode || body.verification_code;
|
|
22
|
+
|
|
23
|
+
if (!verificationCode) {
|
|
24
|
+
return NextResponse.json(
|
|
25
|
+
{
|
|
26
|
+
success: false,
|
|
27
|
+
error: 'Verification code is required',
|
|
28
|
+
code: 'INVALID_REQUEST',
|
|
29
|
+
},
|
|
30
|
+
{ status: 400 }
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Call IDP using idpFetchJSON which auto-injects Bearer token from Redis session
|
|
35
|
+
const result = await idpFetchJSON(req, `${ENV_CONFIG.IDP_URL}/api/ExternalAuth/twofa/email/verify`, {
|
|
36
|
+
method: 'POST',
|
|
37
|
+
headers: { 'Content-Type': 'application/json' },
|
|
38
|
+
body: JSON.stringify({ verification_code: verificationCode }),
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
if (!result.ok) {
|
|
42
|
+
return NextResponse.json(
|
|
43
|
+
{
|
|
44
|
+
success: false,
|
|
45
|
+
error: result.json?.message || 'Verification failed',
|
|
46
|
+
code: result.json?.code || 'IDP_ERROR',
|
|
47
|
+
meta: { attemptedRefresh: result.attemptedRefresh },
|
|
48
|
+
},
|
|
49
|
+
{ status: result.status }
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Unwrap IDP envelope
|
|
54
|
+
const unwrappedData = result.json?.data || result.json;
|
|
55
|
+
|
|
56
|
+
// If we have new tokens, update the session to complete 2FA
|
|
57
|
+
if (unwrappedData.access_token && unwrappedData.refresh_token) {
|
|
58
|
+
// Get session token from NextAuth
|
|
59
|
+
// Support both field names: sessionToken (auth.ts JWT) and redisSessionId (legacy)
|
|
60
|
+
const token = await getTokenTestAware(req);
|
|
61
|
+
const sessionToken = (token?.sessionToken || token?.redisSessionId) as string | undefined;
|
|
62
|
+
|
|
63
|
+
if (sessionToken) {
|
|
64
|
+
console.log('[VERIFY_EMAIL] Updating session with new tokens');
|
|
65
|
+
|
|
66
|
+
// Decode access token to get actual expiration
|
|
67
|
+
let accessTokenExpires = Date.now() + (15 * 60 * 1000); // Default: 15 minutes
|
|
68
|
+
try {
|
|
69
|
+
const decoded = jwtDecode<{ exp?: number }>(unwrappedData.access_token);
|
|
70
|
+
if (decoded?.exp) {
|
|
71
|
+
accessTokenExpires = decoded.exp * 1000; // Convert to milliseconds
|
|
72
|
+
}
|
|
73
|
+
} catch (err) {
|
|
74
|
+
console.warn('[VERIFY_EMAIL] Could not decode access token, using default expiration');
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Decode refresh token to get actual expiration (optional)
|
|
78
|
+
let refreshTokenExpires = Date.now() + (3 * 24 * 60 * 60 * 1000); // Default: 3 days
|
|
79
|
+
try {
|
|
80
|
+
const decoded = jwtDecode<{ exp?: number }>(unwrappedData.refresh_token);
|
|
81
|
+
if (decoded?.exp) {
|
|
82
|
+
refreshTokenExpires = decoded.exp * 1000;
|
|
83
|
+
}
|
|
84
|
+
} catch {
|
|
85
|
+
// Refresh token may not have exp claim, use default
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Update session with new tokens and mark 2FA complete
|
|
89
|
+
await transitionTo2FASession(
|
|
90
|
+
sessionToken,
|
|
91
|
+
{
|
|
92
|
+
accessToken: unwrappedData.access_token,
|
|
93
|
+
refreshToken: unwrappedData.refresh_token,
|
|
94
|
+
accessTokenExpires,
|
|
95
|
+
refreshTokenExpires
|
|
96
|
+
},
|
|
97
|
+
'email' // Store 2FA method for refresh token flow
|
|
98
|
+
);
|
|
99
|
+
|
|
100
|
+
console.log('[VERIFY_EMAIL] Session updated successfully', {
|
|
101
|
+
accessTokenExpires: new Date(accessTokenExpires).toISOString(),
|
|
102
|
+
refreshTokenExpires: new Date(refreshTokenExpires).toISOString()
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Return simplified success response (don't expose tokens to client)
|
|
108
|
+
return NextResponse.json({
|
|
109
|
+
success: true,
|
|
110
|
+
verificationSuccessful: true,
|
|
111
|
+
twoFactorSessionVerified: true,
|
|
112
|
+
message: unwrappedData.message || 'Email code verified successfully'
|
|
113
|
+
}, { status: 200 });
|
|
114
|
+
} catch (error) {
|
|
115
|
+
console.error('[VERIFY_EMAIL] Error:', error);
|
|
116
|
+
return NextResponse.json(
|
|
117
|
+
{
|
|
118
|
+
success: false,
|
|
119
|
+
error: 'Failed to verify code',
|
|
120
|
+
code: 'INTERNAL_ERROR',
|
|
121
|
+
},
|
|
122
|
+
{ status: 500 }
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
@@ -1,125 +1,125 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Verify SMS 2FA Code Handler
|
|
3
|
-
*
|
|
4
|
-
* Verifies the 2FA SMS verification code and completes the 2FA flow.
|
|
5
|
-
* Updates the session with new tokens upon successful verification.
|
|
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
|
-
import { getTokenTestAware } from '../../lib/test-aware-get-token';
|
|
14
|
-
import { transitionTo2FASession } from '../../lib/session-store';
|
|
15
|
-
import { jwtDecode } from '../../lib/jwt-decode';
|
|
16
|
-
|
|
17
|
-
export async function POST(req: NextRequest) {
|
|
18
|
-
try {
|
|
19
|
-
// Parse request body
|
|
20
|
-
const body = await req.json();
|
|
21
|
-
const verificationCode = body.verificationCode || body.verification_code;
|
|
22
|
-
|
|
23
|
-
if (!verificationCode) {
|
|
24
|
-
return NextResponse.json(
|
|
25
|
-
{
|
|
26
|
-
success: false,
|
|
27
|
-
error: 'Verification code is required',
|
|
28
|
-
code: 'INVALID_REQUEST',
|
|
29
|
-
},
|
|
30
|
-
{ status: 400 }
|
|
31
|
-
);
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
// Call IDP using idpFetchJSON which auto-injects Bearer token from Redis session
|
|
35
|
-
const result = await idpFetchJSON(req, `${ENV_CONFIG.IDP_URL}/api/ExternalAuth/twofa/sms/verify`, {
|
|
36
|
-
method: 'POST',
|
|
37
|
-
headers: { 'Content-Type': 'application/json' },
|
|
38
|
-
body: JSON.stringify({ verification_code: verificationCode }),
|
|
39
|
-
});
|
|
40
|
-
|
|
41
|
-
if (!result.ok) {
|
|
42
|
-
return NextResponse.json(
|
|
43
|
-
{
|
|
44
|
-
success: false,
|
|
45
|
-
error: result.json?.message || 'Verification failed',
|
|
46
|
-
code: result.json?.code || 'IDP_ERROR',
|
|
47
|
-
meta: { attemptedRefresh: result.attemptedRefresh },
|
|
48
|
-
},
|
|
49
|
-
{ status: result.status }
|
|
50
|
-
);
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
// Unwrap IDP envelope
|
|
54
|
-
const unwrappedData = result.json?.data || result.json;
|
|
55
|
-
|
|
56
|
-
// If we have new tokens, update the session to complete 2FA
|
|
57
|
-
if (unwrappedData.access_token && unwrappedData.refresh_token) {
|
|
58
|
-
// Get session token from NextAuth
|
|
59
|
-
// Support both field names: sessionToken (auth.ts JWT) and redisSessionId (legacy)
|
|
60
|
-
const token = await getTokenTestAware(req);
|
|
61
|
-
const sessionToken = (token?.sessionToken || token?.redisSessionId) as string | undefined;
|
|
62
|
-
|
|
63
|
-
if (sessionToken) {
|
|
64
|
-
console.log('[VERIFY_SMS] Updating session with new tokens');
|
|
65
|
-
|
|
66
|
-
// Decode access token to get actual expiration
|
|
67
|
-
let accessTokenExpires = Date.now() + (15 * 60 * 1000); // Default: 15 minutes
|
|
68
|
-
try {
|
|
69
|
-
const decoded = jwtDecode<{ exp?: number }>(unwrappedData.access_token);
|
|
70
|
-
if (decoded?.exp) {
|
|
71
|
-
accessTokenExpires = decoded.exp * 1000; // Convert to milliseconds
|
|
72
|
-
}
|
|
73
|
-
} catch (err) {
|
|
74
|
-
console.warn('[VERIFY_SMS] Could not decode access token, using default expiration');
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
// Decode refresh token to get actual expiration (optional)
|
|
78
|
-
let refreshTokenExpires = Date.now() + (3 * 24 * 60 * 60 * 1000); // Default: 3 days
|
|
79
|
-
try {
|
|
80
|
-
const decoded = jwtDecode<{ exp?: number }>(unwrappedData.refresh_token);
|
|
81
|
-
if (decoded?.exp) {
|
|
82
|
-
refreshTokenExpires = decoded.exp * 1000;
|
|
83
|
-
}
|
|
84
|
-
} catch {
|
|
85
|
-
// Refresh token may not have exp claim, use default
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
// Update session with new tokens and mark 2FA complete
|
|
89
|
-
await transitionTo2FASession(
|
|
90
|
-
sessionToken,
|
|
91
|
-
{
|
|
92
|
-
accessToken: unwrappedData.access_token,
|
|
93
|
-
refreshToken: unwrappedData.refresh_token,
|
|
94
|
-
accessTokenExpires,
|
|
95
|
-
refreshTokenExpires
|
|
96
|
-
},
|
|
97
|
-
'sms' // Store 2FA method for refresh token flow
|
|
98
|
-
);
|
|
99
|
-
|
|
100
|
-
console.log('[VERIFY_SMS] Session updated successfully', {
|
|
101
|
-
accessTokenExpires: new Date(accessTokenExpires).toISOString(),
|
|
102
|
-
refreshTokenExpires: new Date(refreshTokenExpires).toISOString()
|
|
103
|
-
});
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
// Return simplified success response (don't expose tokens to client)
|
|
108
|
-
return NextResponse.json({
|
|
109
|
-
success: true,
|
|
110
|
-
verificationSuccessful: true,
|
|
111
|
-
twoFactorSessionVerified: true,
|
|
112
|
-
message: unwrappedData.message || 'SMS code verified successfully'
|
|
113
|
-
}, { status: 200 });
|
|
114
|
-
} catch (error) {
|
|
115
|
-
console.error('[VERIFY_SMS] Error:', error);
|
|
116
|
-
return NextResponse.json(
|
|
117
|
-
{
|
|
118
|
-
success: false,
|
|
119
|
-
error: 'Failed to verify code',
|
|
120
|
-
code: 'INTERNAL_ERROR',
|
|
121
|
-
},
|
|
122
|
-
{ status: 500 }
|
|
123
|
-
);
|
|
124
|
-
}
|
|
125
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* Verify SMS 2FA Code Handler
|
|
3
|
+
*
|
|
4
|
+
* Verifies the 2FA SMS verification code and completes the 2FA flow.
|
|
5
|
+
* Updates the session with new tokens upon successful verification.
|
|
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
|
+
import { getTokenTestAware } from '../../lib/test-aware-get-token';
|
|
14
|
+
import { transitionTo2FASession } from '../../lib/session-store';
|
|
15
|
+
import { jwtDecode } from '../../lib/jwt-decode';
|
|
16
|
+
|
|
17
|
+
export async function POST(req: NextRequest) {
|
|
18
|
+
try {
|
|
19
|
+
// Parse request body
|
|
20
|
+
const body = await req.json();
|
|
21
|
+
const verificationCode = body.verificationCode || body.verification_code;
|
|
22
|
+
|
|
23
|
+
if (!verificationCode) {
|
|
24
|
+
return NextResponse.json(
|
|
25
|
+
{
|
|
26
|
+
success: false,
|
|
27
|
+
error: 'Verification code is required',
|
|
28
|
+
code: 'INVALID_REQUEST',
|
|
29
|
+
},
|
|
30
|
+
{ status: 400 }
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Call IDP using idpFetchJSON which auto-injects Bearer token from Redis session
|
|
35
|
+
const result = await idpFetchJSON(req, `${ENV_CONFIG.IDP_URL}/api/ExternalAuth/twofa/sms/verify`, {
|
|
36
|
+
method: 'POST',
|
|
37
|
+
headers: { 'Content-Type': 'application/json' },
|
|
38
|
+
body: JSON.stringify({ verification_code: verificationCode }),
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
if (!result.ok) {
|
|
42
|
+
return NextResponse.json(
|
|
43
|
+
{
|
|
44
|
+
success: false,
|
|
45
|
+
error: result.json?.message || 'Verification failed',
|
|
46
|
+
code: result.json?.code || 'IDP_ERROR',
|
|
47
|
+
meta: { attemptedRefresh: result.attemptedRefresh },
|
|
48
|
+
},
|
|
49
|
+
{ status: result.status }
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Unwrap IDP envelope
|
|
54
|
+
const unwrappedData = result.json?.data || result.json;
|
|
55
|
+
|
|
56
|
+
// If we have new tokens, update the session to complete 2FA
|
|
57
|
+
if (unwrappedData.access_token && unwrappedData.refresh_token) {
|
|
58
|
+
// Get session token from NextAuth
|
|
59
|
+
// Support both field names: sessionToken (auth.ts JWT) and redisSessionId (legacy)
|
|
60
|
+
const token = await getTokenTestAware(req);
|
|
61
|
+
const sessionToken = (token?.sessionToken || token?.redisSessionId) as string | undefined;
|
|
62
|
+
|
|
63
|
+
if (sessionToken) {
|
|
64
|
+
console.log('[VERIFY_SMS] Updating session with new tokens');
|
|
65
|
+
|
|
66
|
+
// Decode access token to get actual expiration
|
|
67
|
+
let accessTokenExpires = Date.now() + (15 * 60 * 1000); // Default: 15 minutes
|
|
68
|
+
try {
|
|
69
|
+
const decoded = jwtDecode<{ exp?: number }>(unwrappedData.access_token);
|
|
70
|
+
if (decoded?.exp) {
|
|
71
|
+
accessTokenExpires = decoded.exp * 1000; // Convert to milliseconds
|
|
72
|
+
}
|
|
73
|
+
} catch (err) {
|
|
74
|
+
console.warn('[VERIFY_SMS] Could not decode access token, using default expiration');
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Decode refresh token to get actual expiration (optional)
|
|
78
|
+
let refreshTokenExpires = Date.now() + (3 * 24 * 60 * 60 * 1000); // Default: 3 days
|
|
79
|
+
try {
|
|
80
|
+
const decoded = jwtDecode<{ exp?: number }>(unwrappedData.refresh_token);
|
|
81
|
+
if (decoded?.exp) {
|
|
82
|
+
refreshTokenExpires = decoded.exp * 1000;
|
|
83
|
+
}
|
|
84
|
+
} catch {
|
|
85
|
+
// Refresh token may not have exp claim, use default
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Update session with new tokens and mark 2FA complete
|
|
89
|
+
await transitionTo2FASession(
|
|
90
|
+
sessionToken,
|
|
91
|
+
{
|
|
92
|
+
accessToken: unwrappedData.access_token,
|
|
93
|
+
refreshToken: unwrappedData.refresh_token,
|
|
94
|
+
accessTokenExpires,
|
|
95
|
+
refreshTokenExpires
|
|
96
|
+
},
|
|
97
|
+
'sms' // Store 2FA method for refresh token flow
|
|
98
|
+
);
|
|
99
|
+
|
|
100
|
+
console.log('[VERIFY_SMS] Session updated successfully', {
|
|
101
|
+
accessTokenExpires: new Date(accessTokenExpires).toISOString(),
|
|
102
|
+
refreshTokenExpires: new Date(refreshTokenExpires).toISOString()
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Return simplified success response (don't expose tokens to client)
|
|
108
|
+
return NextResponse.json({
|
|
109
|
+
success: true,
|
|
110
|
+
verificationSuccessful: true,
|
|
111
|
+
twoFactorSessionVerified: true,
|
|
112
|
+
message: unwrappedData.message || 'SMS code verified successfully'
|
|
113
|
+
}, { status: 200 });
|
|
114
|
+
} catch (error) {
|
|
115
|
+
console.error('[VERIFY_SMS] Error:', error);
|
|
116
|
+
return NextResponse.json(
|
|
117
|
+
{
|
|
118
|
+
success: false,
|
|
119
|
+
error: 'Failed to verify code',
|
|
120
|
+
code: 'INTERNAL_ERROR',
|
|
121
|
+
},
|
|
122
|
+
{ status: 500 }
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
}
|