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.
Files changed (196) hide show
  1. package/README.md +145 -16
  2. package/dist/adapters/astro.d.ts +0 -1
  3. package/dist/adapters/astro.js +5 -2
  4. package/dist/adapters/cloudflare-workers.d.ts +101 -0
  5. package/dist/adapters/cloudflare-workers.js +494 -0
  6. package/dist/adapters/elysia.d.ts +0 -1
  7. package/dist/adapters/elysia.js +7 -33
  8. package/dist/adapters/express.d.ts +0 -1
  9. package/dist/adapters/express.js +4 -1
  10. package/dist/adapters/hono.d.ts +0 -1
  11. package/dist/adapters/hono.js +7 -27
  12. package/dist/adapters/nextjs.d.ts +0 -1
  13. package/dist/adapters/nextjs.js +5 -2
  14. package/dist/adapters/nuxt.d.ts +0 -1
  15. package/dist/adapters/nuxt.js +5 -2
  16. package/dist/adapters/remix.d.ts +0 -1
  17. package/dist/adapters/remix.js +5 -2
  18. package/dist/adapters/solid-start.d.ts +0 -1
  19. package/dist/adapters/solid-start.js +5 -2
  20. package/dist/adapters/svelte-kit.d.ts +0 -1
  21. package/dist/adapters/svelte-kit.js +5 -2
  22. package/dist/adapters/tanstack-start.d.ts +0 -1
  23. package/dist/adapters/tanstack-start.js +36 -8
  24. package/dist/add-svelte-kit-env-modules.d.ts +0 -1
  25. package/dist/add-svelte-kit-env-modules.js +0 -1
  26. package/dist/auth-adapter.d.ts +5 -2
  27. package/dist/auth-adapter.js +39 -4
  28. package/dist/cli/commands/init.d.ts +0 -1
  29. package/dist/cli/commands/init.js +0 -1
  30. package/dist/cli.d.ts +0 -1
  31. package/dist/cli.js +6 -1
  32. package/dist/config.d.ts +0 -1
  33. package/dist/config.js +0 -1
  34. package/dist/core/handler.d.ts +0 -1
  35. package/dist/core/handler.js +36 -5
  36. package/dist/data.d.ts +10 -4
  37. package/dist/data.js +40 -6
  38. package/dist/geo-service.d.ts +0 -1
  39. package/dist/geo-service.js +0 -1
  40. package/dist/get-tsconfig-info.d.ts +0 -1
  41. package/dist/get-tsconfig-info.js +0 -1
  42. package/dist/index.d.ts +0 -1
  43. package/dist/index.js +0 -1
  44. package/dist/providers/events/helpers.d.ts +3 -2
  45. package/dist/providers/events/helpers.js +303 -93
  46. package/dist/public/assets/main-C-R_HO98.css +1 -0
  47. package/dist/public/assets/main-DDypFItU.js +1302 -0
  48. package/dist/public/index.html +2 -2
  49. package/dist/public/shaders.png +0 -0
  50. package/dist/routes/api-router.d.ts +2 -3
  51. package/dist/routes/api-router.js +0 -1
  52. package/dist/routes.d.ts +2 -3
  53. package/dist/routes.js +813 -172
  54. package/dist/studio.d.ts +0 -1
  55. package/dist/studio.js +6 -4
  56. package/dist/types/events.d.ts +0 -1
  57. package/dist/types/events.js +0 -1
  58. package/dist/types/handler.d.ts +13 -3
  59. package/dist/types/handler.js +0 -1
  60. package/dist/types/index.d.ts +0 -1
  61. package/dist/types/index.js +0 -1
  62. package/dist/utils/access-rules.d.ts +19 -0
  63. package/dist/utils/access-rules.js +126 -0
  64. package/dist/utils/auth-callbacks-injector.d.ts +0 -1
  65. package/dist/utils/auth-callbacks-injector.js +0 -1
  66. package/dist/utils/database-detection.d.ts +0 -1
  67. package/dist/utils/database-detection.js +33 -4
  68. package/dist/utils/event-ingestion.d.ts +0 -1
  69. package/dist/utils/event-ingestion.js +1 -1
  70. package/dist/utils/hook-injector.d.ts +0 -1
  71. package/dist/utils/hook-injector.js +2 -2
  72. package/dist/utils/html-injector.d.ts +1 -7
  73. package/dist/utils/html-injector.js +15 -1
  74. package/dist/utils/org-hooks-injector.d.ts +0 -1
  75. package/dist/utils/org-hooks-injector.js +0 -1
  76. package/dist/utils/package-json.d.ts +1 -1
  77. package/dist/utils/package-json.js +21 -1
  78. package/dist/utils/paths.d.ts +0 -1
  79. package/dist/utils/paths.js +0 -1
  80. package/dist/utils/reason-messages.d.ts +0 -1
  81. package/dist/utils/reason-messages.js +0 -1
  82. package/dist/utils/session.d.ts +0 -1
  83. package/dist/utils/session.js +0 -1
  84. package/dist/utils.d.ts +0 -1
  85. package/dist/utils.js +0 -1
  86. package/package.json +30 -39
  87. package/dist/adapters/astro.d.ts.map +0 -1
  88. package/dist/adapters/astro.js.map +0 -1
  89. package/dist/adapters/elysia.d.ts.map +0 -1
  90. package/dist/adapters/elysia.js.map +0 -1
  91. package/dist/adapters/express.d.ts.map +0 -1
  92. package/dist/adapters/express.js.map +0 -1
  93. package/dist/adapters/hono.d.ts.map +0 -1
  94. package/dist/adapters/hono.js.map +0 -1
  95. package/dist/adapters/nextjs.d.ts.map +0 -1
  96. package/dist/adapters/nextjs.js.map +0 -1
  97. package/dist/adapters/nuxt.d.ts.map +0 -1
  98. package/dist/adapters/nuxt.js.map +0 -1
  99. package/dist/adapters/remix.d.ts.map +0 -1
  100. package/dist/adapters/remix.js.map +0 -1
  101. package/dist/adapters/solid-start.d.ts.map +0 -1
  102. package/dist/adapters/solid-start.js.map +0 -1
  103. package/dist/adapters/svelte-kit.d.ts.map +0 -1
  104. package/dist/adapters/svelte-kit.js.map +0 -1
  105. package/dist/adapters/tanstack-start.d.ts.map +0 -1
  106. package/dist/adapters/tanstack-start.js.map +0 -1
  107. package/dist/add-svelte-kit-env-modules.d.ts.map +0 -1
  108. package/dist/add-svelte-kit-env-modules.js.map +0 -1
  109. package/dist/auth-adapter.d.ts.map +0 -1
  110. package/dist/auth-adapter.js.map +0 -1
  111. package/dist/cli/commands/init.d.ts.map +0 -1
  112. package/dist/cli/commands/init.js.map +0 -1
  113. package/dist/cli.d.ts.map +0 -1
  114. package/dist/cli.js.map +0 -1
  115. package/dist/config.d.ts.map +0 -1
  116. package/dist/config.js.map +0 -1
  117. package/dist/core/handler.d.ts.map +0 -1
  118. package/dist/core/handler.js.map +0 -1
  119. package/dist/data.d.ts.map +0 -1
  120. package/dist/data.js.map +0 -1
  121. package/dist/geo-service.d.ts.map +0 -1
  122. package/dist/geo-service.js.map +0 -1
  123. package/dist/get-tsconfig-info.d.ts.map +0 -1
  124. package/dist/get-tsconfig-info.js.map +0 -1
  125. package/dist/index.d.ts.map +0 -1
  126. package/dist/index.js.map +0 -1
  127. package/dist/providers/events/helpers.d.ts.map +0 -1
  128. package/dist/providers/events/helpers.js.map +0 -1
  129. package/dist/public/assets/main-D7_GjGqW.js +0 -1216
  130. package/dist/public/assets/main-Ru3ybjQp.css +0 -1
  131. package/dist/routes/api-router.d.ts.map +0 -1
  132. package/dist/routes/api-router.js.map +0 -1
  133. package/dist/routes.d.ts.map +0 -1
  134. package/dist/routes.js.map +0 -1
  135. package/dist/studio.d.ts.map +0 -1
  136. package/dist/studio.js.map +0 -1
  137. package/dist/types/events.d.ts.map +0 -1
  138. package/dist/types/events.js.map +0 -1
  139. package/dist/types/handler.d.ts.map +0 -1
  140. package/dist/types/handler.js.map +0 -1
  141. package/dist/types/index.d.ts.map +0 -1
  142. package/dist/types/index.js.map +0 -1
  143. package/dist/utils/auth-callbacks-injector.d.ts.map +0 -1
  144. package/dist/utils/auth-callbacks-injector.js.map +0 -1
  145. package/dist/utils/auth-callbacks-wrapper.d.ts +0 -7
  146. package/dist/utils/auth-callbacks-wrapper.d.ts.map +0 -1
  147. package/dist/utils/auth-callbacks-wrapper.js +0 -133
  148. package/dist/utils/auth-callbacks-wrapper.js.map +0 -1
  149. package/dist/utils/database-detection.d.ts.map +0 -1
  150. package/dist/utils/database-detection.js.map +0 -1
  151. package/dist/utils/database-hook-injector.d.ts +0 -3
  152. package/dist/utils/database-hook-injector.d.ts.map +0 -1
  153. package/dist/utils/database-hook-injector.js +0 -145
  154. package/dist/utils/database-hook-injector.js.map +0 -1
  155. package/dist/utils/email-otp-hooks-injector.d.ts +0 -45
  156. package/dist/utils/email-otp-hooks-injector.d.ts.map +0 -1
  157. package/dist/utils/email-otp-hooks-injector.js +0 -141
  158. package/dist/utils/email-otp-hooks-injector.js.map +0 -1
  159. package/dist/utils/event-ingestion.d.ts.map +0 -1
  160. package/dist/utils/event-ingestion.js.map +0 -1
  161. package/dist/utils/hook-injector.d.ts.map +0 -1
  162. package/dist/utils/hook-injector.js.map +0 -1
  163. package/dist/utils/html-injector.d.ts.map +0 -1
  164. package/dist/utils/html-injector.js.map +0 -1
  165. package/dist/utils/org-hooks-injector.d.ts.map +0 -1
  166. package/dist/utils/org-hooks-injector.js.map +0 -1
  167. package/dist/utils/org-hooks-wrapper.d.ts +0 -80
  168. package/dist/utils/org-hooks-wrapper.d.ts.map +0 -1
  169. package/dist/utils/org-hooks-wrapper.js +0 -807
  170. package/dist/utils/org-hooks-wrapper.js.map +0 -1
  171. package/dist/utils/organization-hooks-wrapper.d.ts +0 -44
  172. package/dist/utils/organization-hooks-wrapper.d.ts.map +0 -1
  173. package/dist/utils/organization-hooks-wrapper.js +0 -345
  174. package/dist/utils/organization-hooks-wrapper.js.map +0 -1
  175. package/dist/utils/package-json.d.ts.map +0 -1
  176. package/dist/utils/package-json.js.map +0 -1
  177. package/dist/utils/paths.d.ts.map +0 -1
  178. package/dist/utils/paths.js.map +0 -1
  179. package/dist/utils/reason-messages.d.ts.map +0 -1
  180. package/dist/utils/reason-messages.js.map +0 -1
  181. package/dist/utils/server-init.d.ts +0 -10
  182. package/dist/utils/server-init.d.ts.map +0 -1
  183. package/dist/utils/server-init.js +0 -40
  184. package/dist/utils/server-init.js.map +0 -1
  185. package/dist/utils/session.d.ts.map +0 -1
  186. package/dist/utils/session.js.map +0 -1
  187. package/dist/utils.d.ts.map +0 -1
  188. package/dist/utils.js.map +0 -1
  189. package/public/assets/main-D7_GjGqW.js +0 -1216
  190. package/public/assets/main-Ru3ybjQp.css +0 -1
  191. package/public/favicon.svg +0 -6
  192. package/public/index.html +0 -14
  193. package/public/logo.png +0 -0
  194. package/public/vite.svg +0 -5
  195. package/scripts/download-geolite2.js +0 -42
  196. 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
