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/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
- // IP Geolocation endpoint
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: 10000 });
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: 10000 });
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
- if (adapter.getUsers) {
498
- const users = await adapter.getUsers();
499
- res.json({ success: true, users });
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
- res.json({ success: true, users: [] });
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