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