better-auth-studio 1.0.79-beta.1 → 1.0.79-beta.2

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.
Files changed (67) hide show
  1. package/dist/adapters/express.d.ts +7 -0
  2. package/dist/adapters/express.d.ts.map +1 -0
  3. package/dist/adapters/express.js +40 -0
  4. package/dist/adapters/express.js.map +1 -0
  5. package/dist/adapters/nextjs.d.ts +16 -0
  6. package/dist/adapters/nextjs.d.ts.map +1 -0
  7. package/dist/adapters/nextjs.js +49 -0
  8. package/dist/adapters/nextjs.js.map +1 -0
  9. package/dist/auth-adapter.d.ts.map +1 -1
  10. package/dist/auth-adapter.js +3 -2
  11. package/dist/auth-adapter.js.map +1 -1
  12. package/dist/cli/commands/init.d.ts +2 -0
  13. package/dist/cli/commands/init.d.ts.map +1 -0
  14. package/dist/cli/commands/init.js +140 -0
  15. package/dist/cli/commands/init.js.map +1 -0
  16. package/dist/cli.js +13 -0
  17. package/dist/cli.js.map +1 -1
  18. package/dist/config.d.ts.map +1 -1
  19. package/dist/config.js +3 -0
  20. package/dist/config.js.map +1 -1
  21. package/dist/core/handler.d.ts +14 -0
  22. package/dist/core/handler.d.ts.map +1 -0
  23. package/dist/core/handler.js +256 -0
  24. package/dist/core/handler.js.map +1 -0
  25. package/dist/index.d.ts +4 -0
  26. package/dist/index.d.ts.map +1 -0
  27. package/dist/index.js +3 -0
  28. package/dist/index.js.map +1 -0
  29. package/dist/public/assets/main-DtDccUVq.js +1155 -0
  30. package/dist/public/assets/main-wa9xKUE4.css +1 -0
  31. package/dist/public/favicon.svg +6 -0
  32. package/dist/public/index.html +14 -0
  33. package/dist/public/logo.png +0 -0
  34. package/dist/public/vite.svg +5 -0
  35. package/dist/routes/api-router.d.ts +21 -0
  36. package/dist/routes/api-router.d.ts.map +1 -0
  37. package/dist/routes/api-router.js +14 -0
  38. package/dist/routes/api-router.js.map +1 -0
  39. package/dist/routes.d.ts +20 -1
  40. package/dist/routes.d.ts.map +1 -1
  41. package/dist/routes.js +680 -178
  42. package/dist/routes.js.map +1 -1
  43. package/dist/studio.d.ts.map +1 -1
  44. package/dist/studio.js +28 -2
  45. package/dist/studio.js.map +1 -1
  46. package/dist/types/handler.d.ts +58 -0
  47. package/dist/types/handler.d.ts.map +1 -0
  48. package/dist/types/handler.js +4 -0
  49. package/dist/types/handler.js.map +1 -0
  50. package/dist/utils/html-injector.d.ts +30 -0
  51. package/dist/utils/html-injector.d.ts.map +1 -0
  52. package/dist/utils/html-injector.js +61 -0
  53. package/dist/utils/html-injector.js.map +1 -0
  54. package/dist/utils/session.d.ts +21 -0
  55. package/dist/utils/session.d.ts.map +1 -0
  56. package/dist/utils/session.js +51 -0
  57. package/dist/utils/session.js.map +1 -0
  58. package/frontend/package.json +64 -0
  59. package/package.json +35 -26
  60. package/public/assets/main-DtDccUVq.js +1155 -0
  61. package/public/assets/main-wa9xKUE4.css +1 -0
  62. package/public/favicon.svg +6 -0
  63. package/public/index.html +3 -3
  64. package/public/logo.png +0 -0
  65. package/public/vite.svg +5 -0
  66. package/public/assets/main-Du6zwcd_.css +0 -1
  67. package/public/assets/main-S8anH3U1.js +0 -1145
package/dist/routes.js CHANGED
@@ -12,6 +12,7 @@ import { possiblePaths } from './config.js';
12
12
  import { getAuthData } from './data.js';
13
13
  import { initializeGeoService, resolveIPLocation, setGeoDbPath } from './geo-service.js';
14
14
  import { detectDatabaseWithDialect } from './utils/database-detection.js';
15
+ import { createStudioSession, decryptSession, encryptSession, isSessionValid, STUDIO_COOKIE_NAME, } from './utils/session.js';
15
16
  const config = {
16
17
  N: 16384,
17
18
  r: 16,
@@ -27,6 +28,27 @@ async function generateKey(password, salt) {
27
28
  maxmem: 128 * config.N * config.r * 2,
28
29
  });
29
30
  }
