@podosoft/podokit 1.1.6 → 1.3.0

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 (203) hide show
  1. package/dist/add.d.ts +3 -1
  2. package/dist/add.js +116 -14
  3. package/dist/deploy-render.js +5 -0
  4. package/dist/index.js +8 -0
  5. package/dist/lib.d.ts +2 -2
  6. package/dist/lib.js +3 -2
  7. package/dist/lockfile.d.ts +3 -1
  8. package/dist/lockfile.js +17 -6
  9. package/dist/templates/fullstack/apps/api/package.json +2 -2
  10. package/dist/templates/fullstack/apps/api/scripts/build.mjs +1 -0
  11. package/dist/templates/fullstack/apps/api/src/app.spec.ts +81 -0
  12. package/dist/templates/fullstack/apps/api/src/app.ts +53 -5
  13. package/dist/templates/fullstack/apps/api/src/config/env.validation.ts +6 -0
  14. package/dist/templates/fullstack/apps/api/src/core/api-contract.ts +10 -1
  15. package/dist/templates/fullstack/apps/api/src/core/services.spec.ts +30 -0
  16. package/dist/templates/fullstack/apps/api/src/core/services.ts +19 -3
  17. package/dist/templates/fullstack/apps/api/src/database/data-source.spec.ts +15 -0
  18. package/dist/templates/fullstack/apps/api/src/database/data-source.ts +3 -0
  19. package/dist/templates/fullstack/apps/api/src/database/database.spec.ts +149 -4
  20. package/dist/templates/fullstack/apps/api/src/database/database.ts +66 -13
  21. package/dist/templates/fullstack/apps/api/src/database/migrate.ts +1 -1
  22. package/dist/templates/fullstack/apps/api/src/health/health.plugin.spec.ts +86 -0
  23. package/dist/templates/fullstack/apps/api/src/health/health.plugin.ts +4 -9
  24. package/dist/templates/fullstack/apps/api/src/health/readiness.service.spec.ts +141 -1
  25. package/dist/templates/fullstack/apps/api/src/health/readiness.service.ts +86 -12
  26. package/dist/templates/fullstack/apps/web/package.json +1 -1
  27. package/dist/templates/fullstack/apps/web/src/lib/server/backend-proxy.test.ts +188 -1
  28. package/dist/templates/fullstack/apps/web/src/lib/server/backend-proxy.ts +30 -0
  29. package/dist/templates/fullstack/apps/web/src/routes/api/[...path]/+server.ts +3 -3
  30. package/dist/templates/fullstack/apps/web/src/routes/api/auth/[...all]/+server.ts +3 -3
  31. package/dist/templates/fullstack/compose.dev.yaml +21 -0
  32. package/dist/templates/fullstack/dot-claude/skills/podokit-sveltekit-route/SKILL.md +8 -1
  33. package/dist/templates/fullstack/dot-env.docker +3 -0
  34. package/dist/templates/fullstack/dot-env.example +14 -0
  35. package/dist/templates/fullstack/infra/k3s/api-deployment.yaml +2 -0
  36. package/dist/templates/fullstack/tests/playwright.extensions.ts +2 -2
  37. package/dist/templates/modules/admin-dashboard/files/apps/api/scripts/bootstrap-admin.mjs +21 -26
  38. package/dist/templates/modules/admin-dashboard/files/apps/api/src/admin-sessions/admin-sessions.migration.spec.ts +74 -0
  39. package/dist/templates/modules/admin-dashboard/files/apps/api/src/admin-sessions/admin-sessions.revoker.spec.ts +25 -0
  40. package/dist/templates/modules/admin-dashboard/files/apps/api/src/admin-sessions/admin-sessions.revoker.ts +26 -0
  41. package/dist/templates/modules/admin-dashboard/files/apps/api/src/admin-sessions/admin-sessions.routes.spec.ts +122 -0
  42. package/dist/templates/modules/admin-dashboard/files/apps/api/src/admin-sessions/admin-sessions.routes.ts +46 -0
  43. package/dist/templates/modules/admin-dashboard/files/apps/api/src/admin-sessions/admin-sessions.service.spec.ts +187 -0
  44. package/dist/templates/modules/admin-dashboard/files/apps/api/src/admin-sessions/admin-sessions.service.ts +160 -0
  45. package/dist/templates/modules/admin-dashboard/files/apps/api/src/admin-users/admin-users.routes.spec.ts +76 -0
  46. package/dist/templates/modules/admin-dashboard/files/apps/api/src/admin-users/admin-users.routes.ts +41 -0
  47. package/dist/templates/modules/admin-dashboard/files/apps/api/src/admin-users/admin-users.service.spec.ts +124 -0
  48. package/dist/templates/modules/admin-dashboard/files/apps/api/src/admin-users/admin-users.service.ts +153 -0
  49. package/dist/templates/modules/admin-dashboard/files/apps/api/src/admin.module.ts +22 -28
  50. package/dist/templates/modules/admin-dashboard/files/apps/api/src/bootstrap-admin-runtime.ts +60 -0
  51. package/dist/templates/modules/admin-dashboard/files/apps/api/src/migrations/1792060000000-IndexSessionCreatedAt.ts +35 -0
  52. package/dist/templates/modules/admin-dashboard/files/apps/api/src/profile-image/profile-image.service.spec.ts +3 -5
  53. package/dist/templates/modules/admin-dashboard/files/apps/api/src/profile-image/profile-image.service.ts +6 -17
  54. package/dist/templates/modules/admin-dashboard/files/apps/api/src/site-assets/site-assets.routes.spec.ts +167 -0
  55. package/dist/templates/modules/admin-dashboard/files/apps/api/src/site-assets/site-assets.routes.ts +50 -0
  56. package/dist/templates/modules/admin-dashboard/files/apps/api/src/site-settings/site-settings.service.spec.ts +158 -0
  57. package/dist/templates/modules/admin-dashboard/files/apps/api/src/site-settings/site-settings.service.ts +78 -28
  58. package/dist/templates/modules/admin-dashboard/files/apps/web/src/app.d.ts +3 -0
  59. package/dist/templates/modules/admin-dashboard/files/apps/web/src/hooks.server.test.ts +124 -0
  60. package/dist/templates/modules/admin-dashboard/files/apps/web/src/hooks.server.ts +19 -11
  61. package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/account-data.server.ts +6 -32
  62. package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/admin/sessions.test.ts +74 -0
  63. package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/admin/sessions.ts +73 -0
  64. package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/admin/users.test.ts +102 -0
  65. package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/admin/users.ts +141 -0
  66. package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/audit-log.test.ts +68 -0
  67. package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/audit-log.ts +108 -0
  68. package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/components/account-page.svelte +14 -8
  69. package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/components/session-idle-timeout.svelte +74 -49
  70. package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/i18n/catalogs/admin-dashboard/en.json +6 -1
  71. package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/i18n/catalogs/admin-dashboard/ko.json +6 -1
  72. package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/i18n/messages.test.ts +29 -0
  73. package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/i18n/messages.ts +23 -6
  74. package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/oauth-query.test.ts +21 -0
  75. package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/oauth-query.ts +15 -0
  76. package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/server/capabilities.test.ts +98 -0
  77. package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/server/capabilities.ts +54 -0
  78. package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/server/guards.test.ts +23 -0
  79. package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/server/guards.ts +2 -0
  80. package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/server/protected-layout.ts +7 -50
  81. package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/server/public-paths.ts +9 -0
  82. package/dist/templates/modules/admin-dashboard/files/apps/web/src/routes/(admin)/admin/audit/+page.svelte +88 -45
  83. package/dist/templates/modules/admin-dashboard/files/apps/web/src/routes/(admin)/admin/sessions/+page.svelte +76 -75
  84. package/dist/templates/modules/admin-dashboard/files/apps/web/src/routes/(admin)/admin/settings/+page.server.ts +15 -14
  85. package/dist/templates/modules/admin-dashboard/files/apps/web/src/routes/(admin)/admin/settings/+page.svelte +2 -2
  86. package/dist/templates/modules/admin-dashboard/files/apps/web/src/routes/(admin)/admin/users/+page.svelte +79 -37
  87. package/dist/templates/modules/admin-dashboard/files/apps/web/src/routes/+layout.server.ts +10 -17
  88. package/dist/templates/modules/admin-dashboard/files/apps/web/src/routes/+layout.ts +10 -0
  89. package/dist/templates/modules/admin-dashboard/files/apps/web/src/routes/oauth2/consent/+page.svelte +4 -3
  90. package/dist/templates/modules/admin-dashboard/files/apps/web/src/routes/setup-2fa/+page.svelte +14 -8
  91. package/dist/templates/modules/admin-dashboard/files/dot-claude/skills/podokit-data-table/SKILL.md +2 -1
  92. package/dist/templates/modules/admin-dashboard/files/tests/api/admin-sessions.api.spec.ts +124 -0
  93. package/dist/templates/modules/admin-dashboard/files/tests/api/admin-users.api.spec.ts +86 -0
  94. package/dist/templates/modules/admin-dashboard/files/tests/api/audit.api.spec.ts +75 -12
  95. package/dist/templates/modules/admin-dashboard/files/tests/api/auth-config.api.spec.ts +1 -0
  96. package/dist/templates/modules/admin-dashboard/files/tests/api/auth.api.spec.ts +35 -1
  97. package/dist/templates/modules/admin-dashboard/files/tests/api/profile-image.api.spec.ts +7 -0
  98. package/dist/templates/modules/admin-dashboard/files/tests/api/site-settings.api.spec.ts +72 -0
  99. package/dist/templates/modules/admin-dashboard/files/tests/api/users-admin.api.spec.ts +36 -2
  100. package/dist/templates/modules/admin-dashboard/files/tests/helpers/disposable-users.ts +5 -2
  101. package/dist/templates/modules/admin-dashboard/files/tests/ui/audit.ui.spec.ts +41 -0
  102. package/dist/templates/modules/admin-dashboard/files/tests/ui/data-table.ui.spec.ts +32 -13
  103. package/dist/templates/modules/admin-dashboard/files/tests/ui/email-admin.ui.spec.ts +0 -1
  104. package/dist/templates/modules/admin-dashboard/files/tests/ui/email.ui.spec.ts +1 -3
  105. package/dist/templates/modules/admin-dashboard/files/tests/ui/oidc.ui.spec.ts +16 -5
  106. package/dist/templates/modules/admin-dashboard/files/tests/ui/request-budget.ui.spec.ts +45 -0
  107. package/dist/templates/modules/admin-dashboard/files/tests/ui/sessions.ui.spec.ts +64 -1
  108. package/dist/templates/modules/admin-dashboard/files/tests/ui/table-toolbar.ui.spec.ts +29 -0
  109. package/dist/templates/modules/admin-dashboard/files/tests/ui/users-crud.ui.spec.ts +8 -1
  110. package/dist/templates/modules/admin-dashboard/files/tests/ui/users.ui.spec.ts +32 -1
  111. package/dist/templates/modules/admin-dashboard/module.manifest.json +3 -2
  112. package/dist/templates/modules/audit-log/files/apps/api/src/audit/README.md +46 -5
  113. package/dist/templates/modules/audit-log/files/apps/api/src/audit/audit-events.ts +19 -7
  114. package/dist/templates/modules/audit-log/files/apps/api/src/audit/audit-hook.ts +2 -0
  115. package/dist/templates/modules/audit-log/files/apps/api/src/audit/audit.migration.spec.ts +126 -3
  116. package/dist/templates/modules/audit-log/files/apps/api/src/audit/audit.module.ts +13 -11
  117. package/dist/templates/modules/audit-log/files/apps/api/src/audit/audit.routes.spec.ts +115 -0
  118. package/dist/templates/modules/audit-log/files/apps/api/src/audit/audit.routes.ts +44 -0
  119. package/dist/templates/modules/audit-log/files/apps/api/src/audit/audit.service.spec.ts +421 -43
  120. package/dist/templates/modules/audit-log/files/apps/api/src/audit/audit.service.ts +382 -35
  121. package/dist/templates/modules/audit-log/files/apps/api/src/migrations/1792000000000-IndexAuditLogFilters.ts +57 -0
  122. package/dist/templates/modules/audit-log/module.manifest.json +7 -4
  123. package/dist/templates/modules/auth/files/apps/api/src/auth/auth-config-store.ts +20 -5
  124. package/dist/templates/modules/auth/files/apps/api/src/auth/auth-provider.ts +24 -29
  125. package/dist/templates/modules/auth/files/apps/api/src/auth/auth.module.ts +28 -13
  126. package/dist/templates/modules/auth/files/apps/api/src/auth/auth.openapi.spec.ts +56 -2
  127. package/dist/templates/modules/auth/files/apps/api/src/auth/auth.openapi.ts +22 -0
  128. package/dist/templates/modules/auth/files/apps/api/src/auth/auth.service.spec.ts +81 -5
  129. package/dist/templates/modules/auth/files/apps/api/src/auth/auth.service.ts +26 -2
  130. package/dist/templates/modules/auth/files/apps/api/src/auth/auth.ts +15 -9
  131. package/dist/templates/modules/auth/files/apps/api/src/auth/config-events.spec.ts +38 -0
  132. package/dist/templates/modules/auth/files/apps/api/src/auth/config-events.ts +43 -0
  133. package/dist/templates/modules/auth/files/apps/api/src/auth/db.spec.ts +24 -0
  134. package/dist/templates/modules/auth/files/apps/api/src/auth/db.ts +31 -2
  135. package/dist/templates/modules/auth/files/apps/api/src/auth/feature-gate.spec.ts +58 -0
  136. package/dist/templates/modules/auth/files/apps/api/src/auth/feature-gate.ts +46 -57
  137. package/dist/templates/modules/auth/files/apps/api/src/auth-config/auth-config.rows.spec.ts +53 -0
  138. package/dist/templates/modules/auth/files/apps/api/src/auth-config/auth-config.rows.ts +65 -0
  139. package/dist/templates/modules/auth/files/apps/api/src/auth-config/auth-config.service.ts +21 -37
  140. package/dist/templates/modules/auth/files/apps/api/src/auth-config/config-param.spec.ts +23 -0
  141. package/dist/templates/modules/auth/files/apps/api/src/auth-config/config-param.ts +10 -0
  142. package/dist/templates/modules/auth/files/apps/api/src/auth-config/session-expiry.spec.ts +33 -0
  143. package/dist/templates/modules/auth/files/apps/api/src/auth-config/session-expiry.ts +29 -0
  144. package/dist/templates/modules/auth/files/apps/api/src/settings/flag-defaults.ts +10 -0
  145. package/dist/templates/modules/auth/files/apps/api/src/settings/settings.service.spec.ts +81 -0
  146. package/dist/templates/modules/auth/files/apps/api/src/settings/settings.service.ts +37 -34
  147. package/dist/templates/modules/auth/files/dot-claude/skills/podokit-configure-auth/references/bootstrap-admin.md +2 -0
  148. package/dist/templates/modules/auth/module.manifest.json +2 -2
  149. package/dist/templates/modules/file-upload/files/apps/api/src/files/files.module.spec.ts +136 -0
  150. package/dist/templates/modules/file-upload/files/apps/api/src/files/files.module.ts +34 -13
  151. package/dist/templates/modules/file-upload/files/apps/api/src/files/upload-keys.spec.ts +60 -0
  152. package/dist/templates/modules/file-upload/files/apps/api/src/files/upload-keys.ts +51 -0
  153. package/dist/templates/modules/file-upload/files/tests/api/files.api.spec.ts +70 -9
  154. package/dist/templates/modules/file-upload/module.manifest.json +9 -3
  155. package/dist/templates/modules/job-progress/files/apps/api/src/progress/job-progress.module.ts +26 -32
  156. package/dist/templates/modules/job-progress/files/apps/api/src/progress/progress.bridge.spec.ts +16 -1
  157. package/dist/templates/modules/job-progress/files/apps/api/src/progress/progress.processor.ts +31 -4
  158. package/dist/templates/modules/job-progress/files/apps/api/src/progress/progress.worker.spec.ts +145 -0
  159. package/dist/templates/modules/job-progress/files/apps/api/src/progress/progress.worker.ts +37 -0
  160. package/dist/templates/modules/job-progress/files/tests/api/progress.api.spec.ts +94 -4
  161. package/dist/templates/modules/job-progress/module.manifest.json +3 -3
  162. package/dist/templates/modules/mailer/files/apps/api/src/mail/mailer.spec.ts +54 -0
  163. package/dist/templates/modules/mailer/files/apps/api/src/mail/mailer.ts +47 -25
  164. package/dist/templates/modules/mailer/module.manifest.json +1 -1
  165. package/dist/templates/modules/object-storage-local/files/apps/api/src/storage-local/download-url.spec.ts +86 -0
  166. package/dist/templates/modules/object-storage-local/files/apps/api/src/storage-local/download-url.ts +159 -0
  167. package/dist/templates/modules/object-storage-local/files/apps/api/src/storage-local/storage-demo.ts +38 -0
  168. package/dist/templates/modules/object-storage-local/files/apps/api/src/storage-local/storage-local.module.spec.ts +172 -4
  169. package/dist/templates/modules/object-storage-local/files/apps/api/src/storage-local/storage-local.module.ts +110 -33
  170. package/dist/templates/modules/object-storage-local/files/tests/api/storage-local.api.spec.ts +88 -0
  171. package/dist/templates/modules/object-storage-local/module.manifest.json +6 -1
  172. package/dist/templates/modules/object-storage-s3/files/apps/api/src/storage/s3-object.store.spec.ts +80 -0
  173. package/dist/templates/modules/object-storage-s3/files/apps/api/src/storage/s3-object.store.ts +13 -7
  174. package/dist/templates/modules/object-storage-s3/files/apps/api/src/storage/storage-demo.spec.ts +156 -0
  175. package/dist/templates/modules/object-storage-s3/files/apps/api/src/storage/storage-demo.ts +38 -0
  176. package/dist/templates/modules/object-storage-s3/files/apps/api/src/storage/storage.module.ts +39 -21
  177. package/dist/templates/modules/object-storage-s3/files/apps/api/src/storage/storage.service.ts +101 -2
  178. package/dist/templates/modules/object-storage-s3/files/tests/api/storage.api.spec.ts +84 -9
  179. package/dist/templates/modules/object-storage-s3/module.manifest.json +8 -3
  180. package/dist/templates/modules/rate-limit/files/apps/api/src/rate-limit/rate-limit.config.spec.ts +9 -0
  181. package/dist/templates/modules/rate-limit/files/apps/api/src/rate-limit/rate-limit.config.ts +7 -0
  182. package/dist/templates/modules/rate-limit/files/apps/api/src/rate-limit/rate-limit.identity.spec.ts +139 -21
  183. package/dist/templates/modules/rate-limit/files/apps/api/src/rate-limit/rate-limit.identity.ts +80 -11
  184. package/dist/templates/modules/rate-limit/files/apps/api/src/rate-limit/rate-limit.module.spec.ts +12 -6
  185. package/dist/templates/modules/rate-limit/files/apps/api/src/rate-limit/rate-limit.module.ts +15 -2
  186. package/dist/templates/modules/rate-limit/files/tests/api/rate-limit.api.spec.ts +46 -0
  187. package/dist/templates/modules/rate-limit/module.manifest.json +4 -3
  188. package/dist/templates/modules/redis/files/apps/api/src/redis/redis-cache.store.spec.ts +125 -0
  189. package/dist/templates/modules/redis/files/apps/api/src/redis/redis-cache.store.ts +27 -13
  190. package/dist/templates/modules/sse/files/apps/api/src/events/event-envelope.ts +123 -0
  191. package/dist/templates/modules/sse/files/apps/api/src/events/event-stream.ts +191 -0
  192. package/dist/templates/modules/sse/files/apps/api/src/events/event-topics.ts +64 -0
  193. package/dist/templates/modules/sse/files/apps/api/src/events/events.module.spec.ts +250 -0
  194. package/dist/templates/modules/sse/files/apps/api/src/events/events.module.ts +121 -15
  195. package/dist/templates/modules/sse/files/apps/api/src/events/events.service.spec.ts +407 -18
  196. package/dist/templates/modules/sse/files/apps/api/src/events/events.service.ts +153 -40
  197. package/dist/templates/modules/sse/files/apps/web/src/lib/events.test.ts +216 -0
  198. package/dist/templates/modules/sse/files/apps/web/src/lib/events.ts +207 -0
  199. package/dist/templates/modules/sse/files/tests/api/events.api.spec.ts +227 -70
  200. package/dist/templates/modules/sse/module.manifest.json +20 -5
  201. package/dist/update.d.ts +17 -0
  202. package/dist/update.js +33 -8
  203. package/package.json +2 -2