- if (errorMessage.includes("Invalid password hash") && adapter?.findMany) {
567
- const users = await adapter.findMany({
568
- model: "user",
569
- where: [{ field: "email", value: email }],
570
- limit: 1,
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
- const userId = users[0].id;
576
- const accounts = await adapter.findMany({
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: "No password set for this account. Please use social login or reset your password.",
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
- const allowedRoles = getAllowedRoles();
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
- image: organization.image,
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 || "Unknown",
1610
- userAgent: session.userAgent || "Unknown",
1611
- activeOrganizationId: session.activeOrganizationId,
1612
- activeTeamId: session.activeTeamId,
1613
- createdAt: session.createdAt,
1614
- updatedAt: session.updatedAt,
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?.message?.includes("not found in schema") ||
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(503).json({
1839
- error: "Events not ready",
1840
- retryable: true,
1841
- details: providerError?.message || String(providerError),
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
- // Cursor-based pagination
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
- events = await adapter.findMany({
1878
- model: "auth_events",
2331
+ const findManyOptions = {
1879
2332
  where,
1880
2333
  orderBy: [{ field: "timestamp", direction: sort === "desc" ? "desc" : "asc" }],
1881
- limit: limit + 1, // Get one extra to check hasMore
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?.message?.includes("not found in schema") ||
1886
- adapterError?.message?.includes("Model") ||
1887
- adapterError?.code === "P2025" ||
1888
- adapterError?.code === "42P01") {
1889
- return res.status(503).json({
1890
- error: "Events not ready",
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
- msg?.includes("Model") ||
1952
- error?.code === "P2025" ||
1953
- error?.code === "42P01") {
1954
- return res.status(503).json({
1955
- error: "Events not ready",
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 (_req, res) => {
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?.message?.includes("not found in schema") ||
2029
- e?.message?.includes("Model") ||
2030
- e?.code === "P2025" ||
2031
- e?.code === "42P01";
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.status(503).json({
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.status(503).json({
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.status(503).json({
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.findMany({
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.status(503).json({
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.status(503).json({
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 sessions = await getAuthData(authConfig, "sessions", { page, limit }, configPath, preloadedAdapter);
2225
- res.json(sessions);
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
- if (!adapter) {
3085
- return res.json({
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
- image: `https://api.dicebear.com/7.x/identicon/svg?seed=${randomSuffix}`,
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
- image: organization.image,
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