better-auth-studio 1.0.79-beta.5 → 1.0.79-beta.50
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/README.md +10 -0
- package/dist/adapters/hono.d.ts +7 -0
- package/dist/adapters/hono.d.ts.map +1 -0
- package/dist/adapters/hono.js +78 -0
- package/dist/adapters/hono.js.map +1 -0
- package/dist/cli/commands/init.d.ts +3 -1
- package/dist/cli/commands/init.d.ts.map +1 -1
- package/dist/cli/commands/init.js +28 -6
- package/dist/cli/commands/init.js.map +1 -1
- package/dist/cli.js +3 -2
- package/dist/cli.js.map +1 -1
- package/dist/core/handler.d.ts.map +1 -1
- package/dist/core/handler.js +167 -52
- package/dist/core/handler.js.map +1 -1
- package/dist/data.d.ts +1 -1
- package/dist/data.d.ts.map +1 -1
- package/dist/data.js +3 -7
- package/dist/data.js.map +1 -1
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -1
- package/dist/public/assets/main-BjWPiv1k.css +1 -0
- package/dist/public/assets/main-D8LhxAuO.js +1155 -0
- package/dist/public/index.html +2 -2
- package/dist/routes.d.ts.map +1 -1
- package/dist/routes.js +750 -147
- package/dist/routes.js.map +1 -1
- package/dist/studio.d.ts.map +1 -1
- package/dist/studio.js +15 -2
- package/dist/studio.js.map +1 -1
- package/package.json +8 -7
- package/public/assets/main-BjWPiv1k.css +1 -0
- package/public/assets/main-D8LhxAuO.js +1155 -0
- package/public/favicon.svg +6 -0
- package/public/index.html +14 -0
- package/public/logo.png +0 -0
- package/public/vite.svg +5 -0
- package/scripts/download-geolite2.js +35 -0
- package/scripts/generate-default-db.js +462 -0
- package/scripts/postinstall.js +96 -0
- package/data/GeoLite2-City.mmdb +0 -0
- package/data/GeoLite2-City.tar.gz +0 -1
- package/dist/public/assets/main-3NIBCudD.js +0 -1155
- package/dist/public/assets/main-DbXDm13A.css +0 -1
package/dist/routes.js
CHANGED
|
@@ -7,6 +7,7 @@ import { hex } from '@better-auth/utils/hex';
|
|
|
7
7
|
import { scryptAsync } from '@noble/hashes/scrypt.js';
|
|
8
8
|
import { Router } from 'express';
|
|
9
9
|
import { createJiti } from 'jiti';
|
|
10
|
+
import { createRequire } from 'module';
|
|
10
11
|
import { createMockAccount, createMockSession, createMockUser, createMockVerification, getAuthAdapter, } from './auth-adapter.js';
|
|
11
12
|
import { possiblePaths } from './config.js';
|
|
12
13
|
import { getAuthData } from './data.js';
|
|
@@ -57,6 +58,20 @@ function getStudioVersion() {
|
|
|
57
58
|
const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf-8'));
|
|
58
59
|
return packageJson.version || '1.0.0';
|
|
59
60
|
}
|
|
61
|
+
const nodeModulesPath = join(process.cwd(), 'node_modules/better-auth-studio/package.json');
|
|
62
|
+
if (existsSync(nodeModulesPath)) {
|
|
63
|
+
const packageJson = JSON.parse(readFileSync(nodeModulesPath, 'utf-8'));
|
|
64
|
+
return packageJson.version || '1.0.0';
|
|
65
|
+
}
|
|
66
|
+
try {
|
|
67
|
+
const require = createRequire(import.meta.url);
|
|
68
|
+
const resolvedPath = require.resolve('better-auth-studio/package.json');
|
|
69
|
+
if (existsSync(resolvedPath)) {
|
|
70
|
+
const packageJson = JSON.parse(readFileSync(resolvedPath, 'utf-8'));
|
|
71
|
+
return packageJson.version || '1.0.0';
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
catch (_resolveError) { }
|
|
60
75
|
}
|
|
61
76
|
catch (_error) { }
|
|
62
77
|
return '1.0.0';
|
|
@@ -252,7 +267,10 @@ async function findAuthConfigPath() {
|
|
|
252
267
|
export function createRoutes(authConfig, configPath, geoDbPath, preloadedAdapter, preloadedAuthOptions, accessConfig, authInstance) {
|
|
253
268
|
const isSelfHosted = !!preloadedAdapter;
|
|
254
269
|
const getAuthConfigSafe = async () => {
|
|
255
|
-
if (isSelfHosted
|
|
270
|
+
if (isSelfHosted) {
|
|
271
|
+
return preloadedAuthOptions || authConfig || null;
|
|
272
|
+
}
|
|
273
|
+
if (preloadedAuthOptions) {
|
|
256
274
|
return preloadedAuthOptions;
|
|
257
275
|
}
|
|
258
276
|
try {
|
|
@@ -270,7 +288,7 @@ export function createRoutes(authConfig, configPath, geoDbPath, preloadedAdapter
|
|
|
270
288
|
catch (_error) {
|
|
271
289
|
// Ignors errors
|
|
272
290
|
}
|
|
273
|
-
return null;
|
|
291
|
+
return authConfig || null;
|
|
274
292
|
};
|
|
275
293
|
const router = Router();
|
|
276
294
|
const base64UrlEncode = (value) => Buffer.from(value)
|
|
@@ -302,6 +320,10 @@ export function createRoutes(authConfig, configPath, geoDbPath, preloadedAdapter
|
|
|
302
320
|
// For self-hosted studio, wrap the preloaded adapter to match expected interface
|
|
303
321
|
return {
|
|
304
322
|
...preloadedAdapter,
|
|
323
|
+
findUnique: preloadedAdapter.findUnique?.bind(preloadedAdapter),
|
|
324
|
+
findOne: preloadedAdapter.findOne?.bind(preloadedAdapter) ||
|
|
325
|
+
preloadedAdapter.findUnique?.bind(preloadedAdapter),
|
|
326
|
+
findFirst: preloadedAdapter.findFirst?.bind(preloadedAdapter),
|
|
305
327
|
findMany: preloadedAdapter.findMany?.bind(preloadedAdapter),
|
|
306
328
|
create: preloadedAdapter.create?.bind(preloadedAdapter),
|
|
307
329
|
update: preloadedAdapter.update?.bind(preloadedAdapter),
|
|
@@ -476,7 +498,7 @@ export function createRoutes(authConfig, configPath, geoDbPath, preloadedAdapter
|
|
|
476
498
|
if (!email || !password) {
|
|
477
499
|
return res.status(400).json({ success: false, message: 'Email and password required' });
|
|
478
500
|
}
|
|
479
|
-
const adapter = await
|
|
501
|
+
const adapter = await getAuthAdapterWithConfig();
|
|
480
502
|
let signInResult = null;
|
|
481
503
|
let signInError = null;
|
|
482
504
|
try {
|
|
@@ -517,7 +539,6 @@ export function createRoutes(authConfig, configPath, geoDbPath, preloadedAdapter
|
|
|
517
539
|
message: 'Password not configured. Please reset your password.',
|
|
518
540
|
});
|
|
519
541
|
}
|
|
520
|
-
console.log({ users, credentialAccount });
|
|
521
542
|
const isValidPassword = await verifyPassword(password, credentialAccount.password);
|
|
522
543
|
if (!isValidPassword) {
|
|
523
544
|
return res.status(401).json({ success: false, message: 'Invalid credentials' });
|
|
@@ -528,7 +549,7 @@ export function createRoutes(authConfig, configPath, geoDbPath, preloadedAdapter
|
|
|
528
549
|
if (!allowedRoles.includes(user.role)) {
|
|
529
550
|
return res.status(403).json({
|
|
530
551
|
success: false,
|
|
531
|
-
message: `Access denied
|
|
552
|
+
message: `Access denied.`,
|
|
532
553
|
userRole: user.role || 'none',
|
|
533
554
|
});
|
|
534
555
|
}
|
|
@@ -577,7 +598,7 @@ export function createRoutes(authConfig, configPath, geoDbPath, preloadedAdapter
|
|
|
577
598
|
if (!allowedRoles.includes(user.role)) {
|
|
578
599
|
return res.status(403).json({
|
|
579
600
|
success: false,
|
|
580
|
-
message: `Access denied
|
|
601
|
+
message: `Access denied.`,
|
|
581
602
|
userRole: user.role || 'none',
|
|
582
603
|
});
|
|
583
604
|
}
|
|
@@ -807,6 +828,19 @@ export function createRoutes(authConfig, configPath, geoDbPath, preloadedAdapter
|
|
|
807
828
|
}
|
|
808
829
|
}
|
|
809
830
|
catch (_error) { }
|
|
831
|
+
let studioVersion = '1.0.0';
|
|
832
|
+
try {
|
|
833
|
+
const response = await fetch('https://registry.npmjs.org/better-auth-studio/latest', {
|
|
834
|
+
signal: AbortSignal.timeout(2000), // 2 second timeout
|
|
835
|
+
});
|
|
836
|
+
if (response.ok) {
|
|
837
|
+
const data = (await response.json());
|
|
838
|
+
studioVersion = data.version || '1.0.0';
|
|
839
|
+
}
|
|
840
|
+
}
|
|
841
|
+
catch (_npmError) {
|
|
842
|
+
studioVersion = getStudioVersion();
|
|
843
|
+
}
|
|
810
844
|
if (databaseType === 'unknown' && !isSelfHosted) {
|
|
811
845
|
const authConfigPath = configPath || (await findAuthConfigPath());
|
|
812
846
|
if (authConfigPath) {
|
|
@@ -883,7 +917,7 @@ export function createRoutes(authConfig, configPath, geoDbPath, preloadedAdapter
|
|
|
883
917
|
disabledPaths: effectiveConfig.disabledPaths || [],
|
|
884
918
|
telemetry: effectiveConfig.telemetry,
|
|
885
919
|
studio: {
|
|
886
|
-
version:
|
|
920
|
+
version: studioVersion,
|
|
887
921
|
nodeVersion: process.version,
|
|
888
922
|
platform: process.platform,
|
|
889
923
|
uptime: process.uptime(),
|
|
@@ -893,7 +927,7 @@ export function createRoutes(authConfig, configPath, geoDbPath, preloadedAdapter
|
|
|
893
927
|
});
|
|
894
928
|
router.get('/api/stats', async (_req, res) => {
|
|
895
929
|
try {
|
|
896
|
-
const stats = await getAuthData(authConfig, 'stats', undefined, configPath);
|
|
930
|
+
const stats = await getAuthData(authConfig, 'stats', undefined, configPath, preloadedAdapter);
|
|
897
931
|
res.json(stats);
|
|
898
932
|
}
|
|
899
933
|
catch (_error) {
|
|
@@ -908,7 +942,7 @@ export function createRoutes(authConfig, configPath, geoDbPath, preloadedAdapter
|
|
|
908
942
|
type: type,
|
|
909
943
|
from: from,
|
|
910
944
|
to: to,
|
|
911
|
-
}, configPath);
|
|
945
|
+
}, configPath, preloadedAdapter);
|
|
912
946
|
res.json(analytics);
|
|
913
947
|
}
|
|
914
948
|
catch (_error) {
|
|
@@ -925,26 +959,17 @@ export function createRoutes(authConfig, configPath, geoDbPath, preloadedAdapter
|
|
|
925
959
|
let organizationPluginEnabled = false;
|
|
926
960
|
let teamsPluginEnabled = false;
|
|
927
961
|
try {
|
|
928
|
-
|
|
929
|
-
if (!betterAuthConfig && !isSelfHosted) {
|
|
930
|
-
const authConfigPath = configPath || (await findAuthConfigPath());
|
|
931
|
-
if (authConfigPath) {
|
|
932
|
-
const { getConfig } = await import('./config.js');
|
|
933
|
-
betterAuthConfig = await getConfig({
|
|
934
|
-
cwd: process.cwd(),
|
|
935
|
-
configPath: authConfigPath,
|
|
936
|
-
shouldThrowOnError: false,
|
|
937
|
-
noCache: true, // Disable cache for real-time plugin checks
|
|
938
|
-
});
|
|
939
|
-
}
|
|
940
|
-
}
|
|
962
|
+
const betterAuthConfig = preloadedAuthOptions || (await getAuthConfigSafe());
|
|
941
963
|
if (betterAuthConfig) {
|
|
942
964
|
const plugins = betterAuthConfig.plugins || [];
|
|
943
965
|
const organizationPlugin = plugins.find((plugin) => plugin.id === 'organization');
|
|
944
966
|
organizationPluginEnabled = !!organizationPlugin;
|
|
945
|
-
teamsPluginEnabled = !!organizationPlugin?.options?.teams?.enabled;
|
|
946
967
|
if (organizationPlugin) {
|
|
947
|
-
teamsPluginEnabled =
|
|
968
|
+
teamsPluginEnabled =
|
|
969
|
+
organizationPlugin.options?.teams?.enabled === true ||
|
|
970
|
+
organizationPlugin.teams?.enabled === true ||
|
|
971
|
+
organizationPlugin.config?.teams?.enabled === true ||
|
|
972
|
+
false;
|
|
948
973
|
}
|
|
949
974
|
}
|
|
950
975
|
}
|
|
@@ -1045,7 +1070,7 @@ export function createRoutes(authConfig, configPath, geoDbPath, preloadedAdapter
|
|
|
1045
1070
|
router.put('/api/users/:userId', async (req, res) => {
|
|
1046
1071
|
try {
|
|
1047
1072
|
const { userId } = req.params;
|
|
1048
|
-
const { name, email, role } = req.body;
|
|
1073
|
+
const { name, email, role, image } = req.body;
|
|
1049
1074
|
const adapter = await getAuthAdapterWithConfig();
|
|
1050
1075
|
if (!adapter || !adapter.update) {
|
|
1051
1076
|
return res.status(500).json({ error: 'Auth adapter not available' });
|
|
@@ -1054,6 +1079,9 @@ export function createRoutes(authConfig, configPath, geoDbPath, preloadedAdapter
|
|
|
1054
1079
|
if (role !== undefined) {
|
|
1055
1080
|
updateData.role = role;
|
|
1056
1081
|
}
|
|
1082
|
+
if (image !== undefined) {
|
|
1083
|
+
updateData.image = image;
|
|
1084
|
+
}
|
|
1057
1085
|
const user = await adapter.update({
|
|
1058
1086
|
model: 'user',
|
|
1059
1087
|
where: [{ field: 'id', value: userId }],
|
|
@@ -1073,9 +1101,36 @@ export function createRoutes(authConfig, configPath, geoDbPath, preloadedAdapter
|
|
|
1073
1101
|
return res.status(400).json({ error: 'Password is required' });
|
|
1074
1102
|
}
|
|
1075
1103
|
const adapter = await getAuthAdapterWithConfig();
|
|
1076
|
-
if (!adapter
|
|
1104
|
+
if (!adapter) {
|
|
1077
1105
|
return res.status(500).json({ error: 'Auth adapter not available' });
|
|
1078
1106
|
}
|
|
1107
|
+
if (!adapter.update) {
|
|
1108
|
+
return res.status(500).json({ error: 'Auth adapter update method not available' });
|
|
1109
|
+
}
|
|
1110
|
+
// Find the credential account first to get its unique id (fixes Prisma error)
|
|
1111
|
+
let credentialAccount = null;
|
|
1112
|
+
if (adapter.findFirst) {
|
|
1113
|
+
credentialAccount = await adapter.findFirst({
|
|
1114
|
+
model: 'account',
|
|
1115
|
+
where: [
|
|
1116
|
+
{ field: 'userId', value: userId },
|
|
1117
|
+
{ field: 'providerId', value: 'credential' },
|
|
1118
|
+
],
|
|
1119
|
+
});
|
|
1120
|
+
}
|
|
1121
|
+
else if (adapter.findMany) {
|
|
1122
|
+
const accounts = await adapter.findMany({
|
|
1123
|
+
model: 'account',
|
|
1124
|
+
where: [
|
|
1125
|
+
{ field: 'userId', value: userId },
|
|
1126
|
+
{ field: 'providerId', value: 'credential' },
|
|
1127
|
+
],
|
|
1128
|
+
});
|
|
1129
|
+
credentialAccount = accounts && accounts.length > 0 ? accounts[0] : null;
|
|
1130
|
+
}
|
|
1131
|
+
if (!credentialAccount) {
|
|
1132
|
+
return res.status(404).json({ error: 'Credential account not found' });
|
|
1133
|
+
}
|
|
1079
1134
|
let hashedPassword = password;
|
|
1080
1135
|
try {
|
|
1081
1136
|
const salt = hex.encode(crypto.getRandomValues(new Uint8Array(16)));
|
|
@@ -1083,17 +1138,18 @@ export function createRoutes(authConfig, configPath, geoDbPath, preloadedAdapter
|
|
|
1083
1138
|
hashedPassword = `${salt}:${hex.encode(key)}`;
|
|
1084
1139
|
}
|
|
1085
1140
|
catch {
|
|
1086
|
-
res.status(500).json({ error: 'Failed to hash password' });
|
|
1141
|
+
return res.status(500).json({ error: 'Failed to hash password' });
|
|
1087
1142
|
}
|
|
1088
|
-
|
|
1143
|
+
// Update using the account's unique id to fix Prisma error
|
|
1144
|
+
const updatedAccount = await adapter.update({
|
|
1089
1145
|
model: 'account',
|
|
1090
|
-
where: [
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1146
|
+
where: [{ field: 'id', value: credentialAccount.id }],
|
|
1147
|
+
update: {
|
|
1148
|
+
password: hashedPassword,
|
|
1149
|
+
updatedAt: new Date().toISOString(),
|
|
1150
|
+
},
|
|
1095
1151
|
});
|
|
1096
|
-
res.json({ success: true, account });
|
|
1152
|
+
res.json({ success: true, account: updatedAccount });
|
|
1097
1153
|
}
|
|
1098
1154
|
catch (error) {
|
|
1099
1155
|
res.status(500).json({ error: 'Failed to update password', message: error?.message });
|
|
@@ -1471,7 +1527,7 @@ export function createRoutes(authConfig, configPath, geoDbPath, preloadedAdapter
|
|
|
1471
1527
|
}
|
|
1472
1528
|
}
|
|
1473
1529
|
catch (_adapterError) { }
|
|
1474
|
-
const result = await getAuthData(authConfig, 'users', { page, limit, search }, configPath);
|
|
1530
|
+
const result = await getAuthData(authConfig, 'users', { page, limit, search }, configPath, preloadedAdapter);
|
|
1475
1531
|
const transformedUsers = (result.data || []).map((user) => ({
|
|
1476
1532
|
id: user.id,
|
|
1477
1533
|
email: user.email,
|
|
@@ -1496,7 +1552,7 @@ export function createRoutes(authConfig, configPath, geoDbPath, preloadedAdapter
|
|
|
1496
1552
|
try {
|
|
1497
1553
|
const page = parseInt(req.query.page, 10) || 1;
|
|
1498
1554
|
const limit = parseInt(req.query.limit, 10) || 20;
|
|
1499
|
-
const sessions = await getAuthData(authConfig, 'sessions', { page, limit }, configPath);
|
|
1555
|
+
const sessions = await getAuthData(authConfig, 'sessions', { page, limit }, configPath, preloadedAdapter);
|
|
1500
1556
|
res.json(sessions);
|
|
1501
1557
|
}
|
|
1502
1558
|
catch (_error) {
|
|
@@ -1505,7 +1561,7 @@ export function createRoutes(authConfig, configPath, geoDbPath, preloadedAdapter
|
|
|
1505
1561
|
});
|
|
1506
1562
|
router.get('/api/providers', async (_req, res) => {
|
|
1507
1563
|
try {
|
|
1508
|
-
const providers = await getAuthData(authConfig, 'providers', undefined, configPath);
|
|
1564
|
+
const providers = await getAuthData(authConfig, 'providers', undefined, configPath, preloadedAdapter);
|
|
1509
1565
|
res.json(providers);
|
|
1510
1566
|
}
|
|
1511
1567
|
catch (_error) {
|
|
@@ -1515,7 +1571,7 @@ export function createRoutes(authConfig, configPath, geoDbPath, preloadedAdapter
|
|
|
1515
1571
|
router.delete('/api/users/:id', async (req, res) => {
|
|
1516
1572
|
try {
|
|
1517
1573
|
const { id } = req.params;
|
|
1518
|
-
await getAuthData(authConfig, 'deleteUser', { id }, configPath);
|
|
1574
|
+
await getAuthData(authConfig, 'deleteUser', { id }, configPath, preloadedAdapter);
|
|
1519
1575
|
res.json({ success: true });
|
|
1520
1576
|
}
|
|
1521
1577
|
catch (_error) {
|
|
@@ -1524,60 +1580,82 @@ export function createRoutes(authConfig, configPath, geoDbPath, preloadedAdapter
|
|
|
1524
1580
|
});
|
|
1525
1581
|
router.get('/api/plugins', async (_req, res) => {
|
|
1526
1582
|
try {
|
|
1527
|
-
const
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1583
|
+
const betterAuthConfig = preloadedAuthOptions || (await getAuthConfigSafe());
|
|
1584
|
+
if (betterAuthConfig) {
|
|
1585
|
+
const plugins = betterAuthConfig.plugins || [];
|
|
1586
|
+
const pluginInfo = plugins.map((plugin) => ({
|
|
1587
|
+
id: plugin.id,
|
|
1588
|
+
name: plugin.name || plugin.id,
|
|
1589
|
+
description: plugin.description || `${plugin.id} plugin for Better Auth`,
|
|
1590
|
+
enabled: true,
|
|
1591
|
+
}));
|
|
1531
1592
|
return res.json({
|
|
1532
|
-
plugins:
|
|
1533
|
-
|
|
1534
|
-
|
|
1593
|
+
plugins: pluginInfo,
|
|
1594
|
+
configPath: isSelfHosted ? null : configPath || null,
|
|
1595
|
+
totalPlugins: pluginInfo.length,
|
|
1535
1596
|
});
|
|
1536
1597
|
}
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1598
|
+
if (!isSelfHosted) {
|
|
1599
|
+
const authConfigPath = configPath
|
|
1600
|
+
? join(process.cwd(), configPath)
|
|
1601
|
+
: await findAuthConfigPath();
|
|
1602
|
+
if (!authConfigPath) {
|
|
1603
|
+
return res.json({
|
|
1604
|
+
plugins: [],
|
|
1605
|
+
error: 'No auth config found',
|
|
1606
|
+
configPath: null,
|
|
1607
|
+
});
|
|
1541
1608
|
}
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1609
|
+
try {
|
|
1610
|
+
let authModule;
|
|
1611
|
+
try {
|
|
1612
|
+
authModule = await safeImportAuthConfig(authConfigPath, true); // Disable cache for real-time plugin checks
|
|
1613
|
+
}
|
|
1614
|
+
catch (_importError) {
|
|
1615
|
+
const content = readFileSync(authConfigPath, 'utf-8');
|
|
1616
|
+
authModule = {
|
|
1617
|
+
auth: {
|
|
1618
|
+
options: {
|
|
1619
|
+
_content: content,
|
|
1620
|
+
plugins: [],
|
|
1621
|
+
},
|
|
1549
1622
|
},
|
|
1550
|
-
}
|
|
1551
|
-
}
|
|
1623
|
+
};
|
|
1624
|
+
}
|
|
1625
|
+
const auth = authModule.auth || authModule.default;
|
|
1626
|
+
if (!auth) {
|
|
1627
|
+
return res.json({
|
|
1628
|
+
plugins: [],
|
|
1629
|
+
error: 'No auth export found',
|
|
1630
|
+
configPath: authConfigPath,
|
|
1631
|
+
});
|
|
1632
|
+
}
|
|
1633
|
+
const plugins = auth.options?.plugins || [];
|
|
1634
|
+
const pluginInfo = plugins.map((plugin) => ({
|
|
1635
|
+
id: plugin.id,
|
|
1636
|
+
name: plugin.name || plugin.id,
|
|
1637
|
+
description: plugin.description || `${plugin.id} plugin for Better Auth`,
|
|
1638
|
+
enabled: true,
|
|
1639
|
+
}));
|
|
1640
|
+
return res.json({
|
|
1641
|
+
plugins: pluginInfo,
|
|
1642
|
+
configPath: authConfigPath,
|
|
1643
|
+
totalPlugins: pluginInfo.length,
|
|
1644
|
+
});
|
|
1552
1645
|
}
|
|
1553
|
-
|
|
1554
|
-
if (!auth) {
|
|
1646
|
+
catch (_error) {
|
|
1555
1647
|
return res.json({
|
|
1556
1648
|
plugins: [],
|
|
1557
|
-
error: '
|
|
1649
|
+
error: 'Failed to load auth config - import failed and regex extraction unavailable',
|
|
1558
1650
|
configPath: authConfigPath,
|
|
1559
1651
|
});
|
|
1560
1652
|
}
|
|
1561
|
-
const plugins = auth.options?.plugins || [];
|
|
1562
|
-
const pluginInfo = plugins.map((plugin) => ({
|
|
1563
|
-
id: plugin.id,
|
|
1564
|
-
name: plugin.name || plugin.id,
|
|
1565
|
-
description: plugin.description || `${plugin.id} plugin for Better Auth`,
|
|
1566
|
-
enabled: true,
|
|
1567
|
-
}));
|
|
1568
|
-
res.json({
|
|
1569
|
-
plugins: pluginInfo,
|
|
1570
|
-
configPath: authConfigPath,
|
|
1571
|
-
totalPlugins: pluginInfo.length,
|
|
1572
|
-
});
|
|
1573
|
-
}
|
|
1574
|
-
catch (_error) {
|
|
1575
|
-
res.json({
|
|
1576
|
-
plugins: [],
|
|
1577
|
-
error: 'Failed to load auth config - import failed and regex extraction unavailable',
|
|
1578
|
-
configPath: authConfigPath,
|
|
1579
|
-
});
|
|
1580
1653
|
}
|
|
1654
|
+
return res.json({
|
|
1655
|
+
plugins: [],
|
|
1656
|
+
error: 'No auth config found',
|
|
1657
|
+
configPath: null,
|
|
1658
|
+
});
|
|
1581
1659
|
}
|
|
1582
1660
|
catch (_error) {
|
|
1583
1661
|
res.status(500).json({ error: 'Failed to fetch plugins' });
|
|
@@ -1585,6 +1663,13 @@ export function createRoutes(authConfig, configPath, geoDbPath, preloadedAdapter
|
|
|
1585
1663
|
});
|
|
1586
1664
|
router.get('/api/database/info', async (_req, res) => {
|
|
1587
1665
|
try {
|
|
1666
|
+
if (isSelfHosted && preloadedAuthOptions) {
|
|
1667
|
+
const database = preloadedAuthOptions.database;
|
|
1668
|
+
return res.json({
|
|
1669
|
+
database: database,
|
|
1670
|
+
configPath: null,
|
|
1671
|
+
});
|
|
1672
|
+
}
|
|
1588
1673
|
const authConfigPath = configPath || (await findAuthConfigPath());
|
|
1589
1674
|
if (!authConfigPath) {
|
|
1590
1675
|
return res.json({
|
|
@@ -1978,7 +2063,6 @@ export function createRoutes(authConfig, configPath, geoDbPath, preloadedAdapter
|
|
|
1978
2063
|
});
|
|
1979
2064
|
}
|
|
1980
2065
|
});
|
|
1981
|
-
// Database Detection endpoint - Auto-detect database from installed packages
|
|
1982
2066
|
router.get('/api/database/detect', async (_req, res) => {
|
|
1983
2067
|
try {
|
|
1984
2068
|
const detectedDb = await detectDatabaseWithDialect();
|
|
@@ -2047,7 +2131,7 @@ export function createRoutes(authConfig, configPath, geoDbPath, preloadedAdapter
|
|
|
2047
2131
|
});
|
|
2048
2132
|
router.post('/api/admin/ban-user', async (req, res) => {
|
|
2049
2133
|
try {
|
|
2050
|
-
const auth = await getAuthConfigSafe();
|
|
2134
|
+
const auth = preloadedAuthOptions || (await getAuthConfigSafe());
|
|
2051
2135
|
if (!auth) {
|
|
2052
2136
|
return res.status(400).json({
|
|
2053
2137
|
success: false,
|
|
@@ -2086,7 +2170,7 @@ export function createRoutes(authConfig, configPath, geoDbPath, preloadedAdapter
|
|
|
2086
2170
|
});
|
|
2087
2171
|
router.post('/api/admin/unban-user', async (req, res) => {
|
|
2088
2172
|
try {
|
|
2089
|
-
const auth = await getAuthConfigSafe();
|
|
2173
|
+
const auth = preloadedAuthOptions || (await getAuthConfigSafe());
|
|
2090
2174
|
if (!auth) {
|
|
2091
2175
|
return res.status(400).json({
|
|
2092
2176
|
success: false,
|
|
@@ -2125,7 +2209,7 @@ export function createRoutes(authConfig, configPath, geoDbPath, preloadedAdapter
|
|
|
2125
2209
|
});
|
|
2126
2210
|
router.get('/api/admin/status', async (_req, res) => {
|
|
2127
2211
|
try {
|
|
2128
|
-
const betterAuthConfig = await getAuthConfigSafe();
|
|
2212
|
+
const betterAuthConfig = preloadedAuthOptions || (await getAuthConfigSafe());
|
|
2129
2213
|
if (!betterAuthConfig) {
|
|
2130
2214
|
return res.json({
|
|
2131
2215
|
enabled: false,
|
|
@@ -2149,8 +2233,6 @@ export function createRoutes(authConfig, configPath, geoDbPath, preloadedAdapter
|
|
|
2149
2233
|
});
|
|
2150
2234
|
}
|
|
2151
2235
|
});
|
|
2152
|
-
// Database Schema Visualization endpoint
|
|
2153
|
-
// Now uses loadContextTables to dynamically load schema from Better Auth context
|
|
2154
2236
|
const CONTEXT_CORE_TABLES = new Set(['user', 'session', 'account', 'verification']);
|
|
2155
2237
|
async function resolveSchemaConfigPath() {
|
|
2156
2238
|
if (configPath) {
|
|
@@ -2160,16 +2242,23 @@ export function createRoutes(authConfig, configPath, geoDbPath, preloadedAdapter
|
|
|
2160
2242
|
}
|
|
2161
2243
|
async function loadContextTables() {
|
|
2162
2244
|
try {
|
|
2245
|
+
if (isSelfHosted && authInstance?.$context) {
|
|
2246
|
+
try {
|
|
2247
|
+
const context = await authInstance.$context;
|
|
2248
|
+
return context?.tables || null;
|
|
2249
|
+
}
|
|
2250
|
+
catch (_error) { }
|
|
2251
|
+
}
|
|
2163
2252
|
const authConfigPath = await resolveSchemaConfigPath();
|
|
2164
2253
|
if (!authConfigPath) {
|
|
2165
2254
|
return null;
|
|
2166
2255
|
}
|
|
2167
2256
|
const authModule = await safeImportAuthConfig(authConfigPath);
|
|
2168
|
-
const
|
|
2169
|
-
if (!
|
|
2257
|
+
const fileAuthInstance = authModule?.auth || authModule?.default;
|
|
2258
|
+
if (!fileAuthInstance?.$context) {
|
|
2170
2259
|
return null;
|
|
2171
2260
|
}
|
|
2172
|
-
const context = await
|
|
2261
|
+
const context = await fileAuthInstance.$context;
|
|
2173
2262
|
return context?.tables || null;
|
|
2174
2263
|
}
|
|
2175
2264
|
catch (_error) {
|
|
@@ -2293,14 +2382,11 @@ export function createRoutes(authConfig, configPath, geoDbPath, preloadedAdapter
|
|
|
2293
2382
|
if (!schema) {
|
|
2294
2383
|
return null;
|
|
2295
2384
|
}
|
|
2296
|
-
// Filter by plugin origin if plugins are specified
|
|
2297
2385
|
if (selectedPlugins && selectedPlugins.length > 0) {
|
|
2298
2386
|
schema.tables = schema.tables.filter((table) => {
|
|
2299
|
-
// Include core tables
|
|
2300
2387
|
if (CONTEXT_CORE_TABLES.has(table.name)) {
|
|
2301
2388
|
return true;
|
|
2302
2389
|
}
|
|
2303
|
-
// Include tables from selected plugins
|
|
2304
2390
|
return selectedPlugins.includes(table.origin);
|
|
2305
2391
|
});
|
|
2306
2392
|
}
|
|
@@ -2352,28 +2438,46 @@ export function createRoutes(authConfig, configPath, geoDbPath, preloadedAdapter
|
|
|
2352
2438
|
});
|
|
2353
2439
|
router.get('/api/plugins/teams/status', async (_req, res) => {
|
|
2354
2440
|
try {
|
|
2355
|
-
const betterAuthConfig = await getAuthConfigSafe();
|
|
2441
|
+
const betterAuthConfig = preloadedAuthOptions || (await getAuthConfigSafe());
|
|
2356
2442
|
if (!betterAuthConfig) {
|
|
2357
2443
|
return res.json({
|
|
2358
2444
|
enabled: false,
|
|
2359
2445
|
error: 'No auth config found',
|
|
2360
|
-
configPath: null,
|
|
2446
|
+
configPath: isSelfHosted ? null : configPath || null,
|
|
2361
2447
|
});
|
|
2362
2448
|
}
|
|
2363
2449
|
const plugins = betterAuthConfig.plugins || [];
|
|
2364
2450
|
const organizationPlugin = plugins.find((plugin) => plugin.id === 'organization');
|
|
2365
2451
|
if (organizationPlugin) {
|
|
2366
|
-
|
|
2452
|
+
let teamsEnabled = false;
|
|
2453
|
+
if (organizationPlugin.options?.teams?.enabled === true) {
|
|
2454
|
+
teamsEnabled = true;
|
|
2455
|
+
}
|
|
2456
|
+
else if (organizationPlugin.teams?.enabled === true) {
|
|
2457
|
+
teamsEnabled = true;
|
|
2458
|
+
}
|
|
2459
|
+
else if (organizationPlugin.config?.teams?.enabled === true) {
|
|
2460
|
+
teamsEnabled = true;
|
|
2461
|
+
}
|
|
2462
|
+
else if (organizationPlugin.options?.teams &&
|
|
2463
|
+
typeof organizationPlugin.options.teams === 'object') {
|
|
2464
|
+
teamsEnabled = organizationPlugin.options.teams.enabled === true;
|
|
2465
|
+
}
|
|
2466
|
+
else if (organizationPlugin.teams && typeof organizationPlugin.teams === 'object') {
|
|
2467
|
+
teamsEnabled = organizationPlugin.teams.enabled === true;
|
|
2468
|
+
}
|
|
2469
|
+
const teamSchema = organizationPlugin.schema;
|
|
2470
|
+
teamsEnabled = 'team' in teamSchema;
|
|
2367
2471
|
return res.json({
|
|
2368
2472
|
enabled: teamsEnabled,
|
|
2369
|
-
configPath: configPath || null,
|
|
2473
|
+
configPath: isSelfHosted ? null : configPath || null,
|
|
2370
2474
|
organizationPlugin: organizationPlugin || null,
|
|
2371
2475
|
});
|
|
2372
2476
|
}
|
|
2373
2477
|
else {
|
|
2374
2478
|
return res.json({
|
|
2375
2479
|
enabled: false,
|
|
2376
|
-
configPath: configPath || null,
|
|
2480
|
+
configPath: isSelfHosted ? null : configPath || null,
|
|
2377
2481
|
organizationPlugin: null,
|
|
2378
2482
|
error: 'Organization plugin not found',
|
|
2379
2483
|
});
|
|
@@ -2648,6 +2752,26 @@ export function createRoutes(authConfig, configPath, geoDbPath, preloadedAdapter
|
|
|
2648
2752
|
if (!adapter.update) {
|
|
2649
2753
|
return res.status(500).json({ error: 'Adapter update method not available' });
|
|
2650
2754
|
}
|
|
2755
|
+
let invitation = null;
|
|
2756
|
+
try {
|
|
2757
|
+
invitation = await adapter.findOne({
|
|
2758
|
+
model: 'invitation',
|
|
2759
|
+
where: [{ field: 'id', value: id }],
|
|
2760
|
+
});
|
|
2761
|
+
}
|
|
2762
|
+
catch (_findError) {
|
|
2763
|
+
if (typeof adapter.findMany === 'function') {
|
|
2764
|
+
const invitations = await adapter.findMany({
|
|
2765
|
+
model: 'invitation',
|
|
2766
|
+
where: [{ field: 'id', value: id }],
|
|
2767
|
+
limit: 1,
|
|
2768
|
+
});
|
|
2769
|
+
invitation = invitations && invitations.length > 0 ? invitations[0] : null;
|
|
2770
|
+
}
|
|
2771
|
+
}
|
|
2772
|
+
if (!invitation) {
|
|
2773
|
+
return res.status(404).json({ error: 'Invitation not found' });
|
|
2774
|
+
}
|
|
2651
2775
|
await adapter.update({
|
|
2652
2776
|
model: 'invitation',
|
|
2653
2777
|
where: [{ field: 'id', value: id }],
|
|
@@ -2658,8 +2782,245 @@ export function createRoutes(authConfig, configPath, geoDbPath, preloadedAdapter
|
|
|
2658
2782
|
});
|
|
2659
2783
|
res.json({ success: true });
|
|
2660
2784
|
}
|
|
2785
|
+
catch (error) {
|
|
2786
|
+
console.error('Error resending invitation:', error);
|
|
2787
|
+
const errorMessage = error instanceof Error ? error.message : 'Failed to resend invitation';
|
|
2788
|
+
res.status(500).json({
|
|
2789
|
+
error: 'Failed to resend invitation',
|
|
2790
|
+
details: isSelfHosted ? errorMessage : undefined,
|
|
2791
|
+
});
|
|
2792
|
+
}
|
|
2793
|
+
});
|
|
2794
|
+
router.get('/api/users/:userId/invitations', async (req, res) => {
|
|
2795
|
+
try {
|
|
2796
|
+
const { userId } = req.params;
|
|
2797
|
+
const adapter = await getAuthAdapterWithConfig();
|
|
2798
|
+
if (!adapter) {
|
|
2799
|
+
return res.status(500).json({ error: 'Auth adapter not available' });
|
|
2800
|
+
}
|
|
2801
|
+
let user;
|
|
2802
|
+
try {
|
|
2803
|
+
user = await adapter.findOne({
|
|
2804
|
+
model: 'user',
|
|
2805
|
+
where: [{ field: 'id', value: userId }],
|
|
2806
|
+
});
|
|
2807
|
+
}
|
|
2808
|
+
catch (error) {
|
|
2809
|
+
console.error('Error fetching user:', error);
|
|
2810
|
+
return res.status(500).json({
|
|
2811
|
+
error: 'Failed to fetch user',
|
|
2812
|
+
details: error?.message || String(error),
|
|
2813
|
+
});
|
|
2814
|
+
}
|
|
2815
|
+
if (!user || !user.email) {
|
|
2816
|
+
return res.json({ success: true, invitations: [] });
|
|
2817
|
+
}
|
|
2818
|
+
if (typeof adapter.findMany !== 'function') {
|
|
2819
|
+
return res.json({ success: true, invitations: [] });
|
|
2820
|
+
}
|
|
2821
|
+
let invitations;
|
|
2822
|
+
try {
|
|
2823
|
+
invitations = await adapter.findMany({
|
|
2824
|
+
model: 'invitation',
|
|
2825
|
+
where: [{ field: 'email', value: user.email }],
|
|
2826
|
+
});
|
|
2827
|
+
}
|
|
2828
|
+
catch (error) {
|
|
2829
|
+
console.error('Error fetching invitations:', error);
|
|
2830
|
+
return res.json({ success: true, invitations: [] });
|
|
2831
|
+
}
|
|
2832
|
+
if (!invitations || invitations.length === 0) {
|
|
2833
|
+
return res.json({ success: true, invitations: [] });
|
|
2834
|
+
}
|
|
2835
|
+
const transformedInvitations = await Promise.all(invitations.map(async (invitation) => {
|
|
2836
|
+
let organizationName = 'Unknown';
|
|
2837
|
+
let teamName;
|
|
2838
|
+
try {
|
|
2839
|
+
if (invitation.organizationId &&
|
|
2840
|
+
(typeof adapter.findOne === 'function' || typeof adapter.findUnique === 'function')) {
|
|
2841
|
+
try {
|
|
2842
|
+
const findMethod = adapter.findOne || adapter.findUnique;
|
|
2843
|
+
const org = await findMethod({
|
|
2844
|
+
model: 'organization',
|
|
2845
|
+
where: [{ field: 'id', value: invitation.organizationId }],
|
|
2846
|
+
});
|
|
2847
|
+
organizationName = org?.name || 'Unknown';
|
|
2848
|
+
}
|
|
2849
|
+
catch (_orgError) {
|
|
2850
|
+
// Ignore org fetch errors
|
|
2851
|
+
}
|
|
2852
|
+
}
|
|
2853
|
+
if (invitation.teamId &&
|
|
2854
|
+
(typeof adapter.findOne === 'function' || typeof adapter.findUnique === 'function')) {
|
|
2855
|
+
try {
|
|
2856
|
+
const findMethod = adapter.findOne || adapter.findUnique;
|
|
2857
|
+
const team = await findMethod({
|
|
2858
|
+
model: 'team',
|
|
2859
|
+
where: [{ field: 'id', value: invitation.teamId }],
|
|
2860
|
+
});
|
|
2861
|
+
teamName = team?.name;
|
|
2862
|
+
}
|
|
2863
|
+
catch (_teamError) { }
|
|
2864
|
+
}
|
|
2865
|
+
}
|
|
2866
|
+
catch (_error) { }
|
|
2867
|
+
return {
|
|
2868
|
+
id: invitation.id,
|
|
2869
|
+
email: invitation.email,
|
|
2870
|
+
role: invitation.role || 'member',
|
|
2871
|
+
status: invitation.status || 'pending',
|
|
2872
|
+
organizationId: invitation.organizationId,
|
|
2873
|
+
organizationName,
|
|
2874
|
+
teamId: invitation.teamId,
|
|
2875
|
+
teamName,
|
|
2876
|
+
inviterId: invitation.inviterId,
|
|
2877
|
+
expiresAt: invitation.expiresAt,
|
|
2878
|
+
createdAt: invitation.createdAt,
|
|
2879
|
+
};
|
|
2880
|
+
}));
|
|
2881
|
+
res.json({ success: true, invitations: transformedInvitations });
|
|
2882
|
+
}
|
|
2883
|
+
catch (error) {
|
|
2884
|
+
console.error('Error in /api/users/:userId/invitations:', error);
|
|
2885
|
+
res.status(500).json({
|
|
2886
|
+
error: 'Failed to fetch invitations',
|
|
2887
|
+
details: error?.message || String(error),
|
|
2888
|
+
});
|
|
2889
|
+
}
|
|
2890
|
+
});
|
|
2891
|
+
router.post('/api/invitations/:id/accept', async (req, res) => {
|
|
2892
|
+
try {
|
|
2893
|
+
const { id } = req.params;
|
|
2894
|
+
const { userId } = req.body;
|
|
2895
|
+
const adapter = await getAuthAdapterWithConfig();
|
|
2896
|
+
if (!adapter) {
|
|
2897
|
+
return res.status(500).json({ error: 'Auth adapter not available' });
|
|
2898
|
+
}
|
|
2899
|
+
if (!userId) {
|
|
2900
|
+
return res.status(400).json({ error: 'User ID is required' });
|
|
2901
|
+
}
|
|
2902
|
+
const invitation = await adapter.findOne({
|
|
2903
|
+
model: 'invitation',
|
|
2904
|
+
where: [{ field: 'id', value: id }],
|
|
2905
|
+
});
|
|
2906
|
+
if (!invitation) {
|
|
2907
|
+
return res.status(404).json({ error: 'Invitation not found' });
|
|
2908
|
+
}
|
|
2909
|
+
if (invitation.status !== 'pending') {
|
|
2910
|
+
return res.status(400).json({ error: 'Invitation is not pending' });
|
|
2911
|
+
}
|
|
2912
|
+
await adapter.update({
|
|
2913
|
+
model: 'invitation',
|
|
2914
|
+
where: [{ field: 'id', value: id }],
|
|
2915
|
+
update: {
|
|
2916
|
+
status: 'accepted',
|
|
2917
|
+
updatedAt: new Date().toISOString(),
|
|
2918
|
+
},
|
|
2919
|
+
});
|
|
2920
|
+
if (invitation.organizationId) {
|
|
2921
|
+
try {
|
|
2922
|
+
// Check if member already exists
|
|
2923
|
+
let existingMember = null;
|
|
2924
|
+
if (typeof adapter.findFirst === 'function') {
|
|
2925
|
+
existingMember = await adapter.findFirst({
|
|
2926
|
+
model: 'member',
|
|
2927
|
+
where: [
|
|
2928
|
+
{ field: 'organizationId', value: invitation.organizationId },
|
|
2929
|
+
{ field: 'userId', value: userId },
|
|
2930
|
+
],
|
|
2931
|
+
});
|
|
2932
|
+
}
|
|
2933
|
+
else if (typeof adapter.findMany === 'function') {
|
|
2934
|
+
const members = await adapter.findMany({
|
|
2935
|
+
model: 'member',
|
|
2936
|
+
where: [
|
|
2937
|
+
{ field: 'organizationId', value: invitation.organizationId },
|
|
2938
|
+
{ field: 'userId', value: userId },
|
|
2939
|
+
],
|
|
2940
|
+
});
|
|
2941
|
+
existingMember = members && members.length > 0 ? members[0] : null;
|
|
2942
|
+
}
|
|
2943
|
+
if (!existingMember) {
|
|
2944
|
+
await adapter.create({
|
|
2945
|
+
model: 'member',
|
|
2946
|
+
data: {
|
|
2947
|
+
organizationId: invitation.organizationId,
|
|
2948
|
+
userId: userId,
|
|
2949
|
+
role: invitation.role || 'member',
|
|
2950
|
+
createdAt: new Date().toISOString(),
|
|
2951
|
+
},
|
|
2952
|
+
});
|
|
2953
|
+
}
|
|
2954
|
+
}
|
|
2955
|
+
catch (error) {
|
|
2956
|
+
console.error('Error creating member:', error);
|
|
2957
|
+
// Ignore errors creating membership
|
|
2958
|
+
}
|
|
2959
|
+
}
|
|
2960
|
+
if (invitation.teamId) {
|
|
2961
|
+
try {
|
|
2962
|
+
let existingMember = null;
|
|
2963
|
+
if (typeof adapter.findFirst === 'function') {
|
|
2964
|
+
existingMember = await adapter.findFirst({
|
|
2965
|
+
model: 'teamMember',
|
|
2966
|
+
where: [
|
|
2967
|
+
{ field: 'teamId', value: invitation.teamId },
|
|
2968
|
+
{ field: 'userId', value: userId },
|
|
2969
|
+
],
|
|
2970
|
+
});
|
|
2971
|
+
}
|
|
2972
|
+
else if (typeof adapter.findMany === 'function') {
|
|
2973
|
+
const members = await adapter.findMany({
|
|
2974
|
+
model: 'teamMember',
|
|
2975
|
+
where: [
|
|
2976
|
+
{ field: 'teamId', value: invitation.teamId },
|
|
2977
|
+
{ field: 'userId', value: userId },
|
|
2978
|
+
],
|
|
2979
|
+
});
|
|
2980
|
+
existingMember = members && members.length > 0 ? members[0] : null;
|
|
2981
|
+
}
|
|
2982
|
+
if (!existingMember) {
|
|
2983
|
+
await adapter.create({
|
|
2984
|
+
model: 'teamMember',
|
|
2985
|
+
data: {
|
|
2986
|
+
teamId: invitation.teamId,
|
|
2987
|
+
userId: userId,
|
|
2988
|
+
createdAt: new Date().toISOString(),
|
|
2989
|
+
},
|
|
2990
|
+
});
|
|
2991
|
+
}
|
|
2992
|
+
}
|
|
2993
|
+
catch (error) {
|
|
2994
|
+
console.error('Error creating team member:', error);
|
|
2995
|
+
// Ignore errors creating team membership
|
|
2996
|
+
}
|
|
2997
|
+
}
|
|
2998
|
+
res.json({ success: true });
|
|
2999
|
+
}
|
|
3000
|
+
catch (error) {
|
|
3001
|
+
console.error('Failed to accept invitation:', error);
|
|
3002
|
+
res.status(500).json({ error: 'Failed to accept invitation' });
|
|
3003
|
+
}
|
|
3004
|
+
});
|
|
3005
|
+
router.post('/api/invitations/:id/reject', async (req, res) => {
|
|
3006
|
+
try {
|
|
3007
|
+
const { id } = req.params;
|
|
3008
|
+
const adapter = await getAuthAdapterWithConfig();
|
|
3009
|
+
if (!adapter) {
|
|
3010
|
+
return res.status(500).json({ error: 'Auth adapter not available' });
|
|
3011
|
+
}
|
|
3012
|
+
await adapter.update({
|
|
3013
|
+
model: 'invitation',
|
|
3014
|
+
where: [{ field: 'id', value: id }],
|
|
3015
|
+
update: {
|
|
3016
|
+
status: 'rejected',
|
|
3017
|
+
updatedAt: new Date().toISOString(),
|
|
3018
|
+
},
|
|
3019
|
+
});
|
|
3020
|
+
res.json({ success: true });
|
|
3021
|
+
}
|
|
2661
3022
|
catch (_error) {
|
|
2662
|
-
res.status(500).json({ error: 'Failed to
|
|
3023
|
+
res.status(500).json({ error: 'Failed to reject invitation' });
|
|
2663
3024
|
}
|
|
2664
3025
|
});
|
|
2665
3026
|
router.delete('/api/invitations/:id', async (req, res) => {
|
|
@@ -2689,7 +3050,13 @@ export function createRoutes(authConfig, configPath, geoDbPath, preloadedAdapter
|
|
|
2689
3050
|
router.post('/api/organizations/:orgId/invitations', async (req, res) => {
|
|
2690
3051
|
try {
|
|
2691
3052
|
const { orgId } = req.params;
|
|
2692
|
-
const { email, role = 'member', inviterId } = req.body;
|
|
3053
|
+
const { email, role = 'member', inviterId, teamId } = req.body;
|
|
3054
|
+
if (!email || typeof email !== 'string') {
|
|
3055
|
+
return res.status(400).json({ error: 'Email is required' });
|
|
3056
|
+
}
|
|
3057
|
+
if (!orgId) {
|
|
3058
|
+
return res.status(400).json({ error: 'Organization ID is required' });
|
|
3059
|
+
}
|
|
2693
3060
|
if (!inviterId) {
|
|
2694
3061
|
return res.status(400).json({ error: 'Inviter ID is required' });
|
|
2695
3062
|
}
|
|
@@ -2697,60 +3064,143 @@ export function createRoutes(authConfig, configPath, geoDbPath, preloadedAdapter
|
|
|
2697
3064
|
if (!adapter) {
|
|
2698
3065
|
return res.status(500).json({ error: 'Auth adapter not available' });
|
|
2699
3066
|
}
|
|
3067
|
+
if (!adapter.create) {
|
|
3068
|
+
return res.status(500).json({ error: 'Adapter create method not available' });
|
|
3069
|
+
}
|
|
3070
|
+
try {
|
|
3071
|
+
const organization = await adapter.findOne({
|
|
3072
|
+
model: 'organization',
|
|
3073
|
+
where: [{ field: 'id', value: orgId }],
|
|
3074
|
+
});
|
|
3075
|
+
if (!organization) {
|
|
3076
|
+
return res.status(404).json({ error: 'Organization not found' });
|
|
3077
|
+
}
|
|
3078
|
+
}
|
|
3079
|
+
catch (orgError) {
|
|
3080
|
+
try {
|
|
3081
|
+
if (typeof adapter.findMany === 'function') {
|
|
3082
|
+
const orgs = await adapter.findMany({
|
|
3083
|
+
model: 'organization',
|
|
3084
|
+
where: [{ field: 'id', value: orgId }],
|
|
3085
|
+
limit: 1,
|
|
3086
|
+
});
|
|
3087
|
+
if (!orgs || orgs.length === 0) {
|
|
3088
|
+
return res.status(404).json({ error: 'Organization not found' });
|
|
3089
|
+
}
|
|
3090
|
+
}
|
|
3091
|
+
}
|
|
3092
|
+
catch (_fallbackError) { }
|
|
3093
|
+
}
|
|
3094
|
+
try {
|
|
3095
|
+
let existingInvitation = null;
|
|
3096
|
+
if (typeof adapter.findFirst === 'function') {
|
|
3097
|
+
existingInvitation = await adapter.findFirst({
|
|
3098
|
+
model: 'invitation',
|
|
3099
|
+
where: [
|
|
3100
|
+
{ field: 'email', value: email.toLowerCase() },
|
|
3101
|
+
{ field: 'organizationId', value: orgId },
|
|
3102
|
+
{ field: 'status', value: 'pending' },
|
|
3103
|
+
],
|
|
3104
|
+
});
|
|
3105
|
+
}
|
|
3106
|
+
else if (typeof adapter.findMany === 'function') {
|
|
3107
|
+
const invitations = await adapter.findMany({
|
|
3108
|
+
model: 'invitation',
|
|
3109
|
+
where: [
|
|
3110
|
+
{ field: 'email', value: email.toLowerCase() },
|
|
3111
|
+
{ field: 'organizationId', value: orgId },
|
|
3112
|
+
{ field: 'status', value: 'pending' },
|
|
3113
|
+
],
|
|
3114
|
+
limit: 1,
|
|
3115
|
+
});
|
|
3116
|
+
existingInvitation = invitations && invitations.length > 0 ? invitations[0] : null;
|
|
3117
|
+
}
|
|
3118
|
+
if (existingInvitation) {
|
|
3119
|
+
return res
|
|
3120
|
+
.status(400)
|
|
3121
|
+
.json({ error: 'A pending invitation already exists for this email' });
|
|
3122
|
+
}
|
|
3123
|
+
}
|
|
3124
|
+
catch (_duplicateCheckError) { }
|
|
2700
3125
|
const invitationData = {
|
|
2701
|
-
email,
|
|
3126
|
+
email: email.toLowerCase(),
|
|
2702
3127
|
role,
|
|
2703
3128
|
organizationId: orgId,
|
|
2704
3129
|
status: 'pending',
|
|
2705
3130
|
expiresAt: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000), // 7 days
|
|
2706
|
-
createdAt: new Date(),
|
|
2707
3131
|
inviterId: inviterId,
|
|
2708
3132
|
};
|
|
2709
|
-
|
|
2710
|
-
|
|
2711
|
-
...invitationData,
|
|
2712
|
-
};
|
|
2713
|
-
if (!adapter.create) {
|
|
2714
|
-
return res.status(500).json({ error: 'Adapter create method not available' });
|
|
3133
|
+
if (teamId) {
|
|
3134
|
+
invitationData.teamId = teamId;
|
|
2715
3135
|
}
|
|
2716
|
-
await adapter.create({
|
|
3136
|
+
const createdInvitation = await adapter.create({
|
|
2717
3137
|
model: 'invitation',
|
|
2718
3138
|
data: {
|
|
2719
|
-
|
|
2720
|
-
email: invitationData.email,
|
|
2721
|
-
role: invitationData.role,
|
|
2722
|
-
status: invitationData.status,
|
|
2723
|
-
inviterId: invitationData.inviterId,
|
|
2724
|
-
expiresAt: invitationData.expiresAt,
|
|
2725
|
-
createdAt: invitationData.createdAt,
|
|
3139
|
+
...invitationData,
|
|
2726
3140
|
},
|
|
2727
3141
|
});
|
|
2728
|
-
|
|
3142
|
+
if (!createdInvitation) {
|
|
3143
|
+
return res.status(500).json({ error: 'Failed to create invitation' });
|
|
3144
|
+
}
|
|
3145
|
+
res.json({ success: true, invitation: createdInvitation });
|
|
2729
3146
|
}
|
|
2730
|
-
catch (
|
|
2731
|
-
|
|
3147
|
+
catch (error) {
|
|
3148
|
+
console.error('Error creating invitation:', error);
|
|
3149
|
+
const errorMessage = error instanceof Error ? error.message : 'Failed to create invitation';
|
|
3150
|
+
res.status(500).json({
|
|
3151
|
+
error: 'Failed to create invitation',
|
|
3152
|
+
details: isSelfHosted ? errorMessage : undefined,
|
|
3153
|
+
});
|
|
2732
3154
|
}
|
|
2733
3155
|
});
|
|
2734
3156
|
router.get('/api/organizations/:orgId/teams', async (req, res) => {
|
|
2735
3157
|
try {
|
|
2736
3158
|
const { orgId } = req.params;
|
|
2737
3159
|
const adapter = await getAuthAdapterWithConfig();
|
|
2738
|
-
if (adapter
|
|
3160
|
+
if (!adapter) {
|
|
3161
|
+
return res.status(500).json({
|
|
3162
|
+
success: false,
|
|
3163
|
+
error: 'Auth adapter not available',
|
|
3164
|
+
teams: [],
|
|
3165
|
+
});
|
|
3166
|
+
}
|
|
3167
|
+
if (typeof adapter.findMany !== 'function') {
|
|
3168
|
+
return res.status(500).json({
|
|
3169
|
+
success: false,
|
|
3170
|
+
error: 'Adapter findMany method not available',
|
|
3171
|
+
teams: [],
|
|
3172
|
+
});
|
|
3173
|
+
}
|
|
3174
|
+
try {
|
|
3175
|
+
let teams = [];
|
|
2739
3176
|
try {
|
|
2740
|
-
|
|
3177
|
+
teams = await adapter.findMany({
|
|
2741
3178
|
model: 'team',
|
|
2742
3179
|
where: [{ field: 'organizationId', value: orgId }],
|
|
2743
3180
|
limit: 10000,
|
|
2744
3181
|
});
|
|
2745
|
-
|
|
2746
|
-
|
|
2747
|
-
|
|
3182
|
+
}
|
|
3183
|
+
catch (whereError) {
|
|
3184
|
+
const allTeams = await adapter.findMany({
|
|
3185
|
+
model: 'team',
|
|
3186
|
+
limit: 10000,
|
|
3187
|
+
});
|
|
3188
|
+
teams = (allTeams || []).filter((team) => team.organizationId === orgId);
|
|
3189
|
+
}
|
|
3190
|
+
if (!teams || teams.length === 0) {
|
|
3191
|
+
return res.json({ success: true, teams: [] });
|
|
3192
|
+
}
|
|
3193
|
+
const transformedTeams = await Promise.all((teams || []).map(async (team) => {
|
|
3194
|
+
try {
|
|
3195
|
+
let memberCount = 0;
|
|
3196
|
+
if (adapter.findMany) {
|
|
3197
|
+
const teamMembers = await adapter.findMany({
|
|
3198
|
+
model: 'teamMember',
|
|
3199
|
+
where: [{ field: 'teamId', value: team.id }],
|
|
3200
|
+
limit: 10000,
|
|
3201
|
+
});
|
|
3202
|
+
memberCount = teamMembers ? teamMembers.length : 0;
|
|
2748
3203
|
}
|
|
2749
|
-
const teamMembers = await adapter.findMany({
|
|
2750
|
-
model: 'teamMember',
|
|
2751
|
-
where: [{ field: 'teamId', value: team.id }],
|
|
2752
|
-
limit: 10000,
|
|
2753
|
-
});
|
|
2754
3204
|
return {
|
|
2755
3205
|
id: team.id,
|
|
2756
3206
|
name: team.name,
|
|
@@ -2758,18 +3208,38 @@ export function createRoutes(authConfig, configPath, geoDbPath, preloadedAdapter
|
|
|
2758
3208
|
metadata: team.metadata,
|
|
2759
3209
|
createdAt: team.createdAt,
|
|
2760
3210
|
updatedAt: team.updatedAt,
|
|
2761
|
-
memberCount:
|
|
3211
|
+
memberCount: memberCount,
|
|
2762
3212
|
};
|
|
2763
|
-
}
|
|
2764
|
-
|
|
2765
|
-
|
|
2766
|
-
|
|
2767
|
-
|
|
3213
|
+
}
|
|
3214
|
+
catch (_error) {
|
|
3215
|
+
return {
|
|
3216
|
+
id: team.id,
|
|
3217
|
+
name: team.name,
|
|
3218
|
+
organizationId: team.organizationId,
|
|
3219
|
+
metadata: team.metadata,
|
|
3220
|
+
createdAt: team.createdAt,
|
|
3221
|
+
updatedAt: team.updatedAt,
|
|
3222
|
+
memberCount: 0,
|
|
3223
|
+
};
|
|
3224
|
+
}
|
|
3225
|
+
}));
|
|
3226
|
+
const validTeams = transformedTeams.filter((team) => team !== null);
|
|
3227
|
+
return res.json({ success: true, teams: validTeams });
|
|
3228
|
+
}
|
|
3229
|
+
catch (error) {
|
|
3230
|
+
return res.json({
|
|
3231
|
+
success: true,
|
|
3232
|
+
teams: [],
|
|
3233
|
+
error: error?.message || 'Failed to fetch teams',
|
|
3234
|
+
});
|
|
2768
3235
|
}
|
|
2769
|
-
res.json({ success: true, teams: [] });
|
|
2770
3236
|
}
|
|
2771
|
-
catch (
|
|
2772
|
-
res.status(500).json({
|
|
3237
|
+
catch (error) {
|
|
3238
|
+
res.status(500).json({
|
|
3239
|
+
success: false,
|
|
3240
|
+
error: 'Failed to fetch teams',
|
|
3241
|
+
message: error?.message || 'Unknown error',
|
|
3242
|
+
});
|
|
2773
3243
|
}
|
|
2774
3244
|
});
|
|
2775
3245
|
router.post('/api/organizations/:orgId/teams', async (req, res) => {
|
|
@@ -2794,7 +3264,7 @@ export function createRoutes(authConfig, configPath, geoDbPath, preloadedAdapter
|
|
|
2794
3264
|
if (!adapter.create) {
|
|
2795
3265
|
return res.status(500).json({ error: 'Adapter create method not available' });
|
|
2796
3266
|
}
|
|
2797
|
-
await adapter.create({
|
|
3267
|
+
const teamResult = await adapter.create({
|
|
2798
3268
|
model: 'team',
|
|
2799
3269
|
data: {
|
|
2800
3270
|
name: teamData.name,
|
|
@@ -2803,6 +3273,9 @@ export function createRoutes(authConfig, configPath, geoDbPath, preloadedAdapter
|
|
|
2803
3273
|
updatedAt: teamData.updatedAt,
|
|
2804
3274
|
},
|
|
2805
3275
|
});
|
|
3276
|
+
if (!teamResult) {
|
|
3277
|
+
return res.status(500).json({ error: 'Failed to create team' });
|
|
3278
|
+
}
|
|
2806
3279
|
res.json({ success: true, team });
|
|
2807
3280
|
}
|
|
2808
3281
|
catch (_error) {
|
|
@@ -2872,39 +3345,83 @@ export function createRoutes(authConfig, configPath, geoDbPath, preloadedAdapter
|
|
|
2872
3345
|
return res.status(400).json({ error: 'userIds array is required' });
|
|
2873
3346
|
}
|
|
2874
3347
|
const adapter = await getAuthAdapterWithConfig();
|
|
2875
|
-
if (!adapter
|
|
3348
|
+
if (!adapter) {
|
|
2876
3349
|
return res.status(500).json({ error: 'Adapter not available' });
|
|
2877
3350
|
}
|
|
3351
|
+
if (!adapter.create) {
|
|
3352
|
+
return res.status(500).json({ error: 'Adapter create method not available' });
|
|
3353
|
+
}
|
|
2878
3354
|
const results = [];
|
|
2879
3355
|
for (const userId of userIds) {
|
|
2880
3356
|
try {
|
|
3357
|
+
let existingMember = null;
|
|
3358
|
+
if (adapter.findMany) {
|
|
3359
|
+
try {
|
|
3360
|
+
const existing = await adapter.findMany({
|
|
3361
|
+
model: 'teamMember',
|
|
3362
|
+
where: [
|
|
3363
|
+
{ field: 'teamId', value: teamId },
|
|
3364
|
+
{ field: 'userId', value: userId },
|
|
3365
|
+
],
|
|
3366
|
+
limit: 1,
|
|
3367
|
+
});
|
|
3368
|
+
existingMember = existing && existing.length > 0 ? existing[0] : null;
|
|
3369
|
+
}
|
|
3370
|
+
catch (_findError) {
|
|
3371
|
+
// if where clause isn't working.
|
|
3372
|
+
try {
|
|
3373
|
+
const allMembers = await adapter.findMany({
|
|
3374
|
+
model: 'teamMember',
|
|
3375
|
+
limit: 10000,
|
|
3376
|
+
});
|
|
3377
|
+
existingMember = (allMembers || []).find((m) => m.teamId === teamId && m.userId === userId);
|
|
3378
|
+
}
|
|
3379
|
+
catch (_fallbackError) { }
|
|
3380
|
+
}
|
|
3381
|
+
}
|
|
3382
|
+
if (existingMember) {
|
|
3383
|
+
results.push({
|
|
3384
|
+
success: false,
|
|
3385
|
+
userId,
|
|
3386
|
+
error: 'User is already a member of this team',
|
|
3387
|
+
});
|
|
3388
|
+
continue;
|
|
3389
|
+
}
|
|
3390
|
+
const now = new Date();
|
|
2881
3391
|
await adapter.create({
|
|
2882
3392
|
model: 'teamMember',
|
|
2883
3393
|
data: {
|
|
2884
3394
|
teamId,
|
|
2885
3395
|
userId,
|
|
2886
3396
|
role: 'member',
|
|
2887
|
-
createdAt:
|
|
3397
|
+
createdAt: now,
|
|
3398
|
+
updatedAt: now,
|
|
2888
3399
|
},
|
|
2889
3400
|
});
|
|
2890
3401
|
results.push({ success: true, userId });
|
|
2891
3402
|
}
|
|
2892
3403
|
catch (error) {
|
|
3404
|
+
const errorMessage = error?.message || error?.toString() || 'Unknown error';
|
|
2893
3405
|
results.push({
|
|
2894
3406
|
success: false,
|
|
2895
3407
|
userId,
|
|
2896
|
-
error:
|
|
3408
|
+
error: errorMessage,
|
|
2897
3409
|
});
|
|
2898
3410
|
}
|
|
2899
3411
|
}
|
|
3412
|
+
const successCount = results.filter((r) => r.success).length;
|
|
2900
3413
|
res.json({
|
|
2901
|
-
success:
|
|
2902
|
-
message: `Added ${
|
|
3414
|
+
success: results.some((r) => r.success),
|
|
3415
|
+
message: `Added ${successCount} member${successCount !== 1 ? 's' : ''}`,
|
|
2903
3416
|
results,
|
|
2904
3417
|
});
|
|
2905
3418
|
}
|
|
2906
|
-
catch (
|
|
2907
|
-
res.status(500).json({
|
|
3419
|
+
catch (error) {
|
|
3420
|
+
res.status(500).json({
|
|
3421
|
+
success: false,
|
|
3422
|
+
error: 'Failed to add team members',
|
|
3423
|
+
message: error?.message || 'Unknown error',
|
|
3424
|
+
});
|
|
2908
3425
|
}
|
|
2909
3426
|
});
|
|
2910
3427
|
router.delete('/api/team-members/:id', async (req, res) => {
|
|
@@ -2974,7 +3491,7 @@ export function createRoutes(authConfig, configPath, geoDbPath, preloadedAdapter
|
|
|
2974
3491
|
});
|
|
2975
3492
|
router.get('/api/plugins/organization/status', async (_req, res) => {
|
|
2976
3493
|
try {
|
|
2977
|
-
const betterAuthConfig = await getAuthConfigSafe();
|
|
3494
|
+
const betterAuthConfig = preloadedAuthOptions || (await getAuthConfigSafe());
|
|
2978
3495
|
if (!betterAuthConfig) {
|
|
2979
3496
|
return res.json({
|
|
2980
3497
|
enabled: false,
|
|
@@ -3106,7 +3623,7 @@ export function createRoutes(authConfig, configPath, geoDbPath, preloadedAdapter
|
|
|
3106
3623
|
try {
|
|
3107
3624
|
const { id } = req.params;
|
|
3108
3625
|
const userData = req.body;
|
|
3109
|
-
const updatedUser = await getAuthData(authConfig, 'updateUser', { id, userData }, configPath);
|
|
3626
|
+
const updatedUser = await getAuthData(authConfig, 'updateUser', { id, userData }, configPath, preloadedAdapter);
|
|
3110
3627
|
res.json({ success: true, user: updatedUser });
|
|
3111
3628
|
}
|
|
3112
3629
|
catch (_error) {
|
|
@@ -3497,6 +4014,92 @@ export function createRoutes(authConfig, configPath, geoDbPath, preloadedAdapter
|
|
|
3497
4014
|
res.status(500).json({ success: false, error: 'Failed to fetch OAuth providers' });
|
|
3498
4015
|
}
|
|
3499
4016
|
});
|
|
4017
|
+
router.get('/api/tools/oauth/credentials', async (req, res) => {
|
|
4018
|
+
try {
|
|
4019
|
+
const { provider, origin } = req.query;
|
|
4020
|
+
if (!provider || typeof provider !== 'string') {
|
|
4021
|
+
return res.status(400).json({
|
|
4022
|
+
success: false,
|
|
4023
|
+
error: 'Provider is required',
|
|
4024
|
+
});
|
|
4025
|
+
}
|
|
4026
|
+
if (!origin || typeof origin !== 'string') {
|
|
4027
|
+
return res.status(400).json({
|
|
4028
|
+
success: false,
|
|
4029
|
+
error: 'Origin is required',
|
|
4030
|
+
});
|
|
4031
|
+
}
|
|
4032
|
+
// TODO: Import getOAuthCredentials at the top of this file:
|
|
4033
|
+
// import { getOAuthCredentials } from './path/to/your/oauth-config';
|
|
4034
|
+
// For now, we'll access it from a function that should be provided
|
|
4035
|
+
// This assumes getOAuthCredentials is available in the scope
|
|
4036
|
+
// You need to import it: import { getOAuthCredentials } from './your-oauth-config-file';
|
|
4037
|
+
// Placeholder - replace this with actual import at top of file
|
|
4038
|
+
const getOAuthCredentials = global.getOAuthCredentials;
|
|
4039
|
+
if (typeof getOAuthCredentials !== 'function') {
|
|
4040
|
+
return res.status(500).json({
|
|
4041
|
+
success: false,
|
|
4042
|
+
error: 'OAuth credentials function not configured. Please import getOAuthCredentials function.',
|
|
4043
|
+
});
|
|
4044
|
+
}
|
|
4045
|
+
const credentialsResult = getOAuthCredentials(provider, origin);
|
|
4046
|
+
// Handle null return (provider not found)
|
|
4047
|
+
if (credentialsResult === null) {
|
|
4048
|
+
return res.status(404).json({
|
|
4049
|
+
success: false,
|
|
4050
|
+
error: 'No credential found',
|
|
4051
|
+
});
|
|
4052
|
+
}
|
|
4053
|
+
// Handle error cases with proper messages as requested
|
|
4054
|
+
if (credentialsResult.error) {
|
|
4055
|
+
if (credentialsResult.error === 'NO_CREDENTIALS_FOUND') {
|
|
4056
|
+
return res.status(404).json({
|
|
4057
|
+
success: false,
|
|
4058
|
+
error: 'No credential found',
|
|
4059
|
+
});
|
|
4060
|
+
}
|
|
4061
|
+
else if (credentialsResult.error === 'INVALID_ORIGIN') {
|
|
4062
|
+
return res.status(400).json({
|
|
4063
|
+
success: false,
|
|
4064
|
+
error: 'Invalid origin. OAuth credentials are only available for localhost origins.',
|
|
4065
|
+
});
|
|
4066
|
+
}
|
|
4067
|
+
else {
|
|
4068
|
+
return res.status(400).json({
|
|
4069
|
+
success: false,
|
|
4070
|
+
error: credentialsResult.error || 'Failed to get OAuth credentials',
|
|
4071
|
+
});
|
|
4072
|
+
}
|
|
4073
|
+
}
|
|
4074
|
+
// Check if result exists and has required fields
|
|
4075
|
+
if (!credentialsResult.result) {
|
|
4076
|
+
return res.status(404).json({
|
|
4077
|
+
success: false,
|
|
4078
|
+
error: 'No credential found',
|
|
4079
|
+
});
|
|
4080
|
+
}
|
|
4081
|
+
const { clientId, clientSecret } = credentialsResult.result;
|
|
4082
|
+
if (!clientId || !clientSecret) {
|
|
4083
|
+
return res.status(404).json({
|
|
4084
|
+
success: false,
|
|
4085
|
+
error: 'No credential found',
|
|
4086
|
+
});
|
|
4087
|
+
}
|
|
4088
|
+
res.json({
|
|
4089
|
+
success: true,
|
|
4090
|
+
clientId,
|
|
4091
|
+
clientSecret,
|
|
4092
|
+
});
|
|
4093
|
+
}
|
|
4094
|
+
catch (error) {
|
|
4095
|
+
console.error('Failed to fetch OAuth credentials:', error);
|
|
4096
|
+
res.status(500).json({
|
|
4097
|
+
success: false,
|
|
4098
|
+
error: 'Failed to fetch OAuth credentials',
|
|
4099
|
+
details: error instanceof Error ? error.message : String(error),
|
|
4100
|
+
});
|
|
4101
|
+
}
|
|
4102
|
+
});
|
|
3500
4103
|
router.post('/api/tools/oauth/test', async (req, res) => {
|
|
3501
4104
|
try {
|
|
3502
4105
|
const { provider } = req.body;
|