@payez/next-mvp 4.1.11 → 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 (178) 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 +256 -42
  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/session-store.js +28 -21
  38. package/dist/lib/startup-init.js +19 -19
  39. package/dist/lib/token-lifecycle.js +40 -4
  40. package/dist/models/SessionModel.d.ts +17 -0
  41. package/dist/pages/login/page.d.ts +10 -1
  42. package/dist/pages/login/page.js +69 -80
  43. package/dist/pages/verify-code/page.d.ts +10 -1
  44. package/dist/pages/verify-code/page.js +72 -4
  45. package/dist/routes/auth/passkey-signin.d.ts +13 -0
  46. package/dist/routes/auth/passkey-signin.js +18 -0
  47. package/dist/routes/auth/viability.d.ts +2 -0
  48. package/dist/routes/auth/viability.js +88 -0
  49. package/dist/routes/mfa/backup-code-verify.d.ts +1 -0
  50. package/dist/routes/mfa/backup-code-verify.js +5 -0
  51. package/dist/routes/mfa/backup-codes.d.ts +10 -0
  52. package/dist/routes/mfa/backup-codes.js +14 -0
  53. package/dist/routes/mfa/methods.d.ts +21 -0
  54. package/dist/routes/mfa/methods.js +10 -0
  55. package/dist/routes/mfa/totp-confirm.d.ts +10 -0
  56. package/dist/routes/mfa/totp-confirm.js +14 -0
  57. package/dist/routes/mfa/totp-enroll.d.ts +10 -0
  58. package/dist/routes/mfa/totp-enroll.js +14 -0
  59. package/dist/routes/mfa/totp-verify.d.ts +14 -0
  60. package/dist/routes/mfa/totp-verify.js +18 -0
  61. package/dist/routes/passkeys/list.d.ts +10 -0
  62. package/dist/routes/passkeys/list.js +14 -0
  63. package/dist/routes/passkeys/manage.d.ts +18 -0
  64. package/dist/routes/passkeys/manage.js +13 -0
  65. package/dist/routes/passkeys/register-options.d.ts +10 -0
  66. package/dist/routes/passkeys/register-options.js +14 -0
  67. package/dist/routes/passkeys/register-verify.d.ts +10 -0
  68. package/dist/routes/passkeys/register-verify.js +14 -0
  69. package/dist/server/auth.d.ts +2 -2
  70. package/dist/server/decode-session.js +37 -3
  71. package/package.json +958 -893
  72. package/scripts/check-internal-url-usage.sh +73 -73
  73. package/src/api/index.ts +17 -17
  74. package/src/api-handlers/account/masked-info.ts +45 -45
  75. package/src/api-handlers/account/profile.ts +99 -80
  76. package/src/api-handlers/account/recovery/initiate.ts +23 -23
  77. package/src/api-handlers/account/recovery/send-code.ts +73 -25
  78. package/src/api-handlers/account/recovery/verify-code.ts +73 -25
  79. package/src/api-handlers/account/reset-password.ts +23 -23
  80. package/src/api-handlers/account/send-code.ts +76 -76
  81. package/src/api-handlers/account/validate-password.ts +118 -118
  82. package/src/api-handlers/account/verify-email.ts +15 -1
  83. package/src/api-handlers/account/verify-sms.ts +15 -1
  84. package/src/api-handlers/admin/stats.ts +249 -249
  85. package/src/api-handlers/anon/preferences.ts +123 -123
  86. package/src/api-handlers/auth/jwks.ts +20 -20
  87. package/src/api-handlers/auth/login.ts +240 -240
  88. package/src/api-handlers/auth/passkey-signin.ts +98 -0
  89. package/src/api-handlers/auth/validate.ts +43 -43
  90. package/src/api-handlers/mfa/backup-code-verify.ts +175 -0
  91. package/src/api-handlers/mfa/backup-codes.ts +41 -0
  92. package/src/api-handlers/mfa/methods.ts +46 -0
  93. package/src/api-handlers/mfa/totp-confirm.ts +42 -0
  94. package/src/api-handlers/mfa/totp-enroll.ts +42 -0
  95. package/src/api-handlers/mfa/totp-verify.ts +156 -0
  96. package/src/api-handlers/passkeys/list.ts +31 -0
  97. package/src/api-handlers/passkeys/manage.ts +54 -0
  98. package/src/api-handlers/passkeys/register-options.ts +42 -0
  99. package/src/api-handlers/passkeys/register-verify.ts +41 -0
  100. package/src/api-handlers/session/viability.ts +161 -142
  101. package/src/auth/better-auth.ts +247 -6
  102. package/src/auth/unauthenticated-routes.ts +2 -2
  103. package/src/auth/utils/idp-client.ts +13 -95
  104. package/src/client/better-auth-client.ts +70 -70
  105. package/src/client/fetchWithSession.ts +21 -21
  106. package/src/client/index.ts +13 -13
  107. package/src/client/useAnonSession.ts +131 -131
  108. package/src/components/admin/VibeAdminContext.tsx +87 -87
  109. package/src/config/logging-config.ts +82 -82
  110. package/src/config/unauthenticated-routes.ts +19 -19
  111. package/src/edge/internal-api-url.ts +65 -65
  112. package/src/edge/middleware.ts +42 -42
  113. package/src/hooks/useProfile.ts +75 -75
  114. package/src/lib/anon-session.ts +213 -213
  115. package/src/lib/auth-secret.ts +137 -137
  116. package/src/lib/demo-mode.ts +17 -17
  117. package/src/lib/idp-client-config.ts +559 -559
  118. package/src/lib/idp-fetch.ts +126 -126
  119. package/src/lib/internal-api.ts +171 -171
  120. package/src/lib/jwt-decode-client.ts +45 -45
  121. package/src/lib/rate-limit-service.ts +9 -9
  122. package/src/lib/redis.ts +26 -26
  123. package/src/lib/refresh-token-validator.ts +64 -64
  124. package/src/lib/secret-validation.ts +8 -8
  125. package/src/lib/session-store.ts +702 -695
  126. package/src/lib/session.ts +34 -34
  127. package/src/lib/startup-init.ts +251 -251
  128. package/src/lib/token-expiry.ts +40 -40
  129. package/src/lib/token-lifecycle.ts +533 -497
  130. package/src/lib/types/api-responses.ts +336 -336
  131. package/src/middleware/auth-decision.ts +43 -43
  132. package/src/models/DecodedAccessToken.ts +16 -16
  133. package/src/models/SessionModel.ts +282 -264
  134. package/src/pages/admin-roles/index.ts +9 -9
  135. package/src/pages/login/page.tsx +123 -228
  136. package/src/pages/security/EnhancedSecurityPage.tsx +9 -9
  137. package/src/pages/verify-code/page.tsx +71 -5
  138. package/src/routes/account/index.ts +31 -31
  139. package/src/routes/account/masked-info.ts +36 -36
  140. package/src/routes/account/send-code.ts +39 -39
  141. package/src/routes/account/verify-email.ts +40 -40
  142. package/src/routes/account/verify-sms.ts +40 -40
  143. package/src/routes/auth/index.ts +22 -22
  144. package/src/routes/auth/passkey-signin.ts +13 -0
  145. package/src/routes/auth/refresh.ts +48 -48
  146. package/src/routes/auth/session.ts +166 -166
  147. package/src/routes/auth/viability.ts +89 -0
  148. package/src/routes/index.ts +17 -17
  149. package/src/routes/mfa/backup-code-verify.ts +1 -0
  150. package/src/routes/mfa/backup-codes.ts +10 -0
  151. package/src/routes/mfa/methods.ts +22 -0
  152. package/src/routes/mfa/totp-confirm.ts +10 -0
  153. package/src/routes/mfa/totp-enroll.ts +10 -0
  154. package/src/routes/mfa/totp-verify.ts +14 -0
  155. package/src/routes/passkeys/list.ts +10 -0
  156. package/src/routes/passkeys/manage.ts +23 -0
  157. package/src/routes/passkeys/register-options.ts +10 -0
  158. package/src/routes/passkeys/register-verify.ts +10 -0
  159. package/src/routes/session/index.ts +7 -7
  160. package/src/routes/session/refresh-viability.ts +17 -17
  161. package/src/server/auth.ts +373 -373
  162. package/src/server/decode-session.ts +235 -202
  163. package/src/services/signalrActivityService.ts +258 -258
  164. package/src/theme/ThemeProvider.tsx +39 -39
  165. package/src/theme/default.ts +33 -33
  166. package/src/theme/index.ts +31 -31
  167. package/src/theme/types.ts +69 -69
  168. package/src/theme/useTheme.ts +57 -57
  169. package/src/theme/utils.ts +40 -40
  170. package/src/types/api.ts +12 -12
  171. package/src/types/auth.d.ts +23 -23
  172. package/src/types/auth.ts +22 -22
  173. package/src/types/logging.ts +11 -11
  174. package/src/types/recovery.ts +54 -54
  175. package/src/utils/api.ts +353 -353
  176. package/src/utils/circuitBreaker.ts +40 -40
  177. package/src/utils/error-message.ts +108 -108
  178. package/src/utils/layout/reservedSpace.ts +124 -124
