better-auth-studio 1.1.3-beta.9 → 1.1.3
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 +145 -16
- package/dist/adapters/astro.d.ts +0 -1
- package/dist/adapters/astro.js +5 -2
- package/dist/adapters/cloudflare-workers.d.ts +101 -0
- package/dist/adapters/cloudflare-workers.js +494 -0
- package/dist/adapters/elysia.d.ts +0 -1
- package/dist/adapters/elysia.js +7 -33
- package/dist/adapters/express.d.ts +0 -1
- package/dist/adapters/express.js +4 -1
- package/dist/adapters/hono.d.ts +0 -1
- package/dist/adapters/hono.js +7 -27
- package/dist/adapters/nextjs.d.ts +0 -1
- package/dist/adapters/nextjs.js +5 -2
- package/dist/adapters/nuxt.d.ts +0 -1
- package/dist/adapters/nuxt.js +5 -2
- package/dist/adapters/remix.d.ts +0 -1
- package/dist/adapters/remix.js +5 -2
- package/dist/adapters/solid-start.d.ts +0 -1
- package/dist/adapters/solid-start.js +5 -2
- package/dist/adapters/svelte-kit.d.ts +0 -1
- package/dist/adapters/svelte-kit.js +5 -2
- package/dist/adapters/tanstack-start.d.ts +0 -1
- package/dist/adapters/tanstack-start.js +36 -8
- package/dist/add-svelte-kit-env-modules.d.ts +0 -1
- package/dist/add-svelte-kit-env-modules.js +0 -1
- package/dist/auth-adapter.d.ts +5 -2
- package/dist/auth-adapter.js +39 -4
- package/dist/cli/commands/init.d.ts +0 -1
- package/dist/cli/commands/init.js +0 -1
- package/dist/cli.d.ts +0 -1
- package/dist/cli.js +6 -1
- package/dist/config.d.ts +0 -1
- package/dist/config.js +0 -1
- package/dist/core/handler.d.ts +0 -1
- package/dist/core/handler.js +36 -5
- package/dist/data.d.ts +10 -4
- package/dist/data.js +40 -6
- package/dist/geo-service.d.ts +0 -1
- package/dist/geo-service.js +0 -1
- package/dist/get-tsconfig-info.d.ts +0 -1
- package/dist/get-tsconfig-info.js +0 -1
- package/dist/index.d.ts +0 -1
- package/dist/index.js +0 -1
- package/dist/providers/events/helpers.d.ts +3 -2
- package/dist/providers/events/helpers.js +303 -93
- package/dist/public/assets/main-C-R_HO98.css +1 -0
- package/dist/public/assets/main-DDypFItU.js +1302 -0
- package/dist/public/index.html +2 -2
- package/dist/public/shaders.png +0 -0
- package/dist/routes/api-router.d.ts +2 -3
- package/dist/routes/api-router.js +0 -1
- package/dist/routes.d.ts +2 -3
- package/dist/routes.js +813 -172
- package/dist/studio.d.ts +0 -1
- package/dist/studio.js +6 -4
- package/dist/types/events.d.ts +0 -1
- package/dist/types/events.js +0 -1
- package/dist/types/handler.d.ts +13 -3
- package/dist/types/handler.js +0 -1
- package/dist/types/index.d.ts +0 -1
- package/dist/types/index.js +0 -1
- package/dist/utils/access-rules.d.ts +19 -0
- package/dist/utils/access-rules.js +126 -0
- package/dist/utils/auth-callbacks-injector.d.ts +0 -1
- package/dist/utils/auth-callbacks-injector.js +0 -1
- package/dist/utils/database-detection.d.ts +0 -1
- package/dist/utils/database-detection.js +33 -4
- package/dist/utils/event-ingestion.d.ts +0 -1
- package/dist/utils/event-ingestion.js +1 -1
- package/dist/utils/hook-injector.d.ts +0 -1
- package/dist/utils/hook-injector.js +2 -2
- package/dist/utils/html-injector.d.ts +1 -7
- package/dist/utils/html-injector.js +15 -1
- package/dist/utils/org-hooks-injector.d.ts +0 -1
- package/dist/utils/org-hooks-injector.js +0 -1
- package/dist/utils/package-json.d.ts +1 -1
- package/dist/utils/package-json.js +21 -1
- package/dist/utils/paths.d.ts +0 -1
- package/dist/utils/paths.js +0 -1
- package/dist/utils/reason-messages.d.ts +0 -1
- package/dist/utils/reason-messages.js +0 -1
- package/dist/utils/session.d.ts +0 -1
- package/dist/utils/session.js +0 -1
- package/dist/utils.d.ts +0 -1
- package/dist/utils.js +0 -1
- package/package.json +30 -39
- package/dist/adapters/astro.d.ts.map +0 -1
- package/dist/adapters/astro.js.map +0 -1
- package/dist/adapters/elysia.d.ts.map +0 -1
- package/dist/adapters/elysia.js.map +0 -1
- package/dist/adapters/express.d.ts.map +0 -1
- package/dist/adapters/express.js.map +0 -1
- package/dist/adapters/hono.d.ts.map +0 -1
- package/dist/adapters/hono.js.map +0 -1
- package/dist/adapters/nextjs.d.ts.map +0 -1
- package/dist/adapters/nextjs.js.map +0 -1
- package/dist/adapters/nuxt.d.ts.map +0 -1
- package/dist/adapters/nuxt.js.map +0 -1
- package/dist/adapters/remix.d.ts.map +0 -1
- package/dist/adapters/remix.js.map +0 -1
- package/dist/adapters/solid-start.d.ts.map +0 -1
- package/dist/adapters/solid-start.js.map +0 -1
- package/dist/adapters/svelte-kit.d.ts.map +0 -1
- package/dist/adapters/svelte-kit.js.map +0 -1
- package/dist/adapters/tanstack-start.d.ts.map +0 -1
- package/dist/adapters/tanstack-start.js.map +0 -1
- package/dist/add-svelte-kit-env-modules.d.ts.map +0 -1
- package/dist/add-svelte-kit-env-modules.js.map +0 -1
- package/dist/auth-adapter.d.ts.map +0 -1
- package/dist/auth-adapter.js.map +0 -1
- package/dist/cli/commands/init.d.ts.map +0 -1
- package/dist/cli/commands/init.js.map +0 -1
- package/dist/cli.d.ts.map +0 -1
- package/dist/cli.js.map +0 -1
- package/dist/config.d.ts.map +0 -1
- package/dist/config.js.map +0 -1
- package/dist/core/handler.d.ts.map +0 -1
- package/dist/core/handler.js.map +0 -1
- package/dist/data.d.ts.map +0 -1
- package/dist/data.js.map +0 -1
- package/dist/geo-service.d.ts.map +0 -1
- package/dist/geo-service.js.map +0 -1
- package/dist/get-tsconfig-info.d.ts.map +0 -1
- package/dist/get-tsconfig-info.js.map +0 -1
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/providers/events/helpers.d.ts.map +0 -1
- package/dist/providers/events/helpers.js.map +0 -1
- package/dist/public/assets/main-D7_GjGqW.js +0 -1216
- package/dist/public/assets/main-Ru3ybjQp.css +0 -1
- package/dist/routes/api-router.d.ts.map +0 -1
- package/dist/routes/api-router.js.map +0 -1
- package/dist/routes.d.ts.map +0 -1
- package/dist/routes.js.map +0 -1
- package/dist/studio.d.ts.map +0 -1
- package/dist/studio.js.map +0 -1
- package/dist/types/events.d.ts.map +0 -1
- package/dist/types/events.js.map +0 -1
- package/dist/types/handler.d.ts.map +0 -1
- package/dist/types/handler.js.map +0 -1
- package/dist/types/index.d.ts.map +0 -1
- package/dist/types/index.js.map +0 -1
- package/dist/utils/auth-callbacks-injector.d.ts.map +0 -1
- package/dist/utils/auth-callbacks-injector.js.map +0 -1
- package/dist/utils/auth-callbacks-wrapper.d.ts +0 -7
- package/dist/utils/auth-callbacks-wrapper.d.ts.map +0 -1
- package/dist/utils/auth-callbacks-wrapper.js +0 -133
- package/dist/utils/auth-callbacks-wrapper.js.map +0 -1
- package/dist/utils/database-detection.d.ts.map +0 -1
- package/dist/utils/database-detection.js.map +0 -1
- package/dist/utils/database-hook-injector.d.ts +0 -3
- package/dist/utils/database-hook-injector.d.ts.map +0 -1
- package/dist/utils/database-hook-injector.js +0 -145
- package/dist/utils/database-hook-injector.js.map +0 -1
- package/dist/utils/email-otp-hooks-injector.d.ts +0 -45
- package/dist/utils/email-otp-hooks-injector.d.ts.map +0 -1
- package/dist/utils/email-otp-hooks-injector.js +0 -141
- package/dist/utils/email-otp-hooks-injector.js.map +0 -1
- package/dist/utils/event-ingestion.d.ts.map +0 -1
- package/dist/utils/event-ingestion.js.map +0 -1
- package/dist/utils/hook-injector.d.ts.map +0 -1
- package/dist/utils/hook-injector.js.map +0 -1
- package/dist/utils/html-injector.d.ts.map +0 -1
- package/dist/utils/html-injector.js.map +0 -1
- package/dist/utils/org-hooks-injector.d.ts.map +0 -1
- package/dist/utils/org-hooks-injector.js.map +0 -1
- package/dist/utils/org-hooks-wrapper.d.ts +0 -80
- package/dist/utils/org-hooks-wrapper.d.ts.map +0 -1
- package/dist/utils/org-hooks-wrapper.js +0 -807
- package/dist/utils/org-hooks-wrapper.js.map +0 -1
- package/dist/utils/organization-hooks-wrapper.d.ts +0 -44
- package/dist/utils/organization-hooks-wrapper.d.ts.map +0 -1
- package/dist/utils/organization-hooks-wrapper.js +0 -345
- package/dist/utils/organization-hooks-wrapper.js.map +0 -1
- package/dist/utils/package-json.d.ts.map +0 -1
- package/dist/utils/package-json.js.map +0 -1
- package/dist/utils/paths.d.ts.map +0 -1
- package/dist/utils/paths.js.map +0 -1
- package/dist/utils/reason-messages.d.ts.map +0 -1
- package/dist/utils/reason-messages.js.map +0 -1
- package/dist/utils/server-init.d.ts +0 -10
- package/dist/utils/server-init.d.ts.map +0 -1
- package/dist/utils/server-init.js +0 -40
- package/dist/utils/server-init.js.map +0 -1
- package/dist/utils/session.d.ts.map +0 -1
- package/dist/utils/session.js.map +0 -1
- package/dist/utils.d.ts.map +0 -1
- package/dist/utils.js.map +0 -1
- package/public/assets/main-D7_GjGqW.js +0 -1216
- package/public/assets/main-Ru3ybjQp.css +0 -1
- package/public/favicon.svg +0 -6
- package/public/index.html +0 -14
- package/public/logo.png +0 -0
- package/public/vite.svg +0 -5
- package/scripts/download-geolite2.js +0 -42
- package/scripts/generate-default-db.js +0 -462
package/dist/routes.js
CHANGED
|
@@ -16,7 +16,8 @@ import { createRequire } from "module";
|
|
|
16
16
|
import { createMockAccount, createMockSession, createMockUser, createMockVerification, getAuthAdapter, } from "./auth-adapter.js";
|
|
17
17
|
import { getPathAliases, possiblePaths } from "./config.js";
|
|
18
18
|
import { getAuthData } from "./data.js";
|
|
19
|
-
import { initializeGeoService, resolveIPLocationAsync, setGeoDbPath } from "./geo-service.js";
|
|
19
|
+
import { initializeGeoService, resolveIPLocation, resolveIPLocationAsync, setGeoDbPath, } from "./geo-service.js";
|
|
20
|
+
import { evaluateRequestAccess, extractClientIp } from "./utils/access-rules.js";
|
|
20
21
|
import { detectDatabaseWithDialect } from "./utils/database-detection.js";
|
|
21
22
|
import { createStudioSession, decryptSession, encryptSession, isSessionValid, STUDIO_COOKIE_NAME, } from "./utils/session.js";
|
|
22
23
|
const config = {
|
|
@@ -315,6 +316,131 @@ export function createRoutes(authConfig, configPath, geoDbPath, preloadedAdapter
|
|
|
315
316
|
const secs = Math.max(1, Math.floor(absMs / 1000));
|
|
316
317
|
return `${secs}s`;
|
|
317
318
|
};
|
|
319
|
+
let resolvedEventModelName = null;
|
|
320
|
+
const isEventModelLookupError = (error) => error?.message?.includes("not found in schema") ||
|
|
321
|
+
error?.message?.includes("not found in model") ||
|
|
322
|
+
error?.message?.includes("Model") ||
|
|
323
|
+
error?.code === "P2025" ||
|
|
324
|
+
error?.code === "42P01";
|
|
325
|
+
const toEventModelCamelCase = (value) => value.replace(/[_-]([a-z])/g, (_match, char) => char.toUpperCase());
|
|
326
|
+
const getEventModelCandidates = () => {
|
|
327
|
+
const baseName = studioConfig?.events?.tableName || "auth_events";
|
|
328
|
+
const candidates = new Set();
|
|
329
|
+
const addForms = (value) => {
|
|
330
|
+
const normalized = value?.trim();
|
|
331
|
+
if (!normalized)
|
|
332
|
+
return;
|
|
333
|
+
const snake = normalized.replace(/-/g, "_");
|
|
334
|
+
const singularSnake = snake.endsWith("s") ? snake.slice(0, -1) : snake;
|
|
335
|
+
const pluralSnake = snake.endsWith("s") ? snake : `${snake}s`;
|
|
336
|
+
const singularCamel = toEventModelCamelCase(singularSnake);
|
|
337
|
+
const pluralCamel = toEventModelCamelCase(pluralSnake);
|
|
338
|
+
[
|
|
339
|
+
normalized,
|
|
340
|
+
snake,
|
|
341
|
+
singularSnake,
|
|
342
|
+
pluralSnake,
|
|
343
|
+
singularCamel,
|
|
344
|
+
pluralCamel,
|
|
345
|
+
singularCamel.charAt(0).toUpperCase() + singularCamel.slice(1),
|
|
346
|
+
pluralCamel.charAt(0).toUpperCase() + pluralCamel.slice(1),
|
|
347
|
+
].forEach((candidate) => {
|
|
348
|
+
if (candidate) {
|
|
349
|
+
candidates.add(candidate);
|
|
350
|
+
}
|
|
351
|
+
});
|
|
352
|
+
};
|
|
353
|
+
addForms(baseName);
|
|
354
|
+
addForms(baseName === "auth_events" ? "auth_event" : "auth_events");
|
|
355
|
+
return Array.from(candidates);
|
|
356
|
+
};
|
|
357
|
+
const getEventFieldCandidates = (field) => {
|
|
358
|
+
switch (field) {
|
|
359
|
+
case "userId":
|
|
360
|
+
return ["userId", "user_id"];
|
|
361
|
+
case "sessionId":
|
|
362
|
+
return ["sessionId", "session_id"];
|
|
363
|
+
case "organizationId":
|
|
364
|
+
return ["organizationId", "organization_id"];
|
|
365
|
+
case "ipAddress":
|
|
366
|
+
return ["ipAddress", "ip_address"];
|
|
367
|
+
case "userAgent":
|
|
368
|
+
return ["userAgent", "user_agent"];
|
|
369
|
+
case "displayMessage":
|
|
370
|
+
return ["displayMessage", "display_message"];
|
|
371
|
+
case "displaySeverity":
|
|
372
|
+
return ["displaySeverity", "display_severity"];
|
|
373
|
+
case "createdAt":
|
|
374
|
+
return ["createdAt", "created_at"];
|
|
375
|
+
default:
|
|
376
|
+
return field ? [field] : [];
|
|
377
|
+
}
|
|
378
|
+
};
|
|
379
|
+
const getEventWhereCandidates = (where) => {
|
|
380
|
+
if (!Array.isArray(where) || where.length === 0) {
|
|
381
|
+
return {
|
|
382
|
+
whereCandidates: [undefined],
|
|
383
|
+
hasAliasedFields: false,
|
|
384
|
+
};
|
|
385
|
+
}
|
|
386
|
+
let hasAliasedFields = false;
|
|
387
|
+
let combinations = [[]];
|
|
388
|
+
for (const clause of where) {
|
|
389
|
+
const fieldCandidates = getEventFieldCandidates(clause?.field);
|
|
390
|
+
if (fieldCandidates.length > 1) {
|
|
391
|
+
hasAliasedFields = true;
|
|
392
|
+
}
|
|
393
|
+
const nextCombinations = fieldCandidates.flatMap((fieldName) => combinations.map((existing) => [...existing, { ...clause, field: fieldName }]));
|
|
394
|
+
if (nextCombinations.length > 0) {
|
|
395
|
+
combinations = nextCombinations;
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
return {
|
|
399
|
+
whereCandidates: combinations.length > 0 ? combinations : [where],
|
|
400
|
+
hasAliasedFields,
|
|
401
|
+
};
|
|
402
|
+
};
|
|
403
|
+
const findManyEventRows = async (adapter, options) => {
|
|
404
|
+
const candidates = Array.from(new Set([resolvedEventModelName, ...getEventModelCandidates()].filter(Boolean)));
|
|
405
|
+
const { where, ...restOptions } = options;
|
|
406
|
+
const { whereCandidates, hasAliasedFields } = getEventWhereCandidates(where);
|
|
407
|
+
let lastLookupError = null;
|
|
408
|
+
let lastSuccessfulEmptyResult = null;
|
|
409
|
+
for (const modelName of candidates) {
|
|
410
|
+
for (let index = 0; index < whereCandidates.length; index += 1) {
|
|
411
|
+
try {
|
|
412
|
+
const result = await adapter.findMany({
|
|
413
|
+
...restOptions,
|
|
414
|
+
...(whereCandidates[index] ? { where: whereCandidates[index] } : {}),
|
|
415
|
+
model: modelName,
|
|
416
|
+
});
|
|
417
|
+
resolvedEventModelName = modelName;
|
|
418
|
+
if (Array.isArray(result) &&
|
|
419
|
+
result.length === 0 &&
|
|
420
|
+
hasAliasedFields &&
|
|
421
|
+
index < whereCandidates.length - 1) {
|
|
422
|
+
lastSuccessfulEmptyResult = result;
|
|
423
|
+
continue;
|
|
424
|
+
}
|
|
425
|
+
return result;
|
|
426
|
+
}
|
|
427
|
+
catch (error) {
|
|
428
|
+
if (isEventModelLookupError(error)) {
|
|
429
|
+
lastLookupError = error;
|
|
430
|
+
continue;
|
|
431
|
+
}
|
|
432
|
+
throw error;
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
if (lastSuccessfulEmptyResult && !lastLookupError) {
|
|
437
|
+
return lastSuccessfulEmptyResult;
|
|
438
|
+
}
|
|
439
|
+
if (lastLookupError) {
|
|
440
|
+
throw lastLookupError;
|
|
441
|
+
}
|
|
442
|
+
throw new Error("Failed to resolve events model");
|
|
443
|
+
};
|
|
318
444
|
if (geoDbPath) {
|
|
319
445
|
setGeoDbPath(geoDbPath);
|
|
320
446
|
}
|
|
@@ -353,6 +479,7 @@ export function createRoutes(authConfig, configPath, geoDbPath, preloadedAdapter
|
|
|
353
479
|
// For self-hosted studio, wrap the preloaded adapter to match expected interface
|
|
354
480
|
return {
|
|
355
481
|
...preloadedAdapter,
|
|
482
|
+
count: preloadedAdapter.count?.bind(preloadedAdapter),
|
|
356
483
|
findUnique: preloadedAdapter.findUnique?.bind(preloadedAdapter),
|
|
357
484
|
findOne: preloadedAdapter.findOne?.bind(preloadedAdapter) ||
|
|
358
485
|
preloadedAdapter.findUnique?.bind(preloadedAdapter),
|
|
@@ -427,6 +554,9 @@ export function createRoutes(authConfig, configPath, geoDbPath, preloadedAdapter
|
|
|
427
554
|
};
|
|
428
555
|
if (isSelfHosted) {
|
|
429
556
|
router.use((req, res, next) => {
|
|
557
|
+
if (!enforceRequestAccess(req, res)) {
|
|
558
|
+
return;
|
|
559
|
+
}
|
|
430
560
|
const path = req.path;
|
|
431
561
|
const publicPaths = [
|
|
432
562
|
"/api/auth/sign-in",
|
|
@@ -514,6 +644,110 @@ export function createRoutes(authConfig, configPath, geoDbPath, preloadedAdapter
|
|
|
514
644
|
return true;
|
|
515
645
|
return allowedEmails.includes(email.toLowerCase());
|
|
516
646
|
};
|
|
647
|
+
const enforceRequestAccess = (req, res) => {
|
|
648
|
+
if (!isSelfHosted) {
|
|
649
|
+
return true;
|
|
650
|
+
}
|
|
651
|
+
const decision = evaluateRequestAccess({
|
|
652
|
+
accessConfig,
|
|
653
|
+
path: req.path,
|
|
654
|
+
method: req.method,
|
|
655
|
+
headers: req.headers,
|
|
656
|
+
ip: req.ip,
|
|
657
|
+
});
|
|
658
|
+
if (decision.allowed) {
|
|
659
|
+
return true;
|
|
660
|
+
}
|
|
661
|
+
res.status(403).json({
|
|
662
|
+
success: false,
|
|
663
|
+
message: decision.message,
|
|
664
|
+
reason: decision.reason,
|
|
665
|
+
...(decision.ipAddress ? { ipAddress: decision.ipAddress } : {}),
|
|
666
|
+
});
|
|
667
|
+
return false;
|
|
668
|
+
};
|
|
669
|
+
const finalizeStudioSignIn = (res, user) => {
|
|
670
|
+
const allowedRoles = getAllowedRoles();
|
|
671
|
+
if (!allowedRoles.includes(user.role || "")) {
|
|
672
|
+
return res.status(403).json({
|
|
673
|
+
success: false,
|
|
674
|
+
message: `Access denied.`,
|
|
675
|
+
userRole: user.role || "none",
|
|
676
|
+
});
|
|
677
|
+
}
|
|
678
|
+
if (!isEmailAllowed(user.email)) {
|
|
679
|
+
return res.status(403).json({
|
|
680
|
+
success: false,
|
|
681
|
+
message: "Access denied. Your email is not authorized to access this dashboard.",
|
|
682
|
+
});
|
|
683
|
+
}
|
|
684
|
+
const studioSession = createStudioSession({
|
|
685
|
+
id: user.id,
|
|
686
|
+
email: user.email,
|
|
687
|
+
name: user.name,
|
|
688
|
+
role: user.role || undefined,
|
|
689
|
+
}, getSessionDuration());
|
|
690
|
+
const encryptedSession = encryptSession(studioSession, getSessionSecret());
|
|
691
|
+
res.cookie(STUDIO_COOKIE_NAME, encryptedSession, {
|
|
692
|
+
httpOnly: true,
|
|
693
|
+
secure: process.env.NODE_ENV === "production",
|
|
694
|
+
sameSite: "lax",
|
|
695
|
+
maxAge: getSessionDuration(),
|
|
696
|
+
path: "/",
|
|
697
|
+
});
|
|
698
|
+
return res.json({
|
|
699
|
+
success: true,
|
|
700
|
+
user: { id: user.id, email: user.email, name: user.name, role: user.role || null },
|
|
701
|
+
});
|
|
702
|
+
};
|
|
703
|
+
const attemptManualCredentialStudioSignIn = async (email, password, adapter) => {
|
|
704
|
+
if (!adapter?.findMany) {
|
|
705
|
+
return { ok: false };
|
|
706
|
+
}
|
|
707
|
+
const users = await adapter.findMany({
|
|
708
|
+
model: "user",
|
|
709
|
+
where: [{ field: "email", value: email }],
|
|
710
|
+
limit: 1,
|
|
711
|
+
});
|
|
712
|
+
if (!users || users.length === 0) {
|
|
713
|
+
return { ok: false, status: 401, message: "Invalid credentials" };
|
|
714
|
+
}
|
|
715
|
+
const userRecord = users[0];
|
|
716
|
+
const userId = userRecord.id;
|
|
717
|
+
const accounts = await adapter.findMany({
|
|
718
|
+
model: "account",
|
|
719
|
+
where: [{ field: "userId", value: userId }],
|
|
720
|
+
limit: 10,
|
|
721
|
+
});
|
|
722
|
+
const credentialAccount = accounts?.find((acc) => acc.providerId === "credential" || acc.providerId === "email");
|
|
723
|
+
if (!credentialAccount) {
|
|
724
|
+
return {
|
|
725
|
+
ok: false,
|
|
726
|
+
status: 401,
|
|
727
|
+
message: "No password set for this account. Please use social login or reset your password.",
|
|
728
|
+
};
|
|
729
|
+
}
|
|
730
|
+
if (!credentialAccount.password) {
|
|
731
|
+
return {
|
|
732
|
+
ok: false,
|
|
733
|
+
status: 401,
|
|
734
|
+
message: "Password not configured. Please reset your password.",
|
|
735
|
+
};
|
|
736
|
+
}
|
|
737
|
+
const isValidPassword = await verifyPassword(password, credentialAccount.password);
|
|
738
|
+
if (!isValidPassword) {
|
|
739
|
+
return { ok: false, status: 401, message: "Invalid credentials" };
|
|
740
|
+
}
|
|
741
|
+
return {
|
|
742
|
+
ok: true,
|
|
743
|
+
user: {
|
|
744
|
+
id: userId,
|
|
745
|
+
email: userRecord.email,
|
|
746
|
+
name: userRecord.name,
|
|
747
|
+
role: userRecord.role,
|
|
748
|
+
},
|
|
749
|
+
};
|
|
750
|
+
};
|
|
517
751
|
const verifyStudioSession = (req) => {
|
|
518
752
|
if (!isSelfHosted) {
|
|
519
753
|
return { valid: true };
|
|
@@ -551,6 +785,18 @@ export function createRoutes(authConfig, configPath, geoDbPath, preloadedAdapter
|
|
|
551
785
|
return res.status(400).json({ success: false, message: "Email and password required" });
|
|
552
786
|
}
|
|
553
787
|
const adapter = await getAuthAdapterWithConfig();
|
|
788
|
+
if (isSelfHosted && adapter?.findMany) {
|
|
789
|
+
const manualResult = await attemptManualCredentialStudioSignIn(email, password, adapter);
|
|
790
|
+
if (manualResult.ok) {
|
|
791
|
+
return finalizeStudioSignIn(res, manualResult.user);
|
|
792
|
+
}
|
|
793
|
+
if (manualResult.status && manualResult.message) {
|
|
794
|
+
return res.status(manualResult.status).json({
|
|
795
|
+
success: false,
|
|
796
|
+
message: manualResult.message,
|
|
797
|
+
});
|
|
798
|
+
}
|
|
799
|
+
}
|
|
554
800
|
let signInResult = null;
|
|
555
801
|
let signInError = null;
|
|
556
802
|
try {
|
|
@@ -563,67 +809,18 @@ export function createRoutes(authConfig, configPath, geoDbPath, preloadedAdapter
|
|
|
563
809
|
}
|
|
564
810
|
if (!signInResult || signInResult.error || signInError) {
|
|
565
811
|
const errorMessage = signInError || signInResult?.error?.message || "Invalid credentials";
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
});
|
|
572
|
-
if (!users || users.length === 0) {
|
|
573
|
-
return res.status(401).json({ success: false, message: "Invalid credentials" });
|
|
812
|
+
const shouldAttemptManualFallback = !!signInError || errorMessage.includes("Invalid password hash");
|
|
813
|
+
if (shouldAttemptManualFallback) {
|
|
814
|
+
const manualResult = await attemptManualCredentialStudioSignIn(email, password, adapter);
|
|
815
|
+
if (manualResult.ok) {
|
|
816
|
+
return finalizeStudioSignIn(res, manualResult.user);
|
|
574
817
|
}
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
model: "account",
|
|
578
|
-
where: [{ field: "userId", value: userId }],
|
|
579
|
-
limit: 10,
|
|
580
|
-
});
|
|
581
|
-
const credentialAccount = accounts?.find((acc) => acc.providerId === "credential" || acc.providerId === "email");
|
|
582
|
-
if (!credentialAccount) {
|
|
583
|
-
return res.status(401).json({
|
|
818
|
+
if (manualResult.status && manualResult.message) {
|
|
819
|
+
return res.status(manualResult.status).json({
|
|
584
820
|
success: false,
|
|
585
|
-
message:
|
|
821
|
+
message: manualResult.message,
|
|
586
822
|
});
|
|
587
823
|
}
|
|
588
|
-
if (!credentialAccount.password) {
|
|
589
|
-
return res.status(401).json({
|
|
590
|
-
success: false,
|
|
591
|
-
message: "Password not configured. Please reset your password.",
|
|
592
|
-
});
|
|
593
|
-
}
|
|
594
|
-
const isValidPassword = await verifyPassword(password, credentialAccount.password);
|
|
595
|
-
if (!isValidPassword) {
|
|
596
|
-
return res.status(401).json({ success: false, message: "Invalid credentials" });
|
|
597
|
-
}
|
|
598
|
-
const userRole = users[0].role;
|
|
599
|
-
const user = { id: userId, email: users[0].email, name: users[0].name, role: userRole };
|
|
600
|
-
const allowedRoles = getAllowedRoles();
|
|
601
|
-
if (!allowedRoles.includes(user.role)) {
|
|
602
|
-
return res.status(403).json({
|
|
603
|
-
success: false,
|
|
604
|
-
message: `Access denied.`,
|
|
605
|
-
userRole: user.role || "none",
|
|
606
|
-
});
|
|
607
|
-
}
|
|
608
|
-
if (!isEmailAllowed(user.email)) {
|
|
609
|
-
return res.status(403).json({
|
|
610
|
-
success: false,
|
|
611
|
-
message: "Access denied. Your email is not authorized to access this dashboard.",
|
|
612
|
-
});
|
|
613
|
-
}
|
|
614
|
-
const studioSession = createStudioSession(user, getSessionDuration());
|
|
615
|
-
const encryptedSession = encryptSession(studioSession, getSessionSecret());
|
|
616
|
-
res.cookie(STUDIO_COOKIE_NAME, encryptedSession, {
|
|
617
|
-
httpOnly: true,
|
|
618
|
-
secure: process.env.NODE_ENV === "production",
|
|
619
|
-
sameSite: "lax",
|
|
620
|
-
maxAge: getSessionDuration(),
|
|
621
|
-
path: "/",
|
|
622
|
-
});
|
|
623
|
-
return res.json({
|
|
624
|
-
success: true,
|
|
625
|
-
user: { id: user.id, email: user.email, name: user.name, role: user.role },
|
|
626
|
-
});
|
|
627
824
|
}
|
|
628
825
|
return res.status(401).json({
|
|
629
826
|
success: false,
|
|
@@ -646,33 +843,7 @@ export function createRoutes(authConfig, configPath, geoDbPath, preloadedAdapter
|
|
|
646
843
|
}
|
|
647
844
|
}
|
|
648
845
|
const user = { ...signInResult.user, role: userRole };
|
|
649
|
-
|
|
650
|
-
if (!allowedRoles.includes(user.role)) {
|
|
651
|
-
return res.status(403).json({
|
|
652
|
-
success: false,
|
|
653
|
-
message: `Access denied.`,
|
|
654
|
-
userRole: user.role || "none",
|
|
655
|
-
});
|
|
656
|
-
}
|
|
657
|
-
if (!isEmailAllowed(user.email)) {
|
|
658
|
-
return res.status(403).json({
|
|
659
|
-
success: false,
|
|
660
|
-
message: "Access denied. Your email is not authorized to access this dashboard.",
|
|
661
|
-
});
|
|
662
|
-
}
|
|
663
|
-
const studioSession = createStudioSession(user, getSessionDuration());
|
|
664
|
-
const encryptedSession = encryptSession(studioSession, getSessionSecret());
|
|
665
|
-
res.cookie(STUDIO_COOKIE_NAME, encryptedSession, {
|
|
666
|
-
httpOnly: true,
|
|
667
|
-
secure: process.env.NODE_ENV === "production",
|
|
668
|
-
sameSite: "lax",
|
|
669
|
-
maxAge: getSessionDuration(),
|
|
670
|
-
path: "/",
|
|
671
|
-
});
|
|
672
|
-
return res.json({
|
|
673
|
-
success: true,
|
|
674
|
-
user: { id: user.id, email: user.email, name: user.name, role: user.role },
|
|
675
|
-
});
|
|
846
|
+
return finalizeStudioSignIn(res, user);
|
|
676
847
|
}
|
|
677
848
|
catch (error) {
|
|
678
849
|
console.error("Sign-in error:", error);
|
|
@@ -1230,6 +1401,256 @@ export function createRoutes(authConfig, configPath, geoDbPath, preloadedAdapter
|
|
|
1230
1401
|
res.status(500).json({ error: "Failed to fetch counts" });
|
|
1231
1402
|
}
|
|
1232
1403
|
});
|
|
1404
|
+
// Dashboard widget APIs
|
|
1405
|
+
router.get("/api/dashboard/recent-users", async (req, res) => {
|
|
1406
|
+
try {
|
|
1407
|
+
const hoursParam = req.query.hours;
|
|
1408
|
+
const hours = Math.min(8760, Math.max(1, parseInt(hoursParam || "24", 10) || 24));
|
|
1409
|
+
const adapter = await getAuthAdapterWithConfig();
|
|
1410
|
+
if (!adapter || !adapter.findMany) {
|
|
1411
|
+
return res.json({ users: [], total: 0 });
|
|
1412
|
+
}
|
|
1413
|
+
const users = await adapter.findMany({ model: "user", limit: 500 }).catch(() => []);
|
|
1414
|
+
const since = new Date(Date.now() - hours * 60 * 60 * 1000);
|
|
1415
|
+
const recent = (users || []).filter((u) => {
|
|
1416
|
+
const createdAt = u.createdAt ? new Date(u.createdAt) : null;
|
|
1417
|
+
return createdAt && createdAt >= since;
|
|
1418
|
+
});
|
|
1419
|
+
const sorted = recent.sort((a, b) => {
|
|
1420
|
+
const ta = new Date(a.createdAt || 0).getTime();
|
|
1421
|
+
const tb = new Date(b.createdAt || 0).getTime();
|
|
1422
|
+
return tb - ta;
|
|
1423
|
+
});
|
|
1424
|
+
const out = sorted.slice(0, 50);
|
|
1425
|
+
// Enrich with country from latest session IP
|
|
1426
|
+
try {
|
|
1427
|
+
const sessions = await adapter.findMany({ model: "session", limit: 10000 }).catch(() => []);
|
|
1428
|
+
const userIdToCountry = new Map();
|
|
1429
|
+
const byUser = new Map();
|
|
1430
|
+
for (const s of sessions || []) {
|
|
1431
|
+
const uid = s.userId || s.user_id;
|
|
1432
|
+
if (!uid)
|
|
1433
|
+
continue;
|
|
1434
|
+
if (!byUser.has(uid))
|
|
1435
|
+
byUser.set(uid, []);
|
|
1436
|
+
byUser.get(uid).push(s);
|
|
1437
|
+
}
|
|
1438
|
+
for (const [uid, userSessions] of byUser) {
|
|
1439
|
+
const sortedSessions = userSessions.sort((a, b) => new Date(b.createdAt || 0).getTime() - new Date(a.createdAt || 0).getTime());
|
|
1440
|
+
const latest = sortedSessions[0];
|
|
1441
|
+
const ip = latest.ipAddress || latest.ip_address;
|
|
1442
|
+
if (ip && ip !== "Unknown") {
|
|
1443
|
+
const loc = resolveIPLocation(ip);
|
|
1444
|
+
if (loc)
|
|
1445
|
+
userIdToCountry.set(uid, {
|
|
1446
|
+
country: loc.country || loc.countryCode || "—",
|
|
1447
|
+
countryCode: loc.countryCode || "",
|
|
1448
|
+
});
|
|
1449
|
+
}
|
|
1450
|
+
}
|
|
1451
|
+
for (const u of out) {
|
|
1452
|
+
const geo = userIdToCountry.get(u.id);
|
|
1453
|
+
u.country = geo?.country || "—";
|
|
1454
|
+
u.countryCode = geo?.countryCode || "";
|
|
1455
|
+
}
|
|
1456
|
+
}
|
|
1457
|
+
catch (_e) { }
|
|
1458
|
+
res.json({ users: out, total: recent.length });
|
|
1459
|
+
}
|
|
1460
|
+
catch (_error) {
|
|
1461
|
+
res.status(500).json({ error: "Failed to fetch recent users", users: [], total: 0 });
|
|
1462
|
+
}
|
|
1463
|
+
});
|
|
1464
|
+
router.get("/api/dashboard/recent-organizations", async (_req, res) => {
|
|
1465
|
+
try {
|
|
1466
|
+
const adapter = await getAuthAdapterWithConfig();
|
|
1467
|
+
if (!adapter || !adapter.findMany) {
|
|
1468
|
+
return res.json({ organizations: [], total: 0 });
|
|
1469
|
+
}
|
|
1470
|
+
const orgs = await adapter.findMany({ model: "organization", limit: 100 }).catch(() => []);
|
|
1471
|
+
const list = (orgs || []).sort((a, b) => {
|
|
1472
|
+
const ta = new Date(a.createdAt || 0).getTime();
|
|
1473
|
+
const tb = new Date(b.createdAt || 0).getTime();
|
|
1474
|
+
return tb - ta;
|
|
1475
|
+
});
|
|
1476
|
+
let withCount = list.slice(0, 20);
|
|
1477
|
+
if (adapter.findMany && typeof adapter.findMany === "function") {
|
|
1478
|
+
const memberCounts = await Promise.all(withCount.map(async (o) => {
|
|
1479
|
+
try {
|
|
1480
|
+
const members = await adapter.findMany({
|
|
1481
|
+
model: "member",
|
|
1482
|
+
where: [{ field: "organizationId", value: o.id }],
|
|
1483
|
+
limit: 1000,
|
|
1484
|
+
});
|
|
1485
|
+
return { id: o.id, count: Array.isArray(members) ? members.length : 0 };
|
|
1486
|
+
}
|
|
1487
|
+
catch {
|
|
1488
|
+
return { id: o.id, count: 0 };
|
|
1489
|
+
}
|
|
1490
|
+
}));
|
|
1491
|
+
const countMap = new Map(memberCounts.map((c) => [c.id, c.count]));
|
|
1492
|
+
withCount = withCount.map((o) => ({ ...o, memberCount: countMap.get(o.id) ?? 0 }));
|
|
1493
|
+
}
|
|
1494
|
+
res.json({ organizations: withCount, total: list.length });
|
|
1495
|
+
}
|
|
1496
|
+
catch (_error) {
|
|
1497
|
+
res.json({ organizations: [], total: 0 });
|
|
1498
|
+
}
|
|
1499
|
+
});
|
|
1500
|
+
router.get("/api/dashboard/recent-teams", async (_req, res) => {
|
|
1501
|
+
try {
|
|
1502
|
+
const adapter = await getAuthAdapterWithConfig();
|
|
1503
|
+
if (!adapter || !adapter.findMany) {
|
|
1504
|
+
return res.json({ teams: [], total: 0 });
|
|
1505
|
+
}
|
|
1506
|
+
const teams = await adapter.findMany({ model: "team", limit: 100 }).catch(() => []);
|
|
1507
|
+
const list = (teams || []).sort((a, b) => {
|
|
1508
|
+
const ta = new Date(a.createdAt || 0).getTime();
|
|
1509
|
+
const tb = new Date(b.createdAt || 0).getTime();
|
|
1510
|
+
return tb - ta;
|
|
1511
|
+
});
|
|
1512
|
+
const out = list.slice(0, 20).map((t) => ({
|
|
1513
|
+
id: t.id,
|
|
1514
|
+
name: t.name,
|
|
1515
|
+
slug: t.slug,
|
|
1516
|
+
organizationId: t.organizationId,
|
|
1517
|
+
createdAt: t.createdAt,
|
|
1518
|
+
}));
|
|
1519
|
+
res.json({ teams: out, total: list.length });
|
|
1520
|
+
}
|
|
1521
|
+
catch (_error) {
|
|
1522
|
+
res.json({ teams: [], total: 0 });
|
|
1523
|
+
}
|
|
1524
|
+
});
|
|
1525
|
+
router.get("/api/dashboard/invitations", async (_req, res) => {
|
|
1526
|
+
try {
|
|
1527
|
+
const adapter = await getAuthAdapterWithConfig();
|
|
1528
|
+
if (!adapter || !adapter.findMany) {
|
|
1529
|
+
return res.json({ invitations: [], total: 0 });
|
|
1530
|
+
}
|
|
1531
|
+
const invitations = await adapter
|
|
1532
|
+
.findMany({
|
|
1533
|
+
model: "invitation",
|
|
1534
|
+
where: [{ field: "status", value: "pending" }],
|
|
1535
|
+
limit: 200,
|
|
1536
|
+
})
|
|
1537
|
+
.catch(() => []);
|
|
1538
|
+
const list = Array.isArray(invitations) ? invitations : [];
|
|
1539
|
+
res.json({ invitations: list, total: list.length });
|
|
1540
|
+
}
|
|
1541
|
+
catch (_error) {
|
|
1542
|
+
res.json({ invitations: [], total: 0 });
|
|
1543
|
+
}
|
|
1544
|
+
});
|
|
1545
|
+
router.get("/api/dashboard/geo-distribution", async (_req, res) => {
|
|
1546
|
+
try {
|
|
1547
|
+
const adapter = await getAuthAdapterWithConfig();
|
|
1548
|
+
if (!adapter || !adapter.findMany) {
|
|
1549
|
+
return res.json({ distribution: [], totalUnique: 0 });
|
|
1550
|
+
}
|
|
1551
|
+
const sessions = await adapter.findMany({ model: "session", limit: 50000 }).catch(() => []);
|
|
1552
|
+
const byCountry = new Map();
|
|
1553
|
+
for (const session of sessions || []) {
|
|
1554
|
+
const ip = session.ipAddress || session.ip_address;
|
|
1555
|
+
if (!ip || ip === "Unknown")
|
|
1556
|
+
continue;
|
|
1557
|
+
const loc = resolveIPLocation(ip);
|
|
1558
|
+
if (!loc)
|
|
1559
|
+
continue;
|
|
1560
|
+
const key = loc.countryCode || loc.country || "XX";
|
|
1561
|
+
const existing = byCountry.get(key);
|
|
1562
|
+
const userId = session.userId || session.user_id;
|
|
1563
|
+
if (existing) {
|
|
1564
|
+
if (userId)
|
|
1565
|
+
existing.userIds.add(userId);
|
|
1566
|
+
}
|
|
1567
|
+
else {
|
|
1568
|
+
const userIds = new Set();
|
|
1569
|
+
if (userId)
|
|
1570
|
+
userIds.add(userId);
|
|
1571
|
+
byCountry.set(key, {
|
|
1572
|
+
country: loc.country,
|
|
1573
|
+
countryCode: loc.countryCode,
|
|
1574
|
+
count: userIds.size,
|
|
1575
|
+
userIds,
|
|
1576
|
+
});
|
|
1577
|
+
}
|
|
1578
|
+
}
|
|
1579
|
+
for (const [, entry] of byCountry) {
|
|
1580
|
+
entry.count = entry.userIds.size;
|
|
1581
|
+
}
|
|
1582
|
+
const distribution = Array.from(byCountry.entries()).map(([code, v]) => ({
|
|
1583
|
+
countryCode: code,
|
|
1584
|
+
country: v.country,
|
|
1585
|
+
uniqueUsers: v.count,
|
|
1586
|
+
}));
|
|
1587
|
+
distribution.sort((a, b) => b.uniqueUsers - a.uniqueUsers);
|
|
1588
|
+
const totalUnique = new Set((sessions || []).map((s) => s.userId || s.user_id).filter(Boolean)).size;
|
|
1589
|
+
res.json({ distribution, totalUnique });
|
|
1590
|
+
}
|
|
1591
|
+
catch (_error) {
|
|
1592
|
+
res.status(500).json({ distribution: [], totalUnique: 0 });
|
|
1593
|
+
}
|
|
1594
|
+
});
|
|
1595
|
+
router.get("/api/dashboard/geo-country-details", async (req, res) => {
|
|
1596
|
+
try {
|
|
1597
|
+
const countryCode = req.query.code || "";
|
|
1598
|
+
if (!countryCode)
|
|
1599
|
+
return res.json({ users: [], sessions: [] });
|
|
1600
|
+
const adapter = await getAuthAdapterWithConfig();
|
|
1601
|
+
if (!adapter || !adapter.findMany)
|
|
1602
|
+
return res.json({ users: [], sessions: [] });
|
|
1603
|
+
const sessions = await adapter.findMany({ model: "session", limit: 50000 }).catch(() => []);
|
|
1604
|
+
const matchingSessions = [];
|
|
1605
|
+
const matchingUserIds = new Set();
|
|
1606
|
+
for (const s of sessions || []) {
|
|
1607
|
+
const ip = s.ipAddress || s.ip_address;
|
|
1608
|
+
if (!ip || ip === "Unknown")
|
|
1609
|
+
continue;
|
|
1610
|
+
const loc = resolveIPLocation(ip);
|
|
1611
|
+
if (!loc)
|
|
1612
|
+
continue;
|
|
1613
|
+
const code = loc.countryCode || "";
|
|
1614
|
+
if (code.toUpperCase() !== countryCode.toUpperCase())
|
|
1615
|
+
continue;
|
|
1616
|
+
const userId = s.userId || s.user_id;
|
|
1617
|
+
if (userId)
|
|
1618
|
+
matchingUserIds.add(userId);
|
|
1619
|
+
matchingSessions.push({
|
|
1620
|
+
id: s.id,
|
|
1621
|
+
userId,
|
|
1622
|
+
ipAddress: ip,
|
|
1623
|
+
userAgent: s.userAgent || s.user_agent || null,
|
|
1624
|
+
createdAt: s.createdAt || s.created_at || null,
|
|
1625
|
+
expiresAt: s.expiresAt || s.expires_at || null,
|
|
1626
|
+
city: loc.city,
|
|
1627
|
+
region: loc.region,
|
|
1628
|
+
country: loc.country,
|
|
1629
|
+
countryCode: loc.countryCode,
|
|
1630
|
+
});
|
|
1631
|
+
}
|
|
1632
|
+
const allUsers = await adapter.findMany({ model: "user", limit: 100000 }).catch(() => []);
|
|
1633
|
+
const matchingUsers = (allUsers || [])
|
|
1634
|
+
.filter((u) => matchingUserIds.has(u.id))
|
|
1635
|
+
.map((u) => ({
|
|
1636
|
+
id: u.id,
|
|
1637
|
+
name: u.name || null,
|
|
1638
|
+
email: u.email || null,
|
|
1639
|
+
image: u.image || null,
|
|
1640
|
+
createdAt: u.createdAt || u.created_at || null,
|
|
1641
|
+
}));
|
|
1642
|
+
matchingSessions.sort((a, b) => new Date(b.createdAt || 0).getTime() - new Date(a.createdAt || 0).getTime());
|
|
1643
|
+
res.json({
|
|
1644
|
+
countryCode,
|
|
1645
|
+
users: matchingUsers,
|
|
1646
|
+
sessions: matchingSessions.slice(0, 100),
|
|
1647
|
+
totalSessions: matchingSessions.length,
|
|
1648
|
+
});
|
|
1649
|
+
}
|
|
1650
|
+
catch (_error) {
|
|
1651
|
+
res.status(500).json({ users: [], sessions: [], totalSessions: 0 });
|
|
1652
|
+
}
|
|
1653
|
+
});
|
|
1233
1654
|
router.get("/api/users/all", async (_req, res) => {
|
|
1234
1655
|
try {
|
|
1235
1656
|
const adapter = await getAuthAdapterWithConfig();
|
|
@@ -1376,6 +1797,12 @@ export function createRoutes(authConfig, configPath, geoDbPath, preloadedAdapter
|
|
|
1376
1797
|
router.delete("/api/users/:userId", async (req, res) => {
|
|
1377
1798
|
try {
|
|
1378
1799
|
const { userId } = req.params;
|
|
1800
|
+
const session = req.studioSession;
|
|
1801
|
+
if (session && session.userId && session.userId === userId) {
|
|
1802
|
+
return res
|
|
1803
|
+
.status(400)
|
|
1804
|
+
.json({ success: false, error: "You cannot delete the currently authenticated user" });
|
|
1805
|
+
}
|
|
1379
1806
|
const adapter = await getAuthAdapterWithConfig();
|
|
1380
1807
|
if (!adapter || !adapter.delete) {
|
|
1381
1808
|
return res.status(500).json({ error: "Auth adapter not available" });
|
|
@@ -1408,7 +1835,7 @@ export function createRoutes(authConfig, configPath, geoDbPath, preloadedAdapter
|
|
|
1408
1835
|
id: organization.id,
|
|
1409
1836
|
name: organization.name || "Unknown Organization",
|
|
1410
1837
|
slug: organization.slug || "unknown",
|
|
1411
|
-
|
|
1838
|
+
logo: organization.logo,
|
|
1412
1839
|
createdAt: organization.createdAt,
|
|
1413
1840
|
}
|
|
1414
1841
|
: {
|
|
@@ -1601,17 +2028,19 @@ export function createRoutes(authConfig, configPath, geoDbPath, preloadedAdapter
|
|
|
1601
2028
|
model: "session",
|
|
1602
2029
|
limit: 10000,
|
|
1603
2030
|
});
|
|
1604
|
-
const userSessions = sessions.filter((session) => session.userId === userId);
|
|
2031
|
+
const userSessions = sessions.filter((session) => (session.userId ?? session.user_id) === userId);
|
|
1605
2032
|
const formattedSessions = userSessions.map((session) => ({
|
|
1606
|
-
id: session.id,
|
|
1607
|
-
token: session.token,
|
|
1608
|
-
expiresAt: session.expiresAt,
|
|
1609
|
-
ipAddress: session.ipAddress
|
|
1610
|
-
userAgent: session.userAgent
|
|
1611
|
-
activeOrganizationId: session.activeOrganizationId
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
2033
|
+
id: session.id ?? session.session_id,
|
|
2034
|
+
token: session.token ?? session.session_token,
|
|
2035
|
+
expiresAt: session.expiresAt ?? session.expires_at ?? session.expires,
|
|
2036
|
+
ipAddress: session.ipAddress ?? session.ip_address ?? session.ip ?? "Unknown",
|
|
2037
|
+
userAgent: session.userAgent ?? session.user_agent ?? null,
|
|
2038
|
+
activeOrganizationId: session.activeOrganizationId ??
|
|
2039
|
+
session.active_organization_id ??
|
|
2040
|
+
session.active_organization,
|
|
2041
|
+
activeTeamId: session.activeTeamId ?? session.active_team_id ?? session.active_team,
|
|
2042
|
+
createdAt: session.createdAt ?? session.created_at,
|
|
2043
|
+
updatedAt: session.updatedAt ?? session.updated_at,
|
|
1615
2044
|
}));
|
|
1616
2045
|
res.json({ sessions: formattedSessions });
|
|
1617
2046
|
}
|
|
@@ -1672,6 +2101,7 @@ export function createRoutes(authConfig, configPath, geoDbPath, preloadedAdapter
|
|
|
1672
2101
|
? {
|
|
1673
2102
|
id: organization.id,
|
|
1674
2103
|
name: organization.name,
|
|
2104
|
+
logo: organization.logo,
|
|
1675
2105
|
}
|
|
1676
2106
|
: null,
|
|
1677
2107
|
};
|
|
@@ -1701,6 +2131,7 @@ export function createRoutes(authConfig, configPath, geoDbPath, preloadedAdapter
|
|
|
1701
2131
|
id: organization.id,
|
|
1702
2132
|
name: organization.name,
|
|
1703
2133
|
slug: organization.slug,
|
|
2134
|
+
logo: organization.logo,
|
|
1704
2135
|
metadata: organization.metadata,
|
|
1705
2136
|
createdAt: organization.createdAt,
|
|
1706
2137
|
updatedAt: organization.updatedAt,
|
|
@@ -1823,22 +2254,45 @@ export function createRoutes(authConfig, configPath, geoDbPath, preloadedAdapter
|
|
|
1823
2254
|
}
|
|
1824
2255
|
return event;
|
|
1825
2256
|
});
|
|
2257
|
+
// Fetch total count for this query (unfiltered by pagination)
|
|
2258
|
+
let total = null;
|
|
2259
|
+
try {
|
|
2260
|
+
if (userId || type) {
|
|
2261
|
+
const countResult = await eventProvider.query({
|
|
2262
|
+
limit: 100000,
|
|
2263
|
+
sort: "desc",
|
|
2264
|
+
userId,
|
|
2265
|
+
type,
|
|
2266
|
+
since,
|
|
2267
|
+
});
|
|
2268
|
+
total = countResult.events?.length ?? null;
|
|
2269
|
+
}
|
|
2270
|
+
else if (eventProvider.count) {
|
|
2271
|
+
total = await eventProvider.count();
|
|
2272
|
+
}
|
|
2273
|
+
else if (eventProvider.getStats) {
|
|
2274
|
+
const stats = await eventProvider.getStats();
|
|
2275
|
+
total = stats.total;
|
|
2276
|
+
}
|
|
2277
|
+
}
|
|
2278
|
+
catch {
|
|
2279
|
+
// Count is best-effort, don't fail the main request
|
|
2280
|
+
}
|
|
1826
2281
|
return res.json({
|
|
1827
2282
|
events: transformedEvents,
|
|
1828
2283
|
hasMore: result.hasMore,
|
|
1829
2284
|
nextCursor: result.nextCursor,
|
|
2285
|
+
total,
|
|
1830
2286
|
});
|
|
1831
2287
|
}
|
|
1832
2288
|
catch (providerError) {
|
|
1833
|
-
const isSchemaError = providerError
|
|
1834
|
-
providerError?.message?.includes("Model") ||
|
|
1835
|
-
providerError?.code === "P2025" ||
|
|
1836
|
-
providerError?.code === "42P01";
|
|
2289
|
+
const isSchemaError = isEventModelLookupError(providerError);
|
|
1837
2290
|
if (isSchemaError) {
|
|
1838
|
-
return res.status(
|
|
1839
|
-
|
|
1840
|
-
|
|
1841
|
-
|
|
2291
|
+
return res.status(200).json({
|
|
2292
|
+
events: [],
|
|
2293
|
+
hasMore: false,
|
|
2294
|
+
total: 0,
|
|
2295
|
+
ready: false,
|
|
1842
2296
|
});
|
|
1843
2297
|
}
|
|
1844
2298
|
console.error("Event provider query failed:", providerError);
|
|
@@ -1856,8 +2310,8 @@ export function createRoutes(authConfig, configPath, geoDbPath, preloadedAdapter
|
|
|
1856
2310
|
return res.status(500).json({ error: "Event provider or adapter not available" });
|
|
1857
2311
|
}
|
|
1858
2312
|
const where = [];
|
|
1859
|
-
//
|
|
1860
|
-
if (after) {
|
|
2313
|
+
// Use cursor-based pagination only when offset is not provided
|
|
2314
|
+
if (offset == null && after) {
|
|
1861
2315
|
if (sort === "desc") {
|
|
1862
2316
|
where.push({ field: "id", operator: "<", value: after });
|
|
1863
2317
|
}
|
|
@@ -1874,22 +2328,23 @@ export function createRoutes(authConfig, configPath, geoDbPath, preloadedAdapter
|
|
|
1874
2328
|
let events = [];
|
|
1875
2329
|
if (adapter.findMany) {
|
|
1876
2330
|
try {
|
|
1877
|
-
|
|
1878
|
-
model: "auth_events",
|
|
2331
|
+
const findManyOptions = {
|
|
1879
2332
|
where,
|
|
1880
2333
|
orderBy: [{ field: "timestamp", direction: sort === "desc" ? "desc" : "asc" }],
|
|
1881
|
-
limit: limit + 1,
|
|
1882
|
-
}
|
|
2334
|
+
limit: limit + 1,
|
|
2335
|
+
};
|
|
2336
|
+
if (offset != null && offset > 0) {
|
|
2337
|
+
findManyOptions.offset = offset;
|
|
2338
|
+
}
|
|
2339
|
+
events = await findManyEventRows(adapter, findManyOptions);
|
|
1883
2340
|
}
|
|
1884
2341
|
catch (adapterError) {
|
|
1885
|
-
if (adapterError
|
|
1886
|
-
|
|
1887
|
-
|
|
1888
|
-
|
|
1889
|
-
|
|
1890
|
-
|
|
1891
|
-
retryable: true,
|
|
1892
|
-
details: adapterError?.message || String(adapterError),
|
|
2342
|
+
if (isEventModelLookupError(adapterError)) {
|
|
2343
|
+
return res.status(200).json({
|
|
2344
|
+
events: [],
|
|
2345
|
+
hasMore: false,
|
|
2346
|
+
total: 0,
|
|
2347
|
+
ready: false,
|
|
1893
2348
|
});
|
|
1894
2349
|
}
|
|
1895
2350
|
throw adapterError;
|
|
@@ -1939,22 +2394,40 @@ export function createRoutes(authConfig, configPath, geoDbPath, preloadedAdapter
|
|
|
1939
2394
|
},
|
|
1940
2395
|
};
|
|
1941
2396
|
});
|
|
2397
|
+
// Fetch total count for the adapter path
|
|
2398
|
+
let total = null;
|
|
2399
|
+
try {
|
|
2400
|
+
if (adapter.findMany) {
|
|
2401
|
+
const countWhere = [];
|
|
2402
|
+
if (type)
|
|
2403
|
+
countWhere.push({ field: "type", value: type });
|
|
2404
|
+
if (userId)
|
|
2405
|
+
countWhere.push({ field: "userId", value: userId });
|
|
2406
|
+
const allForCount = await findManyEventRows(adapter, {
|
|
2407
|
+
where: countWhere.length > 0 ? countWhere : undefined,
|
|
2408
|
+
limit: 100000,
|
|
2409
|
+
});
|
|
2410
|
+
total = Array.isArray(allForCount) ? allForCount.length : null;
|
|
2411
|
+
}
|
|
2412
|
+
}
|
|
2413
|
+
catch {
|
|
2414
|
+
// Count is best-effort
|
|
2415
|
+
}
|
|
1942
2416
|
res.json({
|
|
1943
2417
|
events: transformedEvents,
|
|
1944
2418
|
hasMore,
|
|
1945
2419
|
nextCursor: hasMore ? transformedEvents[transformedEvents.length - 1].id : null,
|
|
2420
|
+
total,
|
|
1946
2421
|
});
|
|
1947
2422
|
}
|
|
1948
2423
|
catch (error) {
|
|
1949
2424
|
const msg = error instanceof Error ? error.message : String(error);
|
|
1950
|
-
if (msg?.includes("not found in schema")
|
|
1951
|
-
|
|
1952
|
-
|
|
1953
|
-
|
|
1954
|
-
|
|
1955
|
-
|
|
1956
|
-
retryable: true,
|
|
1957
|
-
details: msg,
|
|
2425
|
+
if (isEventModelLookupError(error) || msg?.includes("not found in schema")) {
|
|
2426
|
+
return res.status(200).json({
|
|
2427
|
+
events: [],
|
|
2428
|
+
hasMore: false,
|
|
2429
|
+
total: 0,
|
|
2430
|
+
ready: false,
|
|
1958
2431
|
});
|
|
1959
2432
|
}
|
|
1960
2433
|
console.error("Failed to fetch events:", error);
|
|
@@ -1964,8 +2437,9 @@ export function createRoutes(authConfig, configPath, geoDbPath, preloadedAdapter
|
|
|
1964
2437
|
});
|
|
1965
2438
|
}
|
|
1966
2439
|
});
|
|
1967
|
-
router.get("/api/events/count", async (
|
|
2440
|
+
router.get("/api/events/count", async (req, res) => {
|
|
1968
2441
|
try {
|
|
2442
|
+
const userIdFilter = req.query.userId;
|
|
1969
2443
|
const { getEventIngestionProvider, isEventIngestionInitialized } = await import("./utils/event-ingestion.js");
|
|
1970
2444
|
if (!isEventIngestionInitialized()) {
|
|
1971
2445
|
try {
|
|
@@ -2025,10 +2499,51 @@ export function createRoutes(authConfig, configPath, geoDbPath, preloadedAdapter
|
|
|
2025
2499
|
}
|
|
2026
2500
|
}
|
|
2027
2501
|
const eventProvider = getEventIngestionProvider();
|
|
2028
|
-
const isSchemaError = (e) => e
|
|
2029
|
-
|
|
2030
|
-
|
|
2031
|
-
|
|
2502
|
+
const isSchemaError = (e) => isEventModelLookupError(e);
|
|
2503
|
+
// When filtering by userId, use query() to get filtered count
|
|
2504
|
+
if (userIdFilter) {
|
|
2505
|
+
if (eventProvider?.query) {
|
|
2506
|
+
try {
|
|
2507
|
+
const result = await eventProvider.query({
|
|
2508
|
+
limit: 100000,
|
|
2509
|
+
sort: "desc",
|
|
2510
|
+
userId: userIdFilter,
|
|
2511
|
+
});
|
|
2512
|
+
const events = result.events ?? [];
|
|
2513
|
+
const total = events.length;
|
|
2514
|
+
const failed = events.filter((e) => e.status === "failed" || e.display?.severity === "failed").length;
|
|
2515
|
+
const warning = events.filter((e) => e.display?.severity === "warning").length;
|
|
2516
|
+
const info = events.filter((e) => e.display?.severity === "info" || (!e.display?.severity && e.status !== "failed")).length;
|
|
2517
|
+
const success = total - failed - warning - info;
|
|
2518
|
+
return res.json({ total, success, failed, warning, info });
|
|
2519
|
+
}
|
|
2520
|
+
catch (e) {
|
|
2521
|
+
if (isSchemaError(e)) {
|
|
2522
|
+
return res.json({ total: 0, success: 0, failed: 0, warning: 0, info: 0 });
|
|
2523
|
+
}
|
|
2524
|
+
throw e;
|
|
2525
|
+
}
|
|
2526
|
+
}
|
|
2527
|
+
const adapter = await getAuthAdapterWithConfig();
|
|
2528
|
+
if (adapter?.findMany) {
|
|
2529
|
+
try {
|
|
2530
|
+
const events = await findManyEventRows(adapter, {
|
|
2531
|
+
where: [{ field: "userId", value: userIdFilter }],
|
|
2532
|
+
limit: 100000,
|
|
2533
|
+
});
|
|
2534
|
+
const list = Array.isArray(events) ? events : [];
|
|
2535
|
+
const total = list.length;
|
|
2536
|
+
return res.json({ total, success: null, failed: null, warning: null, info: null });
|
|
2537
|
+
}
|
|
2538
|
+
catch (e) {
|
|
2539
|
+
if (isSchemaError(e)) {
|
|
2540
|
+
return res.json({ total: 0, success: 0, failed: 0, warning: 0, info: 0 });
|
|
2541
|
+
}
|
|
2542
|
+
throw e;
|
|
2543
|
+
}
|
|
2544
|
+
}
|
|
2545
|
+
return res.json({ total: 0, success: 0, failed: 0, warning: 0, info: 0 });
|
|
2546
|
+
}
|
|
2032
2547
|
if (eventProvider?.getStats) {
|
|
2033
2548
|
try {
|
|
2034
2549
|
const stats = await eventProvider.getStats();
|
|
@@ -2036,11 +2551,7 @@ export function createRoutes(authConfig, configPath, geoDbPath, preloadedAdapter
|
|
|
2036
2551
|
}
|
|
2037
2552
|
catch (e) {
|
|
2038
2553
|
if (isSchemaError(e)) {
|
|
2039
|
-
return res.
|
|
2040
|
-
error: "Events not ready",
|
|
2041
|
-
retryable: true,
|
|
2042
|
-
details: e?.message || String(e),
|
|
2043
|
-
});
|
|
2554
|
+
return res.json({ total: 0, success: 0, failed: 0, warning: 0, info: 0 });
|
|
2044
2555
|
}
|
|
2045
2556
|
throw e;
|
|
2046
2557
|
}
|
|
@@ -2052,11 +2563,7 @@ export function createRoutes(authConfig, configPath, geoDbPath, preloadedAdapter
|
|
|
2052
2563
|
}
|
|
2053
2564
|
catch (e) {
|
|
2054
2565
|
if (isSchemaError(e)) {
|
|
2055
|
-
return res.
|
|
2056
|
-
error: "Events not ready",
|
|
2057
|
-
retryable: true,
|
|
2058
|
-
details: e?.message || String(e),
|
|
2059
|
-
});
|
|
2566
|
+
return res.json({ total: 0, success: 0, failed: 0, warning: 0, info: 0 });
|
|
2060
2567
|
}
|
|
2061
2568
|
throw e;
|
|
2062
2569
|
}
|
|
@@ -2074,11 +2581,7 @@ export function createRoutes(authConfig, configPath, geoDbPath, preloadedAdapter
|
|
|
2074
2581
|
}
|
|
2075
2582
|
catch (e) {
|
|
2076
2583
|
if (isSchemaError(e)) {
|
|
2077
|
-
return res.
|
|
2078
|
-
error: "Events not ready",
|
|
2079
|
-
retryable: true,
|
|
2080
|
-
details: e?.message || String(e),
|
|
2081
|
-
});
|
|
2584
|
+
return res.json({ total: 0, success: 0, failed: 0, warning: 0, info: 0 });
|
|
2082
2585
|
}
|
|
2083
2586
|
throw e;
|
|
2084
2587
|
}
|
|
@@ -2086,8 +2589,7 @@ export function createRoutes(authConfig, configPath, geoDbPath, preloadedAdapter
|
|
|
2086
2589
|
const adapter = await getAuthAdapterWithConfig();
|
|
2087
2590
|
if (adapter?.findMany) {
|
|
2088
2591
|
try {
|
|
2089
|
-
const events = await adapter
|
|
2090
|
-
model: "auth_events",
|
|
2592
|
+
const events = await findManyEventRows(adapter, {
|
|
2091
2593
|
limit: 100000,
|
|
2092
2594
|
});
|
|
2093
2595
|
const list = Array.isArray(events) ? events : [];
|
|
@@ -2103,11 +2605,7 @@ export function createRoutes(authConfig, configPath, geoDbPath, preloadedAdapter
|
|
|
2103
2605
|
}
|
|
2104
2606
|
catch (e) {
|
|
2105
2607
|
if (isSchemaError(e)) {
|
|
2106
|
-
return res.
|
|
2107
|
-
error: "Events not ready",
|
|
2108
|
-
retryable: true,
|
|
2109
|
-
details: e?.message || String(e),
|
|
2110
|
-
});
|
|
2608
|
+
return res.json({ total: 0, success: 0, failed: 0, warning: 0, info: 0 });
|
|
2111
2609
|
}
|
|
2112
2610
|
throw e;
|
|
2113
2611
|
}
|
|
@@ -2120,11 +2618,7 @@ export function createRoutes(authConfig, configPath, geoDbPath, preloadedAdapter
|
|
|
2120
2618
|
msg?.includes("Model") ||
|
|
2121
2619
|
error?.code === "P2025" ||
|
|
2122
2620
|
error?.code === "42P01") {
|
|
2123
|
-
return res.
|
|
2124
|
-
error: "Events not ready",
|
|
2125
|
-
retryable: true,
|
|
2126
|
-
details: msg,
|
|
2127
|
-
});
|
|
2621
|
+
return res.json({ total: 0, success: 0, failed: 0, warning: 0, info: 0 });
|
|
2128
2622
|
}
|
|
2129
2623
|
console.error("Failed to get events count:", error);
|
|
2130
2624
|
res.status(500).json({
|
|
@@ -2221,8 +2715,14 @@ export function createRoutes(authConfig, configPath, geoDbPath, preloadedAdapter
|
|
|
2221
2715
|
try {
|
|
2222
2716
|
const page = parseInt(req.query.page, 10) || 1;
|
|
2223
2717
|
const limit = parseInt(req.query.limit, 10) || 20;
|
|
2224
|
-
const
|
|
2225
|
-
res.json(
|
|
2718
|
+
const result = await getAuthData(authConfig, "sessions", { page, limit }, configPath, preloadedAdapter);
|
|
2719
|
+
res.json({
|
|
2720
|
+
sessions: result.data || [],
|
|
2721
|
+
total: result.total || 0,
|
|
2722
|
+
page: result.page || page,
|
|
2723
|
+
limit: result.limit || limit,
|
|
2724
|
+
totalPages: result.totalPages || 0,
|
|
2725
|
+
});
|
|
2226
2726
|
}
|
|
2227
2727
|
catch (_error) {
|
|
2228
2728
|
res.status(500).json({ error: "Failed to fetch sessions" });
|
|
@@ -2240,6 +2740,12 @@ export function createRoutes(authConfig, configPath, geoDbPath, preloadedAdapter
|
|
|
2240
2740
|
router.delete("/api/users/:id", async (req, res) => {
|
|
2241
2741
|
try {
|
|
2242
2742
|
const { id } = req.params;
|
|
2743
|
+
const session = req.studioSession;
|
|
2744
|
+
if (session && session.userId && session.userId === id) {
|
|
2745
|
+
return res
|
|
2746
|
+
.status(400)
|
|
2747
|
+
.json({ success: false, error: "You cannot delete the currently authenticated user" });
|
|
2748
|
+
}
|
|
2243
2749
|
await getAuthData(authConfig, "deleteUser", { id }, configPath, preloadedAdapter);
|
|
2244
2750
|
res.json({ success: true });
|
|
2245
2751
|
}
|
|
@@ -3015,6 +3521,7 @@ export function createRoutes(authConfig, configPath, geoDbPath, preloadedAdapter
|
|
|
3015
3521
|
});
|
|
3016
3522
|
const table = {
|
|
3017
3523
|
name,
|
|
3524
|
+
contextKey: key,
|
|
3018
3525
|
displayName: formatDisplayName(name),
|
|
3019
3526
|
origin: tableMeta?.plugin?.id ||
|
|
3020
3527
|
tableMeta?.pluginId ||
|
|
@@ -3049,6 +3556,109 @@ export function createRoutes(authConfig, configPath, geoDbPath, preloadedAdapter
|
|
|
3049
3556
|
tableEntries.sort((a, b) => (a.order ?? 0) - (b.order ?? 0));
|
|
3050
3557
|
return { tables: tableEntries };
|
|
3051
3558
|
}
|
|
3559
|
+
function normalizeModelName(value) {
|
|
3560
|
+
if (!value)
|
|
3561
|
+
return null;
|
|
3562
|
+
return value.trim();
|
|
3563
|
+
}
|
|
3564
|
+
function toSnakeCase(value) {
|
|
3565
|
+
return value
|
|
3566
|
+
.replace(/([a-z0-9])([A-Z])/g, "$1_$2")
|
|
3567
|
+
.replace(/[-\s]+/g, "_")
|
|
3568
|
+
.toLowerCase();
|
|
3569
|
+
}
|
|
3570
|
+
function toCamelCase(value) {
|
|
3571
|
+
const normalized = value.replace(/[-\s]+/g, "_");
|
|
3572
|
+
return normalized.replace(/_([a-z0-9])/gi, (_, char) => char.toUpperCase());
|
|
3573
|
+
}
|
|
3574
|
+
function getTableModelCandidates(table) {
|
|
3575
|
+
const names = [table?.name, table?.contextKey]
|
|
3576
|
+
.map((value) => normalizeModelName(value))
|
|
3577
|
+
.filter(Boolean);
|
|
3578
|
+
return Array.from(new Set(names.flatMap((value) => {
|
|
3579
|
+
const snake = toSnakeCase(value);
|
|
3580
|
+
const camel = toCamelCase(value);
|
|
3581
|
+
return [value, snake, camel].filter(Boolean);
|
|
3582
|
+
})));
|
|
3583
|
+
}
|
|
3584
|
+
function isMissingDatabaseModelError(error) {
|
|
3585
|
+
const message = typeof error?.message === "string" ? error.message.toLowerCase() : "";
|
|
3586
|
+
return (error?.code === "P2021" ||
|
|
3587
|
+
error?.code === "P2025" ||
|
|
3588
|
+
error?.code === "42P01" ||
|
|
3589
|
+
message.includes("not found in schema") ||
|
|
3590
|
+
(message.includes("model") && message.includes("not found")) ||
|
|
3591
|
+
(message.includes("relation") && message.includes("does not exist")) ||
|
|
3592
|
+
(message.includes("table") && message.includes("does not exist")) ||
|
|
3593
|
+
message.includes("unknown table") ||
|
|
3594
|
+
message.includes("no such table"));
|
|
3595
|
+
}
|
|
3596
|
+
async function getDatabaseModelCount(adapter, model) {
|
|
3597
|
+
if (typeof adapter?.count === "function") {
|
|
3598
|
+
const count = await adapter.count({ model });
|
|
3599
|
+
return typeof count === "number" ? count : Number(count ?? 0);
|
|
3600
|
+
}
|
|
3601
|
+
if (typeof adapter?.findMany === "function") {
|
|
3602
|
+
const rows = await adapter.findMany({
|
|
3603
|
+
model,
|
|
3604
|
+
limit: 100000,
|
|
3605
|
+
});
|
|
3606
|
+
return Array.isArray(rows) ? rows.length : 0;
|
|
3607
|
+
}
|
|
3608
|
+
return null;
|
|
3609
|
+
}
|
|
3610
|
+
async function resolveAvailableSchemaTables(schema) {
|
|
3611
|
+
const adapter = await getAuthAdapterWithConfig();
|
|
3612
|
+
if (!adapter ||
|
|
3613
|
+
(typeof adapter.findMany !== "function" && typeof adapter.count !== "function")) {
|
|
3614
|
+
return {
|
|
3615
|
+
tables: schema.tables.map((table) => ({
|
|
3616
|
+
...table,
|
|
3617
|
+
model: getTableModelCandidates(table)[0] || table.name,
|
|
3618
|
+
rowCount: null,
|
|
3619
|
+
})),
|
|
3620
|
+
};
|
|
3621
|
+
}
|
|
3622
|
+
const tables = await Promise.all(schema.tables.map(async (table) => {
|
|
3623
|
+
const candidates = getTableModelCandidates(table);
|
|
3624
|
+
for (const model of candidates) {
|
|
3625
|
+
try {
|
|
3626
|
+
const rowCount = await getDatabaseModelCount(adapter, model);
|
|
3627
|
+
return {
|
|
3628
|
+
...table,
|
|
3629
|
+
model,
|
|
3630
|
+
rowCount,
|
|
3631
|
+
};
|
|
3632
|
+
}
|
|
3633
|
+
catch (error) {
|
|
3634
|
+
if (isMissingDatabaseModelError(error)) {
|
|
3635
|
+
continue;
|
|
3636
|
+
}
|
|
3637
|
+
return {
|
|
3638
|
+
...table,
|
|
3639
|
+
model: candidates[0] || table.name,
|
|
3640
|
+
rowCount: null,
|
|
3641
|
+
};
|
|
3642
|
+
}
|
|
3643
|
+
}
|
|
3644
|
+
return null;
|
|
3645
|
+
}));
|
|
3646
|
+
return {
|
|
3647
|
+
tables: tables.filter(Boolean),
|
|
3648
|
+
};
|
|
3649
|
+
}
|
|
3650
|
+
function buildDatabaseSchemaSummary(schema) {
|
|
3651
|
+
const tables = schema.tables || [];
|
|
3652
|
+
const coreTableCount = tables.filter((table) => CONTEXT_CORE_TABLES.has(table.name)).length;
|
|
3653
|
+
const pluginTableCount = tables.filter((table) => !CONTEXT_CORE_TABLES.has(table.name)).length;
|
|
3654
|
+
return {
|
|
3655
|
+
tableCount: tables.length,
|
|
3656
|
+
coreTableCount,
|
|
3657
|
+
pluginTableCount,
|
|
3658
|
+
fieldCount: tables.reduce((sum, table) => sum + (table.fields?.length || 0), 0),
|
|
3659
|
+
relationshipCount: tables.reduce((sum, table) => sum + (table.relationships?.length || 0), 0),
|
|
3660
|
+
};
|
|
3661
|
+
}
|
|
3052
3662
|
async function generateSchemaFromContext(selectedPlugins) {
|
|
3053
3663
|
try {
|
|
3054
3664
|
const tables = await loadContextTables();
|
|
@@ -3073,28 +3683,34 @@ export function createRoutes(authConfig, configPath, geoDbPath, preloadedAdapter
|
|
|
3073
3683
|
return null;
|
|
3074
3684
|
}
|
|
3075
3685
|
}
|
|
3686
|
+
async function generateResolvedDatabaseSchema(selectedPlugins) {
|
|
3687
|
+
const schema = await generateSchemaFromContext(selectedPlugins);
|
|
3688
|
+
if (!schema) {
|
|
3689
|
+
return null;
|
|
3690
|
+
}
|
|
3691
|
+
const resolvedSchema = await resolveAvailableSchemaTables(schema);
|
|
3692
|
+
return {
|
|
3693
|
+
schema: resolvedSchema,
|
|
3694
|
+
summary: buildDatabaseSchemaSummary(resolvedSchema),
|
|
3695
|
+
};
|
|
3696
|
+
}
|
|
3076
3697
|
router.get("/api/database/schema", async (req, res) => {
|
|
3077
3698
|
try {
|
|
3078
|
-
const adapter = await getAuthAdapterWithConfig();
|
|
3079
3699
|
const { plugins } = req.query;
|
|
3080
3700
|
let selectedPlugins = [];
|
|
3081
3701
|
if (plugins && typeof plugins === "string") {
|
|
3082
3702
|
selectedPlugins = plugins.split(",").filter(Boolean);
|
|
3083
3703
|
}
|
|
3084
|
-
|
|
3085
|
-
|
|
3086
|
-
schema: null,
|
|
3087
|
-
error: "Auth adapter not available",
|
|
3088
|
-
});
|
|
3089
|
-
}
|
|
3090
|
-
const schema = await generateSchemaFromContext(selectedPlugins);
|
|
3091
|
-
if (!schema) {
|
|
3704
|
+
const resolvedDatabaseSchema = await generateResolvedDatabaseSchema(selectedPlugins);
|
|
3705
|
+
if (!resolvedDatabaseSchema) {
|
|
3092
3706
|
return res.json({
|
|
3093
3707
|
success: false,
|
|
3094
3708
|
schema: null,
|
|
3709
|
+
summary: null,
|
|
3095
3710
|
error: "Failed to load schema from Better Auth context. Please ensure your auth configuration is properly set up.",
|
|
3096
3711
|
});
|
|
3097
3712
|
}
|
|
3713
|
+
const { schema, summary } = resolvedDatabaseSchema;
|
|
3098
3714
|
// Extract available plugins from schema
|
|
3099
3715
|
const availablePlugins = Array.from(new Set(schema.tables
|
|
3100
3716
|
.map((table) => table.origin)
|
|
@@ -3102,6 +3718,7 @@ export function createRoutes(authConfig, configPath, geoDbPath, preloadedAdapter
|
|
|
3102
3718
|
res.json({
|
|
3103
3719
|
success: true,
|
|
3104
3720
|
schema: schema,
|
|
3721
|
+
summary,
|
|
3105
3722
|
availablePlugins: availablePlugins,
|
|
3106
3723
|
selectedPlugins: selectedPlugins,
|
|
3107
3724
|
});
|
|
@@ -3511,6 +4128,7 @@ export function createRoutes(authConfig, configPath, geoDbPath, preloadedAdapter
|
|
|
3511
4128
|
}
|
|
3512
4129
|
const transformedInvitations = await Promise.all(invitations.map(async (invitation) => {
|
|
3513
4130
|
let organizationName = "Unknown";
|
|
4131
|
+
let organizationLogo;
|
|
3514
4132
|
let teamName;
|
|
3515
4133
|
try {
|
|
3516
4134
|
if (invitation.organizationId &&
|
|
@@ -3522,6 +4140,7 @@ export function createRoutes(authConfig, configPath, geoDbPath, preloadedAdapter
|
|
|
3522
4140
|
where: [{ field: "id", value: invitation.organizationId }],
|
|
3523
4141
|
});
|
|
3524
4142
|
organizationName = org?.name || "Unknown";
|
|
4143
|
+
organizationLogo = org?.logo;
|
|
3525
4144
|
}
|
|
3526
4145
|
catch (_orgError) {
|
|
3527
4146
|
// Ignore org fetch errors
|
|
@@ -3548,6 +4167,7 @@ export function createRoutes(authConfig, configPath, geoDbPath, preloadedAdapter
|
|
|
3548
4167
|
status: invitation.status || "pending",
|
|
3549
4168
|
organizationId: invitation.organizationId,
|
|
3550
4169
|
organizationName,
|
|
4170
|
+
organizationLogo,
|
|
3551
4171
|
teamId: invitation.teamId,
|
|
3552
4172
|
teamName,
|
|
3553
4173
|
inviterId: invitation.inviterId,
|
|
@@ -4631,7 +5251,7 @@ export function createRoutes(authConfig, configPath, geoDbPath, preloadedAdapter
|
|
|
4631
5251
|
const organizationData = {
|
|
4632
5252
|
name: organizationName,
|
|
4633
5253
|
slug: generateSlug(organizationName),
|
|
4634
|
-
|
|
5254
|
+
logo: `https://api.dicebear.com/7.x/identicon/svg?seed=${randomSuffix}`,
|
|
4635
5255
|
createdAt: new Date(),
|
|
4636
5256
|
updatedAt: new Date(),
|
|
4637
5257
|
};
|
|
@@ -4642,7 +5262,7 @@ export function createRoutes(authConfig, configPath, geoDbPath, preloadedAdapter
|
|
|
4642
5262
|
id: organization.id,
|
|
4643
5263
|
name: organization.name,
|
|
4644
5264
|
slug: organization.slug,
|
|
4645
|
-
|
|
5265
|
+
logo: organization.logo,
|
|
4646
5266
|
createdAt: organization.createdAt,
|
|
4647
5267
|
},
|
|
4648
5268
|
});
|
|
@@ -6689,6 +7309,7 @@ export async function handleStudioApiRequest(ctx) {
|
|
|
6689
7309
|
}
|
|
6690
7310
|
catch { }
|
|
6691
7311
|
}
|
|
7312
|
+
const isSelfHosted = !!preloadedAdapter;
|
|
6692
7313
|
const authOptions = ctx.auth?.options || null;
|
|
6693
7314
|
const router = createRoutes(ctx.auth, ctx.configPath || "", undefined, preloadedAdapter, authOptions, ctx.accessConfig, ctx.auth, ctx.studioConfig);
|
|
6694
7315
|
const [pathname, queryString] = ctx.path.split("?");
|
|
@@ -6700,6 +7321,26 @@ export async function handleStudioApiRequest(ctx) {
|
|
|
6700
7321
|
query[key] = decodeURIComponent(value || "");
|
|
6701
7322
|
});
|
|
6702
7323
|
}
|
|
7324
|
+
if (isSelfHosted) {
|
|
7325
|
+
const accessDecision = evaluateRequestAccess({
|
|
7326
|
+
accessConfig: ctx.accessConfig,
|
|
7327
|
+
path: pathname,
|
|
7328
|
+
method: ctx.method,
|
|
7329
|
+
headers: ctx.headers,
|
|
7330
|
+
ip: ctx.ip,
|
|
7331
|
+
});
|
|
7332
|
+
if (!accessDecision.allowed) {
|
|
7333
|
+
return {
|
|
7334
|
+
status: 403,
|
|
7335
|
+
data: {
|
|
7336
|
+
success: false,
|
|
7337
|
+
message: accessDecision.message,
|
|
7338
|
+
reason: accessDecision.reason,
|
|
7339
|
+
...(accessDecision.ipAddress ? { ipAddress: accessDecision.ipAddress } : {}),
|
|
7340
|
+
},
|
|
7341
|
+
};
|
|
7342
|
+
}
|
|
7343
|
+
}
|
|
6703
7344
|
try {
|
|
6704
7345
|
const route = findMatchingRoute(router, pathname, ctx.method);
|
|
6705
7346
|
if (!route) {
|
|
@@ -6723,6 +7364,7 @@ export async function handleStudioApiRequest(ctx) {
|
|
|
6723
7364
|
path: pathname,
|
|
6724
7365
|
originalUrl: ctx.path,
|
|
6725
7366
|
headers: ctx.headers,
|
|
7367
|
+
ip: ctx.ip || extractClientIp(ctx.headers) || undefined,
|
|
6726
7368
|
body: ctx.body,
|
|
6727
7369
|
query: query,
|
|
6728
7370
|
params: route.params,
|
|
@@ -6800,4 +7442,3 @@ function extractParams(routePath, requestPath) {
|
|
|
6800
7442
|
});
|
|
6801
7443
|
return params;
|
|
6802
7444
|
}
|
|
6803
|
-
//# sourceMappingURL=routes.js.map
|