@@ -1,47 +1,54 @@
1
1
  <script lang="ts">
2
+ import { onMount } from "svelte";
2
3
  import { Badge } from "#lib/components/ui/badge/index.js";
3
4
  import * as Table from "#lib/components/ui/table/index.js";
4
5
  import DataTable, { type DataTableColumn, type SortState, DEFAULT_PAGE_SIZE } from "#lib/components/data-table.svelte";
5
6
  import TableToolbar, { type ToolbarFilter, type ToolbarSearchField } from "#lib/components/table-toolbar.svelte";
6
7
  import { toast } from "svelte-sonner";
7
8
  import { getI18n, fmt, formatDateTime } from "#lib/i18n/index.js";
9
+ import {
10
+ cursorForPage,
11
+ emptyTrail,
12
+ fetchAuditActions,
13
+ fetchAuditLogPage,
14
+ recordPage,
15
+ trailCount,
16
+ type AuditLogEntry,
17
+ type AuditLogFilters,
18
+ type AuditLogOrder,
19
+ } from "#lib/audit-log.js";
8
20
 
9
21
  const i18n = getI18n();
10
- type Entry = {
11
- id: string;
12
- action: string;
13
- actorId: string | null;
14
- actorName: string | null;
15
- actorEmail: string | null;
16
- targetLabel: string | null;
17
- ip: string | null;
18
- createdAt: string;
19
- };
22
+ const PAGE_SIZE = DEFAULT_PAGE_SIZE;
20
23
 
