@payez/next-mvp 4.0.23 → 4.0.25
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/auth/better-auth.d.ts +10 -7
- package/dist/auth/better-auth.js +95 -86
- package/dist/client/better-auth-client.d.ts +0 -8
- package/dist/client/better-auth-client.js +0 -15
- package/dist/components/account/MobileNavDrawer.js +1 -1
- package/dist/server/auth.d.ts +0 -7
- package/package.json +1 -1
- package/src/auth/better-auth.ts +103 -89
- package/src/client/better-auth-client.ts +0 -13
- package/src/components/account/MobileNavDrawer.tsx +2 -2
|
@@ -45,13 +45,6 @@ export declare function createBetterAuthInstance(idpConfig: IDPClientConfig): im
|
|
|
45
45
|
refreshCache: false;
|
|
46
46
|
};
|
|
47
47
|
};
|
|
48
|
-
databaseHooks: {
|
|
49
|
-
session: {
|
|
50
|
-
create: {
|
|
51
|
-
after: (session: any) => Promise<void>;
|
|
52
|
-
};
|
|
53
|
-
};
|
|
54
|
-
};
|
|
55
48
|
advanced: {
|
|
56
49
|
cookiePrefix: string;
|
|
57
50
|
cookies: {
|
|
@@ -105,3 +98,13 @@ export declare function getBetterAuthHandler(): Promise<{
|
|
|
105
98
|
GET: (req: Request) => Promise<Response>;
|
|
106
99
|
POST: (req: Request) => Promise<Response>;
|
|
107
100
|
} | null>;
|
|
101
|
+
/**
|
|
102
|
+
* Exchange OAuth identity for IDP tokens and store in the BA Redis session.
|
|
103
|
+
*
|
|
104
|
+
* Call this from the OAuth callback route AFTER better-auth has processed the
|
|
105
|
+
* callback and created the session. Reads the session token from the Set-Cookie
|
|
106
|
+
* header of the response to find the BA Redis key.
|
|
107
|
+
*
|
|
108
|
+
* This replaces the old databaseHooks approach which doesn't fire in stateless mode.
|
|
109
|
+
*/
|
|
110
|
+
export declare function exchangeOAuthForIdpTokens(sessionToken: string, provider?: string): Promise<boolean>;
|
package/dist/auth/better-auth.js
CHANGED
|
@@ -16,6 +16,7 @@ exports.createBetterAuthInstance = createBetterAuthInstance;
|
|
|
16
16
|
exports.isBetterAuthEnabled = isBetterAuthEnabled;
|
|
17
17
|
exports.getBetterAuthInstance = getBetterAuthInstance;
|
|
18
18
|
exports.getBetterAuthHandler = getBetterAuthHandler;
|
|
19
|
+
exports.exchangeOAuthForIdpTokens = exchangeOAuthForIdpTokens;
|
|
19
20
|
require("server-only");
|
|
20
21
|
const better_auth_1 = require("better-auth");
|
|
21
22
|
const next_js_1 = require("better-auth/next-js");
|
|
@@ -49,17 +50,20 @@ function buildBetterAuthProviders(config) {
|
|
|
49
50
|
function createBetterAuthInstance(idpConfig) {
|
|
50
51
|
const appSlug = idpConfig.clientSlug || (0, app_slug_1.getAppSlug)();
|
|
51
52
|
// Resolve base URL: BETTER_AUTH_URL env > IDP config > localhost fallback
|
|
52
|
-
|
|
53
|
+
// Must include /api/auth since that's where the catch-all route is mounted
|
|
54
|
+
const rawBaseURL = process.env.BETTER_AUTH_URL
|
|
53
55
|
|| idpConfig.baseClientUrl
|
|
54
56
|
|| `http://localhost:${process.env.PORT || '3000'}`;
|
|
57
|
+
const baseURL = rawBaseURL.replace(/\/+$/, '') + '/api/auth';
|
|
55
58
|
return (0, better_auth_1.betterAuth)({
|
|
56
59
|
baseURL,
|
|
57
60
|
secret: idpConfig.nextAuthSecret,
|
|
58
61
|
socialProviders: buildBetterAuthProviders(idpConfig),
|
|
59
62
|
// Trust the app's own origin + any configured base URL
|
|
60
63
|
trustedOrigins: [
|
|
64
|
+
rawBaseURL,
|
|
61
65
|
baseURL,
|
|
62
|
-
...(idpConfig.baseClientUrl
|
|
66
|
+
...(idpConfig.baseClientUrl ? [idpConfig.baseClientUrl] : []),
|
|
63
67
|
'http://localhost:3000',
|
|
64
68
|
'http://localhost:3400',
|
|
65
69
|
'http://localhost:3600',
|
|
@@ -100,90 +104,6 @@ function createBetterAuthInstance(idpConfig) {
|
|
|
100
104
|
refreshCache: false,
|
|
101
105
|
},
|
|
102
106
|
},
|
|
103
|
-
// After social login, exchange Google identity for IDP tokens and store in Redis
|
|
104
|
-
databaseHooks: {
|
|
105
|
-
session: {
|
|
106
|
-
create: {
|
|
107
|
-
after: async (session) => {
|
|
108
|
-
try {
|
|
109
|
-
const userId = session.userId;
|
|
110
|
-
const token = session.token;
|
|
111
|
-
if (!userId || !token)
|
|
112
|
-
return;
|
|
113
|
-
// Look up user from Better Auth's memory/DB to get email
|
|
114
|
-
// The user was just created/found by Better Auth during OAuth
|
|
115
|
-
const baKey = `ba:${appSlug}:${token}`;
|
|
116
|
-
const baRaw = await (0, redis_1.getRedis)().get(baKey).catch(() => null);
|
|
117
|
-
const baData = baRaw ? JSON.parse(baRaw) : null;
|
|
118
|
-
const email = baData?.user?.email;
|
|
119
|
-
const name = baData?.user?.name;
|
|
120
|
-
const image = baData?.user?.image;
|
|
121
|
-
if (!email) {
|
|
122
|
-
console.warn('[BETTER_AUTH] Session created but no email found for IDP token exchange');
|
|
123
|
-
return;
|
|
124
|
-
}
|
|
125
|
-
// Call IDP oauth-callback to get IDP tokens
|
|
126
|
-
const idpUrl = process.env.IDP_URL || '';
|
|
127
|
-
if (!idpUrl) {
|
|
128
|
-
console.warn('[BETTER_AUTH] No IDP URL configured, skipping token exchange');
|
|
129
|
-
return;
|
|
130
|
-
}
|
|
131
|
-
const oauthRes = await fetch(`${idpUrl}/api/ExternalAuth/oauth-callback`, {
|
|
132
|
-
method: 'POST',
|
|
133
|
-
headers: { 'Content-Type': 'application/json' },
|
|
134
|
-
body: JSON.stringify({
|
|
135
|
-
provider: 'google',
|
|
136
|
-
provider_account_id: userId,
|
|
137
|
-
email,
|
|
138
|
-
name,
|
|
139
|
-
image,
|
|
140
|
-
client_id: idpConfig.clientSlug || String(idpConfig.clientId),
|
|
141
|
-
}),
|
|
142
|
-
});
|
|
143
|
-
const oauthResText = await oauthRes.text();
|
|
144
|
-
console.log('[BETTER_AUTH] IDP oauth-callback response:', oauthRes.status, oauthResText.substring(0, 500));
|
|
145
|
-
if (!oauthRes.ok) {
|
|
146
|
-
console.error('[BETTER_AUTH] IDP oauth-callback failed:', oauthRes.status);
|
|
147
|
-
return;
|
|
148
|
-
}
|
|
149
|
-
let idpData;
|
|
150
|
-
try {
|
|
151
|
-
idpData = JSON.parse(oauthResText);
|
|
152
|
-
}
|
|
153
|
-
catch {
|
|
154
|
-
return;
|
|
155
|
-
}
|
|
156
|
-
const result = idpData?.data?.result || idpData?.data || idpData;
|
|
157
|
-
if (!result?.access_token) {
|
|
158
|
-
console.warn('[BETTER_AUTH] IDP oauth-callback returned no access_token. Keys:', Object.keys(result || {}));
|
|
159
|
-
return;
|
|
160
|
-
}
|
|
161
|
-
// Store IDP tokens in the BA Redis session
|
|
162
|
-
if (baData) {
|
|
163
|
-
const requiresTwoFactor = result.user?.requiresTwoFactor ?? result.requiresTwoFactor ?? false;
|
|
164
|
-
baData.idpTokens = {
|
|
165
|
-
idpAccessToken: result.access_token,
|
|
166
|
-
idpRefreshToken: result.refresh_token,
|
|
167
|
-
idpAccessTokenExpires: result.expires_in
|
|
168
|
-
? Date.now() + result.expires_in * 1000
|
|
169
|
-
: Date.now() + 15 * 60 * 1000,
|
|
170
|
-
userId: String(result.user?.user_id || result.user?.id || result.user_id || userId),
|
|
171
|
-
email: result.user?.email || result.email || email,
|
|
172
|
-
name: result.user?.full_name || result.user?.name || result.name || name,
|
|
173
|
-
roles: result.user?.roles || result.roles || [],
|
|
174
|
-
mfaVerified: !requiresTwoFactor,
|
|
175
|
-
};
|
|
176
|
-
await (0, redis_1.getRedis)().setex(baKey, 7 * 24 * 60 * 60, JSON.stringify(baData));
|
|
177
|
-
console.log('[BETTER_AUTH] IDP tokens stored in session for', email);
|
|
178
|
-
}
|
|
179
|
-
}
|
|
180
|
-
catch (err) {
|
|
181
|
-
console.error('[BETTER_AUTH] Post-login IDP exchange failed:', err instanceof Error ? err.message : String(err));
|
|
182
|
-
}
|
|
183
|
-
},
|
|
184
|
-
},
|
|
185
|
-
},
|
|
186
|
-
},
|
|
187
107
|
// Cookie prefix must match slim-middleware expectations ({slug}.session-token)
|
|
188
108
|
advanced: {
|
|
189
109
|
cookiePrefix: appSlug,
|
|
@@ -248,3 +168,92 @@ async function getBetterAuthHandler() {
|
|
|
248
168
|
const auth = await getBetterAuthInstance();
|
|
249
169
|
return (0, next_js_2.toNextJsHandler)(auth);
|
|
250
170
|
}
|
|
171
|
+
/**
|
|
172
|
+
* Exchange OAuth identity for IDP tokens and store in the BA Redis session.
|
|
173
|
+
*
|
|
174
|
+
* Call this from the OAuth callback route AFTER better-auth has processed the
|
|
175
|
+
* callback and created the session. Reads the session token from the Set-Cookie
|
|
176
|
+
* header of the response to find the BA Redis key.
|
|
177
|
+
*
|
|
178
|
+
* This replaces the old databaseHooks approach which doesn't fire in stateless mode.
|
|
179
|
+
*/
|
|
180
|
+
async function exchangeOAuthForIdpTokens(sessionToken, provider = 'google') {
|
|
181
|
+
try {
|
|
182
|
+
const config = await (0, idp_client_config_1.getIDPClientConfig)();
|
|
183
|
+
const appSlug = config.clientSlug || (0, app_slug_1.getAppSlug)();
|
|
184
|
+
const baKey = `ba:${appSlug}:${sessionToken}`;
|
|
185
|
+
// Read the BA session from Redis
|
|
186
|
+
const baRaw = await (0, redis_1.getRedis)().get(baKey).catch(() => null);
|
|
187
|
+
if (!baRaw) {
|
|
188
|
+
console.warn('[BETTER_AUTH] exchangeOAuthForIdpTokens: session not found in Redis for token', sessionToken.substring(0, 10));
|
|
189
|
+
return false;
|
|
190
|
+
}
|
|
191
|
+
const baData = JSON.parse(baRaw);
|
|
192
|
+
const email = baData?.user?.email;
|
|
193
|
+
const name = baData?.user?.name;
|
|
194
|
+
const image = baData?.user?.image;
|
|
195
|
+
const baUserId = baData?.session?.userId || baData?.user?.id;
|
|
196
|
+
if (!email) {
|
|
197
|
+
console.warn('[BETTER_AUTH] exchangeOAuthForIdpTokens: no email in session');
|
|
198
|
+
return false;
|
|
199
|
+
}
|
|
200
|
+
// Call IDP oauth-callback
|
|
201
|
+
const idpUrl = process.env.IDP_URL || '';
|
|
202
|
+
if (!idpUrl) {
|
|
203
|
+
console.warn('[BETTER_AUTH] No IDP_URL configured, skipping token exchange');
|
|
204
|
+
return false;
|
|
205
|
+
}
|
|
206
|
+
console.log('[BETTER_AUTH] Exchanging OAuth identity for IDP tokens:', email);
|
|
207
|
+
const oauthRes = await fetch(`${idpUrl}/api/ExternalAuth/oauth-callback`, {
|
|
208
|
+
method: 'POST',
|
|
209
|
+
headers: { 'Content-Type': 'application/json' },
|
|
210
|
+
body: JSON.stringify({
|
|
211
|
+
provider,
|
|
212
|
+
provider_account_id: baUserId,
|
|
213
|
+
email,
|
|
214
|
+
name,
|
|
215
|
+
image,
|
|
216
|
+
client_id: config.clientSlug || String(config.clientId),
|
|
217
|
+
}),
|
|
218
|
+
});
|
|
219
|
+
const oauthResText = await oauthRes.text();
|
|
220
|
+
console.log('[BETTER_AUTH] IDP oauth-callback response:', oauthRes.status, oauthResText.substring(0, 500));
|
|
221
|
+
if (!oauthRes.ok) {
|
|
222
|
+
console.error('[BETTER_AUTH] IDP oauth-callback failed:', oauthRes.status);
|
|
223
|
+
return false;
|
|
224
|
+
}
|
|
225
|
+
let idpData;
|
|
226
|
+
try {
|
|
227
|
+
idpData = JSON.parse(oauthResText);
|
|
228
|
+
}
|
|
229
|
+
catch {
|
|
230
|
+
return false;
|
|
231
|
+
}
|
|
232
|
+
const result = idpData?.data?.result || idpData?.data || idpData;
|
|
233
|
+
if (!result?.access_token) {
|
|
234
|
+
console.warn('[BETTER_AUTH] IDP oauth-callback returned no access_token. Keys:', Object.keys(result || {}));
|
|
235
|
+
return false;
|
|
236
|
+
}
|
|
237
|
+
// Store IDP tokens in the BA Redis session
|
|
238
|
+
const requiresTwoFactor = result.user?.requiresTwoFactor ?? result.requiresTwoFactor ?? false;
|
|
239
|
+
baData.idpTokens = {
|
|
240
|
+
idpAccessToken: result.access_token,
|
|
241
|
+
idpRefreshToken: result.refresh_token,
|
|
242
|
+
idpAccessTokenExpires: result.expires_in
|
|
243
|
+
? Date.now() + result.expires_in * 1000
|
|
244
|
+
: Date.now() + 15 * 60 * 1000,
|
|
245
|
+
userId: String(result.user?.user_id || result.user?.id || result.user_id || baUserId),
|
|
246
|
+
email: result.user?.email || result.email || email,
|
|
247
|
+
name: result.user?.full_name || result.user?.name || result.name || name,
|
|
248
|
+
roles: result.user?.roles || result.roles || [],
|
|
249
|
+
mfaVerified: !requiresTwoFactor,
|
|
250
|
+
};
|
|
251
|
+
await (0, redis_1.getRedis)().setex(baKey, 7 * 24 * 60 * 60, JSON.stringify(baData));
|
|
252
|
+
console.log('[BETTER_AUTH] IDP tokens stored in session for', email);
|
|
253
|
+
return true;
|
|
254
|
+
}
|
|
255
|
+
catch (err) {
|
|
256
|
+
console.error('[BETTER_AUTH] IDP token exchange failed:', err instanceof Error ? err.message : String(err));
|
|
257
|
+
return false;
|
|
258
|
+
}
|
|
259
|
+
}
|
|
@@ -956,14 +956,6 @@ export declare const useSession: () => {
|
|
|
956
956
|
code?: string | undefined;
|
|
957
957
|
message?: string | undefined;
|
|
958
958
|
}, FetchOptions["throw"] extends true ? true : false>>;
|
|
959
|
-
/**
|
|
960
|
-
* Sign in with a specific OAuth provider via direct redirect.
|
|
961
|
-
*
|
|
962
|
-
* better-auth exposes per-provider endpoints at /api/auth/sign-in/{provider}
|
|
963
|
-
* (e.g. /api/auth/sign-in/google). The generic signIn.social() method POSTs
|
|
964
|
-
* to /api/auth/sign-in/social which doesn't exist — use this instead.
|
|
965
|
-
*/
|
|
966
|
-
export declare function signInWithProvider(provider: string, callbackURL?: string): void;
|
|
967
959
|
/**
|
|
968
960
|
* NextAuth-compatible useSession wrapper.
|
|
969
961
|
*
|
|
@@ -10,7 +10,6 @@
|
|
|
10
10
|
*/
|
|
11
11
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
12
|
exports.signOut = exports.signIn = exports.useSession = exports.authClient = void 0;
|
|
13
|
-
exports.signInWithProvider = signInWithProvider;
|
|
14
13
|
exports.useSessionCompat = useSessionCompat;
|
|
15
14
|
exports.signOutCompat = signOutCompat;
|
|
16
15
|
const react_1 = require("better-auth/react");
|
|
@@ -20,20 +19,6 @@ exports.authClient = (0, react_1.createAuthClient)({
|
|
|
20
19
|
});
|
|
21
20
|
// Convenience exports
|
|
22
21
|
exports.useSession = exports.authClient.useSession, exports.signIn = exports.authClient.signIn, exports.signOut = exports.authClient.signOut;
|
|
23
|
-
/**
|
|
24
|
-
* Sign in with a specific OAuth provider via direct redirect.
|
|
25
|
-
*
|
|
26
|
-
* better-auth exposes per-provider endpoints at /api/auth/sign-in/{provider}
|
|
27
|
-
* (e.g. /api/auth/sign-in/google). The generic signIn.social() method POSTs
|
|
28
|
-
* to /api/auth/sign-in/social which doesn't exist — use this instead.
|
|
29
|
-
*/
|
|
30
|
-
function signInWithProvider(provider, callbackURL) {
|
|
31
|
-
const params = new URLSearchParams();
|
|
32
|
-
if (callbackURL)
|
|
33
|
-
params.set('callbackURL', callbackURL);
|
|
34
|
-
const qs = params.toString();
|
|
35
|
-
window.location.href = `/api/auth/sign-in/${provider}${qs ? '?' + qs : ''}`;
|
|
36
|
-
}
|
|
37
22
|
/**
|
|
38
23
|
* NextAuth-compatible useSession wrapper.
|
|
39
24
|
*
|
|
@@ -44,7 +44,7 @@ function MobileNavDrawer({ isOpen, onClose, navItems, customSections, basePath =
|
|
|
44
44
|
onSignIn();
|
|
45
45
|
}
|
|
46
46
|
else {
|
|
47
|
-
|
|
47
|
+
better_auth_client_1.authClient.signIn.social({ provider: 'google', callbackURL: signInCallbackUrl });
|
|
48
48
|
}
|
|
49
49
|
};
|
|
50
50
|
const handleSectionItemClick = (item) => {
|
package/dist/server/auth.d.ts
CHANGED
|
@@ -28,13 +28,6 @@ export declare function getAuthInstance(): Promise<import("better-auth/types").A
|
|
|
28
28
|
refreshCache: false;
|
|
29
29
|
};
|
|
30
30
|
};
|
|
31
|
-
databaseHooks: {
|
|
32
|
-
session: {
|
|
33
|
-
create: {
|
|
34
|
-
after: (session: any) => Promise<void>;
|
|
35
|
-
};
|
|
36
|
-
};
|
|
37
|
-
};
|
|
38
31
|
advanced: {
|
|
39
32
|
cookiePrefix: string;
|
|
40
33
|
cookies: {
|
package/package.json
CHANGED
package/src/auth/better-auth.ts
CHANGED
|
@@ -58,9 +58,11 @@ export function createBetterAuthInstance(idpConfig: IDPClientConfig) {
|
|
|
58
58
|
const appSlug = idpConfig.clientSlug || getAppSlug();
|
|
59
59
|
|
|
60
60
|
// Resolve base URL: BETTER_AUTH_URL env > IDP config > localhost fallback
|
|
61
|
-
|
|
61
|
+
// Must include /api/auth since that's where the catch-all route is mounted
|
|
62
|
+
const rawBaseURL = process.env.BETTER_AUTH_URL
|
|
62
63
|
|| idpConfig.baseClientUrl
|
|
63
64
|
|| `http://localhost:${process.env.PORT || '3000'}`;
|
|
65
|
+
const baseURL = rawBaseURL.replace(/\/+$/, '') + '/api/auth';
|
|
64
66
|
|
|
65
67
|
return betterAuth({
|
|
66
68
|
baseURL,
|
|
@@ -70,8 +72,9 @@ export function createBetterAuthInstance(idpConfig: IDPClientConfig) {
|
|
|
70
72
|
|
|
71
73
|
// Trust the app's own origin + any configured base URL
|
|
72
74
|
trustedOrigins: [
|
|
75
|
+
rawBaseURL,
|
|
73
76
|
baseURL,
|
|
74
|
-
...(idpConfig.baseClientUrl
|
|
77
|
+
...(idpConfig.baseClientUrl ? [idpConfig.baseClientUrl] : []),
|
|
75
78
|
'http://localhost:3000',
|
|
76
79
|
'http://localhost:3400',
|
|
77
80
|
'http://localhost:3600',
|
|
@@ -109,93 +112,6 @@ export function createBetterAuthInstance(idpConfig: IDPClientConfig) {
|
|
|
109
112
|
},
|
|
110
113
|
},
|
|
111
114
|
|
|
112
|
-
// After social login, exchange Google identity for IDP tokens and store in Redis
|
|
113
|
-
databaseHooks: {
|
|
114
|
-
session: {
|
|
115
|
-
create: {
|
|
116
|
-
after: async (session: any) => {
|
|
117
|
-
try {
|
|
118
|
-
const userId = session.userId;
|
|
119
|
-
const token = session.token;
|
|
120
|
-
if (!userId || !token) return;
|
|
121
|
-
|
|
122
|
-
// Look up user from Better Auth's memory/DB to get email
|
|
123
|
-
// The user was just created/found by Better Auth during OAuth
|
|
124
|
-
const baKey = `ba:${appSlug}:${token}`;
|
|
125
|
-
const baRaw = await getRedis().get(baKey).catch(() => null);
|
|
126
|
-
const baData = baRaw ? JSON.parse(baRaw) : null;
|
|
127
|
-
const email = baData?.user?.email;
|
|
128
|
-
const name = baData?.user?.name;
|
|
129
|
-
const image = baData?.user?.image;
|
|
130
|
-
|
|
131
|
-
if (!email) {
|
|
132
|
-
console.warn('[BETTER_AUTH] Session created but no email found for IDP token exchange');
|
|
133
|
-
return;
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
// Call IDP oauth-callback to get IDP tokens
|
|
137
|
-
const idpUrl = process.env.IDP_URL || '';
|
|
138
|
-
if (!idpUrl) {
|
|
139
|
-
console.warn('[BETTER_AUTH] No IDP URL configured, skipping token exchange');
|
|
140
|
-
return;
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
const oauthRes = await fetch(`${idpUrl}/api/ExternalAuth/oauth-callback`, {
|
|
144
|
-
method: 'POST',
|
|
145
|
-
headers: { 'Content-Type': 'application/json' },
|
|
146
|
-
body: JSON.stringify({
|
|
147
|
-
provider: 'google',
|
|
148
|
-
provider_account_id: userId,
|
|
149
|
-
email,
|
|
150
|
-
name,
|
|
151
|
-
image,
|
|
152
|
-
client_id: idpConfig.clientSlug || String(idpConfig.clientId),
|
|
153
|
-
}),
|
|
154
|
-
});
|
|
155
|
-
|
|
156
|
-
const oauthResText = await oauthRes.text();
|
|
157
|
-
console.log('[BETTER_AUTH] IDP oauth-callback response:', oauthRes.status, oauthResText.substring(0, 500));
|
|
158
|
-
|
|
159
|
-
if (!oauthRes.ok) {
|
|
160
|
-
console.error('[BETTER_AUTH] IDP oauth-callback failed:', oauthRes.status);
|
|
161
|
-
return;
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
let idpData: any;
|
|
165
|
-
try { idpData = JSON.parse(oauthResText); } catch { return; }
|
|
166
|
-
const result = idpData?.data?.result || idpData?.data || idpData;
|
|
167
|
-
|
|
168
|
-
if (!result?.access_token) {
|
|
169
|
-
console.warn('[BETTER_AUTH] IDP oauth-callback returned no access_token. Keys:', Object.keys(result || {}));
|
|
170
|
-
return;
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
// Store IDP tokens in the BA Redis session
|
|
174
|
-
if (baData) {
|
|
175
|
-
const requiresTwoFactor = result.user?.requiresTwoFactor ?? result.requiresTwoFactor ?? false;
|
|
176
|
-
baData.idpTokens = {
|
|
177
|
-
idpAccessToken: result.access_token,
|
|
178
|
-
idpRefreshToken: result.refresh_token,
|
|
179
|
-
idpAccessTokenExpires: result.expires_in
|
|
180
|
-
? Date.now() + result.expires_in * 1000
|
|
181
|
-
: Date.now() + 15 * 60 * 1000,
|
|
182
|
-
userId: String(result.user?.user_id || result.user?.id || result.user_id || userId),
|
|
183
|
-
email: result.user?.email || result.email || email,
|
|
184
|
-
name: result.user?.full_name || result.user?.name || result.name || name,
|
|
185
|
-
roles: result.user?.roles || result.roles || [],
|
|
186
|
-
mfaVerified: !requiresTwoFactor,
|
|
187
|
-
};
|
|
188
|
-
await getRedis().setex(baKey, 7 * 24 * 60 * 60, JSON.stringify(baData));
|
|
189
|
-
console.log('[BETTER_AUTH] IDP tokens stored in session for', email);
|
|
190
|
-
}
|
|
191
|
-
} catch (err) {
|
|
192
|
-
console.error('[BETTER_AUTH] Post-login IDP exchange failed:', err instanceof Error ? err.message : String(err));
|
|
193
|
-
}
|
|
194
|
-
},
|
|
195
|
-
},
|
|
196
|
-
},
|
|
197
|
-
},
|
|
198
|
-
|
|
199
115
|
// Cookie prefix must match slim-middleware expectations ({slug}.session-token)
|
|
200
116
|
advanced: {
|
|
201
117
|
cookiePrefix: appSlug,
|
|
@@ -269,3 +185,101 @@ export async function getBetterAuthHandler(): Promise<{ GET: (req: Request) => P
|
|
|
269
185
|
const auth = await getBetterAuthInstance();
|
|
270
186
|
return toNextJsHandler(auth);
|
|
271
187
|
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Exchange OAuth identity for IDP tokens and store in the BA Redis session.
|
|
191
|
+
*
|
|
192
|
+
* Call this from the OAuth callback route AFTER better-auth has processed the
|
|
193
|
+
* callback and created the session. Reads the session token from the Set-Cookie
|
|
194
|
+
* header of the response to find the BA Redis key.
|
|
195
|
+
*
|
|
196
|
+
* This replaces the old databaseHooks approach which doesn't fire in stateless mode.
|
|
197
|
+
*/
|
|
198
|
+
export async function exchangeOAuthForIdpTokens(
|
|
199
|
+
sessionToken: string,
|
|
200
|
+
provider: string = 'google'
|
|
201
|
+
): Promise<boolean> {
|
|
202
|
+
try {
|
|
203
|
+
const config = await getIDPClientConfig();
|
|
204
|
+
const appSlug = config.clientSlug || getAppSlug();
|
|
205
|
+
const baKey = `ba:${appSlug}:${sessionToken}`;
|
|
206
|
+
|
|
207
|
+
// Read the BA session from Redis
|
|
208
|
+
const baRaw = await getRedis().get(baKey).catch(() => null);
|
|
209
|
+
if (!baRaw) {
|
|
210
|
+
console.warn('[BETTER_AUTH] exchangeOAuthForIdpTokens: session not found in Redis for token', sessionToken.substring(0, 10));
|
|
211
|
+
return false;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
const baData = JSON.parse(baRaw);
|
|
215
|
+
const email = baData?.user?.email;
|
|
216
|
+
const name = baData?.user?.name;
|
|
217
|
+
const image = baData?.user?.image;
|
|
218
|
+
const baUserId = baData?.session?.userId || baData?.user?.id;
|
|
219
|
+
|
|
220
|
+
if (!email) {
|
|
221
|
+
console.warn('[BETTER_AUTH] exchangeOAuthForIdpTokens: no email in session');
|
|
222
|
+
return false;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// Call IDP oauth-callback
|
|
226
|
+
const idpUrl = process.env.IDP_URL || '';
|
|
227
|
+
if (!idpUrl) {
|
|
228
|
+
console.warn('[BETTER_AUTH] No IDP_URL configured, skipping token exchange');
|
|
229
|
+
return false;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
console.log('[BETTER_AUTH] Exchanging OAuth identity for IDP tokens:', email);
|
|
233
|
+
|
|
234
|
+
const oauthRes = await fetch(`${idpUrl}/api/ExternalAuth/oauth-callback`, {
|
|
235
|
+
method: 'POST',
|
|
236
|
+
headers: { 'Content-Type': 'application/json' },
|
|
237
|
+
body: JSON.stringify({
|
|
238
|
+
provider,
|
|
239
|
+
provider_account_id: baUserId,
|
|
240
|
+
email,
|
|
241
|
+
name,
|
|
242
|
+
image,
|
|
243
|
+
client_id: config.clientSlug || String(config.clientId),
|
|
244
|
+
}),
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
const oauthResText = await oauthRes.text();
|
|
248
|
+
console.log('[BETTER_AUTH] IDP oauth-callback response:', oauthRes.status, oauthResText.substring(0, 500));
|
|
249
|
+
|
|
250
|
+
if (!oauthRes.ok) {
|
|
251
|
+
console.error('[BETTER_AUTH] IDP oauth-callback failed:', oauthRes.status);
|
|
252
|
+
return false;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
let idpData: any;
|
|
256
|
+
try { idpData = JSON.parse(oauthResText); } catch { return false; }
|
|
257
|
+
const result = idpData?.data?.result || idpData?.data || idpData;
|
|
258
|
+
|
|
259
|
+
if (!result?.access_token) {
|
|
260
|
+
console.warn('[BETTER_AUTH] IDP oauth-callback returned no access_token. Keys:', Object.keys(result || {}));
|
|
261
|
+
return false;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// Store IDP tokens in the BA Redis session
|
|
265
|
+
const requiresTwoFactor = result.user?.requiresTwoFactor ?? result.requiresTwoFactor ?? false;
|
|
266
|
+
baData.idpTokens = {
|
|
267
|
+
idpAccessToken: result.access_token,
|
|
268
|
+
idpRefreshToken: result.refresh_token,
|
|
269
|
+
idpAccessTokenExpires: result.expires_in
|
|
270
|
+
? Date.now() + result.expires_in * 1000
|
|
271
|
+
: Date.now() + 15 * 60 * 1000,
|
|
272
|
+
userId: String(result.user?.user_id || result.user?.id || result.user_id || baUserId),
|
|
273
|
+
email: result.user?.email || result.email || email,
|
|
274
|
+
name: result.user?.full_name || result.user?.name || result.name || name,
|
|
275
|
+
roles: result.user?.roles || result.roles || [],
|
|
276
|
+
mfaVerified: !requiresTwoFactor,
|
|
277
|
+
};
|
|
278
|
+
await getRedis().setex(baKey, 7 * 24 * 60 * 60, JSON.stringify(baData));
|
|
279
|
+
console.log('[BETTER_AUTH] IDP tokens stored in session for', email);
|
|
280
|
+
return true;
|
|
281
|
+
} catch (err) {
|
|
282
|
+
console.error('[BETTER_AUTH] IDP token exchange failed:', err instanceof Error ? err.message : String(err));
|
|
283
|
+
return false;
|
|
284
|
+
}
|
|
285
|
+
}
|
|
@@ -18,19 +18,6 @@ export const authClient = createAuthClient({
|
|
|
18
18
|
// Convenience exports
|
|
19
19
|
export const { useSession, signIn, signOut } = authClient;
|
|
20
20
|
|
|
21
|
-
/**
|
|
22
|
-
* Sign in with a specific OAuth provider via direct redirect.
|
|
23
|
-
*
|
|
24
|
-
* better-auth exposes per-provider endpoints at /api/auth/sign-in/{provider}
|
|
25
|
-
* (e.g. /api/auth/sign-in/google). The generic signIn.social() method POSTs
|
|
26
|
-
* to /api/auth/sign-in/social which doesn't exist — use this instead.
|
|
27
|
-
*/
|
|
28
|
-
export function signInWithProvider(provider: string, callbackURL?: string) {
|
|
29
|
-
const params = new URLSearchParams();
|
|
30
|
-
if (callbackURL) params.set('callbackURL', callbackURL);
|
|
31
|
-
const qs = params.toString();
|
|
32
|
-
window.location.href = `/api/auth/sign-in/${provider}${qs ? '?' + qs : ''}`;
|
|
33
|
-
}
|
|
34
21
|
|
|
35
22
|
/**
|
|
36
23
|
* NextAuth-compatible useSession wrapper.
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
3
|
import { useEffect, useCallback } from 'react';
|
|
4
|
-
import { authClient
|
|
4
|
+
import { authClient } from '../../client/better-auth-client';
|
|
5
5
|
import { usePathname } from 'next/navigation';
|
|
6
6
|
import Image from 'next/image';
|
|
7
7
|
import Link from 'next/link';
|
|
@@ -90,7 +90,7 @@ export function MobileNavDrawer({
|
|
|
90
90
|
if (onSignIn) {
|
|
91
91
|
onSignIn();
|
|
92
92
|
} else {
|
|
93
|
-
|
|
93
|
+
authClient.signIn.social({ provider: 'google', callbackURL: signInCallbackUrl });
|
|
94
94
|
}
|
|
95
95
|
};
|
|
96
96
|
|