@payez/next-mvp 4.1.10 → 4.1.12
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/api-handlers/auth/refresh.js +15 -38
- package/dist/auth/better-auth.js +9 -18
- package/dist/client/better-auth-client.d.ts +14 -2
- package/dist/lib/ensure-fresh-access-token.js +7 -30
- 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/package.json +4 -3
- 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 +9 -14
- 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 +7 -30
- 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 -470
- package/src/lib/types/api-responses.ts +336 -336
- package/src/middleware/auth-decision.ts +43 -43
- package/src/models/DecodedAccessToken.ts +16 -16
- package/src/models/SessionModel.ts +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
|
@@ -1,143 +1,143 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Session Viability Check API Handler for `@payez/next-mvp`
|
|
3
|
-
*
|
|
4
|
-
* This API route is called by the middleware to securely check if a session is valid.
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
import { NextRequest, NextResponse } from 'next/server';
|
|
8
|
-
import { getSession as getRedisSession, getBetterAuthSession } from '../../lib/session-store';
|
|
9
|
-
import { getSession } from '../../server/auth';
|
|
10
|
-
import { isInitializationFailed, ensureInitialized } from '../../lib/startup-init';
|
|
11
|
-
import { getIDPClientConfig } from '../../lib/idp-client-config';
|
|
12
|
-
|
|
13
|
-
export async function GET(req: NextRequest) {
|
|
14
|
-
try {
|
|
15
|
-
// Ensure initialization is complete (auth signing secret resolved from IDP)
|
|
16
|
-
if (!process.env.BETTER_AUTH_SECRET && !process.env.NEXTAUTH_SECRET) {
|
|
17
|
-
try {
|
|
18
|
-
await ensureInitialized();
|
|
19
|
-
} catch (error) {
|
|
20
|
-
// Initialization failed - return 503
|
|
21
|
-
console.error('[API Viability] Initialization failed - returning 503');
|
|
22
|
-
return NextResponse.json(
|
|
23
|
-
{
|
|
24
|
-
error: 'Service Unavailable',
|
|
25
|
-
message: 'Authentication service is not properly configured',
|
|
26
|
-
code: 'AUTH_NOT_INITIALIZED'
|
|
27
|
-
},
|
|
28
|
-
{ status: 503 }
|
|
29
|
-
);
|
|
30
|
-
}
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
// Double-check after initialization attempt
|
|
34
|
-
if (isInitializationFailed()) {
|
|
35
|
-
console.error('[API Viability] Initialization failed - returning 503');
|
|
36
|
-
return NextResponse.json(
|
|
37
|
-
{
|
|
38
|
-
error: 'Service Unavailable',
|
|
39
|
-
message: 'Authentication service is not properly configured',
|
|
40
|
-
code: 'AUTH_NOT_INITIALIZED'
|
|
41
|
-
},
|
|
42
|
-
{ status: 503 }
|
|
43
|
-
);
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
// Get session from Better Auth
|
|
47
|
-
const betterAuthSession = await getSession(req);
|
|
48
|
-
|
|
49
|
-
// Debug logging
|
|
50
|
-
if (!betterAuthSession) {
|
|
51
|
-
console.warn('[VIABILITY] getSession returned null');
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
const sessionToken = betterAuthSession?.session?.token as string | undefined;
|
|
55
|
-
if (betterAuthSession && sessionToken) {
|
|
56
|
-
// Try legacy session store first, then Better Auth format
|
|
57
|
-
let sessionData = await getRedisSession(sessionToken);
|
|
58
|
-
if (!sessionData) {
|
|
59
|
-
// Better Auth stores sessions with ba:{appSlug}:{token} prefix
|
|
60
|
-
sessionData = await getBetterAuthSession(sessionToken);
|
|
61
|
-
if (sessionData) {
|
|
62
|
-
console.log('[VIABILITY] Found session in Better Auth store');
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
if (sessionData) {
|
|
66
|
-
// The session exists in Redis
|
|
67
|
-
|
|
68
|
-
// Check if access token is expired (for middleware decision-making)
|
|
69
|
-
const accessTokenExpires = sessionData.idpAccessTokenExpires || 0;
|
|
70
|
-
const accessTokenExpired = accessTokenExpires < Date.now();
|
|
71
|
-
|
|
72
|
-
// Get requires2FA from cached client config (client-wide setting)
|
|
73
|
-
let requires2FA = true; // Default to true for security
|
|
74
|
-
try {
|
|
75
|
-
const cachedConfig = await getIDPClientConfig();
|
|
76
|
-
requires2FA = cachedConfig.authSettings?.require2FA ?? true;
|
|
77
|
-
} catch (e) {
|
|
78
|
-
console.warn('[API Viability] Could not get client config, defaulting requires2FA to true');
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
// CRITICAL: OAuth sessions without refresh token always need 2FA completion
|
|
82
|
-
// to obtain a refresh token from the IDP. The OAuth callback sets mfaVerified=false
|
|
83
|
-
// when no refresh_token is present. This must override the client config.
|
|
84
|
-
if (sessionData.mfaVerified === false && !sessionData.idpRefreshToken) {
|
|
85
|
-
requires2FA = true;
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
// CRITICAL: Check if MFA has expired (2FA TTL enforcement)
|
|
89
|
-
// The session may have mfaVerified=true from days ago, but if mfaExpiresAt
|
|
90
|
-
// has passed, we must treat 2FA as incomplete to force re-verification.
|
|
91
|
-
const mfaExpiresAt = sessionData.mfaExpiresAt || 0;
|
|
92
|
-
const mfaExpired = mfaExpiresAt > 0 && mfaExpiresAt < Date.now();
|
|
93
|
-
// Check both field names for compatibility (mfaVerified is the normalized name)
|
|
94
|
-
const sessionMfaComplete = sessionData.mfaVerified ?? (sessionData as any).twoFactorComplete ?? false;
|
|
95
|
-
const effectiveTwoFactorComplete = sessionMfaComplete && !mfaExpired;
|
|
96
|
-
|
|
97
|
-
console.log('[VIABILITY] Session 2FA check:', {
|
|
98
|
-
sessionToken: sessionToken.substring(0, 8) + '...',
|
|
99
|
-
mfaVerified: sessionData.mfaVerified,
|
|
100
|
-
twoFactorComplete: (sessionData as any).twoFactorComplete,
|
|
101
|
-
sessionMfaComplete,
|
|
102
|
-
mfaExpired,
|
|
103
|
-
effectiveTwoFactorComplete,
|
|
104
|
-
});
|
|
105
|
-
|
|
106
|
-
if (mfaExpired && sessionMfaComplete) {
|
|
107
|
-
console.warn('[API Viability] MFA expired - forcing 2FA re-verification', {
|
|
108
|
-
mfaExpiresAt: new Date(mfaExpiresAt).toISOString(),
|
|
109
|
-
now: new Date().toISOString(),
|
|
110
|
-
hoursExpiredAgo: ((Date.now() - mfaExpiresAt) / (1000 * 60 * 60)).toFixed(1)
|
|
111
|
-
});
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
const response = {
|
|
115
|
-
authenticated: true,
|
|
116
|
-
sessionToken, // Include token for middleware tracking
|
|
117
|
-
// 2FA fields - critical for middleware redirect logic
|
|
118
|
-
requires2FA, // From cached client config (client-wide setting)
|
|
119
|
-
twoFactorComplete: effectiveTwoFactorComplete, // From session, BUT respects MFA TTL
|
|
120
|
-
// Token status for refresh decisions
|
|
121
|
-
accessTokenExpired,
|
|
122
|
-
hasRefreshToken: !!sessionData.idpRefreshToken
|
|
123
|
-
};
|
|
124
|
-
return NextResponse.json(response);
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
// CRITICAL: Cookie exists but Redis session is missing (stale cookie state)
|
|
128
|
-
// Return sessionToken so middleware can detect this and clear the stale cookie
|
|
129
|
-
console.warn('[VIABILITY] Stale cookie detected - session not in Redis');
|
|
130
|
-
return NextResponse.json({
|
|
131
|
-
authenticated: false,
|
|
132
|
-
sessionToken // Include token to enable stale cookie detection
|
|
133
|
-
});
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
// If there's no token at all, it's not authenticated
|
|
137
|
-
return NextResponse.json({ authenticated: false });
|
|
138
|
-
|
|
139
|
-
} catch (error) {
|
|
140
|
-
console.error('[API Viability] Error checking session viability:', error);
|
|
141
|
-
return NextResponse.json({ authenticated: false }, { status: 500 });
|
|
142
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* Session Viability Check API Handler for `@payez/next-mvp`
|
|
3
|
+
*
|
|
4
|
+
* This API route is called by the middleware to securely check if a session is valid.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { NextRequest, NextResponse } from 'next/server';
|
|
8
|
+
import { getSession as getRedisSession, getBetterAuthSession } from '../../lib/session-store';
|
|
9
|
+
import { getSession } from '../../server/auth';
|
|
10
|
+
import { isInitializationFailed, ensureInitialized } from '../../lib/startup-init';
|
|
11
|
+
import { getIDPClientConfig } from '../../lib/idp-client-config';
|
|
12
|
+
|
|
13
|
+
export async function GET(req: NextRequest) {
|
|
14
|
+
try {
|
|
15
|
+
// Ensure initialization is complete (auth signing secret resolved from IDP)
|
|
16
|
+
if (!process.env.BETTER_AUTH_SECRET && !process.env.NEXTAUTH_SECRET) {
|
|
17
|
+
try {
|
|
18
|
+
await ensureInitialized();
|
|
19
|
+
} catch (error) {
|
|
20
|
+
// Initialization failed - return 503
|
|
21
|
+
console.error('[API Viability] Initialization failed - returning 503');
|
|
22
|
+
return NextResponse.json(
|
|
23
|
+
{
|
|
24
|
+
error: 'Service Unavailable',
|
|
25
|
+
message: 'Authentication service is not properly configured',
|
|
26
|
+
code: 'AUTH_NOT_INITIALIZED'
|
|
27
|
+
},
|
|
28
|
+
{ status: 503 }
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Double-check after initialization attempt
|
|
34
|
+
if (isInitializationFailed()) {
|
|
35
|
+
console.error('[API Viability] Initialization failed - returning 503');
|
|
36
|
+
return NextResponse.json(
|
|
37
|
+
{
|
|
38
|
+
error: 'Service Unavailable',
|
|
39
|
+
message: 'Authentication service is not properly configured',
|
|
40
|
+
code: 'AUTH_NOT_INITIALIZED'
|
|
41
|
+
},
|
|
42
|
+
{ status: 503 }
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Get session from Better Auth
|
|
47
|
+
const betterAuthSession = await getSession(req);
|
|
48
|
+
|
|
49
|
+
// Debug logging
|
|
50
|
+
if (!betterAuthSession) {
|
|
51
|
+
console.warn('[VIABILITY] getSession returned null');
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const sessionToken = betterAuthSession?.session?.token as string | undefined;
|
|
55
|
+
if (betterAuthSession && sessionToken) {
|
|
56
|
+
// Try legacy session store first, then Better Auth format
|
|
57
|
+
let sessionData = await getRedisSession(sessionToken);
|
|
58
|
+
if (!sessionData) {
|
|
59
|
+
// Better Auth stores sessions with ba:{appSlug}:{token} prefix
|
|
60
|
+
sessionData = await getBetterAuthSession(sessionToken);
|
|
61
|
+
if (sessionData) {
|
|
62
|
+
console.log('[VIABILITY] Found session in Better Auth store');
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
if (sessionData) {
|
|
66
|
+
// The session exists in Redis
|
|
67
|
+
|
|
68
|
+
// Check if access token is expired (for middleware decision-making)
|
|
69
|
+
const accessTokenExpires = sessionData.idpAccessTokenExpires || 0;
|
|
70
|
+
const accessTokenExpired = accessTokenExpires < Date.now();
|
|
71
|
+
|
|
72
|
+
// Get requires2FA from cached client config (client-wide setting)
|
|
73
|
+
let requires2FA = true; // Default to true for security
|
|
74
|
+
try {
|
|
75
|
+
const cachedConfig = await getIDPClientConfig();
|
|
76
|
+
requires2FA = cachedConfig.authSettings?.require2FA ?? true;
|
|
77
|
+
} catch (e) {
|
|
78
|
+
console.warn('[API Viability] Could not get client config, defaulting requires2FA to true');
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// CRITICAL: OAuth sessions without refresh token always need 2FA completion
|
|
82
|
+
// to obtain a refresh token from the IDP. The OAuth callback sets mfaVerified=false
|
|
83
|
+
// when no refresh_token is present. This must override the client config.
|
|
84
|
+
if (sessionData.mfaVerified === false && !sessionData.idpRefreshToken) {
|
|
85
|
+
requires2FA = true;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// CRITICAL: Check if MFA has expired (2FA TTL enforcement)
|
|
89
|
+
// The session may have mfaVerified=true from days ago, but if mfaExpiresAt
|
|
90
|
+
// has passed, we must treat 2FA as incomplete to force re-verification.
|
|
91
|
+
const mfaExpiresAt = sessionData.mfaExpiresAt || 0;
|
|
92
|
+
const mfaExpired = mfaExpiresAt > 0 && mfaExpiresAt < Date.now();
|
|
93
|
+
// Check both field names for compatibility (mfaVerified is the normalized name)
|
|
94
|
+
const sessionMfaComplete = sessionData.mfaVerified ?? (sessionData as any).twoFactorComplete ?? false;
|
|
95
|
+
const effectiveTwoFactorComplete = sessionMfaComplete && !mfaExpired;
|
|
96
|
+
|
|
97
|
+
console.log('[VIABILITY] Session 2FA check:', {
|
|
98
|
+
sessionToken: sessionToken.substring(0, 8) + '...',
|
|
99
|
+
mfaVerified: sessionData.mfaVerified,
|
|
100
|
+
twoFactorComplete: (sessionData as any).twoFactorComplete,
|
|
101
|
+
sessionMfaComplete,
|
|
102
|
+
mfaExpired,
|
|
103
|
+
effectiveTwoFactorComplete,
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
if (mfaExpired && sessionMfaComplete) {
|
|
107
|
+
console.warn('[API Viability] MFA expired - forcing 2FA re-verification', {
|
|
108
|
+
mfaExpiresAt: new Date(mfaExpiresAt).toISOString(),
|
|
109
|
+
now: new Date().toISOString(),
|
|
110
|
+
hoursExpiredAgo: ((Date.now() - mfaExpiresAt) / (1000 * 60 * 60)).toFixed(1)
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const response = {
|
|
115
|
+
authenticated: true,
|
|
116
|
+
sessionToken, // Include token for middleware tracking
|
|
117
|
+
// 2FA fields - critical for middleware redirect logic
|
|
118
|
+
requires2FA, // From cached client config (client-wide setting)
|
|
119
|
+
twoFactorComplete: effectiveTwoFactorComplete, // From session, BUT respects MFA TTL
|
|
120
|
+
// Token status for refresh decisions
|
|
121
|
+
accessTokenExpired,
|
|
122
|
+
hasRefreshToken: !!sessionData.idpRefreshToken
|
|
123
|
+
};
|
|
124
|
+
return NextResponse.json(response);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// CRITICAL: Cookie exists but Redis session is missing (stale cookie state)
|
|
128
|
+
// Return sessionToken so middleware can detect this and clear the stale cookie
|
|
129
|
+
console.warn('[VIABILITY] Stale cookie detected - session not in Redis');
|
|
130
|
+
return NextResponse.json({
|
|
131
|
+
authenticated: false,
|
|
132
|
+
sessionToken // Include token to enable stale cookie detection
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// If there's no token at all, it's not authenticated
|
|
137
|
+
return NextResponse.json({ authenticated: false });
|
|
138
|
+
|
|
139
|
+
} catch (error) {
|
|
140
|
+
console.error('[API Viability] Error checking session viability:', error);
|
|
141
|
+
return NextResponse.json({ authenticated: false }, { status: 500 });
|
|
142
|
+
}
|
|
143
143
|
}
|
package/src/auth/better-auth.ts
CHANGED
|
@@ -18,7 +18,6 @@ import type { IDPClientConfig } from '../lib/idp-client-config';
|
|
|
18
18
|
import { getIDPClientConfig } from '../lib/idp-client-config';
|
|
19
19
|
import { getAppSlug } from '../lib/app-slug';
|
|
20
20
|
import { getRedis } from '../lib/redis';
|
|
21
|
-
import { SESSION_TTL } from '../lib/session-store';
|
|
22
21
|
|
|
23
22
|
/**
|
|
24
23
|
* Better Auth social provider config shape.
|
|
@@ -124,19 +123,15 @@ export function createBetterAuthInstance(
|
|
|
124
123
|
// Redis-backed session storage via secondaryStorage
|
|
125
124
|
secondaryStorage: {
|
|
126
125
|
get: async (key: string) => {
|
|
127
|
-
|
|
128
|
-
return await getRedis().get(`ba:${appSlug}:${key}`);
|
|
129
|
-
} catch { return null; }
|
|
126
|
+
return await getRedis().get(`ba:${appSlug}:${key}`);
|
|
130
127
|
},
|
|
131
128
|
set: async (key: string, value: string, ttl?: number) => {
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
}
|
|
139
|
-
} catch { /* Redis unavailable — cookie cache still works */ }
|
|
129
|
+
const redis = getRedis();
|
|
130
|
+
if (ttl) {
|
|
131
|
+
await redis.setex(`ba:${appSlug}:${key}`, ttl, value);
|
|
132
|
+
} else {
|
|
133
|
+
await redis.setex(`ba:${appSlug}:${key}`, 7 * 24 * 60 * 60, value);
|
|
134
|
+
}
|
|
140
135
|
},
|
|
141
136
|
delete: async (key: string) => {
|
|
142
137
|
try {
|
|
@@ -365,14 +360,14 @@ export async function exchangeOAuthForIdpTokens(
|
|
|
365
360
|
|
|
366
361
|
// Store in BA Redis session (for decodeSession)
|
|
367
362
|
baData.idpTokens = idpTokenData;
|
|
368
|
-
await getRedis().setex(baKey,
|
|
363
|
+
await getRedis().setex(baKey, 7 * 24 * 60 * 60, JSON.stringify(baData));
|
|
369
364
|
|
|
370
365
|
// Write to canonical session store so refresh handler and token lifecycle can find the tokens.
|
|
371
366
|
// Key format: {sessionPrefix}{token} — same key that getSession() reads from.
|
|
372
367
|
try {
|
|
373
368
|
const { getSessionPrefix } = await import('../lib/app-slug');
|
|
374
369
|
const canonicalKey = `${getSessionPrefix()}${sessionToken}`;
|
|
375
|
-
await getRedis().setex(canonicalKey,
|
|
370
|
+
await getRedis().setex(canonicalKey, 7 * 24 * 60 * 60, JSON.stringify({
|
|
376
371
|
...idpTokenData,
|
|
377
372
|
oauthProvider: provider,
|
|
378
373
|
}));
|
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
// src/auth/unauthenticated-routes.ts
|
|
2
|
-
// Re-exporting from route-config for backward compatibility
|
|
1
|
+
// src/auth/unauthenticated-routes.ts
|
|
2
|
+
// Re-exporting from route-config for backward compatibility
|
|
3
3
|
export * from './route-config';
|
|
@@ -1,70 +1,70 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Better Auth Client.
|
|
3
|
-
*
|
|
4
|
-
* Import from '@payez/next-mvp/client/better-auth-client'.
|
|
5
|
-
*
|
|
6
|
-
* Includes useSessionCompat() — returns a { data, status } shape so existing
|
|
7
|
-
* components written against the legacy hook don't need destructure changes.
|
|
8
|
-
*/
|
|
9
|
-
|
|
10
|
-
import { createAuthClient } from 'better-auth/react';
|
|
11
|
-
import { magicLinkClient } from 'better-auth/client/plugins';
|
|
12
|
-
import { useMemo } from 'react';
|
|
13
|
-
|
|
14
|
-
export const authClient = createAuthClient({
|
|
15
|
-
// baseURL derived from BETTER_AUTH_URL or window.location.origin
|
|
16
|
-
plugins: [magicLinkClient()],
|
|
17
|
-
});
|
|
18
|
-
|
|
19
|
-
// Convenience exports
|
|
20
|
-
export const { useSession, signIn, signOut } = authClient;
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
/**
|
|
24
|
-
* NextAuth-compatible useSession wrapper.
|
|
25
|
-
*
|
|
26
|
-
* Maps Better Auth's { data, error, isPending } to NextAuth's { data, status, update }.
|
|
27
|
-
* Drop-in replacement — no destructure changes needed in consuming components.
|
|
28
|
-
*/
|
|
29
|
-
export function useSessionCompat() {
|
|
30
|
-
const baSession = authClient.useSession();
|
|
31
|
-
|
|
32
|
-
const status = useMemo(() => {
|
|
33
|
-
if (baSession.isPending) return 'loading' as const;
|
|
34
|
-
if (baSession.data) return 'authenticated' as const;
|
|
35
|
-
return 'unauthenticated' as const;
|
|
36
|
-
}, [baSession.isPending, baSession.data]);
|
|
37
|
-
|
|
38
|
-
// Map Better Auth session shape to NextAuth session shape
|
|
39
|
-
const data = useMemo(() => {
|
|
40
|
-
if (!baSession.data) return null;
|
|
41
|
-
return {
|
|
42
|
-
...baSession.data,
|
|
43
|
-
user: baSession.data.user,
|
|
44
|
-
expires: '', // Better Auth handles expiry differently
|
|
45
|
-
};
|
|
46
|
-
}, [baSession.data]);
|
|
47
|
-
|
|
48
|
-
return {
|
|
49
|
-
data,
|
|
50
|
-
status,
|
|
51
|
-
update: async () => {
|
|
52
|
-
// Better Auth doesn't have a direct "refresh session" call.
|
|
53
|
-
// Force refetch by invalidating the query.
|
|
54
|
-
// TODO: Wire to proper session refresh when available.
|
|
55
|
-
return data;
|
|
56
|
-
},
|
|
57
|
-
};
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
/**
|
|
61
|
-
* NextAuth-compatible signOut wrapper.
|
|
62
|
-
*
|
|
63
|
-
* Maps NextAuth signOut({ redirect, callbackUrl }) to Better Auth.
|
|
64
|
-
*/
|
|
65
|
-
export async function signOutCompat(options?: { redirect?: boolean; callbackUrl?: string }) {
|
|
66
|
-
await authClient.signOut();
|
|
67
|
-
if (options?.redirect !== false && options?.callbackUrl) {
|
|
68
|
-
window.location.href = options.callbackUrl;
|
|
69
|
-
}
|
|
70
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* Better Auth Client.
|
|
3
|
+
*
|
|
4
|
+
* Import from '@payez/next-mvp/client/better-auth-client'.
|
|
5
|
+
*
|
|
6
|
+
* Includes useSessionCompat() — returns a { data, status } shape so existing
|
|
7
|
+
* components written against the legacy hook don't need destructure changes.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { createAuthClient } from 'better-auth/react';
|
|
11
|
+
import { magicLinkClient } from 'better-auth/client/plugins';
|
|
12
|
+
import { useMemo } from 'react';
|
|
13
|
+
|
|
14
|
+
export const authClient = createAuthClient({
|
|
15
|
+
// baseURL derived from BETTER_AUTH_URL or window.location.origin
|
|
16
|
+
plugins: [magicLinkClient()],
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
// Convenience exports
|
|
20
|
+
export const { useSession, signIn, signOut } = authClient;
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* NextAuth-compatible useSession wrapper.
|
|
25
|
+
*
|
|
26
|
+
* Maps Better Auth's { data, error, isPending } to NextAuth's { data, status, update }.
|
|
27
|
+
* Drop-in replacement — no destructure changes needed in consuming components.
|
|
28
|
+
*/
|
|
29
|
+
export function useSessionCompat() {
|
|
30
|
+
const baSession = authClient.useSession();
|
|
31
|
+
|
|
32
|
+
const status = useMemo(() => {
|
|
33
|
+
if (baSession.isPending) return 'loading' as const;
|
|
34
|
+
if (baSession.data) return 'authenticated' as const;
|
|
35
|
+
return 'unauthenticated' as const;
|
|
36
|
+
}, [baSession.isPending, baSession.data]);
|
|
37
|
+
|
|
38
|
+
// Map Better Auth session shape to NextAuth session shape
|
|
39
|
+
const data = useMemo(() => {
|
|
40
|
+
if (!baSession.data) return null;
|
|
41
|
+
return {
|
|
42
|
+
...baSession.data,
|
|
43
|
+
user: baSession.data.user,
|
|
44
|
+
expires: '', // Better Auth handles expiry differently
|
|
45
|
+
};
|
|
46
|
+
}, [baSession.data]);
|
|
47
|
+
|
|
48
|
+
return {
|
|
49
|
+
data,
|
|
50
|
+
status,
|
|
51
|
+
update: async () => {
|
|
52
|
+
// Better Auth doesn't have a direct "refresh session" call.
|
|
53
|
+
// Force refetch by invalidating the query.
|
|
54
|
+
// TODO: Wire to proper session refresh when available.
|
|
55
|
+
return data;
|
|
56
|
+
},
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* NextAuth-compatible signOut wrapper.
|
|
62
|
+
*
|
|
63
|
+
* Maps NextAuth signOut({ redirect, callbackUrl }) to Better Auth.
|
|
64
|
+
*/
|
|
65
|
+
export async function signOutCompat(options?: { redirect?: boolean; callbackUrl?: string }) {
|
|
66
|
+
await authClient.signOut();
|
|
67
|
+
if (options?.redirect !== false && options?.callbackUrl) {
|
|
68
|
+
window.location.href = options.callbackUrl;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
@@ -1,21 +1,21 @@
|
|
|
1
|
-
import { useRef } from 'react';
|
|
2
|
-
|
|
3
|
-
export async function fetchWithSession(input: RequestInfo | URL, init: RequestInit = {}, opts: { retry?: number } = {}): Promise<Response> {
|
|
4
|
-
const retry = opts.retry ?? 1;
|
|
5
|
-
const doFetch = async (): Promise<Response> => {
|
|
6
|
-
const res = await fetch(input, { ...init, credentials: 'include' });
|
|
7
|
-
if (res.ok) return res;
|
|
8
|
-
if (res.status === 401 && retry > 0) {
|
|
9
|
-
const rf = await fetch('/api/auth/refresh', { method: 'POST', headers: { 'Accept': 'application/json' }, credentials: 'include' });
|
|
10
|
-
if (rf.ok) return fetchWithSession(input, init, { retry: retry - 1 });
|
|
11
|
-
}
|
|
12
|
-
if ((res.status === 409 || res.status === 503) && retry > 0) {
|
|
13
|
-
const retryAfter = res.headers.get('Retry-After');
|
|
14
|
-
const waitMs = retryAfter ? parseInt(retryAfter) * 1000 : 1000;
|
|
15
|
-
await new Promise(r => setTimeout(r, Math.min(waitMs, 3000)));
|
|
16
|
-
return fetchWithSession(input, init, { retry: retry - 1 });
|
|
17
|
-
}
|
|
18
|
-
return res;
|
|
19
|
-
};
|
|
20
|
-
return doFetch();
|
|
21
|
-
}
|
|
1
|
+
import { useRef } from 'react';
|
|
2
|
+
|
|
3
|
+
export async function fetchWithSession(input: RequestInfo | URL, init: RequestInit = {}, opts: { retry?: number } = {}): Promise<Response> {
|
|
4
|
+
const retry = opts.retry ?? 1;
|
|
5
|
+
const doFetch = async (): Promise<Response> => {
|
|
6
|
+
const res = await fetch(input, { ...init, credentials: 'include' });
|
|
7
|
+
if (res.ok) return res;
|
|
8
|
+
if (res.status === 401 && retry > 0) {
|
|
9
|
+
const rf = await fetch('/api/auth/refresh', { method: 'POST', headers: { 'Accept': 'application/json' }, credentials: 'include' });
|
|
10
|
+
if (rf.ok) return fetchWithSession(input, init, { retry: retry - 1 });
|
|
11
|
+
}
|
|
12
|
+
if ((res.status === 409 || res.status === 503) && retry > 0) {
|
|
13
|
+
const retryAfter = res.headers.get('Retry-After');
|
|
14
|
+
const waitMs = retryAfter ? parseInt(retryAfter) * 1000 : 1000;
|
|
15
|
+
await new Promise(r => setTimeout(r, Math.min(waitMs, 3000)));
|
|
16
|
+
return fetchWithSession(input, init, { retry: retry - 1 });
|
|
17
|
+
}
|
|
18
|
+
return res;
|
|
19
|
+
};
|
|
20
|
+
return doFetch();
|
|
21
|
+
}
|
package/src/client/index.ts
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Client-Side Exports
|
|
3
|
-
*
|
|
4
|
-
* This module exports only client-safe code for use in browser environments.
|
|
5
|
-
* Server-only utilities and Node.js dependencies are excluded.
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
// Client-side fetch utility
|
|
9
|
-
export { fetchWithAuth } from './fetch-with-auth';
|
|
10
|
-
|
|
11
|
-
// Authentication context and hooks
|
|
12
|
-
export { AuthProvider, useAuthConfig, useAuthMode, useFederatedProviders, useFederatedAuthEnabled, useTraditionalAuthEnabled } from './AuthContext';
|
|
13
|
-
export type { AuthConfig } from '../types/auth';
|
|
1
|
+
/**
|
|
2
|
+
* Client-Side Exports
|
|
3
|
+
*
|
|
4
|
+
* This module exports only client-safe code for use in browser environments.
|
|
5
|
+
* Server-only utilities and Node.js dependencies are excluded.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
// Client-side fetch utility
|
|
9
|
+
export { fetchWithAuth } from './fetch-with-auth';
|
|
10
|
+
|
|
11
|
+
// Authentication context and hooks
|
|
12
|
+
export { AuthProvider, useAuthConfig, useAuthMode, useFederatedProviders, useFederatedAuthEnabled, useTraditionalAuthEnabled } from './AuthContext';
|
|
13
|
+
export type { AuthConfig } from '../types/auth';
|