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.
- package/dist/adapters/express.d.ts +7 -0
- package/dist/adapters/express.d.ts.map +1 -0
- package/dist/adapters/express.js +40 -0
- package/dist/adapters/express.js.map +1 -0
- package/dist/adapters/nextjs.d.ts +16 -0
- package/dist/adapters/nextjs.d.ts.map +1 -0
- package/dist/adapters/nextjs.js +49 -0
- package/dist/adapters/nextjs.js.map +1 -0
- package/dist/auth-adapter.d.ts.map +1 -1
- package/dist/auth-adapter.js +3 -2
- package/dist/auth-adapter.js.map +1 -1
- package/dist/cli/commands/init.d.ts +2 -0
- package/dist/cli/commands/init.d.ts.map +1 -0
- package/dist/cli/commands/init.js +140 -0
- package/dist/cli/commands/init.js.map +1 -0
- package/dist/cli.js +13 -0
- package/dist/cli.js.map +1 -1
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +3 -0
- package/dist/config.js.map +1 -1
- package/dist/core/handler.d.ts +14 -0
- package/dist/core/handler.d.ts.map +1 -0
- package/dist/core/handler.js +256 -0
- package/dist/core/handler.js.map +1 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -0
- package/dist/public/assets/main-DtDccUVq.js +1155 -0
- package/dist/public/assets/main-wa9xKUE4.css +1 -0
- package/dist/public/favicon.svg +6 -0
- package/dist/public/index.html +14 -0
- package/dist/public/logo.png +0 -0
- package/dist/public/vite.svg +5 -0
- package/dist/routes/api-router.d.ts +21 -0
- package/dist/routes/api-router.d.ts.map +1 -0
- package/dist/routes/api-router.js +14 -0
- package/dist/routes/api-router.js.map +1 -0
- package/dist/routes.d.ts +20 -1
- package/dist/routes.d.ts.map +1 -1
- package/dist/routes.js +680 -178
- package/dist/routes.js.map +1 -1
- package/dist/studio.d.ts.map +1 -1
- package/dist/studio.js +28 -2
- package/dist/studio.js.map +1 -1
- package/dist/types/handler.d.ts +58 -0
- package/dist/types/handler.d.ts.map +1 -0
- package/dist/types/handler.js +4 -0
- package/dist/types/handler.js.map +1 -0
- package/dist/utils/html-injector.d.ts +30 -0
- package/dist/utils/html-injector.d.ts.map +1 -0
- package/dist/utils/html-injector.js +61 -0
- package/dist/utils/html-injector.js.map +1 -0
- package/dist/utils/session.d.ts +21 -0
- package/dist/utils/session.d.ts.map +1 -0
- package/dist/utils/session.js +51 -0
- package/dist/utils/session.js.map +1 -0
- package/frontend/package.json +64 -0
- package/package.json +35 -26
- package/public/assets/main-DtDccUVq.js +1155 -0
- package/public/assets/main-wa9xKUE4.css +1 -0
- package/public/favicon.svg +6 -0
- package/public/index.html +3 -3
- package/public/logo.png +0 -0
- package/public/vite.svg +5 -0
- package/public/assets/main-Du6zwcd_.css +0 -1
- 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
|
-
|
|
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
|
|
390
|
-
if (
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
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 =
|
|
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:
|
|
412
|
-
baseURL:
|
|
413
|
-
basePath:
|
|
414
|
-
secret:
|
|
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:
|
|
841
|
+
adapter: effectiveConfig.database?.adapter || databaseAdapter,
|
|
418
842
|
version: databaseVersion,
|
|
419
|
-
casing:
|
|
420
|
-
debugLogs:
|
|
421
|
-
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:
|
|
426
|
-
emailAndPassword:
|
|
427
|
-
socialProviders:
|
|
428
|
-
?
|
|
429
|
-
type:
|
|
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
|
-
|
|
856
|
+
id: id,
|
|
857
|
+
name: id,
|
|
858
|
+
redirectURI: provider.redirectURI,
|
|
859
|
+
enabled: !!(provider.clientId && provider.clientSecret),
|
|
433
860
|
...provider,
|
|
434
861
|
}))
|
|
435
|
-
:
|
|
862
|
+
: [],
|
|
436
863
|
user: {
|
|
437
|
-
modelName:
|
|
864
|
+
modelName: effectiveConfig.user?.modelName || 'user',
|
|
438
865
|
changeEmail: {
|
|
439
|
-
enabled:
|
|
866
|
+
enabled: effectiveConfig.user?.changeEmail?.enabled || false,
|
|
440
867
|
},
|
|
441
868
|
deleteUser: {
|
|
442
|
-
enabled:
|
|
443
|
-
deleteTokenExpiresIn:
|
|
869
|
+
enabled: effectiveConfig.user?.deleteUser?.enabled || false,
|
|
870
|
+
deleteTokenExpiresIn: effectiveConfig.user?.deleteUser?.deleteTokenExpiresIn || 86400,
|
|
444
871
|
},
|
|
445
872
|
},
|
|
446
|
-
session:
|
|
447
|
-
account:
|
|
873
|
+
session: effectiveConfig.session,
|
|
874
|
+
account: effectiveConfig.account,
|
|
448
875
|
verification: {
|
|
449
|
-
modelName:
|
|
450
|
-
disableCleanup:
|
|
876
|
+
modelName: effectiveConfig.verification?.modelName || 'verification',
|
|
877
|
+
disableCleanup: effectiveConfig.verification?.disableCleanup || false,
|
|
451
878
|
},
|
|
452
|
-
trustedOrigins:
|
|
453
|
-
rateLimit:
|
|
454
|
-
advanced:
|
|
455
|
-
disabledPaths:
|
|
456
|
-
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
|
-
|
|
501
|
-
if (
|
|
502
|
-
const
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
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
|
|
1411
|
-
|
|
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', `${
|
|
1416
|
-
|
|
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
|
|
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: '
|
|
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
|
|
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: '
|
|
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
|
|
1714
|
-
if (!
|
|
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:
|
|
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
|
|
1954
|
-
if (!
|
|
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
|
-
|
|
1962
|
-
|
|
1963
|
-
|
|
1964
|
-
|
|
1965
|
-
|
|
1966
|
-
|
|
1967
|
-
|
|
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
|
-
|
|
1970
|
-
|
|
1971
|
-
|
|
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
|
-
|
|
1992
|
-
|
|
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
|
|
2595
|
-
if (!
|
|
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
|
-
|
|
2603
|
-
|
|
2604
|
-
|
|
2605
|
-
|
|
2606
|
-
|
|
2607
|
-
|
|
2608
|
-
|
|
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
|
|
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 (
|
|
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
|
|
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
|