21
- let entries = $state<Entry[]>([]);
24
+ let entries = $state<AuditLogEntry[]>([]);
25
+ let actions = $state<string[]>([]);
22
26
  let page = $state(1);
27
+ let shownPage = $state(1);
28
+ let trail = $state(emptyTrail());
23
29
  let sort = $state<SortState | null>({ key: "createdAt", dir: "desc" });
24
30
  let search = $state("");
25
- let appliedSearch = $state("");
26
- let filterValues = $state<Record<string, string>>({ action: "" });
27
- let appliedFilters = $state<Record<string, string>>({ action: "" });
28
31
  let searchField = $state("actor");
29
- let appliedSearchField = $state("actor");
32
+ let filterValues = $state<Record<string, string>>({ action: "" });
33
+ let applied = $state<AuditLogFilters>({ action: "", actor: "", target: "" });
34
+ let trailOrder: AuditLogOrder = "desc";
35
+ let latestRequest = 0;
30
36
 
31
- const columns = $derived<DataTableColumn<Entry>[]>([
37
+ // The server pages by time with a cursor, so time is the only sortable column.
38
+ const columns = $derived<DataTableColumn<AuditLogEntry>[]>([
32
39
  { key: "createdAt", label: i18n.t.audit.when, sortable: true, class: "whitespace-nowrap" },
33
- { key: "actor", label: i18n.t.audit.actor, sortable: true, value: (e) => e.actorName ?? e.actorEmail },
34
- { key: "action", label: i18n.t.audit.action, sortable: true },
35
- { key: "target", label: i18n.t.audit.target, sortable: true, value: (e) => e.targetLabel },
36
- { key: "ip", label: i18n.t.audit.ip, sortable: true },
40
+ { key: "actor", label: i18n.t.audit.actor },
41
+ { key: "action", label: i18n.t.audit.action },
42
+ { key: "target", label: i18n.t.audit.target },
43
+ { key: "ip", label: i18n.t.audit.ip },
37
44
  ]);
38
45
 
39
- const actions = $derived([...new Set(entries.map((e) => e.action))].sort());
46
+ const actionOptions = $derived([...new Set([...actions, ...(applied.action ? [applied.action] : [])])].sort());
40
47
  const filters = $derived<ToolbarFilter[]>([
41
48
  {
42
49
  key: "action",
43
50
  label: i18n.t.audit.action,
44
- options: [{ value: "", label: i18n.t.toolbar.all }, ...actions.map((a) => ({ value: a, label: a }))],
51
+ options: [{ value: "", label: i18n.t.toolbar.all }, ...actionOptions.map((a) => ({ value: a, label: a }))],
45
52
  },
46
53
  ]);
47
54
 
@@ -50,33 +57,63 @@
50
57
  { value: "target", label: i18n.t.audit.target },
51
58
  ]);