@@ -1,240 +1,240 @@
1
- /**
2
- * Authentication Login API Handler
3
- *
4
- * Handles user authentication against the external IDP service.
5
- * This handler is designed to be imported and used by Next.js applications
6
- * using the @payez/next-mvp package.
7
- *
8
- * @version 2.0
9
- * @requires No authentication (public endpoint)
10
- */
11
-
12
- import { NextRequest, NextResponse } from 'next/server';
13
-
14
- // Add protection headers to all responses
15
- function addSecurityHeaders(response: NextResponse) {
16
- // Content Security Policy - restrictive for auth endpoints
17
- response.headers.set('Content-Security-Policy', "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; connect-src 'self'; frame-ancestors 'none'; base-uri 'self'; form-action 'self';");
18
-
19
- // Prevent clickjacking
20
- response.headers.set('X-Frame-Options', 'DENY');
21
-
22
- // Prevent MIME type sniffing
23
- response.headers.set('X-Content-Type-Options', 'nosniff');
24
-
25
- // Enable XSS protection
26
- response.headers.set('X-XSS-Protection', '1; mode=block');
27
-
28
- // Strict transport security for HTTPS
29
- response.headers.set('Strict-Transport-Security', 'max-age=31536000; includeSubDomains; preload');
30
-
31
- // Referrer policy
32
- response.headers.set('Referrer-Policy', 'strict-origin-when-cross-origin');
33
-
34
- // Permissions policy
35
- response.headers.set('Permissions-Policy', 'geolocation=(), microphone=(), camera=(), payment=(), usb=(), magnetometer=(), gyroscope=(), accelerometer=()');
36
-
37
- // Cache control for sensitive auth endpoints
38
- response.headers.set('Cache-Control', 'no-store, no-cache, must-revalidate, proxy-revalidate');
39
- response.headers.set('Pragma', 'no-cache');
40
- response.headers.set('Expires', '0');
41
-
42
- return response;
43
- }
44
-
45
- interface LoginRequest {
46
- username_or_email: string;
47
- password: string;
48
- client_id?: string;
49
- client_type?: string;
50
- two_factor_code?: string;
51
- two_factor_recovery_code?: string;
52
- }
53
-
54
- interface LoginConfig {
55
- idpBaseUrl: string;
56
- clientId: string;
57
- loginEndpoint?: string;
58
- clientType?: string; // "partner" or "idp", defaults to "partner" for CryptAply
59
- }
60
-
61
- /**
62
- * Creates a login handler for Next.js API routes
63
- *
64
- * @param config Configuration for IDP connection
65
- * @returns Next.js POST handler function
66
- *
67
- * @example
68
- * ```typescript
69
- * // In your app's /app/api/auth/login/route.ts
70
- * import { createLoginHandler } from '@payez/next-mvp/api-handlers/auth/login';
71
- *
72
- * export const POST = createLoginHandler({
73
- * idpBaseUrl: process.env.IDP_URL!,
74
- * clientId: process.env.CLIENT_ID!,
75
- * loginEndpoint: '/api/ExternalAuth/login'
76
- * });
77
- * ```
78
- */
79
- export function createLoginHandler(config: LoginConfig) {
80
- const { idpBaseUrl, clientId, loginEndpoint = '/api/ExternalAuth/login', clientType = 'partner' } = config;
81
-
82
- return async function POST(req: NextRequest) {
83
- let body: LoginRequest;
84
-
85
- try {
86
- body = await req.json();
87
- } catch (parseError) {
88
- return NextResponse.json(
89
- { error: 'Invalid JSON format' },
90
- { status: 400 }
91
- );
92
- }
93
-
94
- const { username_or_email, password, client_type, two_factor_code, two_factor_recovery_code } = body;
95
-
96
- // Validate required fields
97
- if (!username_or_email || typeof username_or_email !== 'string' || username_or_email.trim() === '') {
98
- return NextResponse.json(
99
- { error: 'username_or_email is required' },
100
- { status: 400 }
101
- );
102
- }
103
-
104
- if (!password || typeof password !== 'string' || password.trim() === '') {
105
- return NextResponse.json(
106
- { error: 'Password is required' },
107
- { status: 400 }
108
- );
109
- }
110
-
111
- const idpUrl = `${idpBaseUrl}${loginEndpoint}`;
112
- const payload: any = {
113
- username_or_email,
114
- password,
115
- client_id: clientId,
116
- client_type: client_type || clientType // Use provided client_type or config default
117
- };
118
-
119
- // Add optional 2FA fields if provided
120
- if (two_factor_code) {
121
- payload.two_factor_code = two_factor_code;
122
- }
123
- if (two_factor_recovery_code) {
124
- payload.two_factor_recovery_code = two_factor_recovery_code;
125
- }
126
-
127
- // Extract client headers for forwarding to IDP
128
- const clientHeaders: Record<string, string> = {};
129
-
130
- // Forward client IP if available
131
- const clientIp = req.headers.get('x-forwarded-for') || req.headers.get('x-real-ip');
132
- if (clientIp) {
133
- clientHeaders['X-Forwarded-For'] = clientIp.split(',')[0].trim();
134
- }
135
-
136
- // Forward user agent if available
137
- const userAgent = req.headers.get('user-agent');
138
- if (userAgent) {
139
- clientHeaders['User-Agent'] = userAgent;
140
- }
141
-
142
- const headers = {
143
- 'Content-Type': 'application/json',
144
- 'Accept': 'application/json',
145
- 'Cookie': 'X-Device-Id=payez-PayEz-6c8201c35c1b44c9b2e823b36b95a718',
146
- ...clientHeaders
147
- };
148
-
149
- try {
150
- const backendResponse = await fetch(idpUrl, {
151
- method: 'POST',
152
- headers,
153
- body: JSON.stringify(payload),
154
- });
155
-
156
- const rawText = await backendResponse.text();
157
- let data: any;
158
-
159
- try {
160
- data = JSON.parse(rawText);
161
- } catch (jsonError) {
162
- console.error('[LOGIN] IDP returned non-JSON response', {
163
- response: rawText,
164
- status: backendResponse.status
165
- });
166
- return NextResponse.json(
167
- { error: 'Invalid response from authentication service' },
168
- { status: 502 }
169
- );
170
- }
171
-
172
- if (!backendResponse.ok) {
173
- // Handle rate limiting specifically
174
- if (backendResponse.status === 429) {
175
- const retryAfter = backendResponse.headers.get('retry-after');
176
- const retryAfterSeconds = retryAfter ? parseInt(retryAfter) : 60;
177
-
178
- return addSecurityHeaders(NextResponse.json({
179
- success: false,
180
- error: 'RATE_LIMIT_EXCEEDED',
181
- message: 'Too many login attempts. Please try again later.',
182
- retryAfter: retryAfterSeconds
183
- }, { status: 429 }));
184
- }
185
-
186
- // Pass through error from IDP
187
- console.error('[LOGIN] IDP service error', {
188
- status: backendResponse.status,
189
- error: data
190
- });
191
- return addSecurityHeaders(NextResponse.json(data, { status: backendResponse.status }));
192
- }
193
-
194
- if (!data.success) {
195
- // IDP returned a canonical error envelope with HTTP 200
196
- console.error('[LOGIN] IDP authentication failure', { error: data });
197
- return addSecurityHeaders(NextResponse.json(data, { status: 200 }));
198
- }
199
-
200
- // Normalize response shape for NextAuth authorize()
201
- const normalized = {
202
- success: data?.success === true,
203
- result: (data?.data && data?.data?.result) ? data.data.result : (data?.result ?? data),
204
- // Preserve original payload for debugging/compatibility
205
- original: data
206
- };
207
-
208
- return addSecurityHeaders(NextResponse.json(normalized as any));
209
-
210
- } catch (error) {
211
- // Handle network and other errors
212
- console.error('[LOGIN] FETCH FAILED:', {
213
- error: error instanceof Error ? error.message : String(error),
214
- idpUrl
215
- });
216
-
217
- if (error instanceof Error && error.message.includes('fetch')) {
218
- return NextResponse.json(
219
- { error: 'Unable to connect to authentication service' },
220
- { status: 503 }
221
- );
222
- }
223
-
224
- return NextResponse.json(
225
- { error: 'An unexpected error occurred during login' },
226
- { status: 500 }
227
- );
228
- }
229
- };
230
- }
231
-
232
- /**
233
- * Default export for backward compatibility
234
- * Requires environment variables: IDP_URL, CLIENT_ID
235
- */
236
- export const POST = createLoginHandler({
237
- idpBaseUrl: process.env.IDP_URL!,
238
- clientId: process.env.CLIENT_ID || 'payez_default_client',
239
- loginEndpoint: '/api/ExternalAuth/login'
240
- });
1
+ /**
2
+ * Authentication Login API Handler
3
+ *
4
+ * Handles user authentication against the external IDP service.
5
+ * This handler is designed to be imported and used by Next.js applications
6
+ * using the @payez/next-mvp package.
7
+ *
8
+ * @version 2.0
9
+ * @requires No authentication (public endpoint)
10
+ */
11
+
12
+ import { NextRequest, NextResponse } from 'next/server';
13
+
14
+ // Add protection headers to all responses
15
+ function addSecurityHeaders(response: NextResponse) {
16
+ // Content Security Policy - restrictive for auth endpoints
17
+ response.headers.set('Content-Security-Policy', "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; connect-src 'self'; frame-ancestors 'none'; base-uri 'self'; form-action 'self';");
18
+
19
+ // Prevent clickjacking
20
+ response.headers.set('X-Frame-Options', 'DENY');
21
+
22
+ // Prevent MIME type sniffing
23
+ response.headers.set('X-Content-Type-Options', 'nosniff');
24
+
25
+ // Enable XSS protection
26
+ response.headers.set('X-XSS-Protection', '1; mode=block');
27
+
28
+ // Strict transport security for HTTPS
29
+ response.headers.set('Strict-Transport-Security', 'max-age=31536000; includeSubDomains; preload');
30
+
31
+ // Referrer policy
32
+ response.headers.set('Referrer-Policy', 'strict-origin-when-cross-origin');
33
+
34
+ // Permissions policy
35
+ response.headers.set('Permissions-Policy', 'geolocation=(), microphone=(), camera=(), payment=(), usb=(), magnetometer=(), gyroscope=(), accelerometer=()');
36
+
37
+ // Cache control for sensitive auth endpoints
38
+ response.headers.set('Cache-Control', 'no-store, no-cache, must-revalidate, proxy-revalidate');
39
+ response.headers.set('Pragma', 'no-cache');
40
+ response.headers.set('Expires', '0');
41
+
42
+ return response;
43
+ }
44
+
45
+ interface LoginRequest {
46
+ username_or_email: string;
47
+ password: string;
48
+ client_id?: string;
49
+ client_type?: string;
50
+ two_factor_code?: string;
51
+ two_factor_recovery_code?: string;
52
+ }
53
+
54
+ interface LoginConfig {
55
+ idpBaseUrl: string;
56
+ clientId: string;
57
+ loginEndpoint?: string;
58
+ clientType?: string; // "partner" or "idp", defaults to "partner" for CryptAply
59
+ }
60
+
61
+ /**
62
+ * Creates a login handler for Next.js API routes
63
+ *
64
+ * @param config Configuration for IDP connection
65
+ * @returns Next.js POST handler function
66
+ *
67
+ * @example
68
+ * ```typescript
69
+ * // In your app's /app/api/auth/login/route.ts
70
+ * import { createLoginHandler } from '@payez/next-mvp/api-handlers/auth/login';
71
+ *
72
+ * export const POST = createLoginHandler({
73
+ * idpBaseUrl: process.env.IDP_URL!,
74
+ * clientId: process.env.CLIENT_ID!,
75
+ * loginEndpoint: '/api/ExternalAuth/login'
76
+ * });
77
+ * ```
78
+ */
79
+ export function createLoginHandler(config: LoginConfig) {
80
+ const { idpBaseUrl, clientId, loginEndpoint = '/api/ExternalAuth/login', clientType = 'partner' } = config;
81
+
82
+ return async function POST(req: NextRequest) {
83
+ let body: LoginRequest;
84
+
85
+ try {
86
+ body = await req.json();
87
+ } catch (parseError) {
88
+ return NextResponse.json(
89
+ { error: 'Invalid JSON format' },
90
+ { status: 400 }
91
+ );
92
+ }
93
+
94
+ const { username_or_email, password, client_type, two_factor_code, two_factor_recovery_code } = body;
95
+
96
+ // Validate required fields
97
+ if (!username_or_email || typeof username_or_email !== 'string' || username_or_email.trim() === '') {
98
+ return NextResponse.json(
99
+ { error: 'username_or_email is required' },
100
+ { status: 400 }
101
+ );
102
+ }
103
+
104
+ if (!password || typeof password !== 'string' || password.trim() === '') {
105
+ return NextResponse.json(
106
+ { error: 'Password is required' },
107
+ { status: 400 }
108
+ );
109
+ }
110
+
111
+ const idpUrl = `${idpBaseUrl}${loginEndpoint}`;
112
+ const payload: any = {
113
+ username_or_email,
114
+ password,
115
+ client_id: clientId,
116
+ client_type: client_type || clientType // Use provided client_type or config default
117
+ };
118
+
119
+ // Add optional 2FA fields if provided
120
+ if (two_factor_code) {
121
+ payload.two_factor_code = two_factor_code;
122
+ }
123
+ if (two_factor_recovery_code) {
124
+ payload.two_factor_recovery_code = two_factor_recovery_code;
125
+ }
126
+
127
+ // Extract client headers for forwarding to IDP
128
+ const clientHeaders: Record<string, string> = {};
129
+
130
+ // Forward client IP if available
131
+ const clientIp = req.headers.get('x-forwarded-for') || req.headers.get('x-real-ip');
132
+ if (clientIp) {
133
+ clientHeaders['X-Forwarded-For'] = clientIp.split(',')[0].trim();
134
+ }
135
+
136
+ // Forward user agent if available
137
+ const userAgent = req.headers.get('user-agent');
138
+ if (userAgent) {
139
+ clientHeaders['User-Agent'] = userAgent;
140
+ }
141
+
142
+ const headers = {
143
+ 'Content-Type': 'application/json',
144
+ 'Accept': 'application/json',
145
+ 'Cookie': 'X-Device-Id=payez-PayEz-6c8201c35c1b44c9b2e823b36b95a718',
146
+ ...clientHeaders
147
+ };
148
+
149
+ try {
150
+ const backendResponse = await fetch(idpUrl, {
151
+ method: 'POST',
152
+ headers,
153
+ body: JSON.stringify(payload),
154
+ });
155
+
156
+ const rawText = await backendResponse.text();
157
+ let data: any;
158
+
159
+ try {
160
+ data = JSON.parse(rawText);
161
+ } catch (jsonError) {
162
+ console.error('[LOGIN] IDP returned non-JSON response', {
163
+ response: rawText,
164
+ status: backendResponse.status
165
+ });
166
+ return NextResponse.json(
167
+ { error: 'Invalid response from authentication service' },
168
+ { status: 502 }
169
+ );
170
+ }
171
+
172
+ if (!backendResponse.ok) {
173
+ // Handle rate limiting specifically
174
+ if (backendResponse.status === 429) {
175
+ const retryAfter = backendResponse.headers.get('retry-after');
176
+ const retryAfterSeconds = retryAfter ? parseInt(retryAfter) : 60;
177
+
178
+ return addSecurityHeaders(NextResponse.json({
179
+ success: false,
180
+ error: 'RATE_LIMIT_EXCEEDED',
181
+ message: 'Too many login attempts. Please try again later.',
182
+ retryAfter: retryAfterSeconds
183
+ }, { status: 429 }));
184
+ }
185
+
186
+ // Pass through error from IDP
187
+ console.error('[LOGIN] IDP service error', {
188
+ status: backendResponse.status,
189
+ error: data
190
+ });
191
+ return addSecurityHeaders(NextResponse.json(data, { status: backendResponse.status }));
192
+ }
193
+
194
+ if (!data.success) {
195
+ // IDP returned a canonical error envelope with HTTP 200
196
+ console.error('[LOGIN] IDP authentication failure', { error: data });
197
+ return addSecurityHeaders(NextResponse.json(data, { status: 200 }));
198
+ }
199
+
200
+ // Normalize response shape for NextAuth authorize()
201
+ const normalized = {
202
+ success: data?.success === true,
203
+ result: (data?.data && data?.data?.result) ? data.data.result : (data?.result ?? data),
204
+ // Preserve original payload for debugging/compatibility
205
+ original: data
206
+ };
207
+
208
+ return addSecurityHeaders(NextResponse.json(normalized as any));
209
+
210
+ } catch (error) {
211
+ // Handle network and other errors
212
+ console.error('[LOGIN] FETCH FAILED:', {
213
+ error: error instanceof Error ? error.message : String(error),
214
+ idpUrl
215
+ });
216
+
217
+ if (error instanceof Error && error.message.includes('fetch')) {
218
+ return NextResponse.json(
219
+ { error: 'Unable to connect to authentication service' },
220
+ { status: 503 }
221
+ );
222
+ }
223
+
224
+ return NextResponse.json(
225
+ { error: 'An unexpected error occurred during login' },
226
+ { status: 500 }
227
+ );
228
+ }
229
+ };
230
+ }
231
+
232
+ /**
233
+ * Default export for backward compatibility
234
+ * Requires environment variables: IDP_URL, CLIENT_ID
235
+ */
236
+ export const POST = createLoginHandler({
237
+ idpBaseUrl: process.env.IDP_URL!,
238
+ clientId: process.env.CLIENT_ID || 'payez_default_client',
239
+ loginEndpoint: '/api/ExternalAuth/login'
240
+ });
@@ -0,0 +1,98 @@
1
+ import { NextRequest, NextResponse } from 'next/server';
2
+ import { ENV_CONFIG } from '../../config/env';
3
+ import { getIDPClientConfig } from '../../lib/idp-client-config';
4
+
5
+ /**
6
+ * Passkey SIGN-IN ceremony (ANONYMOUS — no session exists yet):
7
+ * optionsPOST -> POST /api/ExternalAuth/passwordless/passkey/options
8
+ * verifyPOST -> POST /api/ExternalAuth/passwordless/passkey/verify
9
+ *
10
+ * Anon idiom copied from auth/better-auth.ts (exchangeOAuthForIdpTokens):
11
+ * direct fetch to the IDP, client identified by client_id from the resolved
12
+ * IDP client config (slug preferred, numeric id fallback) — sent BOTH as the
13
+ * X-Client-Id header (the probe-verified routing key) and merged into the
14
+ * body when the caller did not supply one.
15
+ *
16
+ * The verify response is passed through WITHOUT unwrapping: this is a
17
+ * sign-in, its payload carries the grant the caller's session layer consumes.
18
+ */
19
+
20
+ async function clientKey(): Promise<string> {
21
+ const config = await getIDPClientConfig();
22
+ return config.clientSlug || String(config.clientId);
23
+ }
24
+
25
+ async function anonCall(req: NextRequest, path: string) {
26
+ let parsed: Record<string, unknown> = {};
27
+ try {
28
+ const raw = await req.text();
29
+ if (raw && raw.trim().length > 0) parsed = JSON.parse(raw);
30
+ } catch {}
31
+
32
+ const key = await clientKey();
33
+ const res = await fetch(`${ENV_CONFIG.IDP_URL}${path}`, {
34
+ method: 'POST',
35
+ headers: { 'Content-Type': 'application/json', 'X-Client-Id': key },
36
+ body: JSON.stringify({ client_id: key, ...parsed }),
37
+ cache: 'no-store',
38
+ });
39
+ const json = await res.json().catch(() => null);
40
+ return { res, json };
41
+ }
42
+
43
+ export async function optionsPOST(req: NextRequest) {
44
+ const { res, json } = await anonCall(req, '/api/ExternalAuth/passwordless/passkey/options');
45
+ return NextResponse.json(json ?? { success: false, message: 'Empty upstream response' }, { status: res.status });
46
+ }
47
+
48
+ /**
49
+ * VERIFY DOES NOT USE anonCall — IT MUST RESHAPE, NOT SPREAD.
50
+ *
51
+ * The IDP contract is three fields (PasskeyModels.cs:36 PasskeyAuthVerifyRequest):
52
+ * assertion_id : string
53
+ * response : the WHOLE AuthenticatorAssertionRawResponse from
54
+ * navigator.credentials.get()
55
+ * client_id : string
56
+ *
57
+ * anonCall SPREADS the caller's body (`{ client_id, ...parsed }`). For options
58
+ * that is harmless. For verify it is fatal: spreading an assertion hoists
59
+ * id/rawId/type to the top level and leaves `response` holding only the INNER
60
+ * {clientDataJSON, authenticatorData, signature, userHandle}. The IDP's guard
61
+ * then answers "response is required" in 4-12ms WITHOUT REACHING THE CRYPTO —
62
+ * a timing signature DotNetPert used to prove the caller was still spreading
63
+ * long after the graft-side plugin had been fixed.
64
+ *
65
+ * This route and the Better Auth plugin endpoint are TWO PATHS to the same IDP
66
+ * endpoint. Fixing only the plugin left this one live, spreading, and reachable
67
+ * on a URL one segment away from the correct one — which is what kept the
68
+ * sign-in leg red across several runs.
69
+ *
70
+ * Accepts the assertion either already nested (`{ assertion_id, response }`) or
71
+ * flat (`{ assertion_id, id, rawId, type, response }`) and normalises both, so a
72
+ * caller written against either shape works.
73
+ */
74
+ export async function verifyPOST(req: NextRequest) {
75
+ let parsed: Record<string, unknown> = {};
76
+ try {
77
+ const raw = await req.text();
78
+ if (raw && raw.trim().length > 0) parsed = JSON.parse(raw);
79
+ } catch { /* empty body -> the IDP's own validation answers */ }
80
+
81
+ const { assertion_id: assertionId, response, ...rest } = parsed;
82
+
83
+ // Already-nested callers send `response` holding a full assertion (it has an
84
+ // `id`). Flat callers send the assertion's own fields alongside assertion_id,
85
+ // and their `response` is the inner attestation half — not a whole assertion.
86
+ const nested = response && typeof response === 'object' && 'id' in (response as object);
87
+ const assertion = nested ? response : { ...rest, response };
88
+
89
+ const key = await clientKey();
90
+ const res = await fetch(`${ENV_CONFIG.IDP_URL}/api/ExternalAuth/passwordless/passkey/verify`, {
91
+ method: 'POST',
92
+ headers: { 'Content-Type': 'application/json', 'X-Client-Id': key },
93
+ body: JSON.stringify({ client_id: key, assertion_id: assertionId, response: assertion }),
94
+ cache: 'no-store',
95
+ });
96
+ const json = await res.json().catch(() => null);
97
+ return NextResponse.json(json ?? { success: false, message: 'Empty upstream response' }, { status: res.status });
98
+ }
@@ -1,44 +1,44 @@
1
- import { NextRequest, NextResponse } from 'next/server';
2
- import { idpFetchJSON } from '../../lib/idp-fetch';
3
- import { ENV_CONFIG, API_ENDPOINTS } from '../../config/env';
4
- import { getTokenTestAware } from '../../lib/test-aware-get-token';
5
- import { getSession } from '../../lib/session-store';
6
-
7
- /**
8
- * Validates current access token with the IDP and returns normalized info.
9
- * Sources access token from Authorization header or Redis session via cookie.
10
- */
11
- export async function GET(req: NextRequest) {
12
- try {
13
- // 1) Prefer Authorization header if present
14
- let bearer: string | undefined = undefined;
15
- const authHeader = req.headers.get('authorization') || req.headers.get('Authorization');
16
- if (authHeader && /^Bearer\s+/i.test(authHeader)) bearer = authHeader.replace(/^Bearer\s+/i, '').trim();
17
-
18
- // 2) If not, resolve from session
19
- if (!bearer) {
20
- const tok = await getTokenTestAware(req);
21
- const sessionToken = tok?.sessionToken as string | undefined;
22
- if (sessionToken) {
23
- const sess = await getSession(sessionToken);
24
- bearer = sess?.accessToken;
25
- }
26
- }
27
-
28
- if (!bearer) {
29
- return NextResponse.json({ success: false, error: { code: 'UNAUTHORIZED', message: 'No access token available' } }, { status: 401 });
30
- }
31
-
32
- const url = `${ENV_CONFIG.IDP_URL}${API_ENDPOINTS.externalAuth.validate}`;
33
- const result = await idpFetchJSON(req, url, { method: 'GET', headers: { Authorization: `Bearer ${bearer}` } });
34
-
35
- if (!result.ok) {
36
- return NextResponse.json({ success: false, error: { code: 'UPSTREAM_SERVICE_ERROR', status: result.status }, data: result.json }, { status: result.status });
37
- }
38
-
39
- // Passthrough normalized
40
- return NextResponse.json(result.json ?? { success: true }, { status: 200 });
41
- } catch (e: any) {
42
- return NextResponse.json({ success: false, error: { code: 'INTERNAL_SERVER_ERROR', message: e?.message || 'validate error' } }, { status: 500 });
43
- }
1
+ import { NextRequest, NextResponse } from 'next/server';
2
+ import { idpFetchJSON } from '../../lib/idp-fetch';
3
+ import { ENV_CONFIG, API_ENDPOINTS } from '../../config/env';
4
+ import { getTokenTestAware } from '../../lib/test-aware-get-token';
5
+ import { getSession } from '../../lib/session-store';
6
+
7
+ /**
8
+ * Validates current access token with the IDP and returns normalized info.
9
+ * Sources access token from Authorization header or Redis session via cookie.
10
+ */
11
+ export async function GET(req: NextRequest) {
12
+ try {
13
+ // 1) Prefer Authorization header if present
14
+ let bearer: string | undefined = undefined;
15
+ const authHeader = req.headers.get('authorization') || req.headers.get('Authorization');
16
+ if (authHeader && /^Bearer\s+/i.test(authHeader)) bearer = authHeader.replace(/^Bearer\s+/i, '').trim();
17
+
18
+ // 2) If not, resolve from session
19
+ if (!bearer) {
20
+ const tok = await getTokenTestAware(req);
21
+ const sessionToken = tok?.sessionToken as string | undefined;
22
+ if (sessionToken) {
23
+ const sess = await getSession(sessionToken);
24
+ bearer = sess?.accessToken;
25
+ }
26
+ }
27
+
28
+ if (!bearer) {
29
+ return NextResponse.json({ success: false, error: { code: 'UNAUTHORIZED', message: 'No access token available' } }, { status: 401 });
30
+ }
31
+
32
+ const url = `${ENV_CONFIG.IDP_URL}${API_ENDPOINTS.externalAuth.validate}`;
33
+ const result = await idpFetchJSON(req, url, { method: 'GET', headers: { Authorization: `Bearer ${bearer}` } });
34
+
35
+ if (!result.ok) {
36
+ return NextResponse.json({ success: false, error: { code: 'UPSTREAM_SERVICE_ERROR', status: result.status }, data: result.json }, { status: result.status });
37
+ }
38
+
39
+ // Passthrough normalized
40
+ return NextResponse.json(result.json ?? { success: true }, { status: 200 });
41
+ } catch (e: any) {
42
+ return NextResponse.json({ success: false, error: { code: 'INTERNAL_SERVER_ERROR', message: e?.message || 'validate error' } }, { status: 500 });
43
+ }
44
44
  }