@payez/next-mvp 4.1.11 → 4.1.13
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/auth/refresh.js +15 -38
- package/dist/auth/better-auth.d.ts +0 -3
- package/dist/auth/better-auth.js +164 -69
- package/dist/client/better-auth-client.d.ts +14 -2
- package/dist/lib/ensure-fresh-access-token.js +8 -42
- package/dist/lib/session-store.d.ts +0 -1
- package/dist/lib/session-store.js +29 -33
- package/dist/lib/startup-init.js +19 -19
- package/dist/lib/token-lifecycle.d.ts +0 -2
- package/dist/lib/token-lifecycle.js +3 -21
- package/package.json +5 -4
- 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 +80 -80
- package/src/api-handlers/account/recovery/initiate.ts +23 -23
- package/src/api-handlers/account/recovery/send-code.ts +25 -25
- package/src/api-handlers/account/recovery/verify-code.ts +25 -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/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/refresh.ts +15 -38
- package/src/api-handlers/auth/validate.ts +43 -43
- package/src/api-handlers/session/viability.ts +142 -142
- package/src/auth/better-auth.ts +130 -29
- package/src/auth/unauthenticated-routes.ts +2 -2
- 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 +8 -42
- 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 +692 -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 +470 -497
- 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 +264 -264
- package/src/pages/admin-roles/index.ts +9 -9
- package/src/pages/security/EnhancedSecurityPage.tsx +9 -9
- 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/refresh.ts +48 -48
- package/src/routes/auth/session.ts +166 -166
- package/src/routes/index.ts +17 -17
- 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 +202 -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
package/dist/lib/startup-init.js
CHANGED
|
@@ -213,25 +213,25 @@ async function performInitialization() {
|
|
|
213
213
|
const connectionLine = isConnectionError
|
|
214
214
|
? '║ 🔌 CONNECTION REFUSED - IDP appears to be down ║\n║ ║\n'
|
|
215
215
|
: '';
|
|
216
|
-
console.error(`
|
|
217
|
-
╔══════════════════════════════════════════════════════════════╗
|
|
218
|
-
║ ❌ FATAL: BETTER_AUTH_SECRET NOT AVAILABLE ║
|
|
219
|
-
║ ║
|
|
220
|
-
║ The app cannot start without a valid auth signing secret. ║
|
|
221
|
-
║ This should be fetched from IDP at startup. ║
|
|
222
|
-
║ ║
|
|
223
|
-
${connectionLine}║ Possible causes: ║
|
|
224
|
-
║ • IDP is not running or unreachable ║
|
|
225
|
-
║ • CLIENT_ID is not registered in IDP ║
|
|
226
|
-
║ • IDP_URL is incorrect ║
|
|
227
|
-
║ • Network connectivity issue ║
|
|
228
|
-
║ ║
|
|
229
|
-
║ Current config: ║
|
|
230
|
-
║ • IDP_URL: ${idpUrl}║
|
|
231
|
-
║ • CLIENT_ID: ${clientId}║
|
|
232
|
-
╚══════════════════════════════════════════════════════════════╝
|
|
233
|
-
|
|
234
|
-
[STARTUP] Error: ${errorMsg}
|
|
216
|
+
console.error(`
|
|
217
|
+
╔══════════════════════════════════════════════════════════════╗
|
|
218
|
+
║ ❌ FATAL: BETTER_AUTH_SECRET NOT AVAILABLE ║
|
|
219
|
+
║ ║
|
|
220
|
+
║ The app cannot start without a valid auth signing secret. ║
|
|
221
|
+
║ This should be fetched from IDP at startup. ║
|
|
222
|
+
║ ║
|
|
223
|
+
${connectionLine}║ Possible causes: ║
|
|
224
|
+
║ • IDP is not running or unreachable ║
|
|
225
|
+
║ • CLIENT_ID is not registered in IDP ║
|
|
226
|
+
║ • IDP_URL is incorrect ║
|
|
227
|
+
║ • Network connectivity issue ║
|
|
228
|
+
║ ║
|
|
229
|
+
║ Current config: ║
|
|
230
|
+
║ • IDP_URL: ${idpUrl}║
|
|
231
|
+
║ • CLIENT_ID: ${clientId}║
|
|
232
|
+
╚══════════════════════════════════════════════════════════════╝
|
|
233
|
+
|
|
234
|
+
[STARTUP] Error: ${errorMsg}
|
|
235
235
|
`);
|
|
236
236
|
// Re-throw so callers know initialization failed
|
|
237
237
|
throw lastInitError;
|
|
@@ -31,8 +31,6 @@ export interface TokenError {
|
|
|
31
31
|
error: 'NO_SESSION' | 'NO_TOKEN' | 'EXPIRED' | 'REFRESH_FAILED' | 'SESSION_EXPIRED_NO_REFRESH';
|
|
32
32
|
message: string;
|
|
33
33
|
terminal?: boolean;
|
|
34
|
-
code?: string;
|
|
35
|
-
resolution?: string;
|
|
36
34
|
}
|
|
37
35
|
export type EnsureFreshTokenResult = TokenResult | TokenError;
|
|
38
36
|
/**
|
|
@@ -150,16 +150,7 @@ async function triggerRefresh(request, sessionToken, retryCount = 0) {
|
|
|
150
150
|
// Don't retry - user must re-authenticate
|
|
151
151
|
if (responseData.code === 'NO_REFRESH_TOKEN' || responseData.terminal === true) {
|
|
152
152
|
console.error('[TOKEN_LIFECYCLE] TERMINAL: Session has no refresh token - user must re-login');
|
|
153
|
-
|
|
154
|
-
// message in `error` and the code in `code`; carry both up so the consumer
|
|
155
|
-
// can tell the user WHY (not just "session expired") and stop a futile loop.
|
|
156
|
-
return {
|
|
157
|
-
success: false,
|
|
158
|
-
terminal: true,
|
|
159
|
-
code: responseData.code,
|
|
160
|
-
message: responseData.error,
|
|
161
|
-
resolution: responseData.resolution,
|
|
162
|
-
};
|
|
153
|
+
return { success: false, terminal: true, code: responseData.code };
|
|
163
154
|
}
|
|
164
155
|
// For other 401s, check if maybe session was refreshed by another request
|
|
165
156
|
if (response.status === 401 && retryCount < maxRetries) {
|
|
@@ -327,21 +318,12 @@ async function ensureFreshToken(request) {
|
|
|
327
318
|
if (!refreshResult.success) {
|
|
328
319
|
// Check for terminal state - session cannot be recovered
|
|
329
320
|
if (refreshResult.terminal) {
|
|
330
|
-
console.error('[TOKEN_LIFECYCLE] TERMINAL: Session expired - redirect to login'
|
|
331
|
-
code: refreshResult.code,
|
|
332
|
-
message: refreshResult.message,
|
|
333
|
-
});
|
|
334
|
-
// WO-1: keep `error` as the stable terminal category (existing consumers
|
|
335
|
-
// branch on SESSION_EXPIRED_NO_REFRESH) but surface the verbatim upstream
|
|
336
|
-
// reason via code/message/resolution so the user learns WHY and a genuinely
|
|
337
|
-
// dead session (e.g. unapproved account) doesn't loop login→bounce→login.
|
|
321
|
+
console.error('[TOKEN_LIFECYCLE] TERMINAL: Session expired with no refresh token - redirect to login');
|
|
338
322
|
return {
|
|
339
323
|
success: false,
|
|
340
324
|
error: 'SESSION_EXPIRED_NO_REFRESH',
|
|
341
|
-
message:
|
|
325
|
+
message: 'Session expired. Please sign in again.',
|
|
342
326
|
terminal: true,
|
|
343
|
-
code: refreshResult.code,
|
|
344
|
-
resolution: refreshResult.resolution,
|
|
345
327
|
};
|
|
346
328
|
}
|
|
347
329
|
console.warn('[TOKEN_LIFECYCLE] Refresh failed');
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@payez/next-mvp",
|
|
3
|
-
"version": "4.1.
|
|
3
|
+
"version": "4.1.13",
|
|
4
4
|
"sideEffects": false,
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -10,11 +10,12 @@
|
|
|
10
10
|
"src"
|
|
11
11
|
],
|
|
12
12
|
"scripts": {
|
|
13
|
-
"build": "npx tsc --project tsconfig.json",
|
|
13
|
+
"build": "node -e \"const fs=require('fs');fs.rmSync('dist',{recursive:true,force:true});fs.rmSync('tsconfig.tsbuildinfo',{force:true})\" && npx tsc --project tsconfig.json",
|
|
14
14
|
"prepublishOnly": "npm run build",
|
|
15
15
|
"dev": "next dev",
|
|
16
16
|
"dev:local": "pwsh -NoProfile -ExecutionPolicy Bypass -File scripts/dev-local.ps1",
|
|
17
|
-
"dev:broker": "pwsh -NoProfile -ExecutionPolicy Bypass -File scripts/dev-broker.ps1"
|
|
17
|
+
"dev:broker": "pwsh -NoProfile -ExecutionPolicy Bypass -File scripts/dev-broker.ps1",
|
|
18
|
+
"prepack": "npm run build"
|
|
18
19
|
},
|
|
19
20
|
"exports": {
|
|
20
21
|
".": {
|
|
@@ -890,4 +891,4 @@
|
|
|
890
891
|
"typescript": "^5.4.5",
|
|
891
892
|
"zustand": "^4.5.5"
|
|
892
893
|
}
|
|
893
|
-
}
|
|
894
|
+
}
|
|
@@ -1,73 +1,73 @@
|
|
|
1
|
-
#!/bin/bash
|
|
2
|
-
#
|
|
3
|
-
# CI Check: Detect incorrect internal API URL patterns
|
|
4
|
-
#
|
|
5
|
-
# This script fails if code is using req.url or req.nextUrl.origin to construct
|
|
6
|
-
# URLs for internal API calls. These patterns cause 403 errors in Kubernetes
|
|
7
|
-
# because they use the external HTTPS URL instead of INTERNAL_API_URL.
|
|
8
|
-
#
|
|
9
|
-
# CORRECT: Use internalFetch() or internalRefresh() from @payez/next-mvp/lib/internal-api
|
|
10
|
-
# INCORRECT: new URL('/api/...', req.url) or fetch(`${req.nextUrl.origin}/api/...`)
|
|
11
|
-
#
|
|
12
|
-
# NOTE: new URL(req.url) alone is FINE - it just parses the incoming request URL.
|
|
13
|
-
# The problem is when you construct a NEW path using req.url as the base.
|
|
14
|
-
#
|
|
15
|
-
|
|
16
|
-
set -e
|
|
17
|
-
|
|
18
|
-
SEARCH_PATH="${1:-.}"
|
|
19
|
-
ERRORS_FOUND=0
|
|
20
|
-
|
|
21
|
-
echo "Checking for incorrect internal URL patterns in: $SEARCH_PATH"
|
|
22
|
-
echo "========================================================================"
|
|
23
|
-
|
|
24
|
-
# Pattern 1: new URL('/api/...', req.url) - constructing internal API URL from request
|
|
25
|
-
# This catches: new URL('/api/...', req.url) and new URL("/api/...", req.url)
|
|
26
|
-
# But NOT: new URL(req.url) which is just parsing the request
|
|
27
|
-
echo ""
|
|
28
|
-
echo "Checking for: new URL('/api/...', req.url) pattern"
|
|
29
|
-
if grep -rn --include="*.ts" --include="*.tsx" -E "new URL\(['\"][^'\"]*['\"],\s*req\.url" "$SEARCH_PATH" 2>/dev/null | grep -v "node_modules" | grep -v "\.d\.ts"; then
|
|
30
|
-
echo "ERROR: Found 'new URL(path, req.url)' pattern. Use internalFetch() instead."
|
|
31
|
-
ERRORS_FOUND=1
|
|
32
|
-
fi
|
|
33
|
-
|
|
34
|
-
# Pattern 2: new URL('/api/...', req.nextUrl) or new URL(..., req.nextUrl.origin)
|
|
35
|
-
echo ""
|
|
36
|
-
echo "Checking for: new URL(..., req.nextUrl) pattern"
|
|
37
|
-
if grep -rn --include="*.ts" --include="*.tsx" -E "new URL\([^)]+,\s*req\.nextUrl" "$SEARCH_PATH" 2>/dev/null | grep -v "node_modules" | grep -v "\.d\.ts"; then
|
|
38
|
-
echo "ERROR: Found 'new URL(..., req.nextUrl)' pattern. Use internalFetch() instead."
|
|
39
|
-
ERRORS_FOUND=1
|
|
40
|
-
fi
|
|
41
|
-
|
|
42
|
-
# Pattern 3: fetch with req.nextUrl.origin and /api/
|
|
43
|
-
echo ""
|
|
44
|
-
echo "Checking for: fetch with req.nextUrl.origin and /api/"
|
|
45
|
-
if grep -rn --include="*.ts" --include="*.tsx" "req\.nextUrl\.origin.*\/api\/" "$SEARCH_PATH" 2>/dev/null | grep -v "node_modules" | grep -v "\.d\.ts"; then
|
|
46
|
-
echo "ERROR: Found 'req.nextUrl.origin' used with internal API route. Use internalFetch() instead."
|
|
47
|
-
ERRORS_FOUND=1
|
|
48
|
-
fi
|
|
49
|
-
|
|
50
|
-
# Pattern 4: Direct fetch to /api/auth/refresh without using internalRefresh (warning only)
|
|
51
|
-
echo ""
|
|
52
|
-
echo "Checking for: Direct fetch to /api/auth/refresh (should use internalRefresh)"
|
|
53
|
-
if grep -rn --include="*.ts" --include="*.tsx" "fetch.*\/api\/auth\/refresh" "$SEARCH_PATH" 2>/dev/null | grep -v "node_modules" | grep -v "\.d\.ts" | grep -v "internal-api\.ts"; then
|
|
54
|
-
echo "WARNING: Found direct fetch to /api/auth/refresh. Consider using internalRefresh() helper."
|
|
55
|
-
# Not failing on this one as it might be intentional in some cases (client-side refresh)
|
|
56
|
-
fi
|
|
57
|
-
|
|
58
|
-
echo ""
|
|
59
|
-
echo "========================================================================"
|
|
60
|
-
|
|
61
|
-
if [ $ERRORS_FOUND -eq 1 ]; then
|
|
62
|
-
echo "FAILED: Dangerous internal URL patterns detected!"
|
|
63
|
-
echo ""
|
|
64
|
-
echo "Fix: Import and use helpers from @payez/next-mvp/lib/internal-api:"
|
|
65
|
-
echo " import { internalFetch, internalRefresh } from '@payez/next-mvp/lib/internal-api';"
|
|
66
|
-
echo ""
|
|
67
|
-
echo "Why: Server-to-server calls must use INTERNAL_API_URL (http://service.namespace.svc.cluster.local)"
|
|
68
|
-
echo " not the external URL from the request (https://api.example.com) which fails in Kubernetes."
|
|
69
|
-
exit 1
|
|
70
|
-
else
|
|
71
|
-
echo "PASSED: No dangerous internal URL patterns found."
|
|
72
|
-
exit 0
|
|
73
|
-
fi
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
#
|
|
3
|
+
# CI Check: Detect incorrect internal API URL patterns
|
|
4
|
+
#
|
|
5
|
+
# This script fails if code is using req.url or req.nextUrl.origin to construct
|
|
6
|
+
# URLs for internal API calls. These patterns cause 403 errors in Kubernetes
|
|
7
|
+
# because they use the external HTTPS URL instead of INTERNAL_API_URL.
|
|
8
|
+
#
|
|
9
|
+
# CORRECT: Use internalFetch() or internalRefresh() from @payez/next-mvp/lib/internal-api
|
|
10
|
+
# INCORRECT: new URL('/api/...', req.url) or fetch(`${req.nextUrl.origin}/api/...`)
|
|
11
|
+
#
|
|
12
|
+
# NOTE: new URL(req.url) alone is FINE - it just parses the incoming request URL.
|
|
13
|
+
# The problem is when you construct a NEW path using req.url as the base.
|
|
14
|
+
#
|
|
15
|
+
|
|
16
|
+
set -e
|
|
17
|
+
|
|
18
|
+
SEARCH_PATH="${1:-.}"
|
|
19
|
+
ERRORS_FOUND=0
|
|
20
|
+
|
|
21
|
+
echo "Checking for incorrect internal URL patterns in: $SEARCH_PATH"
|
|
22
|
+
echo "========================================================================"
|
|
23
|
+
|
|
24
|
+
# Pattern 1: new URL('/api/...', req.url) - constructing internal API URL from request
|
|
25
|
+
# This catches: new URL('/api/...', req.url) and new URL("/api/...", req.url)
|
|
26
|
+
# But NOT: new URL(req.url) which is just parsing the request
|
|
27
|
+
echo ""
|
|
28
|
+
echo "Checking for: new URL('/api/...', req.url) pattern"
|
|
29
|
+
if grep -rn --include="*.ts" --include="*.tsx" -E "new URL\(['\"][^'\"]*['\"],\s*req\.url" "$SEARCH_PATH" 2>/dev/null | grep -v "node_modules" | grep -v "\.d\.ts"; then
|
|
30
|
+
echo "ERROR: Found 'new URL(path, req.url)' pattern. Use internalFetch() instead."
|
|
31
|
+
ERRORS_FOUND=1
|
|
32
|
+
fi
|
|
33
|
+
|
|
34
|
+
# Pattern 2: new URL('/api/...', req.nextUrl) or new URL(..., req.nextUrl.origin)
|
|
35
|
+
echo ""
|
|
36
|
+
echo "Checking for: new URL(..., req.nextUrl) pattern"
|
|
37
|
+
if grep -rn --include="*.ts" --include="*.tsx" -E "new URL\([^)]+,\s*req\.nextUrl" "$SEARCH_PATH" 2>/dev/null | grep -v "node_modules" | grep -v "\.d\.ts"; then
|
|
38
|
+
echo "ERROR: Found 'new URL(..., req.nextUrl)' pattern. Use internalFetch() instead."
|
|
39
|
+
ERRORS_FOUND=1
|
|
40
|
+
fi
|
|
41
|
+
|
|
42
|
+
# Pattern 3: fetch with req.nextUrl.origin and /api/
|
|
43
|
+
echo ""
|
|
44
|
+
echo "Checking for: fetch with req.nextUrl.origin and /api/"
|
|
45
|
+
if grep -rn --include="*.ts" --include="*.tsx" "req\.nextUrl\.origin.*\/api\/" "$SEARCH_PATH" 2>/dev/null | grep -v "node_modules" | grep -v "\.d\.ts"; then
|
|
46
|
+
echo "ERROR: Found 'req.nextUrl.origin' used with internal API route. Use internalFetch() instead."
|
|
47
|
+
ERRORS_FOUND=1
|
|
48
|
+
fi
|
|
49
|
+
|
|
50
|
+
# Pattern 4: Direct fetch to /api/auth/refresh without using internalRefresh (warning only)
|
|
51
|
+
echo ""
|
|
52
|
+
echo "Checking for: Direct fetch to /api/auth/refresh (should use internalRefresh)"
|
|
53
|
+
if grep -rn --include="*.ts" --include="*.tsx" "fetch.*\/api\/auth\/refresh" "$SEARCH_PATH" 2>/dev/null | grep -v "node_modules" | grep -v "\.d\.ts" | grep -v "internal-api\.ts"; then
|
|
54
|
+
echo "WARNING: Found direct fetch to /api/auth/refresh. Consider using internalRefresh() helper."
|
|
55
|
+
# Not failing on this one as it might be intentional in some cases (client-side refresh)
|
|
56
|
+
fi
|
|
57
|
+
|
|
58
|
+
echo ""
|
|
59
|
+
echo "========================================================================"
|
|
60
|
+
|
|
61
|
+
if [ $ERRORS_FOUND -eq 1 ]; then
|
|
62
|
+
echo "FAILED: Dangerous internal URL patterns detected!"
|
|
63
|
+
echo ""
|
|
64
|
+
echo "Fix: Import and use helpers from @payez/next-mvp/lib/internal-api:"
|
|
65
|
+
echo " import { internalFetch, internalRefresh } from '@payez/next-mvp/lib/internal-api';"
|
|
66
|
+
echo ""
|
|
67
|
+
echo "Why: Server-to-server calls must use INTERNAL_API_URL (http://service.namespace.svc.cluster.local)"
|
|
68
|
+
echo " not the external URL from the request (https://api.example.com) which fails in Kubernetes."
|
|
69
|
+
exit 1
|
|
70
|
+
else
|
|
71
|
+
echo "PASSED: No dangerous internal URL patterns found."
|
|
72
|
+
exit 0
|
|
73
|
+
fi
|
package/src/api/index.ts
CHANGED
|
@@ -1,18 +1,18 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @payez/next-mvp API Module Exports
|
|
3
|
-
*
|
|
4
|
-
* Provides enhanced API route handlers with automatic token management
|
|
5
|
-
*
|
|
6
|
-
* @version 2.0.0
|
|
7
|
-
* @since auth-ready-v2
|
|
8
|
-
*/
|
|
9
|
-
|
|
10
|
-
export {
|
|
11
|
-
createAuthHandler,
|
|
12
|
-
type AuthContext,
|
|
13
|
-
type AuthHandlerOptions,
|
|
14
|
-
type HandlerFunction
|
|
15
|
-
} from './auth-handler';
|
|
16
|
-
|
|
17
|
-
// Default export for convenience
|
|
1
|
+
/**
|
|
2
|
+
* @payez/next-mvp API Module Exports
|
|
3
|
+
*
|
|
4
|
+
* Provides enhanced API route handlers with automatic token management
|
|
5
|
+
*
|
|
6
|
+
* @version 2.0.0
|
|
7
|
+
* @since auth-ready-v2
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
export {
|
|
11
|
+
createAuthHandler,
|
|
12
|
+
type AuthContext,
|
|
13
|
+
type AuthHandlerOptions,
|
|
14
|
+
type HandlerFunction
|
|
15
|
+
} from './auth-handler';
|
|
16
|
+
|
|
17
|
+
// Default export for convenience
|
|
18
18
|
export { default } from './auth-handler';
|
|
@@ -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
|
+
}
|