52
59
 
53
- const filtered = $derived(
54
- entries.filter((e) => {
55
- if (appliedFilters.action && e.action !== appliedFilters.action) return false;
56
- if (appliedSearch) {
57
- const hay = (
58
- appliedSearchField === "target"
59
- ? (e.targetLabel ?? "")
60
- : `${e.actorName ?? ""} ${e.actorEmail ?? ""}`
61
- ).toLowerCase();
62
- if (!hay.includes(appliedSearch.toLowerCase())) return false;
63
- }
64
- return true;
65
- }),
60
+ const count = $derived(trailCount(trail, PAGE_SIZE));
61
+ const first = $derived((shownPage - 1) * PAGE_SIZE + 1);
62
+ const label = $derived(
63
+ trail.end || entries.length === 0
64
+ ? fmt(i18n.t.audit.total, { count })
65
+ : fmt(i18n.t.audit.range, { from: first, to: first + entries.length - 1 }),
66
66
  );
67
67
 
68
- async function load(): Promise<void> {
68
+ function currentOrder(): AuditLogOrder {
69
+ return sort?.dir === "asc" ? "asc" : "desc";
70
+ }
71
+
72
+ async function load(target: number): Promise<void> {
73
+ const cursor = cursorForPage(trail, target);
74
+ if (cursor === undefined) {
75
+ page = shownPage;
76
+ return;
77
+ }
78
+ const request = ++latestRequest;
69
79
  try {
70
- const res = await fetch("/api/audit-logs");
71
- if (!res.ok) throw new Error();
72
- entries = (await res.json()) as Entry[];
80
+ const result = await fetchAuditLogPage(fetch, applied, trailOrder, cursor, PAGE_SIZE);
81
+ if (request !== latestRequest) return;
82
+ entries = result.entries;
83
+ page = target;
84
+ shownPage = target;
85
+ trail = recordPage(trail, target, result.entries.length, result.nextCursor);
73
86
  } catch {
87
+ if (request !== latestRequest) return;
88
+ page = shownPage;
74
89
  toast.error(i18n.t.audit.loadFailed);
75
90
  }
76
91
  }
77
92
 
78
- $effect(() => {
79
- void load();
93
+ function restart(): void {
94
+ trailOrder = currentOrder();
95
+ trail = emptyTrail();
96
+ void load(1);
97
+ }
98
+
99
+ function applySearch(): void {
100
+ const value = search.trim();
101
+ applied = {
102
+ action: filterValues.action ?? "",
103
+ actor: searchField === "actor" ? value : "",
104
+ target: searchField === "target" ? value : "",
105
+ };
106
+ restart();
107
+ }
108
+
109
+ function onTableChange(state: { page: number }): void {
110
+ if (currentOrder() !== trailOrder) restart();
111
+ else void load(state.page);
112
+ }
113
+
114
+ onMount(() => {
115
+ restart();
116
+ void fetchAuditActions(fetch).then((codes) => { actions = codes; }, () => undefined);
80
117
  });
81
118
  </script>
82
119
 
@@ -89,21 +126,27 @@
89
126
  {searchFields}
90
127
  bind:searchField
91
128
  bind:search
129
+ searchPlaceholder={searchField === "target" ? i18n.t.audit.targetHint : i18n.t.audit.actorHint}
92
130
  filterHeading={i18n.t.toolbar.filter}
93
131
  searchHeading={i18n.t.toolbar.search}
94
132
  searchButton={i18n.t.toolbar.searchButton}
95
- onSearch={() => { appliedSearch = search; appliedSearchField = searchField; appliedFilters = { ...filterValues }; page = 1; }}
133
+ onSearch={applySearch}
96
134
  />
97
135
 
98
136
  <DataTable
99
137
  {columns}
100
- rows={filtered}
138
+ rows={entries}
101
139
  getKey={(e) => e.id}
102
140
  empty={i18n.t.audit.empty}
141
+ ariaLabel={i18n.t.audit.title}
103
142
  bind:sort
143
+ manualSort
104
144
  bind:page
105
- perPage={DEFAULT_PAGE_SIZE}
106
- label={fmt(i18n.t.audit.total, { count: filtered.length })}
145
+ perPage={PAGE_SIZE}
146
+ total={count}
147
+ manualPagination
148
+ {label}
149
+ onChange={onTableChange}
107
150
  >
108
151
  {#snippet row(e)}
109
152
  <Table.Cell class="text-muted-foreground whitespace-nowrap">{formatDateTime(e.createdAt)}</Table.Cell>
@@ -1,4 +1,5 @@
1
1
  <script lang="ts">
2
+ import { untrack } from "svelte";
2
3
  import { Button } from "#lib/components/ui/button/index.js";
3
4
  import { Badge } from "#lib/components/ui/badge/index.js";
4
5
  import * as Table from "#lib/components/ui/table/index.js";
@@ -8,100 +9,94 @@
8
9
  import EllipsisIcon from "@lucide/svelte/icons/ellipsis";
9
10
  import { toast } from "svelte-sonner";
10
11
  import { api } from "#lib/api.js";
12
+ import { KeysetPages, listAdminSessions, revokeAdminSession, type AdminSession } from "#lib/admin/sessions.js";
11
13
  import { getI18n, fmt, formatDateTime } from "#lib/i18n/index.js";
12
14
  import { deviceLabel } from "#lib/device-label.js";
13
15
 
14
16
  const i18n = getI18n();
17
+ const PAGE_SIZE = DEFAULT_PAGE_SIZE;
18
+ // The API pages newest first by keyset, so the order is fixed rather than sortable.
19
+ const sort: SortState = { key: "createdAt", dir: "desc" };
20
+ const pages = new KeysetPages();
15
21
 
16
- // better-auth has no "list all sessions" endpoint, so fan out listUserSessions
17
- // over a page of users and flatten. Bounded to USER_LIMIT users — adequate for a
18
- // starter admin view; paginate/scope further for large deployments.
19
- const USER_LIMIT = 50;
20
- type Row = {
21
- id: string;
22
- token: string;
23
- userId: string;
24
- userName: string;
25
- userEmail: string;
26
- userAgent?: string | null;
27
- ipAddress?: string | null;
28
- createdAt: string | Date;
29
- expiresAt?: string | Date | null;
30
- impersonatedBy?: string | null;
31
- };
32
-
33
- let rows = $state<Row[]>([]);
22
+ let rows = $state<AdminSession[]>([]);
23
+ let total = $state(0);
24
+ let pagerTotal = $state(0);
34
25
  let page = $state(1);
35
- // No default sort: keep the fan-out order so the newest session isn't required
36
- // to sit on page 1. Sorting is available by clicking a column header.
37
- let sort = $state<SortState | null>(null);
38
26
  let loading = $state(false);
39
27
  let busy = $state(false);
40
28
  let search = $state("");
41
29
  let appliedSearch = $state("");
42
- let searchField = $state("email");
43
- let appliedSearchField = $state("email");
30
+ let searchField = $state("user");
31
+ let latestRequest = 0;
32
+ let shownPage = 1;
44
33
 
45
- const columns = $derived<DataTableColumn<Row>[]>([
46
- { key: "user", label: i18n.t.adminSessions.user, sortable: true, value: (r) => r.userName },
47
- { key: "device", label: i18n.t.adminSessions.device, sortable: true, value: (r) => deviceLabel(r.userAgent) ?? r.userAgent ?? "" },
48
- { key: "ip", label: i18n.t.adminSessions.ip, sortable: true, value: (r) => r.ipAddress },
49
- { key: "createdAt", label: i18n.t.adminSessions.since, sortable: true },
50
- { key: "expiresAt", label: i18n.t.adminSessions.expires, sortable: true, value: (r) => (r.expiresAt ? new Date(r.expiresAt).getTime() : 0) },
34
+ const columns = $derived<DataTableColumn<AdminSession>[]>([
35
+ { key: "user", label: i18n.t.adminSessions.user },
36
+ { key: "device", label: i18n.t.adminSessions.device },
37
+ { key: "ip", label: i18n.t.adminSessions.ip },
38
+ { key: "createdAt", label: i18n.t.adminSessions.since },
39
+ { key: "expiresAt", label: i18n.t.adminSessions.expires },
51
40
  { key: "actions", label: "", class: "w-10" },
52
41
  ]);
53
42
 
54
- const searchFields = $derived<ToolbarSearchField[]>([
55
- { value: "email", label: i18n.t.users.email },
56
- { value: "name", label: i18n.t.users.name },
57
- ]);
58
-
59
- const filtered = $derived(
60
- rows.filter((r) => {
61
- if (!appliedSearch) return true;
62
- const hay = (appliedSearchField === "name" ? r.userName : r.userEmail).toLowerCase();
63
- return hay.includes(appliedSearch.toLowerCase());
64
- }),
65
- );
43
+ const searchFields = $derived<ToolbarSearchField[]>([{ value: "user", label: i18n.t.adminSessions.user }]);
66
44
 
67
- async function load(): Promise<void> {
45
+ // Loads one page; a newer request supersedes any that is still in flight.
46
+ async function load(target: number): Promise<void> {
47
+ const cursor = pages.cursorFor(target);
48
+ if (cursor === undefined) return;
49
+ const request = ++latestRequest;
68
50
  loading = true;
69
- const { data, error } = await api.auth.admin.listUsers({ query: { limit: USER_LIMIT, offset: 0 } });
70
- if (error) {
71
- loading = false;
72
- toast.error(error.message ?? i18n.t.adminSessions.loadFailed);
73
- return;
51
+ try {
52
+ const result = await listAdminSessions(api, { limit: PAGE_SIZE, cursor, search: appliedSearch });
53
+ if (request !== latestRequest) return;
54
+ // A revoke can empty the last page; show the one before it instead.
55
+ if (result.items.length === 0 && target > 1) return load(target - 1);
56
+ pages.record(target, result.nextCursor);
57
+ rows = result.items;
58
+ total = result.total;
59
+ pagerTotal = pages.pagerTotal(result.total, PAGE_SIZE);
60
+ page = shownPage = target;
61
+ } catch {
62
+ if (request !== latestRequest) return;
63
+ page = shownPage;
64
+ toast.error(i18n.t.adminSessions.loadFailed);
65
+ } finally {
66
+ if (request === latestRequest) loading = false;
74
67
  }
75
- const users = (data?.users ?? []) as Array<{ id: string; name: string; email: string }>;
76
- const perUser = await Promise.all(
77
- users.map(async (u) => {
78
- const { data: res } = await api.auth.admin.listUserSessions({ userId: u.id });
79
- const sessions = (res?.sessions ?? []) as Array<Omit<Row, "userId" | "userName" | "userEmail">>;
80
- return sessions.map((s) => ({ ...s, userId: u.id, userName: u.name, userEmail: u.email }));
81
- }),
82
- );
83
- rows = perUser.flat();
84
- loading = false;
85
68
  }
86
69
 
87
- async function act(promise: Promise<{ error?: { message?: string } | null }>, ok: string): Promise<void> {
70
+ function applySearch(): void {
71
+ appliedSearch = search;
72
+ pages.reset();
73
+ void load(1);
74
+ }
75
+
76
+ async function revoke(row: AdminSession): Promise<void> {
88
77
  busy = true;
89
- const { error } = await promise;
90
- busy = false;
91
- if (error) toast.error(error.message ?? i18n.t.adminSessions.loadFailed);
92
- else {
93
- toast.success(ok);
94
- await load();
78
+ try {
79
+ await revokeAdminSession(api, row.id);
80
+ toast.success(i18n.t.adminSessions.revoked);
81
+ await load(page);
82
+ } catch {
83
+ toast.error(i18n.t.adminSessions.revokeFailed);
84
+ } finally {
85
+ busy = false;
95
86
  }
96
87
  }
97
88
 
98
- const revoke = (row: Row) =>
99
- act(api.auth.admin.revokeUserSession({ sessionToken: row.token }), i18n.t.adminSessions.revoked);
100
- const revokeAll = (row: Row) =>
101
- act(api.auth.admin.revokeUserSessions({ userId: row.userId }), i18n.t.adminSessions.allRevoked);
89
+ async function revokeAll(row: AdminSession): Promise<void> {
90
+ busy = true;
91
+ const { error } = await api.auth.admin.revokeUserSessions({ userId: row.userId });
92
+ busy = false;
93
+ if (error) return void toast.error(i18n.t.adminSessions.revokeFailed);
94
+ toast.success(i18n.t.adminSessions.allRevoked);
95
+ await load(page);
96
+ }
102
97
 
103
98
  $effect(() => {
104
- void load();
99
+ untrack(() => void load(1));
105
100
  });
106
101
  </script>
107
102
 
@@ -112,20 +107,26 @@
112
107
  {searchFields}
113
108
  bind:searchField
114
109
  bind:search
110
+ searchPlaceholder={i18n.t.adminSessions.searchPlaceholder}
115
111
  searchHeading={i18n.t.toolbar.search}
116
112
  searchButton={i18n.t.toolbar.searchButton}
117
- onSearch={() => { appliedSearch = search; appliedSearchField = searchField; page = 1; }}
113
+ onSearch={applySearch}
118
114
  />
119
115
 
120
116
  <DataTable
121
117
  {columns}
122
- rows={filtered}
118
+ {rows}
123
119
  getKey={(r) => r.id}
124
120
  empty={loading ? "…" : i18n.t.adminSessions.empty}
125
- bind:sort
121
+ ariaLabel={i18n.t.adminSessions.title}
122
+ {sort}
123
+ manualSort
124
+ manualPagination
126
125
  bind:page
127
- perPage={DEFAULT_PAGE_SIZE}
128
- label={fmt(i18n.t.adminSessions.total, { count: filtered.length })}
126
+ perPage={PAGE_SIZE}
127
+ total={pagerTotal}
128
+ label={fmt(i18n.t.adminSessions.total, { count: total })}
129
+ onChange={(state) => void load(state.page)}
129
130
  >
130
131
  {#snippet row(r)}
131
132
  <Table.Cell>
@@ -138,7 +139,7 @@
138
139
  <Table.Cell class="max-w-xs truncate" title={r.userAgent ?? ""}>{deviceLabel(r.userAgent) ?? r.userAgent ?? i18n.t.adminSessions.unknown}</Table.Cell>
139
140
  <Table.Cell class="text-muted-foreground">{r.ipAddress ?? "—"}</Table.Cell>
140
141
  <Table.Cell class="text-muted-foreground">{formatDateTime(r.createdAt)}</Table.Cell>
141
- <Table.Cell class="text-muted-foreground">{r.expiresAt ? formatDateTime(r.expiresAt) : "—"}</Table.Cell>
142
+ <Table.Cell class="text-muted-foreground">{formatDateTime(r.expiresAt)}</Table.Cell>
142
143
  <Table.Cell>
143
144
  <DropdownMenu.Root>
144
145
  <DropdownMenu.Trigger>
@@ -1,4 +1,5 @@
1
1
  import type { PageServerLoad } from "./$types";
2
+ import { getCapabilities, type CapabilitiesEvent } from "#lib/server/capabilities.js";
2
3
  import { requireAdmin } from "#lib/server/guards.js";
3
4
 
4
5
  // Non-secret view of the DB-backed auth config (OAuth providers, SMTP, server
@@ -20,23 +21,23 @@ export type AuthConfigView = {
20
21
  };
21
22
  };
22
23
 
23
- export const load: PageServerLoad = async ({ locals, fetch }) => {
24
- requireAdmin(locals.user, locals);
25
- let authConfig: AuthConfigView | null = null;
24
+ async function loadAuthConfig(fetch: CapabilitiesEvent["fetch"]): Promise<AuthConfigView | null> {
26
25
  try {
27
26
  const res = await fetch("/api/account/auth-config");
28
- if (res.ok) authConfig = (await res.json()) as AuthConfigView;
27
+ if (res.ok) return (await res.json()) as AuthConfigView;
29
28
  } catch {
30
29
  /* leave null — the page shows the DB-config sections as unavailable */
31
30
  }
32
- // The require-two-factor policy lives in the feature-flag store (not the typed
33
- // Capabilities), so read it from its own endpoint for the Settings toggle.
34
- let require2fa = false;
35
- try {
36
- const res = await fetch("/api/account/require-2fa");
37
- if (res.ok) require2fa = ((await res.json()) as { require2fa?: boolean }).require2fa === true;
38
- } catch {
39
- /* default off */
40
- }
41
- return { authConfig, require2fa };
31
+ return null;
32
+ }
33
+
34
+ export const load: PageServerLoad = async ({ locals, fetch }) => {
35
+ requireAdmin(locals.user, locals);
36
+ // The admin layout loads the same capabilities in this request, so the
37
+ // require-two-factor policy comes from that shared response.
38
+ const [authConfig, capabilities] = await Promise.all([
39
+ loadAuthConfig(fetch),
40
+ getCapabilities({ locals, fetch }),
41
+ ]);
42
+ return { authConfig, require2fa: capabilities.require2fa };
42
43
  };
@@ -24,8 +24,8 @@
24
24
  const ac = $derived(data.authConfig);
25
25
 
26
26
  type EditableFlag = "twoFactor" | "magicLink" | "emailOtp" | "username" | "multiSession" | "phoneNumber" | "apiKey" | "passkey" | "organization" | "oidcProvider";
27
- // require2fa is a policy flag stored alongside the feature flags but not part of
28
- // the typed Capabilities, so it is toggled but never read from `caps`.
27
+ // require2fa is an enrolment policy rather than a feature, so its card reads
28
+ // `data.require2fa` instead of the feature flags in `caps`.
29
29
  type ToggleFlag = EditableFlag | "require2fa";
30
30
  type ServerKey = "requireEmailVerification" | "requireSignupApproval" | "hibp" | "allowDelete" | "auditLog";
31
31