better-auth-studio 1.0.27 → 1.0.30
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-adapter.d.ts +1 -0
- package/dist/auth-adapter.d.ts.map +1 -1
- package/dist/auth-adapter.js.map +1 -1
- package/dist/cli.js +1 -1
- package/dist/cli.js.map +1 -1
- package/dist/config.d.ts +3 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +3 -0
- package/dist/config.js.map +1 -1
- package/dist/data.d.ts +1 -1
- package/dist/data.d.ts.map +1 -1
- package/dist/data.js +284 -2
- package/dist/data.js.map +1 -1
- package/dist/routes.d.ts.map +1 -1
- package/dist/routes.js +652 -12
- package/dist/routes.js.map +1 -1
- package/package.json +4 -3
- package/public/assets/main-CvE7bhI5.css +1 -0
- package/public/assets/main-Dzvwip90.js +674 -0
- package/public/index.html +2 -2
- package/public/assets/main-DWecCRCR.js +0 -405
- package/public/assets/main-FzP6n_gH.css +0 -1
package/dist/routes.js
CHANGED
|
@@ -1,12 +1,30 @@
|
|
|
1
1
|
import { existsSync, readFileSync } from 'node:fs';
|
|
2
2
|
import { dirname, join } from 'node:path';
|
|
3
3
|
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
4
|
+
// @ts-expect-error
|
|
5
|
+
import { hex } from '@better-auth/utils/hex';
|
|
6
|
+
import { scryptAsync } from '@noble/hashes/scrypt.js';
|
|
4
7
|
import { Router } from 'express';
|
|
5
8
|
import { createJiti } from 'jiti';
|
|
6
9
|
import { createMockAccount, createMockSession, createMockUser, createMockVerification, getAuthAdapter, } from './auth-adapter.js';
|
|
7
10
|
import { getAuthData } from './data.js';
|
|
8
11
|
import { initializeGeoService, resolveIPLocation, setGeoDbPath } from './geo-service.js';
|
|
9
12
|
import { detectDatabaseWithDialect } from './utils/database-detection.js';
|
|
13
|
+
const config = {
|
|
14
|
+
N: 16384,
|
|
15
|
+
r: 16,
|
|
16
|
+
p: 1,
|
|
17
|
+
dkLen: 64,
|
|
18
|
+
};
|
|
19
|
+
async function generateKey(password, salt) {
|
|
20
|
+
return await scryptAsync(password.normalize('NFKC'), salt, {
|
|
21
|
+
N: config.N,
|
|
22
|
+
p: config.p,
|
|
23
|
+
r: config.r,
|
|
24
|
+
dkLen: config.dkLen,
|
|
25
|
+
maxmem: 128 * config.N * config.r * 2,
|
|
26
|
+
});
|
|
27
|
+
}
|
|
10
28
|
function getStudioVersion() {
|
|
11
29
|
try {
|
|
12
30
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
@@ -212,7 +230,63 @@ export function createRoutes(authConfig, configPath, geoDbPath) {
|
|
|
212
230
|
},
|
|
213
231
|
});
|
|
214
232
|
});
|
|
215
|
-
|
|
233
|
+
router.get('/api/version-check', async (_req, res) => {
|
|
234
|
+
try {
|
|
235
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
236
|
+
const projectRoot = join(__dirname, '..');
|
|
237
|
+
let currentVersion = '1.0.0';
|
|
238
|
+
try {
|
|
239
|
+
const betterAuthPkgPath = join(projectRoot, 'node_modules', 'better-auth', 'package.json');
|
|
240
|
+
if (existsSync(betterAuthPkgPath)) {
|
|
241
|
+
const betterAuthPkg = JSON.parse(readFileSync(betterAuthPkgPath, 'utf-8'));
|
|
242
|
+
currentVersion = betterAuthPkg.version || '1.0.0';
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
catch (error) {
|
|
246
|
+
try {
|
|
247
|
+
const packageJsonPath = join(projectRoot, 'package.json');
|
|
248
|
+
if (existsSync(packageJsonPath)) {
|
|
249
|
+
const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf-8'));
|
|
250
|
+
const versionString = packageJson.dependencies?.['better-auth'] ||
|
|
251
|
+
packageJson.devDependencies?.['better-auth'] ||
|
|
252
|
+
'1.0.0';
|
|
253
|
+
currentVersion = versionString.replace(/[\^~>=<]/g, '');
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
catch { }
|
|
257
|
+
}
|
|
258
|
+
let latestVersion = currentVersion;
|
|
259
|
+
let isOutdated = false;
|
|
260
|
+
try {
|
|
261
|
+
const npmResponse = await fetch('https://registry.npmjs.org/better-auth/latest');
|
|
262
|
+
if (npmResponse.ok) {
|
|
263
|
+
const npmData = await npmResponse.json();
|
|
264
|
+
latestVersion = npmData.version || currentVersion;
|
|
265
|
+
isOutdated = currentVersion !== latestVersion;
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
catch (fetchError) {
|
|
269
|
+
console.error('Failed to fetch latest version from npm:', fetchError);
|
|
270
|
+
latestVersion = currentVersion;
|
|
271
|
+
isOutdated = false;
|
|
272
|
+
}
|
|
273
|
+
res.json({
|
|
274
|
+
current: currentVersion,
|
|
275
|
+
latest: latestVersion,
|
|
276
|
+
isOutdated,
|
|
277
|
+
updateCommand: 'npm install better-auth@latest',
|
|
278
|
+
});
|
|
279
|
+
}
|
|
280
|
+
catch (error) {
|
|
281
|
+
console.error('Version check error:', error);
|
|
282
|
+
res.status(500).json({
|
|
283
|
+
error: 'Failed to check version',
|
|
284
|
+
current: 'unknown',
|
|
285
|
+
latest: 'unknown',
|
|
286
|
+
isOutdated: false,
|
|
287
|
+
});
|
|
288
|
+
}
|
|
289
|
+
});
|
|
216
290
|
router.post('/api/geo/resolve', (req, res) => {
|
|
217
291
|
try {
|
|
218
292
|
const { ipAddress } = req.body;
|
|
@@ -246,6 +320,14 @@ export function createRoutes(authConfig, configPath, geoDbPath) {
|
|
|
246
320
|
let databaseDialect = 'unknown';
|
|
247
321
|
let databaseAdapter = 'unknown';
|
|
248
322
|
let databaseVersion = 'unknown';
|
|
323
|
+
let adapterConfig = null;
|
|
324
|
+
try {
|
|
325
|
+
const adapterResult = await getAuthAdapterWithConfig();
|
|
326
|
+
if (adapterResult && adapterResult.options?.adapterConfig) {
|
|
327
|
+
adapterConfig = adapterResult.options.adapterConfig;
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
catch (_error) { }
|
|
249
331
|
try {
|
|
250
332
|
const detectedDb = await detectDatabaseWithDialect();
|
|
251
333
|
if (detectedDb) {
|
|
@@ -291,6 +373,7 @@ export function createRoutes(authConfig, configPath, geoDbPath) {
|
|
|
291
373
|
casing: authConfig.database?.casing || 'camel',
|
|
292
374
|
debugLogs: authConfig.database?.debugLogs || false,
|
|
293
375
|
url: authConfig.database?.url,
|
|
376
|
+
adapterConfig: adapterConfig,
|
|
294
377
|
},
|
|
295
378
|
emailVerification: {
|
|
296
379
|
sendOnSignUp: authConfig.emailVerification?.sendOnSignUp || false,
|
|
@@ -363,6 +446,8 @@ export function createRoutes(authConfig, configPath, geoDbPath) {
|
|
|
363
446
|
max: authConfig.rateLimit?.max || 100,
|
|
364
447
|
storage: authConfig.rateLimit?.storage || 'memory',
|
|
365
448
|
modelName: authConfig.rateLimit?.modelName || 'rateLimit',
|
|
449
|
+
customStorage: authConfig.rateLimit?.customStorage || null,
|
|
450
|
+
customRules: authConfig.rateLimit?.customRules || [],
|
|
366
451
|
},
|
|
367
452
|
advanced: {
|
|
368
453
|
ipAddress: {
|
|
@@ -407,6 +492,21 @@ export function createRoutes(authConfig, configPath, geoDbPath) {
|
|
|
407
492
|
res.status(500).json({ error: 'Failed to fetch statistics' });
|
|
408
493
|
}
|
|
409
494
|
});
|
|
495
|
+
router.get('/api/analytics', async (req, res) => {
|
|
496
|
+
try {
|
|
497
|
+
const { period = 'ALL', type = 'users', from, to } = req.query;
|
|
498
|
+
const analytics = await getAuthData(authConfig, 'analytics', {
|
|
499
|
+
period: period,
|
|
500
|
+
type: type,
|
|
501
|
+
from: from,
|
|
502
|
+
to: to,
|
|
503
|
+
}, configPath);
|
|
504
|
+
res.json(analytics);
|
|
505
|
+
}
|
|
506
|
+
catch (_error) {
|
|
507
|
+
res.status(500).json({ error: 'Failed to fetch analytics' });
|
|
508
|
+
}
|
|
509
|
+
});
|
|
410
510
|
router.get('/api/counts', async (_req, res) => {
|
|
411
511
|
try {
|
|
412
512
|
const adapter = await getAuthAdapterWithConfig();
|
|
@@ -429,6 +529,7 @@ export function createRoutes(authConfig, configPath, geoDbPath) {
|
|
|
429
529
|
const plugins = betterAuthConfig.plugins || [];
|
|
430
530
|
const organizationPlugin = plugins.find((plugin) => plugin.id === 'organization');
|
|
431
531
|
organizationPluginEnabled = !!organizationPlugin;
|
|
532
|
+
teamsPluginEnabled = !!organizationPlugin?.options?.teams?.enabled;
|
|
432
533
|
if (organizationPlugin) {
|
|
433
534
|
teamsPluginEnabled = organizationPlugin.options?.teams?.enabled === true;
|
|
434
535
|
}
|
|
@@ -442,14 +543,14 @@ export function createRoutes(authConfig, configPath, geoDbPath) {
|
|
|
442
543
|
if (adapter) {
|
|
443
544
|
try {
|
|
444
545
|
if (typeof adapter.findMany === 'function') {
|
|
445
|
-
const users = await adapter.findMany({ model: 'user', limit:
|
|
546
|
+
const users = await adapter.findMany({ model: 'user', limit: 100000 });
|
|
446
547
|
userCount = users?.length || 0;
|
|
447
548
|
}
|
|
448
549
|
}
|
|
449
550
|
catch (_error) { }
|
|
450
551
|
try {
|
|
451
552
|
if (typeof adapter.findMany === 'function') {
|
|
452
|
-
const sessions = await adapter.findMany({ model: 'session', limit:
|
|
553
|
+
const sessions = await adapter.findMany({ model: 'session', limit: 100000 });
|
|
453
554
|
sessionCount = sessions?.length || 0;
|
|
454
555
|
}
|
|
455
556
|
}
|
|
@@ -494,13 +595,15 @@ export function createRoutes(authConfig, configPath, geoDbPath) {
|
|
|
494
595
|
if (!adapter) {
|
|
495
596
|
return res.status(500).json({ error: 'Auth adapter not available' });
|
|
496
597
|
}
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
598
|
+
let users = [];
|
|
599
|
+
if (adapter.findMany) {
|
|
600
|
+
// Use findMany with high limit to get all users
|
|
601
|
+
users = await adapter.findMany({ model: 'user', limit: 100000 }).catch(() => []);
|
|
500
602
|
}
|
|
501
|
-
else {
|
|
502
|
-
|
|
603
|
+
else if (adapter.getUsers) {
|
|
604
|
+
users = await adapter.getUsers();
|
|
503
605
|
}
|
|
606
|
+
res.json({ success: true, users });
|
|
504
607
|
}
|
|
505
608
|
catch (_error) {
|
|
506
609
|
res.status(500).json({ error: 'Failed to fetch users' });
|
|
@@ -547,6 +650,40 @@ export function createRoutes(authConfig, configPath, geoDbPath) {
|
|
|
547
650
|
res.status(500).json({ error: 'Failed to update user' });
|
|
548
651
|
}
|
|
549
652
|
});
|
|
653
|
+
router.put('/api/users/:userId/password', async (req, res) => {
|
|
654
|
+
try {
|
|
655
|
+
const { userId } = req.params;
|
|
656
|
+
const { password } = req.body;
|
|
657
|
+
if (!password) {
|
|
658
|
+
return res.status(400).json({ error: 'Password is required' });
|
|
659
|
+
}
|
|
660
|
+
const adapter = await getAuthAdapterWithConfig();
|
|
661
|
+
if (!adapter || !adapter.update) {
|
|
662
|
+
return res.status(500).json({ error: 'Auth adapter not available' });
|
|
663
|
+
}
|
|
664
|
+
let hashedPassword = password;
|
|
665
|
+
try {
|
|
666
|
+
const salt = hex.encode(crypto.getRandomValues(new Uint8Array(16)));
|
|
667
|
+
const key = await generateKey(password, salt);
|
|
668
|
+
hashedPassword = `${salt}:${hex.encode(key)}`;
|
|
669
|
+
}
|
|
670
|
+
catch {
|
|
671
|
+
res.status(500).json({ error: 'Failed to hash password' });
|
|
672
|
+
}
|
|
673
|
+
const account = await adapter.update({
|
|
674
|
+
model: 'account',
|
|
675
|
+
where: [
|
|
676
|
+
{ field: 'userId', value: userId },
|
|
677
|
+
{ field: 'providerId', value: 'credential' },
|
|
678
|
+
],
|
|
679
|
+
update: { password: hashedPassword },
|
|
680
|
+
});
|
|
681
|
+
res.json({ success: true, account });
|
|
682
|
+
}
|
|
683
|
+
catch (error) {
|
|
684
|
+
res.status(500).json({ error: 'Failed to update password', message: error?.message });
|
|
685
|
+
}
|
|
686
|
+
});
|
|
550
687
|
router.delete('/api/users/:userId', async (req, res) => {
|
|
551
688
|
try {
|
|
552
689
|
const { userId } = req.params;
|
|
@@ -629,12 +766,14 @@ export function createRoutes(authConfig, configPath, geoDbPath) {
|
|
|
629
766
|
organizationName: organization
|
|
630
767
|
? organization.name || 'Unknown Organization'
|
|
631
768
|
: 'Unknown Organization',
|
|
769
|
+
organizationSlug: organization ? organization.slug || 'unknown' : 'unknown',
|
|
632
770
|
}
|
|
633
771
|
: {
|
|
634
772
|
id: membership.teamId,
|
|
635
773
|
name: 'Unknown Team',
|
|
636
774
|
organizationId: 'unknown',
|
|
637
775
|
organizationName: 'Unknown Organization',
|
|
776
|
+
organizationSlug: 'unknown',
|
|
638
777
|
},
|
|
639
778
|
role: membership.role || 'member',
|
|
640
779
|
joinedAt: membership.createdAt,
|
|
@@ -728,7 +867,7 @@ export function createRoutes(authConfig, configPath, geoDbPath) {
|
|
|
728
867
|
if (!adapter || !adapter.delete) {
|
|
729
868
|
return res.status(500).json({ error: 'Auth adapter not available' });
|
|
730
869
|
}
|
|
731
|
-
await adapter.delete({ model: 'session', id: sessionId });
|
|
870
|
+
await adapter.delete({ model: 'session', where: [{ field: 'id', value: sessionId }] });
|
|
732
871
|
res.json({ success: true });
|
|
733
872
|
}
|
|
734
873
|
catch (_error) {
|
|
@@ -821,12 +960,11 @@ export function createRoutes(authConfig, configPath, geoDbPath) {
|
|
|
821
960
|
try {
|
|
822
961
|
const adapter = await getAuthAdapterWithConfig();
|
|
823
962
|
if (adapter && typeof adapter.findMany === 'function') {
|
|
824
|
-
// If limit is very high (like 10000), fetch all users without pagination
|
|
825
963
|
const shouldPaginate = limit < 1000;
|
|
826
964
|
const fetchLimit = shouldPaginate ? limit : undefined;
|
|
827
965
|
const allUsers = await adapter.findMany({
|
|
828
966
|
model: 'user',
|
|
829
|
-
limit: fetchLimit
|
|
967
|
+
limit: fetchLimit,
|
|
830
968
|
});
|
|
831
969
|
let filteredUsers = allUsers || [];
|
|
832
970
|
if (search) {
|
|
@@ -1048,6 +1186,60 @@ export function createRoutes(authConfig, configPath, geoDbPath) {
|
|
|
1048
1186
|
res.status(500).json({ error: 'Failed to fetch database info' });
|
|
1049
1187
|
}
|
|
1050
1188
|
});
|
|
1189
|
+
router.get('/api/database/test', async (_req, res) => {
|
|
1190
|
+
try {
|
|
1191
|
+
const adapter = await getAuthAdapterWithConfig();
|
|
1192
|
+
if (!adapter || !adapter.findMany) {
|
|
1193
|
+
return res.status(500).json({ error: 'Auth adapter not available' });
|
|
1194
|
+
}
|
|
1195
|
+
const result = await adapter.findMany({
|
|
1196
|
+
model: 'user',
|
|
1197
|
+
limit: 1,
|
|
1198
|
+
});
|
|
1199
|
+
return res.json({ success: true, result: result });
|
|
1200
|
+
}
|
|
1201
|
+
catch (error) {
|
|
1202
|
+
res.status(500).json({
|
|
1203
|
+
success: false,
|
|
1204
|
+
error: 'Failed to test database connection',
|
|
1205
|
+
message: error instanceof Error ? error.message : 'Unknown error',
|
|
1206
|
+
});
|
|
1207
|
+
}
|
|
1208
|
+
});
|
|
1209
|
+
router.post('/api/tools/migrations/run', async (req, res) => {
|
|
1210
|
+
try {
|
|
1211
|
+
const { provider, script } = req.body;
|
|
1212
|
+
if (!provider) {
|
|
1213
|
+
return res.status(400).json({ success: false, error: 'Migration provider is required' });
|
|
1214
|
+
}
|
|
1215
|
+
console.log('');
|
|
1216
|
+
console.log('='.repeat(80));
|
|
1217
|
+
console.log(`🛠 Migration Tool → Provider: ${provider}`);
|
|
1218
|
+
if (script) {
|
|
1219
|
+
console.log('📄 Migration script received:');
|
|
1220
|
+
console.log(script);
|
|
1221
|
+
}
|
|
1222
|
+
else {
|
|
1223
|
+
console.log('ℹ️ No script payload provided.');
|
|
1224
|
+
}
|
|
1225
|
+
console.log('='.repeat(80));
|
|
1226
|
+
console.log('');
|
|
1227
|
+
// This endpoint does not execute arbitrary scripts for safety. It simply
|
|
1228
|
+
// acknowledges receipt so the frontend can present instructions.
|
|
1229
|
+
return res.json({
|
|
1230
|
+
success: true,
|
|
1231
|
+
message: 'Migration script received. Review the server logs for details.',
|
|
1232
|
+
});
|
|
1233
|
+
}
|
|
1234
|
+
catch (error) {
|
|
1235
|
+
console.error('Migration tool error:', error);
|
|
1236
|
+
res.status(500).json({
|
|
1237
|
+
success: false,
|
|
1238
|
+
error: 'Failed to process migration request',
|
|
1239
|
+
message: error instanceof Error ? error.message : 'Unknown error',
|
|
1240
|
+
});
|
|
1241
|
+
}
|
|
1242
|
+
});
|
|
1051
1243
|
// Database Detection endpoint - Auto-detect database from installed packages
|
|
1052
1244
|
router.get('/api/database/detect', async (_req, res) => {
|
|
1053
1245
|
try {
|
|
@@ -2605,7 +2797,7 @@ export function createRoutes(authConfig, configPath, geoDbPath) {
|
|
|
2605
2797
|
if (adapter && typeof adapter.findMany === 'function') {
|
|
2606
2798
|
const allOrganizations = await adapter.findMany({
|
|
2607
2799
|
model: 'organization',
|
|
2608
|
-
limit: limit
|
|
2800
|
+
limit: limit,
|
|
2609
2801
|
});
|
|
2610
2802
|
res.json({ organizations: allOrganizations });
|
|
2611
2803
|
}
|
|
@@ -3004,6 +3196,454 @@ export function createRoutes(authConfig, configPath, geoDbPath) {
|
|
|
3004
3196
|
res.status(500).json({ error: 'Failed to seed organizations' });
|
|
3005
3197
|
}
|
|
3006
3198
|
});
|
|
3199
|
+
// OAuth Test Endpoints
|
|
3200
|
+
router.get('/api/tools/oauth/providers', async (_req, res) => {
|
|
3201
|
+
const result = await getAuthAdapterWithConfig();
|
|
3202
|
+
try {
|
|
3203
|
+
const providers = authConfig.socialProviders || [];
|
|
3204
|
+
res.json({
|
|
3205
|
+
success: true,
|
|
3206
|
+
providers: providers.map((provider) => ({
|
|
3207
|
+
id: provider.id || provider.type,
|
|
3208
|
+
name: provider.name || provider.id || provider.type,
|
|
3209
|
+
type: provider.type || provider.id,
|
|
3210
|
+
enabled: provider.enabled !== false,
|
|
3211
|
+
})),
|
|
3212
|
+
});
|
|
3213
|
+
}
|
|
3214
|
+
catch (error) {
|
|
3215
|
+
res.status(500).json({ success: false, error: 'Failed to fetch OAuth providers' });
|
|
3216
|
+
}
|
|
3217
|
+
});
|
|
3218
|
+
router.post('/api/tools/oauth/test', async (req, res) => {
|
|
3219
|
+
try {
|
|
3220
|
+
const { provider } = req.body;
|
|
3221
|
+
if (!provider) {
|
|
3222
|
+
return res.status(400).json({ success: false, error: 'Provider is required' });
|
|
3223
|
+
}
|
|
3224
|
+
// Check if provider exists
|
|
3225
|
+
const providers = authConfig.socialProviders || [];
|
|
3226
|
+
const selectedProvider = providers.find((p) => (p.id || p.type) === provider);
|
|
3227
|
+
if (!selectedProvider) {
|
|
3228
|
+
return res.status(404).json({ success: false, error: 'Provider not found' });
|
|
3229
|
+
}
|
|
3230
|
+
// Generate test session ID
|
|
3231
|
+
const testSessionId = `oauth-test-${Date.now()}-${Math.random().toString(36).substring(7)}`;
|
|
3232
|
+
// Store the test session
|
|
3233
|
+
oauthTestSessions.set(testSessionId, {
|
|
3234
|
+
provider,
|
|
3235
|
+
startTime: Date.now(),
|
|
3236
|
+
status: 'pending',
|
|
3237
|
+
});
|
|
3238
|
+
const studioBaseUrl = `${req.protocol}://${req.get('host')}`;
|
|
3239
|
+
res.json({
|
|
3240
|
+
success: true,
|
|
3241
|
+
startUrl: `${studioBaseUrl}/api/tools/oauth/start?testSessionId=${encodeURIComponent(testSessionId)}&provider=${encodeURIComponent(provider)}`,
|
|
3242
|
+
testSessionId,
|
|
3243
|
+
provider: selectedProvider.name || selectedProvider.id || selectedProvider.type,
|
|
3244
|
+
});
|
|
3245
|
+
}
|
|
3246
|
+
catch (error) {
|
|
3247
|
+
console.error('OAuth test error:', error);
|
|
3248
|
+
res.status(500).json({
|
|
3249
|
+
success: false,
|
|
3250
|
+
error: 'Failed to initiate OAuth test',
|
|
3251
|
+
details: error instanceof Error ? error.message : String(error),
|
|
3252
|
+
});
|
|
3253
|
+
}
|
|
3254
|
+
});
|
|
3255
|
+
// Store OAuth test sessions and results temporarily
|
|
3256
|
+
const oauthTestSessions = new Map();
|
|
3257
|
+
const oauthTestResults = new Map();
|
|
3258
|
+
router.get('/api/tools/oauth/start', async (req, res) => {
|
|
3259
|
+
try {
|
|
3260
|
+
const { testSessionId, provider } = req.query;
|
|
3261
|
+
if (!testSessionId || !provider) {
|
|
3262
|
+
return res
|
|
3263
|
+
.status(400)
|
|
3264
|
+
.send('<html><body style="background:#000;color:#fff;font-family:monospace;padding:20px;">Missing test session or provider</body></html>');
|
|
3265
|
+
}
|
|
3266
|
+
const session = oauthTestSessions.get(testSessionId);
|
|
3267
|
+
if (!session || session.provider !== provider) {
|
|
3268
|
+
return res
|
|
3269
|
+
.status(404)
|
|
3270
|
+
.send('<html><body style="background:#000;color:#fff;font-family:monospace;padding:20px;">OAuth test session not found</body></html>');
|
|
3271
|
+
}
|
|
3272
|
+
const authBaseUrl = authConfig.baseURL || 'http://localhost:3000';
|
|
3273
|
+
const basePath = authConfig.basePath || '/api/auth';
|
|
3274
|
+
const payload = {
|
|
3275
|
+
provider,
|
|
3276
|
+
additionalData: { testSessionId },
|
|
3277
|
+
};
|
|
3278
|
+
res.setHeader('Content-Type', 'text/html');
|
|
3279
|
+
res.send(`
|
|
3280
|
+
<!DOCTYPE html>
|
|
3281
|
+
<html>
|
|
3282
|
+
<head>
|
|
3283
|
+
<title>Starting OAuth Test</title>
|
|
3284
|
+
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
3285
|
+
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
3286
|
+
<link href="https://fonts.googleapis.com/css2?family=Geist:wght@300;400;500;600&family=Geist+Mono:wght@400;500;600&display=swap" rel="stylesheet">
|
|
3287
|
+
<style>
|
|
3288
|
+
:root { color-scheme: dark; }
|
|
3289
|
+
body {
|
|
3290
|
+
background: #0b0b0f;
|
|
3291
|
+
color: #fff;
|
|
3292
|
+
font-family: "Geist", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
|
3293
|
+
display: flex;
|
|
3294
|
+
align-items: center;
|
|
3295
|
+
justify-content: center;
|
|
3296
|
+
min-height: 100vh;
|
|
3297
|
+
margin: 0;
|
|
3298
|
+
}
|
|
3299
|
+
.box {
|
|
3300
|
+
text-align: center;
|
|
3301
|
+
max-width: 520px;
|
|
3302
|
+
font-family: "Geist Mono", monospace;
|
|
3303
|
+
}
|
|
3304
|
+
h1 {
|
|
3305
|
+
font-family: "Geist", sans-serif;
|
|
3306
|
+
letter-spacing: 0.08em;
|
|
3307
|
+
text-transform: uppercase;
|
|
3308
|
+
font-weight: 500;
|
|
3309
|
+
}
|
|
3310
|
+
p {
|
|
3311
|
+
font-family: "Geist Mono", monospace;
|
|
3312
|
+
font-size: 13px;
|
|
3313
|
+
color: #9ca3af;
|
|
3314
|
+
}
|
|
3315
|
+
.spinner {
|
|
3316
|
+
border: 3px solid rgba(255,255,255,0.12);
|
|
3317
|
+
border-top: 3px solid #fff;
|
|
3318
|
+
border-radius: 50%;
|
|
3319
|
+
width: 40px;
|
|
3320
|
+
height: 40px;
|
|
3321
|
+
animation: spin 1s linear infinite;
|
|
3322
|
+
margin: 24px auto;
|
|
3323
|
+
}
|
|
3324
|
+
@keyframes spin { 0% { transform: rotate(0deg);} 100% { transform: rotate(360deg);} }
|
|
3325
|
+
button {
|
|
3326
|
+
background: #111118;
|
|
3327
|
+
border: 1px solid #27272a;
|
|
3328
|
+
color: #fff;
|
|
3329
|
+
padding: 10px 16px;
|
|
3330
|
+
border-radius: 6px;
|
|
3331
|
+
cursor: pointer;
|
|
3332
|
+
font-family: "Geist", sans-serif;
|
|
3333
|
+
text-transform: uppercase;
|
|
3334
|
+
letter-spacing: 0.08em;
|
|
3335
|
+
font-size: 12px;
|
|
3336
|
+
}
|
|
3337
|
+
button:hover { background: #1d1d26; }
|
|
3338
|
+
</style>
|
|
3339
|
+
</head>
|
|
3340
|
+
<body>
|
|
3341
|
+
<div class="box">
|
|
3342
|
+
<h1>Preparing OAuth Test…</h1>
|
|
3343
|
+
<p id="status">Contacting Better Auth to generate a secure state.</p>
|
|
3344
|
+
<div class="spinner" id="spinner"></div>
|
|
3345
|
+
<button id="retry" style="display:none;">Retry</button>
|
|
3346
|
+
</div>
|
|
3347
|
+
<script>
|
|
3348
|
+
const payload = ${JSON.stringify(payload)};
|
|
3349
|
+
const endpoint = ${JSON.stringify(`${authBaseUrl}${basePath}/sign-in/social`)};
|
|
3350
|
+
const statusEl = document.getElementById('status');
|
|
3351
|
+
const retryBtn = document.getElementById('retry');
|
|
3352
|
+
const spinner = document.getElementById('spinner');
|
|
3353
|
+
|
|
3354
|
+
const postToParent = (data) => {
|
|
3355
|
+
if (window.opener) {
|
|
3356
|
+
try {
|
|
3357
|
+
window.opener.postMessage({
|
|
3358
|
+
type: 'oauth_test_state',
|
|
3359
|
+
...data,
|
|
3360
|
+
}, window.location.origin);
|
|
3361
|
+
} catch (err) {
|
|
3362
|
+
console.error('postMessage failed', err);
|
|
3363
|
+
}
|
|
3364
|
+
}
|
|
3365
|
+
};
|
|
3366
|
+
|
|
3367
|
+
async function startOAuth() {
|
|
3368
|
+
try {
|
|
3369
|
+
const response = await fetch(endpoint, {
|
|
3370
|
+
method: 'POST',
|
|
3371
|
+
credentials: 'include',
|
|
3372
|
+
headers: {
|
|
3373
|
+
'Content-Type': 'application/json'
|
|
3374
|
+
},
|
|
3375
|
+
body: JSON.stringify(payload)
|
|
3376
|
+
});
|
|
3377
|
+
|
|
3378
|
+
if (response.redirected) {
|
|
3379
|
+
postToParent({ status: 'redirect', testSessionId: payload.additionalData?.testSessionId });
|
|
3380
|
+
window.location.href = response.url;
|
|
3381
|
+
return;
|
|
3382
|
+
}
|
|
3383
|
+
|
|
3384
|
+
let data = null;
|
|
3385
|
+
const contentType = response.headers.get('content-type') || '';
|
|
3386
|
+
if (contentType.includes('application/json')) {
|
|
3387
|
+
data = await response.json().catch(() => null);
|
|
3388
|
+
}
|
|
3389
|
+
|
|
3390
|
+
const redirectUrl = data?.url || data?.redirect || data?.location;
|
|
3391
|
+
if (redirectUrl) {
|
|
3392
|
+
postToParent({ status: 'redirect', testSessionId: payload.additionalData?.testSessionId });
|
|
3393
|
+
window.location.href = redirectUrl;
|
|
3394
|
+
return;
|
|
3395
|
+
}
|
|
3396
|
+
|
|
3397
|
+
if (response.status >= 400) {
|
|
3398
|
+
throw new Error(data?.message || 'Better Auth returned an error');
|
|
3399
|
+
}
|
|
3400
|
+
|
|
3401
|
+
throw new Error('Unable to determine OAuth redirect URL.');
|
|
3402
|
+
} catch (error) {
|
|
3403
|
+
console.error('Failed to start OAuth test', error);
|
|
3404
|
+
statusEl.textContent = 'Failed to start OAuth test: ' + (error?.message || error);
|
|
3405
|
+
spinner.style.display = 'none';
|
|
3406
|
+
retryBtn.style.display = 'inline-flex';
|
|
3407
|
+
postToParent({
|
|
3408
|
+
status: 'error',
|
|
3409
|
+
testSessionId: payload.additionalData?.testSessionId,
|
|
3410
|
+
error: error?.message || String(error),
|
|
3411
|
+
});
|
|
3412
|
+
}
|
|
3413
|
+
}
|
|
3414
|
+
|
|
3415
|
+
retryBtn.addEventListener('click', () => {
|
|
3416
|
+
spinner.style.display = 'block';
|
|
3417
|
+
retryBtn.style.display = 'none';
|
|
3418
|
+
statusEl.textContent = 'Retrying…';
|
|
3419
|
+
startOAuth();
|
|
3420
|
+
});
|
|
3421
|
+
|
|
3422
|
+
startOAuth();
|
|
3423
|
+
</script>
|
|
3424
|
+
</body>
|
|
3425
|
+
</html>
|
|
3426
|
+
`);
|
|
3427
|
+
}
|
|
3428
|
+
catch (error) {
|
|
3429
|
+
console.error('OAuth start error:', error);
|
|
3430
|
+
res
|
|
3431
|
+
.status(500)
|
|
3432
|
+
.send('<html><body style="background:#000;color:#fff;font-family:monospace;padding:20px;">Failed to start OAuth test</body></html>');
|
|
3433
|
+
}
|
|
3434
|
+
});
|
|
3435
|
+
router.get('/api/tools/oauth/callback', async (req, res) => {
|
|
3436
|
+
try {
|
|
3437
|
+
const { testSessionId, error: oauthError } = req.query;
|
|
3438
|
+
if (!testSessionId) {
|
|
3439
|
+
return res.send(`<html><body style="background:#000;color:#fff;text-align:center;">
|
|
3440
|
+
<h1>OAuth Test Failed</h1>
|
|
3441
|
+
<p>Missing test session</p>
|
|
3442
|
+
<script>setTimeout(() => window.close(), 3000);</script>
|
|
3443
|
+
</body></html>`);
|
|
3444
|
+
}
|
|
3445
|
+
const testSession = oauthTestSessions.get(testSessionId);
|
|
3446
|
+
if (!testSession) {
|
|
3447
|
+
return res.send(`<html><body style="background:#000;color:#fff;text-align:center;">
|
|
3448
|
+
<h1>OAuth Test Failed</h1>
|
|
3449
|
+
<p>Test session not found or expired</p>
|
|
3450
|
+
<script>setTimeout(() => window.close(), 3000);</script>
|
|
3451
|
+
</body></html>`);
|
|
3452
|
+
}
|
|
3453
|
+
const result = {
|
|
3454
|
+
testSessionId: testSessionId,
|
|
3455
|
+
provider: testSession.provider,
|
|
3456
|
+
success: !oauthError,
|
|
3457
|
+
error: oauthError,
|
|
3458
|
+
timestamp: new Date().toISOString(),
|
|
3459
|
+
};
|
|
3460
|
+
oauthTestResults.set(testSessionId, result);
|
|
3461
|
+
res.send(`
|
|
3462
|
+
<!DOCTYPE html>
|
|
3463
|
+
<html>
|
|
3464
|
+
<head>
|
|
3465
|
+
<title>OAuth Test ${oauthError ? 'Failed' : 'Success'}</title>
|
|
3466
|
+
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
3467
|
+
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
3468
|
+
<link href="https://fonts.googleapis.com/css2?family=Geist:wght@300;400;500;600&family=Geist+Mono:wght@400;500&display=swap" rel="stylesheet">
|
|
3469
|
+
<style>
|
|
3470
|
+
body {
|
|
3471
|
+
font-family: "Geist", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
|
3472
|
+
background: #000;
|
|
3473
|
+
color: #fff;
|
|
3474
|
+
display: flex;
|
|
3475
|
+
align-items: center;
|
|
3476
|
+
justify-content: center;
|
|
3477
|
+
min-height: 100vh;
|
|
3478
|
+
text-align: center;
|
|
3479
|
+
margin: 0;
|
|
3480
|
+
padding: 24px;
|
|
3481
|
+
}
|
|
3482
|
+
h1 {
|
|
3483
|
+
font-weight: 500;
|
|
3484
|
+
letter-spacing: 0.1em;
|
|
3485
|
+
text-transform: uppercase;
|
|
3486
|
+
}
|
|
3487
|
+
p {
|
|
3488
|
+
font-family: "Geist Mono", monospace;
|
|
3489
|
+
font-size: 13px;
|
|
3490
|
+
color: #9ca3af;
|
|
3491
|
+
}
|
|
3492
|
+
.success { color: #4f4; }
|
|
3493
|
+
.error { color: #f44; }
|
|
3494
|
+
.box {
|
|
3495
|
+
max-width: 520px;
|
|
3496
|
+
}
|
|
3497
|
+
</style>
|
|
3498
|
+
</head>
|
|
3499
|
+
<body>
|
|
3500
|
+
<div class="box">
|
|
3501
|
+
<h1 class="${oauthError ? 'error' : 'success'}">
|
|
3502
|
+
${oauthError ? '❌ OAuth Test Failed' : '✅ OAuth Test Completed'}
|
|
3503
|
+
</h1>
|
|
3504
|
+
<p>${oauthError ? oauthError : 'Waiting for account creation...'}</p>
|
|
3505
|
+
</div>
|
|
3506
|
+
<script>
|
|
3507
|
+
if (window.opener) {
|
|
3508
|
+
window.opener.postMessage({
|
|
3509
|
+
type: 'oauth_test_result',
|
|
3510
|
+
result: ${JSON.stringify(result)}
|
|
3511
|
+
}, '*');
|
|
3512
|
+
setTimeout(() => window.close(), 1500);
|
|
3513
|
+
}
|
|
3514
|
+
</script>
|
|
3515
|
+
</body>
|
|
3516
|
+
</html>
|
|
3517
|
+
`);
|
|
3518
|
+
}
|
|
3519
|
+
catch (error) {
|
|
3520
|
+
console.error('OAuth callback error:', error);
|
|
3521
|
+
res.send('<html><body style="background:#000;color:#fff;text-align:center;"><h1>OAuth Test Error</h1><p>Callback processing failed</p></body></html>');
|
|
3522
|
+
}
|
|
3523
|
+
});
|
|
3524
|
+
router.get('/api/tools/oauth/status', async (req, res) => {
|
|
3525
|
+
try {
|
|
3526
|
+
const { testSessionId } = req.query;
|
|
3527
|
+
if (!testSessionId) {
|
|
3528
|
+
return res.json({ hasResult: false });
|
|
3529
|
+
}
|
|
3530
|
+
const cached = oauthTestResults.get(testSessionId);
|
|
3531
|
+
if (cached) {
|
|
3532
|
+
oauthTestResults.delete(testSessionId);
|
|
3533
|
+
return res.json({ hasResult: true, result: cached });
|
|
3534
|
+
}
|
|
3535
|
+
const session = oauthTestSessions.get(testSessionId);
|
|
3536
|
+
if (!session) {
|
|
3537
|
+
return res.json({ hasResult: false });
|
|
3538
|
+
}
|
|
3539
|
+
const adapter = await getAuthAdapterWithConfig();
|
|
3540
|
+
if (!adapter || !adapter.findMany) {
|
|
3541
|
+
return res.json({ hasResult: false });
|
|
3542
|
+
}
|
|
3543
|
+
const startTime = session.startTime;
|
|
3544
|
+
const provider = session.provider;
|
|
3545
|
+
const parseDate = (value) => {
|
|
3546
|
+
if (!value)
|
|
3547
|
+
return 0;
|
|
3548
|
+
const date = value instanceof Date ? value : new Date(value);
|
|
3549
|
+
return date.getTime();
|
|
3550
|
+
};
|
|
3551
|
+
const bufferMs = 5000;
|
|
3552
|
+
const threshold = startTime - bufferMs;
|
|
3553
|
+
let recentAccount = null;
|
|
3554
|
+
let recentSession = null;
|
|
3555
|
+
try {
|
|
3556
|
+
const accounts = await adapter.findMany({
|
|
3557
|
+
model: 'account',
|
|
3558
|
+
where: [{ field: 'providerId', value: provider }],
|
|
3559
|
+
limit: 50,
|
|
3560
|
+
});
|
|
3561
|
+
const accountCandidate = accounts
|
|
3562
|
+
.map((account) => ({
|
|
3563
|
+
account,
|
|
3564
|
+
created: parseDate(account.createdAt || account.created_at || account.updatedAt || account.updated_at),
|
|
3565
|
+
}))
|
|
3566
|
+
.filter((entry) => entry.created >= threshold)
|
|
3567
|
+
.sort((a, b) => b.created - a.created)[0];
|
|
3568
|
+
recentAccount = accountCandidate?.account ?? null;
|
|
3569
|
+
}
|
|
3570
|
+
catch (accountError) {
|
|
3571
|
+
console.error('Failed to fetch accounts:', accountError);
|
|
3572
|
+
}
|
|
3573
|
+
try {
|
|
3574
|
+
const sessions = await adapter.findMany({
|
|
3575
|
+
model: 'session',
|
|
3576
|
+
limit: 50,
|
|
3577
|
+
});
|
|
3578
|
+
const sessionCandidate = sessions
|
|
3579
|
+
.map((sessionItem) => ({
|
|
3580
|
+
session: sessionItem,
|
|
3581
|
+
created: parseDate(sessionItem.createdAt ||
|
|
3582
|
+
sessionItem.created_at ||
|
|
3583
|
+
sessionItem.updatedAt ||
|
|
3584
|
+
sessionItem.updated_at),
|
|
3585
|
+
}))
|
|
3586
|
+
.filter((entry) => entry.created >= threshold)
|
|
3587
|
+
.sort((a, b) => b.created - a.created)[0];
|
|
3588
|
+
recentSession = sessionCandidate?.session ?? null;
|
|
3589
|
+
}
|
|
3590
|
+
catch (sessionError) {
|
|
3591
|
+
console.error('Failed to fetch sessions:', sessionError);
|
|
3592
|
+
}
|
|
3593
|
+
if (recentAccount || recentSession) {
|
|
3594
|
+
let userInfo = null;
|
|
3595
|
+
try {
|
|
3596
|
+
const userId = recentAccount?.userId || recentSession?.userId;
|
|
3597
|
+
if (userId) {
|
|
3598
|
+
const users = await adapter.findMany({
|
|
3599
|
+
model: 'user',
|
|
3600
|
+
where: [{ field: 'id', value: userId }],
|
|
3601
|
+
limit: 1,
|
|
3602
|
+
});
|
|
3603
|
+
if (users && users.length > 0) {
|
|
3604
|
+
const user = users[0];
|
|
3605
|
+
userInfo = {
|
|
3606
|
+
id: user.id,
|
|
3607
|
+
name: user.name,
|
|
3608
|
+
email: user.email,
|
|
3609
|
+
image: user.image,
|
|
3610
|
+
};
|
|
3611
|
+
}
|
|
3612
|
+
}
|
|
3613
|
+
}
|
|
3614
|
+
catch (userError) {
|
|
3615
|
+
console.error('Failed to fetch user info:', userError);
|
|
3616
|
+
}
|
|
3617
|
+
const result = {
|
|
3618
|
+
testSessionId: testSessionId,
|
|
3619
|
+
provider,
|
|
3620
|
+
success: true,
|
|
3621
|
+
userInfo,
|
|
3622
|
+
account: recentAccount
|
|
3623
|
+
? {
|
|
3624
|
+
id: recentAccount.id,
|
|
3625
|
+
userId: recentAccount.userId,
|
|
3626
|
+
}
|
|
3627
|
+
: null,
|
|
3628
|
+
session: recentSession
|
|
3629
|
+
? {
|
|
3630
|
+
id: recentSession.id,
|
|
3631
|
+
userId: recentSession.userId,
|
|
3632
|
+
}
|
|
3633
|
+
: null,
|
|
3634
|
+
timestamp: new Date().toISOString(),
|
|
3635
|
+
};
|
|
3636
|
+
oauthTestResults.set(testSessionId, result);
|
|
3637
|
+
oauthTestSessions.delete(testSessionId);
|
|
3638
|
+
return res.json({ hasResult: true, result });
|
|
3639
|
+
}
|
|
3640
|
+
res.json({ hasResult: false });
|
|
3641
|
+
}
|
|
3642
|
+
catch (error) {
|
|
3643
|
+
console.error('OAuth status error:', error);
|
|
3644
|
+
res.status(500).json({ hasResult: false, error: 'Failed to check status' });
|
|
3645
|
+
}
|
|
3646
|
+
});
|
|
3007
3647
|
return router;
|
|
3008
3648
|
}
|
|
3009
3649
|
//# sourceMappingURL=routes.js.map
|