31
+ async function verifyPassword(password, storedHash) {
32
+ if (!storedHash || typeof storedHash !== 'string') {
33
+ return false;
34
+ }
35
+ const parts = storedHash.split(':');
36
+ if (parts.length !== 2) {
37
+ return false;
38
+ }
39
+ const [salt, storedKey] = parts;
40
+ if (!salt || !storedKey) {
41
+ return false;
42
+ }
43
+ try {
44
+ const key = await generateKey(password, salt);
45
+ const keyHex = hex.encode(key);
46
+ return keyHex === storedKey;
47
+ }
48
+ catch {
49
+ return false;
50
+ }
51
+ }
30
52
  function getStudioVersion() {
31
53
  try {
32
54
  const __dirname = dirname(fileURLToPath(import.meta.url));
@@ -227,7 +249,29 @@ async function findAuthConfigPath() {
227
249
  }
228
250
  return null;
229
251
  }
230
- export function createRoutes(authConfig, configPath, geoDbPath) {
252
+ export function createRoutes(authConfig, configPath, geoDbPath, preloadedAdapter, preloadedAuthOptions, accessConfig, authInstance) {
253
+ const isSelfHosted = !!preloadedAdapter;
254
+ const getAuthConfigSafe = async () => {
255
+ if (isSelfHosted && preloadedAuthOptions) {
256
+ return preloadedAuthOptions;
257
+ }
258
+ try {
259
+ const authConfigPath = configPath || (await findAuthConfigPath());
260
+ if (authConfigPath) {
261
+ const { getConfig } = await import('./config.js');
262
+ return await getConfig({
263
+ cwd: process.cwd(),
264
+ configPath: authConfigPath,
265
+ shouldThrowOnError: false,
266
+ noCache: true,
267
+ });
268
+ }
269
+ }
270
+ catch (_error) {
271
+ // Ignors errors
272
+ }
273
+ return null;
274
+ };
231
275
  const router = Router();
232
276
  const base64UrlEncode = (value) => Buffer.from(value)
233
277
  .toString('base64')
@@ -252,7 +296,105 @@ export function createRoutes(authConfig, configPath, geoDbPath) {
252
296
  setGeoDbPath(geoDbPath);
253
297
  }
254
298
  initializeGeoService().catch(console.error);
255
- const getAuthAdapterWithConfig = () => getAuthAdapter(configPath);
299
+ // Use preloaded adapter if available (self-hosted), otherwise load from config file (CLI)
300
+ const getAuthAdapterWithConfig = async () => {
301
+ if (preloadedAdapter) {
302
+ // For self-hosted studio, wrap the preloaded adapter to match expected interface
303
+ return {
304
+ ...preloadedAdapter,
305
+ findMany: preloadedAdapter.findMany?.bind(preloadedAdapter),
306
+ create: preloadedAdapter.create?.bind(preloadedAdapter),
307
+ update: preloadedAdapter.update?.bind(preloadedAdapter),
308
+ delete: preloadedAdapter.delete?.bind(preloadedAdapter),
309
+ createUser: async (data) => {
310
+ return await preloadedAdapter.create({
311
+ model: 'user',
312
+ data: {
313
+ createdAt: new Date(),
314
+ updatedAt: new Date(),
315
+ emailVerified: false,
316
+ name: data.name,
317
+ email: data.email?.toLowerCase(),
318
+ role: data.role || null,
319
+ image: data.image || `https://api.dicebear.com/7.x/avataaars/svg?seed=${data.email}`,
320
+ },
321
+ });
322
+ },
323
+ createSession: async (data) => {
324
+ return await preloadedAdapter.create({
325
+ model: 'session',
326
+ data: { createdAt: new Date(), updatedAt: new Date(), ...data },
327
+ });
328
+ },
329
+ createAccount: async (data) => {
330
+ return await preloadedAdapter.create({
331
+ model: 'account',
332
+ data: { createdAt: new Date(), updatedAt: new Date(), ...data },
333
+ });
334
+ },
335
+ createVerification: async (data) => {
336
+ return await preloadedAdapter.create({
337
+ model: 'verification',
338
+ data: { createdAt: new Date(), updatedAt: new Date(), ...data },
339
+ });
340
+ },
341
+ createOrganization: async (data) => {
342
+ return await preloadedAdapter.create({
343
+ model: 'organization',
344
+ data: { createdAt: new Date(), updatedAt: new Date(), ...data },
345
+ });
346
+ },
347
+ getUsers: async () => {
348
+ try {
349
+ if (typeof preloadedAdapter.findMany === 'function') {
350
+ return (await preloadedAdapter.findMany({ model: 'user' })) || [];
351
+ }
352
+ return [];
353
+ }
354
+ catch {
355
+ return [];
356
+ }
357
+ },
358
+ getSessions: async () => {
359
+ try {
360
+ if (typeof preloadedAdapter.findMany === 'function') {
361
+ return (await preloadedAdapter.findMany({ model: 'session' })) || [];
362
+ }
363
+ return [];
364
+ }
365
+ catch {
366
+ return [];
367
+ }
368
+ },
369
+ };
370
+ }
371
+ return getAuthAdapter(configPath);
372
+ };
373
+ if (isSelfHosted) {
374
+ router.use((req, res, next) => {
375
+ const path = req.path;
376
+ const publicPaths = [
377
+ '/api/auth/sign-in',
378
+ '/api/auth/session',
379
+ '/api/auth/logout',
380
+ '/api/auth/verify',
381
+ '/api/auth/oauth',
382
+ '/api/health',
383
+ ];
384
+ const isPublic = publicPaths.some((p) => path.startsWith(p));
385
+ if (isPublic) {
386
+ return next();
387
+ }
388
+ if (path.startsWith('/api/')) {
389
+ const result = verifyStudioSession(req);
390
+ if (!result.valid) {
391
+ return res.status(401).json({ error: 'Unauthorized', message: result.error });
392
+ }
393
+ req.studioSession = result.session;
394
+ }
395
+ next();
396
+ });
397
+ }
256
398
  router.get('/api/health', (_req, res) => {
257
399
  const uptime = process.uptime();
258
400
  const hours = Math.floor(uptime / 3600);
@@ -277,6 +419,284 @@ export function createRoutes(authConfig, configPath, geoDbPath) {
277
419
  },
278
420
  });
279
421
  });
422
+ const getSessionSecret = () => {
423
+ return (accessConfig?.secret ||
424
+ preloadedAuthOptions?.secret ||
425
+ process.env.BETTER_AUTH_SECRET ||
426
+ 'studio-default-secret');
427
+ };
428
+ const getAllowedRoles = () => {
429
+ return accessConfig?.roles || ['admin'];
430
+ };
431
+ const getSessionDuration = () => {
432
+ return (accessConfig?.sessionDuration || 7 * 24 * 60 * 60) * 1000;
433
+ };
434
+ const getAllowedEmails = () => {
435
+ return accessConfig?.allowEmails && accessConfig.allowEmails.length > 0
436
+ ? accessConfig.allowEmails.map((e) => e.toLowerCase())
437
+ : null;
438
+ };
439
+ const isEmailAllowed = (email) => {
440
+ const allowedEmails = getAllowedEmails();
441
+ if (!allowedEmails)
442
+ return true;
443
+ return allowedEmails.includes(email.toLowerCase());
444
+ };
445
+ const verifyStudioSession = (req) => {
446
+ if (!isSelfHosted) {
447
+ return { valid: true };
448
+ }
449
+ const sessionCookie = req.cookies?.[STUDIO_COOKIE_NAME];
450
+ if (!sessionCookie) {
451
+ return { valid: false, error: 'No session cookie' };
452
+ }
453
+ const session = decryptSession(sessionCookie, getSessionSecret());
454
+ if (!isSessionValid(session)) {
455
+ return { valid: false, error: 'Session expired' };
456
+ }
457
+ return { valid: true, session };
458
+ };
459
+ const requireAuth = (req, res, next) => {
460
+ if (!isSelfHosted) {
461
+ return next();
462
+ }
463
+ const result = verifyStudioSession(req);
464
+ if (!result.valid) {
465
+ return res.status(401).json({ error: 'Unauthorized', message: result.error });
466
+ }
467
+ req.studioSession = result.session;
468
+ next();
469
+ };
470
+ router.post('/api/auth/sign-in', async (req, res) => {
471
+ try {
472
+ if (!authInstance) {
473
+ return res.status(500).json({ success: false, message: 'Auth not configured' });
474
+ }
475
+ const { email, password } = req.body;
476
+ if (!email || !password) {
477
+ return res.status(400).json({ success: false, message: 'Email and password required' });
478
+ }
479
+ const adapter = await getAuthAdapter();
480
+ let signInResult = null;
481
+ let signInError = null;
482
+ try {
483
+ signInResult = await authInstance.api.signInEmail({
484
+ body: { email, password },
485
+ });
486
+ }
487
+ catch (err) {
488
+ signInError = err?.message || 'Sign-in failed';
489
+ }
490
+ if (!signInResult || signInResult.error || signInError) {
491
+ const errorMessage = signInError || signInResult?.error?.message || 'Invalid credentials';
492
+ if (errorMessage.includes('Invalid password hash') && adapter?.findMany) {
493
+ const users = await adapter.findMany({
494
+ model: 'user',
495
+ where: [{ field: 'email', value: email }],
496
+ limit: 1,
497
+ });
498
+ if (!users || users.length === 0) {
499
+ return res.status(401).json({ success: false, message: 'Invalid credentials' });
500
+ }
501
+ const userId = users[0].id;
502
+ const accounts = await adapter.findMany({
503
+ model: 'account',
504
+ where: [{ field: 'userId', value: userId }],
505
+ limit: 10,
506
+ });
507
+ const credentialAccount = accounts?.find((acc) => acc.providerId === 'credential' || acc.providerId === 'email');
508
+ if (!credentialAccount) {
509
+ return res.status(401).json({
510
+ success: false,
511
+ message: 'No password set for this account. Please use social login or reset your password.',
512
+ });
513
+ }
514
+ if (!credentialAccount.password) {
515
+ return res.status(401).json({
516
+ success: false,
517
+ message: 'Password not configured. Please reset your password.',
518
+ });
519
+ }
520
+ const isValidPassword = await verifyPassword(password, credentialAccount.password);
521
+ if (!isValidPassword) {
522
+ return res.status(401).json({ success: false, message: 'Invalid credentials' });
523
+ }
524
+ const userRole = users[0].role;
525
+ const user = { id: userId, email: users[0].email, name: users[0].name, role: userRole };
526
+ const allowedRoles = getAllowedRoles();
527
+ if (!allowedRoles.includes(user.role)) {
528
+ return res.status(403).json({
529
+ success: false,
530
+ message: `Access denied. Required role: ${allowedRoles.join(' or ')}`,
531
+ userRole: user.role || 'none',
532
+ });
533
+ }
534
+ if (!isEmailAllowed(user.email)) {
535
+ return res.status(403).json({
536
+ success: false,
537
+ message: 'Access denied. Your email is not authorized to access this dashboard.',
538
+ });
539
+ }
540
+ const studioSession = createStudioSession(user, getSessionDuration());
541
+ const encryptedSession = encryptSession(studioSession, getSessionSecret());
542
+ res.cookie(STUDIO_COOKIE_NAME, encryptedSession, {
543
+ httpOnly: true,
544
+ secure: process.env.NODE_ENV === 'production',
545
+ sameSite: 'lax',
546
+ maxAge: getSessionDuration(),
547
+ path: '/',
548
+ });
549
+ return res.json({
550
+ success: true,
551
+ user: { id: user.id, email: user.email, name: user.name, role: user.role },
552
+ });
553
+ }
554
+ return res.status(401).json({
555
+ success: false,
556
+ message: errorMessage,
557
+ });
558
+ }
559
+ const userId = signInResult.user?.id;
560
+ if (!userId) {
561
+ return res.status(401).json({ success: false, message: 'Invalid credentials' });
562
+ }
563
+ let userRole = null;
564
+ if (adapter?.findMany) {
565
+ const users = await adapter.findMany({
566
+ model: 'user',
567
+ where: [{ field: 'id', value: userId }],
568
+ limit: 1,
569
+ });
570
+ if (users && users.length > 0) {
571
+ userRole = users[0].role;
572
+ }
573
+ }
574
+ const user = { ...signInResult.user, role: userRole };
575
+ const allowedRoles = getAllowedRoles();
576
+ if (!allowedRoles.includes(user.role)) {
577
+ return res.status(403).json({
578
+ success: false,
579
+ message: `Access denied. Required role: ${allowedRoles.join(' or ')}`,
580
+ userRole: user.role || 'none',
581
+ });
582
+ }
583
+ if (!isEmailAllowed(user.email)) {
584
+ return res.status(403).json({
585
+ success: false,
586
+ message: 'Access denied. Your email is not authorized to access this dashboard.',
587
+ });
588
+ }
589
+ const studioSession = createStudioSession(user, getSessionDuration());
590
+ const encryptedSession = encryptSession(studioSession, getSessionSecret());
591
+ res.cookie(STUDIO_COOKIE_NAME, encryptedSession, {
592
+ httpOnly: true,
593
+ secure: process.env.NODE_ENV === 'production',
594
+ sameSite: 'lax',
595
+ maxAge: getSessionDuration(),
596
+ path: '/',
597
+ });
598
+ return res.json({
599
+ success: true,
600
+ user: { id: user.id, email: user.email, name: user.name, role: user.role },
601
+ });
602
+ }
603
+ catch (error) {
604
+ console.error('Sign-in error:', error);
605
+ return res.status(401).json({
606
+ success: false,
607
+ message: error?.message || 'Invalid credentials',
608
+ });
609
+ }
610
+ });
611
+ router.get('/api/auth/oauth/:provider', async (req, res) => {
612
+ try {
613
+ if (!authInstance) {
614
+ return res.status(500).json({ success: false, message: 'Auth not configured' });
615
+ }
616
+ const provider = req.params.provider;
617
+ const callbackURL = req.query.callbackURL;
618
+ const authBasePath = authInstance.options?.basePath || '/api/auth';
619
+ const oauthUrl = `${authBasePath}/sign-in/${provider}?callbackURL=${encodeURIComponent(callbackURL || '/')}`;
620
+ return res.redirect(oauthUrl);
621
+ }
622
+ catch (error) {
623
+ console.error('OAuth redirect error:', error);
624
+ return res.status(500).json({ success: false, message: 'OAuth redirect failed' });
625
+ }
626
+ });
627
+ router.post('/api/auth/verify', async (req, res) => {
628
+ try {
629
+ if (!authInstance) {
630
+ return res.status(500).json({ success: false, message: 'Auth not configured' });
631
+ }
632
+ const session = await authInstance.api.getSession({ headers: req.headers });
633
+ if (!session?.user) {
634
+ return res.status(401).json({ success: false, message: 'Not authenticated' });
635
+ }
636
+ const user = session.user;
637
+ const allowedRoles = getAllowedRoles();
638
+ if (!allowedRoles.includes(user.role)) {
639
+ return res.status(403).json({
640
+ success: false,
641
+ message: `Access denied. Required role: ${allowedRoles.join(' or ')}`,
642
+ userRole: user.role || 'none',
643
+ });
644
+ }
645
+ if (!isEmailAllowed(user.email)) {
646
+ return res.status(403).json({
647
+ success: false,
648
+ message: 'Access denied. Your email is not authorized to access this dashboard.',
649
+ });
650
+ }
651
+ const studioSession = createStudioSession(user, getSessionDuration());
652
+ const encryptedSession = encryptSession(studioSession, getSessionSecret());
653
+ res.cookie(STUDIO_COOKIE_NAME, encryptedSession, {
654
+ httpOnly: true,
655
+ secure: process.env.NODE_ENV === 'production',
656
+ sameSite: 'lax',
657
+ maxAge: getSessionDuration(),
658
+ path: '/',
659
+ });
660
+ return res.json({
661
+ success: true,
662
+ user: { id: user.id, email: user.email, name: user.name, role: user.role },
663
+ });
664
+ }
665
+ catch (error) {
666
+ console.error('Auth verify error:', error);
667
+ return res.status(500).json({ success: false, message: 'Failed to verify session' });
668
+ }
669
+ });
670
+ router.get('/api/auth/session', (req, res) => {
671
+ const sessionCookie = req.cookies?.[STUDIO_COOKIE_NAME];
672
+ if (!sessionCookie) {
673
+ return res.json({ authenticated: false });
674
+ }
675
+ const session = decryptSession(sessionCookie, getSessionSecret());
676
+ if (!isSessionValid(session)) {
677
+ return res.json({ authenticated: false, reason: 'expired' });
678
+ }
679
+ return res.json({
680
+ authenticated: true,
681
+ user: {
682
+ id: session.userId,
683
+ email: session.email,
684
+ name: session.name,
685
+ role: session.role,
686
+ image: session.image,
687
+ },
688
+ });
689
+ });
690
+ router.get('/api/auth/logout', (_req, res) => {
691
+ res.cookie(STUDIO_COOKIE_NAME, '', {
692
+ httpOnly: true,
693
+ secure: process.env.NODE_ENV === 'production',
694
+ sameSite: 'lax',
695
+ maxAge: 0,
696
+ path: '/',
697
+ });
698
+ return res.json({ success: true, message: 'Logged out' });
699
+ });
280
700
  router.get('/api/version-check', async (_req, res) => {
281
701
  try {
282
702
  const __dirname = dirname(fileURLToPath(import.meta.url));
@@ -361,6 +781,7 @@ export function createRoutes(authConfig, configPath, geoDbPath) {
361
781
  }
362
782
  });
363
783
  router.get('/api/config', async (_req, res) => {
784
+ const effectiveConfig = preloadedAuthOptions || authConfig;
364
785
  let databaseType = 'unknown';
365
786
  let databaseDialect = 'unknown';
366
787
  let databaseAdapter = 'unknown';
@@ -385,22 +806,25 @@ export function createRoutes(authConfig, configPath, geoDbPath) {
385
806
  }
386
807
  }
387
808
  catch (_error) { }
388
- if (databaseType === 'unknown') {
389
- const configPath = await findAuthConfigPath();
390
- if (configPath) {
391
- const content = readFileSync(configPath, 'utf-8');
392
- if (content.includes('drizzleAdapter')) {
393
- databaseType = 'Drizzle';
394
- }
395
- else if (content.includes('prismaAdapter')) {
396
- databaseType = 'Prisma';
397
- }
398
- else if (content.includes('better-sqlite3') || content.includes('new Database(')) {
399
- databaseType = 'SQLite';
809
+ if (databaseType === 'unknown' && !isSelfHosted) {
810
+ const authConfigPath = configPath || (await findAuthConfigPath());
811
+ if (authConfigPath) {
812
+ try {
813
+ const content = readFileSync(authConfigPath, 'utf-8');
814
+ if (content.includes('drizzleAdapter')) {
815
+ databaseType = 'Drizzle';
816
+ }
817
+ else if (content.includes('prismaAdapter')) {
818
+ databaseType = 'Prisma';
819
+ }
820
+ else if (content.includes('better-sqlite3') || content.includes('new Database(')) {
821
+ databaseType = 'SQLite';
822
+ }
400
823
  }
824
+ catch (_error) { }
401
825
  }
402
826
  if (databaseType === 'unknown') {
403
- let type = authConfig.database?.type || authConfig.database?.adapter || 'unknown';
827
+ let type = effectiveConfig.database?.type || effectiveConfig.database?.adapter || 'unknown';
404
828
  if (type && type !== 'unknown') {
405
829
  type = type.charAt(0).toUpperCase() + type.slice(1);
406
830
  }
@@ -408,52 +832,55 @@ export function createRoutes(authConfig, configPath, geoDbPath) {
408
832
  }
409
833
  }
410
834
  const config = {
411
- appName: authConfig.appName || 'Better Auth',
412
- baseURL: authConfig.baseURL || process.env.BETTER_AUTH_URL,
413
- basePath: authConfig.basePath || '/api/auth',
414
- secret: authConfig.secret ? 'Configured' : 'Not set',
835
+ appName: effectiveConfig.appName || 'Better Auth',
836
+ baseURL: effectiveConfig.baseURL || process.env.BETTER_AUTH_URL,
837
+ basePath: effectiveConfig.basePath || '/api/auth',
838
+ secret: effectiveConfig.secret ? 'Configured' : 'Not set',
415
839
  database: {
416
840
  type: databaseType,
417
- adapter: authConfig.database?.adapter || databaseAdapter,
841
+ adapter: effectiveConfig.database?.adapter || databaseAdapter,
418
842
  version: databaseVersion,
419
- casing: authConfig.database?.casing || 'camel',
420
- debugLogs: authConfig.database?.debugLogs || false,
421
- url: authConfig.database?.url,
843
+ casing: effectiveConfig.database?.casing || 'camel',
844
+ debugLogs: effectiveConfig.database?.debugLogs || false,
845
+ url: effectiveConfig.database?.url,
422
846
  adapterConfig: adapterConfig,
423
847
  dialect: adapterProvider,
424
848
  },
425
- emailVerification: authConfig.emailVerification,
426
- emailAndPassword: authConfig.emailAndPassword,
427
- socialProviders: authConfig.socialProviders
428
- ? authConfig.socialProviders.map((provider) => ({
429
- type: provider.id,
849
+ emailVerification: effectiveConfig.emailVerification,
850
+ emailAndPassword: effectiveConfig.emailAndPassword,
851
+ socialProviders: effectiveConfig.socialProviders
852
+ ? Object.entries(effectiveConfig.socialProviders).map(([id, provider]) => ({
853
+ type: id,
430
854
  clientId: provider.clientId,
431
855
  clientSecret: provider.clientSecret,
432
- redirectUri: provider.redirectUri,
856
+ id: id,
857
+ name: id,
858
+ redirectURI: provider.redirectURI,
859
+ enabled: !!(provider.clientId && provider.clientSecret),
433
860
  ...provider,
434
861
  }))
435
- : authConfig.providers || [],
862
+ : [],
436
863
  user: {
437
- modelName: authConfig.user?.modelName || 'user',
864
+ modelName: effectiveConfig.user?.modelName || 'user',
438
865
  changeEmail: {
439
- enabled: authConfig.user?.changeEmail?.enabled || false,
866
+ enabled: effectiveConfig.user?.changeEmail?.enabled || false,
440
867
  },
441
868
  deleteUser: {
442
- enabled: authConfig.user?.deleteUser?.enabled || false,
443
- deleteTokenExpiresIn: authConfig.user?.deleteUser?.deleteTokenExpiresIn || 86400,
869
+ enabled: effectiveConfig.user?.deleteUser?.enabled || false,
870
+ deleteTokenExpiresIn: effectiveConfig.user?.deleteUser?.deleteTokenExpiresIn || 86400,
444
871
  },
445
872
  },
446
- session: authConfig.session,
447
- account: authConfig.account,
873
+ session: effectiveConfig.session,
874
+ account: effectiveConfig.account,
448
875
  verification: {
449
- modelName: authConfig.verification?.modelName || 'verification',
450
- disableCleanup: authConfig.verification?.disableCleanup || false,
876
+ modelName: effectiveConfig.verification?.modelName || 'verification',
877
+ disableCleanup: effectiveConfig.verification?.disableCleanup || false,
451
878
  },
452
- trustedOrigins: authConfig.trustedOrigins,
453
- rateLimit: authConfig.rateLimit,
454
- advanced: authConfig.advanced,
455
- disabledPaths: authConfig.disabledPaths || [],
456
- telemetry: authConfig.telemetry,
879
+ trustedOrigins: effectiveConfig.trustedOrigins,
880
+ rateLimit: effectiveConfig.rateLimit,
881
+ advanced: effectiveConfig.advanced,
882
+ disabledPaths: effectiveConfig.disabledPaths || [],
883
+ telemetry: effectiveConfig.telemetry,
457
884
  studio: {
458
885
  version: getStudioVersion(),
459
886
  nodeVersion: process.version,
@@ -497,23 +924,26 @@ export function createRoutes(authConfig, configPath, geoDbPath) {
497
924
  let organizationPluginEnabled = false;
498
925
  let teamsPluginEnabled = false;
499
926
  try {
500
- const authConfigPath = configPath || (await findAuthConfigPath());
501
- if (authConfigPath) {
502
- const { getConfig } = await import('./config.js');
503
- const betterAuthConfig = await getConfig({
504
- cwd: process.cwd(),
505
- configPath: authConfigPath,
506
- shouldThrowOnError: false,
507
- noCache: true, // Disable cache for real-time plugin checks
508
- });
509
- if (betterAuthConfig) {
510
- const plugins = betterAuthConfig.plugins || [];
511
- const organizationPlugin = plugins.find((plugin) => plugin.id === 'organization');
512
- organizationPluginEnabled = !!organizationPlugin;
513
- teamsPluginEnabled = !!organizationPlugin?.options?.teams?.enabled;
514
- if (organizationPlugin) {
515
- teamsPluginEnabled = organizationPlugin.options?.teams?.enabled === true;
516
- }
927
+ let betterAuthConfig = preloadedAuthOptions;
928
+ if (!betterAuthConfig && !isSelfHosted) {
929
+ const authConfigPath = configPath || (await findAuthConfigPath());
930
+ if (authConfigPath) {
931
+ const { getConfig } = await import('./config.js');
932
+ betterAuthConfig = await getConfig({
933
+ cwd: process.cwd(),
934
+ configPath: authConfigPath,
935
+ shouldThrowOnError: false,
936
+ noCache: true, // Disable cache for real-time plugin checks
937
+ });
938
+ }
939
+ }
940
+ if (betterAuthConfig) {
941
+ const plugins = betterAuthConfig.plugins || [];
942
+ const organizationPlugin = plugins.find((plugin) => plugin.id === 'organization');
943
+ organizationPluginEnabled = !!organizationPlugin;
944
+ teamsPluginEnabled = !!organizationPlugin?.options?.teams?.enabled;
945
+ if (organizationPlugin) {
946
+ teamsPluginEnabled = organizationPlugin.options?.teams?.enabled === true;
517
947
  }
518
948
  }
519
949
  }
@@ -1109,7 +1539,6 @@ export function createRoutes(authConfig, configPath, geoDbPath) {
1109
1539
  authModule = await safeImportAuthConfig(authConfigPath, true); // Disable cache for real-time plugin checks
1110
1540
  }
1111
1541
  catch (_importError) {
1112
- // Fallback: read file content directly
1113
1542
  const content = readFileSync(authConfigPath, 'utf-8');
1114
1543
  authModule = {
1115
1544
  auth: {
@@ -1407,13 +1836,22 @@ export function createRoutes(authConfig, configPath, geoDbPath) {
1407
1836
  addResult('Database', 'Connection String', 'pass', 'Database connection string is configured');
1408
1837
  }
1409
1838
  // 3. OAuth/Social Providers
1410
- const socialProviders = authConfig.socialProviders || [];
1411
- if (socialProviders.length === 0) {
1839
+ const socialProvidersRaw = (preloadedAuthOptions || authConfig || {}).socialProviders || {};
1840
+ const effectiveSocialProviders = Array.isArray(socialProvidersRaw)
1841
+ ? socialProvidersRaw
1842
+ : Object.entries(socialProvidersRaw).map(([id, p]) => ({
1843
+ id,
1844
+ type: id,
1845
+ name: id,
1846
+ ...p,
1847
+ enabled: !!(p.clientId && p.clientSecret),
1848
+ }));
1849
+ if (effectiveSocialProviders.length === 0) {
1412
1850
  addResult('OAuth Providers', 'Providers', 'warning', 'No OAuth providers configured', 'This is optional. Add social providers if you need OAuth authentication', 'info');
1413
1851
  }
1414
1852
  else {
1415
- addResult('OAuth Providers', 'Providers', 'pass', `${socialProviders.length} OAuth provider(s) configured`);
1416
- socialProviders.forEach((provider) => {
1853
+ addResult('OAuth Providers', 'Providers', 'pass', `${effectiveSocialProviders.length} OAuth provider(s) configured`);
1854
+ effectiveSocialProviders.forEach((provider) => {
1417
1855
  if (provider.enabled) {
1418
1856
  if (!provider.clientId) {
1419
1857
  addResult('OAuth Providers', `${provider.name} - Client ID`, 'fail', `${provider.name} is enabled but clientId is missing`, `Add clientId for ${provider.name} in your auth config`, 'error');
@@ -1608,23 +2046,11 @@ export function createRoutes(authConfig, configPath, geoDbPath) {
1608
2046
  });
1609
2047
  router.post('/api/admin/ban-user', async (req, res) => {
1610
2048
  try {
1611
- const authConfigPath = configPath || (await findAuthConfigPath());
1612
- if (!authConfigPath) {
1613
- return res.status(400).json({
1614
- success: false,
1615
- error: 'No auth config found',
1616
- });
1617
- }
1618
- const { getConfig } = await import('./config.js');
1619
- const auth = await getConfig({
1620
- cwd: process.cwd(),
1621
- configPath: authConfigPath,
1622
- shouldThrowOnError: false,
1623
- });
2049
+ const auth = await getAuthConfigSafe();
1624
2050
  if (!auth) {
1625
2051
  return res.status(400).json({
1626
2052
  success: false,
1627
- error: 'Failed to load auth config',
2053
+ error: 'No auth config found',
1628
2054
  });
1629
2055
  }
1630
2056
  const plugins = auth.plugins || [];
@@ -1659,23 +2085,11 @@ export function createRoutes(authConfig, configPath, geoDbPath) {
1659
2085
  });
1660
2086
  router.post('/api/admin/unban-user', async (req, res) => {
1661
2087
  try {
1662
- const authConfigPath = configPath || (await findAuthConfigPath());
1663
- if (!authConfigPath) {
1664
- return res.status(400).json({
1665
- success: false,
1666
- error: 'No auth config found',
1667
- });
1668
- }
1669
- const { getConfig } = await import('./config.js');
1670
- const auth = await getConfig({
1671
- cwd: process.cwd(),
1672
- configPath: authConfigPath,
1673
- shouldThrowOnError: false,
1674
- });
2088
+ const auth = await getAuthConfigSafe();
1675
2089
  if (!auth) {
1676
2090
  return res.status(400).json({
1677
2091
  success: false,
1678
- error: 'Failed to load auth config',
2092
+ error: 'No auth config found',
1679
2093
  });
1680
2094
  }
1681
2095
  const plugins = auth.plugins || [];
@@ -1710,32 +2124,19 @@ export function createRoutes(authConfig, configPath, geoDbPath) {
1710
2124
  });
1711
2125
  router.get('/api/admin/status', async (_req, res) => {
1712
2126
  try {
1713
- const authConfigPath = configPath || (await findAuthConfigPath());
1714
- if (!authConfigPath) {
2127
+ const betterAuthConfig = await getAuthConfigSafe();
2128
+ if (!betterAuthConfig) {
1715
2129
  return res.json({
1716
2130
  enabled: false,
1717
2131
  error: 'No auth config found',
1718
2132
  configPath: null,
1719
2133
  });
1720
2134
  }
1721
- const { getConfig } = await import('./config.js');
1722
- const betterAuthConfig = await getConfig({
1723
- cwd: process.cwd(),
1724
- configPath: authConfigPath,
1725
- shouldThrowOnError: false,
1726
- });
1727
- if (!betterAuthConfig) {
1728
- return res.json({
1729
- enabled: false,
1730
- error: 'Failed to load auth config',
1731
- configPath: authConfigPath,
1732
- });
1733
- }
1734
2135
  const plugins = betterAuthConfig.plugins || [];
1735
2136
  const adminPlugin = plugins.find((plugin) => plugin.id === 'admin');
1736
2137
  res.json({
1737
2138
  enabled: !!adminPlugin,
1738
- configPath: authConfigPath,
2139
+ configPath: configPath || null,
1739
2140
  adminPlugin: adminPlugin || null,
1740
2141
  });
1741
2142
  }
@@ -1950,51 +2351,32 @@ export function createRoutes(authConfig, configPath, geoDbPath) {
1950
2351
  });
1951
2352
  router.get('/api/plugins/teams/status', async (_req, res) => {
1952
2353
  try {
1953
- const authConfigPath = configPath || (await findAuthConfigPath());
1954
- if (!authConfigPath) {
2354
+ const betterAuthConfig = await getAuthConfigSafe();
2355
+ if (!betterAuthConfig) {
1955
2356
  return res.json({
1956
2357
  enabled: false,
1957
2358
  error: 'No auth config found',
1958
2359
  configPath: null,
1959
2360
  });
1960
2361
  }
1961
- try {
1962
- const { getConfig } = await import('./config.js');
1963
- const betterAuthConfig = await getConfig({
1964
- cwd: process.cwd(),
1965
- configPath: authConfigPath,
1966
- shouldThrowOnError: false,
1967
- noCache: true, // Disable cache for real-time plugin status checks
2362
+ const plugins = betterAuthConfig.plugins || [];
2363
+ const organizationPlugin = plugins.find((plugin) => plugin.id === 'organization');
2364
+ if (organizationPlugin) {
2365
+ const teamsEnabled = organizationPlugin.options?.teams?.enabled === true;
2366
+ return res.json({
2367
+ enabled: teamsEnabled,
2368
+ configPath: configPath || null,
2369
+ organizationPlugin: organizationPlugin || null,
1968
2370
  });
1969
- if (betterAuthConfig) {
1970
- const plugins = betterAuthConfig.plugins || [];
1971
- const organizationPlugin = plugins.find((plugin) => plugin.id === 'organization');
1972
- if (organizationPlugin) {
1973
- const teamsEnabled = organizationPlugin.options?.teams?.enabled === true;
1974
- return res.json({
1975
- enabled: teamsEnabled,
1976
- configPath: authConfigPath,
1977
- organizationPlugin: organizationPlugin || null,
1978
- });
1979
- }
1980
- else {
1981
- return res.json({
1982
- enabled: false,
1983
- configPath: authConfigPath,
1984
- organizationPlugin: null,
1985
- error: 'Organization plugin not found',
1986
- });
1987
- }
1988
- }
1989
- res.json({
2371
+ }
2372
+ else {
2373
+ return res.json({
1990
2374
  enabled: false,
1991
- error: 'Failed to load auth config - getConfig failed and regex extraction unavailable',
1992
- configPath: authConfigPath,
2375
+ configPath: configPath || null,
2376
+ organizationPlugin: null,
2377
+ error: 'Organization plugin not found',
1993
2378
  });
1994
2379
  }
1995
- catch (_error) {
1996
- res.status(500).json({ error: 'Failed to check teams status' });
1997
- }
1998
2380
  }
1999
2381
  catch (_error) {
2000
2382
  res.status(500).json({ error: 'Failed to check teams status' });
@@ -2591,41 +2973,22 @@ export function createRoutes(authConfig, configPath, geoDbPath) {
2591
2973
  });
2592
2974
  router.get('/api/plugins/organization/status', async (_req, res) => {
2593
2975
  try {
2594
- const authConfigPath = configPath || (await findAuthConfigPath());
2595
- if (!authConfigPath) {
2976
+ const betterAuthConfig = await getAuthConfigSafe();
2977
+ if (!betterAuthConfig) {
2596
2978
  return res.json({
2597
2979
  enabled: false,
2598
2980
  error: 'No auth config found',
2599
2981
  configPath: null,
2600
2982
  });
2601
2983
  }
2602
- try {
2603
- const { getConfig } = await import('./config.js');
2604
- const betterAuthConfig = await getConfig({
2605
- cwd: process.cwd(),
2606
- configPath: authConfigPath,
2607
- shouldThrowOnError: false,
2608
- noCache: true,
2609
- });
2610
- if (betterAuthConfig) {
2611
- const plugins = betterAuthConfig?.plugins || [];
2612
- const hasOrganizationPlugin = plugins.find((plugin) => plugin.id === 'organization');
2613
- return res.json({
2614
- enabled: !!hasOrganizationPlugin,
2615
- configPath: authConfigPath,
2616
- availablePlugins: plugins.map((p) => p.id) || [],
2617
- organizationPlugin: hasOrganizationPlugin || null,
2618
- });
2619
- }
2620
- res.json({
2621
- enabled: false,
2622
- error: 'Failed to load auth config - getConfig failed and regex extraction unavailable',
2623
- configPath: authConfigPath,
2624
- });
2625
- }
2626
- catch (_error) {
2627
- res.status(500).json({ error: 'Failed to check plugin status' });
2628
- }
2984
+ const plugins = betterAuthConfig?.plugins || [];
2985
+ const hasOrganizationPlugin = plugins.find((plugin) => plugin.id === 'organization');
2986
+ return res.json({
2987
+ enabled: !!hasOrganizationPlugin,
2988
+ configPath: configPath || null,
2989
+ availablePlugins: plugins.map((p) => p.id) || [],
2990
+ organizationPlugin: hasOrganizationPlugin || null,
2991
+ });
2629
2992
  }
2630
2993
  catch (_error) {
2631
2994
  res.status(500).json({ error: 'Failed to check plugin status' });
@@ -2854,7 +3217,6 @@ export function createRoutes(authConfig, configPath, geoDbPath) {
2854
3217
  if (!adapter) {
2855
3218
  return res.status(500).json({ error: 'Auth adapter not available' });
2856
3219
  }
2857
- // @ts-expect-error
2858
3220
  const user = await adapter.findOne({
2859
3221
  model: 'user',
2860
3222
  where: [{ field: 'id', value: userId }],
@@ -2908,7 +3270,6 @@ export function createRoutes(authConfig, configPath, geoDbPath) {
2908
3270
  if (!adapter) {
2909
3271
  return res.status(500).json({ error: 'Auth adapter not available' });
2910
3272
  }
2911
- // @ts-expect-error
2912
3273
  const user = await adapter.findOne({
2913
3274
  model: 'user',
2914
3275
  where: [{ field: 'id', value: userId }],
@@ -3109,7 +3470,17 @@ export function createRoutes(authConfig, configPath, geoDbPath) {
3109
3470
  });
3110
3471
  router.get('/api/tools/oauth/providers', async (_req, res) => {
3111
3472
  try {
3112
- const providers = authConfig.socialProviders || [];
3473
+ const effectiveConfig = preloadedAuthOptions || authConfig || {};
3474
+ const socialProviders = effectiveConfig.socialProviders || {};
3475
+ const providers = Array.isArray(socialProviders)
3476
+ ? socialProviders
3477
+ : Object.entries(socialProviders).map(([id, provider]) => ({
3478
+ id,
3479
+ name: provider.name || id,
3480
+ type: id,
3481
+ enabled: !!(provider.clientId && provider.clientSecret),
3482
+ ...provider,
3483
+ }));
3113
3484
  res.json({
3114
3485
  success: true,
3115
3486
  providers: providers.map((provider) => ({
@@ -3120,7 +3491,8 @@ export function createRoutes(authConfig, configPath, geoDbPath) {
3120
3491
  })),
3121
3492
  });
3122
3493
  }
3123
- catch (_error) {
3494
+ catch (error) {
3495
+ console.error('Failed to fetch OAuth providers:', error);
3124
3496
  res.status(500).json({ success: false, error: 'Failed to fetch OAuth providers' });
3125
3497
  }
3126
3498
  });
@@ -3130,7 +3502,15 @@ export function createRoutes(authConfig, configPath, geoDbPath) {
3130
3502
  if (!provider) {
3131
3503
  return res.status(400).json({ success: false, error: 'Provider is required' });
3132
3504
  }
3133
- const providers = authConfig.socialProviders || [];
3505
+ const effectiveConfig = preloadedAuthOptions || authConfig || {};
3506
+ const socialProviders = effectiveConfig.socialProviders || {};
3507
+ const providers = Array.isArray(socialProviders)
3508
+ ? socialProviders
3509
+ : Object.entries(socialProviders).map(([id, p]) => ({
3510
+ id,
3511
+ type: id,
3512
+ ...p,
3513
+ }));
3134
3514
  const selectedProvider = providers.find((p) => (p.id || p.type) === provider);
3135
3515
  if (!selectedProvider) {
3136
3516
  return res.status(404).json({ success: false, error: 'Provider not found' });
@@ -5004,4 +5384,126 @@ export const authClient = createAuthClient({
5004
5384
  });
5005
5385
  return router;
5006
5386
  }
5387
+ export async function handleStudioApiRequest(ctx) {
5388
+ let preloadedAdapter = null;
5389
+ if (ctx.auth) {
5390
+ try {
5391
+ const context = await ctx.auth.$context;
5392
+ if (context?.adapter) {
5393
+ preloadedAdapter = context.adapter;
5394
+ }
5395
+ }
5396
+ catch { }
5397
+ }
5398
+ const authOptions = ctx.auth?.options || null;
5399
+ const router = createRoutes(ctx.auth, ctx.configPath || '', undefined, preloadedAdapter, authOptions, ctx.accessConfig, ctx.auth);
5400
+ const [pathname, queryString] = ctx.path.split('?');
5401
+ const query = {};
5402
+ if (queryString) {
5403
+ queryString.split('&').forEach((param) => {
5404
+ const [key, value] = param.split('=');
5405
+ if (key)
5406
+ query[key] = decodeURIComponent(value || '');
5407
+ });
5408
+ }
5409
+ try {
5410
+ const route = findMatchingRoute(router, pathname, ctx.method);
5411
+ if (!route) {
5412
+ return { status: 404, data: { error: 'Not found', path: pathname } };
5413
+ }
5414
+ const cookies = [];
5415
+ const parseCookies = (cookieHeader) => {
5416
+ const result = {};
5417
+ if (cookieHeader) {
5418
+ cookieHeader.split(';').forEach((cookie) => {
5419
+ const [key, ...rest] = cookie.split('=');
5420
+ if (key)
5421
+ result[key.trim()] = rest.join('=').trim();
5422
+ });
5423
+ }
5424
+ return result;
5425
+ };
5426
+ const mockReq = {
5427
+ method: ctx.method,
5428
+ url: ctx.path,
5429
+ path: pathname,
5430
+ originalUrl: ctx.path,
5431
+ headers: ctx.headers,
5432
+ body: ctx.body,
5433
+ query: query,
5434
+ params: route.params,
5435
+ cookies: parseCookies(ctx.headers['cookie'] || ctx.headers['Cookie'] || ''),
5436
+ };
5437
+ let responseStatus = 200;
5438
+ let responseData = {};
5439
+ const mockRes = {
5440
+ status: (code) => {
5441
+ responseStatus = code;
5442
+ return mockRes;
5443
+ },
5444
+ json: (data) => {
5445
+ responseData = data;
5446
+ return mockRes;
5447
+ },
5448
+ send: (data) => {
5449
+ responseData = data;
5450
+ return mockRes;
5451
+ },
5452
+ cookie: (name, value, options) => {
5453
+ cookies.push({ name, value, options });
5454
+ return mockRes;
5455
+ },
5456
+ redirect: (url) => {
5457
+ responseStatus = 302;
5458
+ responseData = { redirect: url };
5459
+ return mockRes;
5460
+ },
5461
+ };
5462
+ await route.handler(mockReq, mockRes);
5463
+ return { status: responseStatus, data: responseData, cookies };
5464
+ }
5465
+ catch (error) {
5466
+ console.error('Studio API error:', error);
5467
+ return { status: 500, data: { error: 'Internal server error' } };
5468
+ }
5469
+ }
5470
+ function findMatchingRoute(router, path, method) {
5471
+ const routes = router.stack || [];
5472
+ for (const layer of routes) {
5473
+ if (layer.route) {
5474
+ const routePath = layer.route.path;
5475
+ const routeMethods = Object.keys(layer.route.methods);
5476
+ if (routeMethods.includes(method.toLowerCase())) {
5477
+ const params = extractParams(routePath, path);
5478
+ if (params !== null) {
5479
+ return {
5480
+ handler: layer.route.stack[0].handle,
5481
+ params,
5482
+ };
5483
+ }
5484
+ }
5485
+ }
5486
+ }
5487
+ return null;
5488
+ }
5489
+ function extractParams(routePath, requestPath) {
5490
+ if (routePath === requestPath)
5491
+ return {};
5492
+ const paramNames = [];
5493
+ const routeRegex = routePath
5494
+ .replace(/:([^/]+)/g, (_, paramName) => {
5495
+ paramNames.push(paramName);
5496
+ return '([^/]+)';
5497
+ })
5498
+ .replace(/\*/g, '.*');
5499
+ const regex = new RegExp(`^${routeRegex}$`);
5500
+ const match = requestPath.match(regex);
5501
+ if (!match)
5502
+ return null;
5503
+ const params = {};
5504
+ paramNames.forEach((name, index) => {
5505
+ params[name] = match[index + 1];
5506
+ });
5507
+ return params;
5508
+ }
5007
5509
  //# sourceMappingURL=routes.js.map