@rebasepro/server-core 0.4.0 → 0.6.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 (282) hide show
  1. package/README.md +62 -25
  2. package/dist/{server-core/src/api → api}/errors.d.ts +20 -2
  3. package/dist/{server-core/src/api → api}/rest/api-generator.d.ts +8 -2
  4. package/dist/{server-core/src/api → api}/types.d.ts +2 -0
  5. package/dist/auth/admin-user-ops.d.ts +79 -0
  6. package/dist/{server-core/src/auth → auth}/api-keys/index.d.ts +2 -2
  7. package/dist/{server-core/src/auth → auth}/auth-hooks.d.ts +37 -9
  8. package/dist/{server-core/src/auth → auth}/builtin-auth-adapter.d.ts +4 -4
  9. package/dist/{server-core/src/auth → auth}/index.d.ts +5 -2
  10. package/dist/{server-core/src/auth → auth}/jwt.d.ts +10 -0
  11. package/dist/auth/mfa-routes.d.ts +6 -0
  12. package/dist/auth/reset-password-admin.d.ts +29 -0
  13. package/dist/auth/session-routes.d.ts +25 -0
  14. package/dist/backend-CIxN4FVm.js +15 -0
  15. package/dist/backend-CIxN4FVm.js.map +1 -0
  16. package/dist/chunk-Dze3rakg.js +42 -0
  17. package/dist/{server-core/src/cron → cron}/cron-scheduler.d.ts +1 -1
  18. package/dist/dist-CZKP-Xz4.js +832 -0
  19. package/dist/dist-CZKP-Xz4.js.map +1 -0
  20. package/dist/env.d.ts +102 -0
  21. package/dist/from-VbwD7xRf.js +3849 -0
  22. package/dist/from-VbwD7xRf.js.map +1 -0
  23. package/dist/{server-core/src/index.d.ts → index.d.ts} +1 -0
  24. package/dist/index.es.js +60871 -50424
  25. package/dist/index.es.js.map +1 -1
  26. package/dist/index.umd.js +73151 -51256
  27. package/dist/index.umd.js.map +1 -1
  28. package/dist/init/docs.d.ts +4 -0
  29. package/dist/init/health.d.ts +2 -0
  30. package/dist/init/middlewares.d.ts +10 -0
  31. package/dist/init/shutdown.d.ts +11 -0
  32. package/dist/init/storage.d.ts +5 -0
  33. package/dist/{server-core/src/init.d.ts → init.d.ts} +4 -4
  34. package/dist/jwt-DHcQRGC3.js +4168 -0
  35. package/dist/jwt-DHcQRGC3.js.map +1 -0
  36. package/dist/logger-BYU66ENZ.js +94 -0
  37. package/dist/logger-BYU66ENZ.js.map +1 -0
  38. package/dist/ms-BeBSuOXN.js +125 -0
  39. package/dist/ms-BeBSuOXN.js.map +1 -0
  40. package/dist/multipart-parser-CedBDOeC.js +299 -0
  41. package/dist/multipart-parser-CedBDOeC.js.map +1 -0
  42. package/dist/src-COaj0G3P.js +1182 -0
  43. package/dist/src-COaj0G3P.js.map +1 -0
  44. package/dist/{server-core/src/storage → storage}/image-transform.d.ts +3 -1
  45. package/dist/utils/request-id.d.ts +4 -0
  46. package/package.json +29 -29
  47. package/src/api/collections_for_test/callbacks_test_collection.ts +7 -6
  48. package/src/api/errors.ts +95 -20
  49. package/src/api/graphql/graphql-schema-generator.ts +15 -10
  50. package/src/api/logs-routes.ts +5 -2
  51. package/src/api/openapi-generator.ts +1 -1
  52. package/src/api/rest/api-generator-count.test.ts +21 -10
  53. package/src/api/rest/api-generator.ts +151 -36
  54. package/src/api/rest/query-parser.ts +18 -12
  55. package/src/api/server.ts +13 -2
  56. package/src/api/types.ts +2 -0
  57. package/src/auth/adapter-middleware.ts +17 -10
  58. package/src/auth/admin-user-ops.ts +236 -0
  59. package/src/auth/api-keys/api-key-middleware.ts +15 -9
  60. package/src/auth/api-keys/api-key-permission-guard.ts +1 -1
  61. package/src/auth/api-keys/api-key-routes.ts +3 -3
  62. package/src/auth/api-keys/api-key-store.ts +14 -8
  63. package/src/auth/api-keys/index.ts +2 -2
  64. package/src/auth/apple-oauth.ts +4 -3
  65. package/src/auth/auth-hooks.ts +50 -10
  66. package/src/auth/bitbucket-oauth.ts +5 -4
  67. package/src/auth/builtin-auth-adapter.ts +64 -54
  68. package/src/auth/custom-auth-adapter.ts +4 -5
  69. package/src/auth/discord-oauth.ts +5 -4
  70. package/src/auth/facebook-oauth.ts +5 -4
  71. package/src/auth/github-oauth.ts +6 -5
  72. package/src/auth/gitlab-oauth.ts +5 -4
  73. package/src/auth/google-oauth.ts +2 -1
  74. package/src/auth/index.ts +6 -2
  75. package/src/auth/jwt.ts +15 -4
  76. package/src/auth/linkedin-oauth.ts +4 -3
  77. package/src/auth/mfa-routes.ts +299 -0
  78. package/src/auth/mfa.ts +2 -1
  79. package/src/auth/microsoft-oauth.ts +5 -4
  80. package/src/auth/middleware.ts +5 -4
  81. package/src/auth/rate-limiter.ts +1 -1
  82. package/src/auth/reset-password-admin.ts +144 -0
  83. package/src/auth/rls-scope.ts +1 -1
  84. package/src/auth/routes.ts +52 -600
  85. package/src/auth/session-routes.ts +341 -0
  86. package/src/auth/slack-oauth.ts +6 -5
  87. package/src/auth/spotify-oauth.ts +5 -4
  88. package/src/auth/twitter-oauth.ts +4 -3
  89. package/src/collections/loader.ts +5 -4
  90. package/src/cron/cron-scheduler.test.ts +23 -11
  91. package/src/cron/cron-scheduler.ts +14 -8
  92. package/src/cron/cron-store.ts +1 -1
  93. package/src/email/smtp-email-service.ts +3 -2
  94. package/src/env.ts +11 -10
  95. package/src/index.ts +1 -0
  96. package/src/init/docs.ts +47 -0
  97. package/src/init/health.ts +37 -0
  98. package/src/init/middlewares.ts +61 -0
  99. package/src/init/shutdown.ts +56 -0
  100. package/src/init/storage.ts +57 -0
  101. package/src/init.ts +93 -295
  102. package/src/serve-spa.ts +12 -5
  103. package/src/services/driver-registry.ts +4 -3
  104. package/src/storage/S3StorageController.ts +1 -2
  105. package/src/storage/image-transform.ts +25 -10
  106. package/src/storage/routes.ts +30 -11
  107. package/src/storage/storage-registry.ts +4 -3
  108. package/src/storage/tus-handler.ts +12 -12
  109. package/src/utils/request-id.ts +40 -0
  110. package/src/utils/request-logger.ts +6 -0
  111. package/test/api-generator.test.ts +90 -2
  112. package/test/api-key-permission-guard.test.ts +24 -12
  113. package/test/auth-routes.test.ts +5 -3
  114. package/test/backend-hooks-data.test.ts +99 -30
  115. package/test/custom-auth-adapter.test.ts +16 -15
  116. package/test/email-templates.test.ts +10 -5
  117. package/test/env.test.ts +10 -10
  118. package/test/function-loader.test.ts +7 -4
  119. package/test/graphql-schema-generator.test.ts +554 -0
  120. package/test/jwt-security.test.ts +1 -1
  121. package/test/query-parser.test.ts +0 -1
  122. package/test/reset-password-admin.test.ts +113 -0
  123. package/test/singleton.test.ts +5 -5
  124. package/test/smtp-email-service.test.ts +21 -21
  125. package/test/webhook-service.test.ts +22 -11
  126. package/tsconfig.json +2 -0
  127. package/tsconfig.prod.json +3 -0
  128. package/vite.config.ts +5 -5
  129. package/dist/common/src/collections/CollectionRegistry.d.ts +0 -56
  130. package/dist/common/src/collections/default-collections.d.ts +0 -9
  131. package/dist/common/src/collections/index.d.ts +0 -2
  132. package/dist/common/src/data/buildRebaseData.d.ts +0 -14
  133. package/dist/common/src/data/query_builder.d.ts +0 -55
  134. package/dist/common/src/index.d.ts +0 -4
  135. package/dist/common/src/util/builders.d.ts +0 -57
  136. package/dist/common/src/util/callbacks.d.ts +0 -6
  137. package/dist/common/src/util/collections.d.ts +0 -11
  138. package/dist/common/src/util/common.d.ts +0 -2
  139. package/dist/common/src/util/conditions.d.ts +0 -26
  140. package/dist/common/src/util/entities.d.ts +0 -58
  141. package/dist/common/src/util/enums.d.ts +0 -3
  142. package/dist/common/src/util/index.d.ts +0 -16
  143. package/dist/common/src/util/navigation_from_path.d.ts +0 -34
  144. package/dist/common/src/util/navigation_utils.d.ts +0 -20
  145. package/dist/common/src/util/parent_references_from_path.d.ts +0 -6
  146. package/dist/common/src/util/paths.d.ts +0 -14
  147. package/dist/common/src/util/permissions.d.ts +0 -6
  148. package/dist/common/src/util/references.d.ts +0 -2
  149. package/dist/common/src/util/relations.d.ts +0 -22
  150. package/dist/common/src/util/resolutions.d.ts +0 -72
  151. package/dist/common/src/util/storage.d.ts +0 -24
  152. package/dist/index-Cr1D21av.js +0 -49
  153. package/dist/index-Cr1D21av.js.map +0 -1
  154. package/dist/server-core/src/auth/admin-routes.d.ts +0 -27
  155. package/dist/server-core/src/env.d.ts +0 -137
  156. package/dist/types/src/controllers/analytics_controller.d.ts +0 -7
  157. package/dist/types/src/controllers/auth.d.ts +0 -104
  158. package/dist/types/src/controllers/client.d.ts +0 -168
  159. package/dist/types/src/controllers/collection_registry.d.ts +0 -46
  160. package/dist/types/src/controllers/customization_controller.d.ts +0 -60
  161. package/dist/types/src/controllers/data.d.ts +0 -207
  162. package/dist/types/src/controllers/data_driver.d.ts +0 -218
  163. package/dist/types/src/controllers/database_admin.d.ts +0 -11
  164. package/dist/types/src/controllers/dialogs_controller.d.ts +0 -36
  165. package/dist/types/src/controllers/effective_role.d.ts +0 -4
  166. package/dist/types/src/controllers/email.d.ts +0 -36
  167. package/dist/types/src/controllers/index.d.ts +0 -18
  168. package/dist/types/src/controllers/local_config_persistence.d.ts +0 -20
  169. package/dist/types/src/controllers/navigation.d.ts +0 -225
  170. package/dist/types/src/controllers/registry.d.ts +0 -63
  171. package/dist/types/src/controllers/side_dialogs_controller.d.ts +0 -67
  172. package/dist/types/src/controllers/side_entity_controller.d.ts +0 -97
  173. package/dist/types/src/controllers/snackbar.d.ts +0 -24
  174. package/dist/types/src/controllers/storage.d.ts +0 -171
  175. package/dist/types/src/index.d.ts +0 -4
  176. package/dist/types/src/rebase_context.d.ts +0 -122
  177. package/dist/types/src/types/auth_adapter.d.ts +0 -301
  178. package/dist/types/src/types/backend.d.ts +0 -536
  179. package/dist/types/src/types/backend_hooks.d.ts +0 -172
  180. package/dist/types/src/types/builders.d.ts +0 -15
  181. package/dist/types/src/types/chips.d.ts +0 -5
  182. package/dist/types/src/types/collections.d.ts +0 -941
  183. package/dist/types/src/types/component_ref.d.ts +0 -47
  184. package/dist/types/src/types/cron.d.ts +0 -102
  185. package/dist/types/src/types/data_source.d.ts +0 -64
  186. package/dist/types/src/types/database_adapter.d.ts +0 -94
  187. package/dist/types/src/types/entities.d.ts +0 -145
  188. package/dist/types/src/types/entity_actions.d.ts +0 -104
  189. package/dist/types/src/types/entity_callbacks.d.ts +0 -173
  190. package/dist/types/src/types/entity_link_builder.d.ts +0 -7
  191. package/dist/types/src/types/entity_overrides.d.ts +0 -10
  192. package/dist/types/src/types/entity_views.d.ts +0 -87
  193. package/dist/types/src/types/export_import.d.ts +0 -21
  194. package/dist/types/src/types/formex.d.ts +0 -40
  195. package/dist/types/src/types/index.d.ts +0 -28
  196. package/dist/types/src/types/locales.d.ts +0 -4
  197. package/dist/types/src/types/modify_collections.d.ts +0 -5
  198. package/dist/types/src/types/plugins.d.ts +0 -282
  199. package/dist/types/src/types/properties.d.ts +0 -1181
  200. package/dist/types/src/types/property_config.d.ts +0 -74
  201. package/dist/types/src/types/relations.d.ts +0 -336
  202. package/dist/types/src/types/slots.d.ts +0 -262
  203. package/dist/types/src/types/translations.d.ts +0 -900
  204. package/dist/types/src/types/user_management_delegate.d.ts +0 -86
  205. package/dist/types/src/types/websockets.d.ts +0 -78
  206. package/dist/types/src/users/index.d.ts +0 -1
  207. package/dist/types/src/users/user.d.ts +0 -50
  208. package/src/auth/admin-routes.ts +0 -534
  209. package/test/admin-routes.test.ts +0 -544
  210. package/test/backend-hooks-admin.test.ts +0 -388
  211. package/test.ts +0 -6
  212. /package/dist/{server-core/src/api → api}/ast-schema-editor.d.ts +0 -0
  213. /package/dist/{server-core/src/api → api}/collections_for_test/callbacks_test_collection.d.ts +0 -0
  214. /package/dist/{server-core/src/api → api}/graphql/graphql-schema-generator.d.ts +0 -0
  215. /package/dist/{server-core/src/api → api}/graphql/index.d.ts +0 -0
  216. /package/dist/{server-core/src/api → api}/index.d.ts +0 -0
  217. /package/dist/{server-core/src/api → api}/logs-routes.d.ts +0 -0
  218. /package/dist/{server-core/src/api → api}/openapi-generator.d.ts +0 -0
  219. /package/dist/{server-core/src/api → api}/rest/index.d.ts +0 -0
  220. /package/dist/{server-core/src/api → api}/rest/query-parser.d.ts +0 -0
  221. /package/dist/{server-core/src/api → api}/schema-editor-routes.d.ts +0 -0
  222. /package/dist/{server-core/src/api → api}/server.d.ts +0 -0
  223. /package/dist/{server-core/src/auth → auth}/adapter-middleware.d.ts +0 -0
  224. /package/dist/{server-core/src/auth → auth}/api-keys/api-key-middleware.d.ts +0 -0
  225. /package/dist/{server-core/src/auth → auth}/api-keys/api-key-permission-guard.d.ts +0 -0
  226. /package/dist/{server-core/src/auth → auth}/api-keys/api-key-routes.d.ts +0 -0
  227. /package/dist/{server-core/src/auth → auth}/api-keys/api-key-store.d.ts +0 -0
  228. /package/dist/{server-core/src/auth → auth}/api-keys/api-key-types.d.ts +0 -0
  229. /package/dist/{server-core/src/auth → auth}/apple-oauth.d.ts +0 -0
  230. /package/dist/{server-core/src/auth → auth}/bitbucket-oauth.d.ts +0 -0
  231. /package/dist/{server-core/src/auth → auth}/crypto-utils.d.ts +0 -0
  232. /package/dist/{server-core/src/auth → auth}/custom-auth-adapter.d.ts +0 -0
  233. /package/dist/{server-core/src/auth → auth}/discord-oauth.d.ts +0 -0
  234. /package/dist/{server-core/src/auth → auth}/facebook-oauth.d.ts +0 -0
  235. /package/dist/{server-core/src/auth → auth}/github-oauth.d.ts +0 -0
  236. /package/dist/{server-core/src/auth → auth}/gitlab-oauth.d.ts +0 -0
  237. /package/dist/{server-core/src/auth → auth}/google-oauth.d.ts +0 -0
  238. /package/dist/{server-core/src/auth → auth}/interfaces.d.ts +0 -0
  239. /package/dist/{server-core/src/auth → auth}/linkedin-oauth.d.ts +0 -0
  240. /package/dist/{server-core/src/auth → auth}/mfa.d.ts +0 -0
  241. /package/dist/{server-core/src/auth → auth}/microsoft-oauth.d.ts +0 -0
  242. /package/dist/{server-core/src/auth → auth}/middleware.d.ts +0 -0
  243. /package/dist/{server-core/src/auth → auth}/password.d.ts +0 -0
  244. /package/dist/{server-core/src/auth → auth}/rate-limiter.d.ts +0 -0
  245. /package/dist/{server-core/src/auth → auth}/rls-scope.d.ts +0 -0
  246. /package/dist/{server-core/src/auth → auth}/routes.d.ts +0 -0
  247. /package/dist/{server-core/src/auth → auth}/slack-oauth.d.ts +0 -0
  248. /package/dist/{server-core/src/auth → auth}/spotify-oauth.d.ts +0 -0
  249. /package/dist/{server-core/src/auth → auth}/twitter-oauth.d.ts +0 -0
  250. /package/dist/{server-core/src/collections → collections}/BackendCollectionRegistry.d.ts +0 -0
  251. /package/dist/{server-core/src/collections → collections}/loader.d.ts +0 -0
  252. /package/dist/{server-core/src/cron → cron}/cron-loader.d.ts +0 -0
  253. /package/dist/{server-core/src/cron → cron}/cron-routes.d.ts +0 -0
  254. /package/dist/{server-core/src/cron → cron}/cron-store.d.ts +0 -0
  255. /package/dist/{server-core/src/cron → cron}/index.d.ts +0 -0
  256. /package/dist/{server-core/src/db → db}/interfaces.d.ts +0 -0
  257. /package/dist/{server-core/src/email → email}/index.d.ts +0 -0
  258. /package/dist/{server-core/src/email → email}/smtp-email-service.d.ts +0 -0
  259. /package/dist/{server-core/src/email → email}/templates.d.ts +0 -0
  260. /package/dist/{server-core/src/email → email}/types.d.ts +0 -0
  261. /package/dist/{server-core/src/functions → functions}/function-loader.d.ts +0 -0
  262. /package/dist/{server-core/src/functions → functions}/function-routes.d.ts +0 -0
  263. /package/dist/{server-core/src/functions → functions}/index.d.ts +0 -0
  264. /package/dist/{server-core/src/history → history}/history-routes.d.ts +0 -0
  265. /package/dist/{server-core/src/history → history}/index.d.ts +0 -0
  266. /package/dist/{server-core/src/serve-spa.d.ts → serve-spa.d.ts} +0 -0
  267. /package/dist/{server-core/src/services → services}/driver-registry.d.ts +0 -0
  268. /package/dist/{server-core/src/services → services}/webhook-service.d.ts +0 -0
  269. /package/dist/{server-core/src/singleton.d.ts → singleton.d.ts} +0 -0
  270. /package/dist/{server-core/src/storage → storage}/LocalStorageController.d.ts +0 -0
  271. /package/dist/{server-core/src/storage → storage}/S3StorageController.d.ts +0 -0
  272. /package/dist/{server-core/src/storage → storage}/index.d.ts +0 -0
  273. /package/dist/{server-core/src/storage → storage}/routes.d.ts +0 -0
  274. /package/dist/{server-core/src/storage → storage}/storage-registry.d.ts +0 -0
  275. /package/dist/{server-core/src/storage → storage}/tus-handler.d.ts +0 -0
  276. /package/dist/{server-core/src/storage → storage}/types.d.ts +0 -0
  277. /package/dist/{server-core/src/types → types}/index.d.ts +0 -0
  278. /package/dist/{server-core/src/utils → utils}/dev-port.d.ts +0 -0
  279. /package/dist/{server-core/src/utils → utils}/logger.d.ts +0 -0
  280. /package/dist/{server-core/src/utils → utils}/logging.d.ts +0 -0
  281. /package/dist/{server-core/src/utils → utils}/request-logger.d.ts +0 -0
  282. /package/dist/{server-core/src/utils → utils}/sql.d.ts +0 -0
@@ -1,534 +0,0 @@
1
- import { Hono } from "hono";
2
- import { ApiError, errorHandler } from "../api/errors";
3
- import type { AuthRepository } from "./interfaces";
4
- import { requireAuth, requireAdmin, createRequireAuth } from "./middleware";
5
- import type { AuthHooks } from "./auth-hooks";
6
- import { resolveAuthHooks } from "./auth-hooks";
7
- import { AuthModuleConfig } from "./routes";
8
- import type { BackendHooks, AdminUser, BackendHookContext } from "@rebasepro/types";
9
-
10
- interface AdminRouteOptions extends AuthModuleConfig {
11
- serviceKey?: string;
12
- /**
13
- * Callback to persistently mark bootstrap as completed.
14
- * Invoked after the first admin user is promoted via POST /admin/bootstrap.
15
- */
16
- setBootstrapCompleted?: () => Promise<void>;
17
- /**
18
- * Backend-level hooks for intercepting admin data.
19
- */
20
- hooks?: BackendHooks;
21
- /**
22
- * Auth hooks for customizing password hashing, credential
23
- * verification, lifecycle hooks, etc.
24
- */
25
- authHooks?: AuthHooks;
26
- }
27
- import { HonoEnv } from "../api/types";
28
- import { randomBytes, createHash } from "crypto";
29
- import { getUserInvitationTemplate, getPasswordResetTemplate } from "../email/templates";
30
-
31
- /**
32
- * Generate a cryptographically secure random password that meets strength requirements.
33
- */
34
- function generateSecurePassword(): string {
35
- const upper = "ABCDEFGHJKLMNPQRSTUVWXYZ";
36
- const lower = "abcdefghjkmnpqrstuvwxyz";
37
- const digits = "23456789";
38
- const all = upper + lower + digits;
39
-
40
- // Guarantee at least one of each required class
41
- const pick = (chars: string) => chars[Math.floor(Math.random() * chars.length)];
42
- const parts = [pick(upper), pick(lower), pick(digits)];
43
-
44
- // Fill remaining with random chars (16 total)
45
- for (let i = parts.length; i < 16; i++) {
46
- parts.push(pick(all));
47
- }
48
-
49
- // Shuffle
50
- for (let i = parts.length - 1; i > 0; i--) {
51
- const j = Math.floor(Math.random() * (i + 1));
52
- [parts[i], parts[j]] = [parts[j], parts[i]];
53
- }
54
- return parts.join("");
55
- }
56
-
57
- /**
58
- * Generate a secure random token
59
- */
60
- function generateSecureToken(): string {
61
- return randomBytes(40).toString("hex");
62
- }
63
-
64
- /**
65
- * Hash a token for database storage
66
- */
67
- function hashToken(token: string): string {
68
- return createHash("sha256").update(token).digest("hex");
69
- }
70
-
71
- /**
72
- * Create admin routes for user and role management
73
- */
74
- export function createAdminRoutes(config: AdminRouteOptions): Hono<HonoEnv> {
75
- const router = new Hono<HonoEnv>();
76
- const authRepo = config.authRepo;
77
- const { emailService, emailConfig, hooks, authHooks } = config;
78
- const ops = resolveAuthHooks(authHooks);
79
-
80
- /** Build a BackendHookContext from Hono's context object */
81
- function buildHookContext(c: { get: (key: string) => unknown }, method: BackendHookContext["method"]): BackendHookContext {
82
- const user = c.get("user") as { userId: string; roles?: string[] } | undefined;
83
- return {
84
- requestUser: user ? { userId: user.userId, roles: user.roles ?? [] } : undefined,
85
- method
86
- };
87
- }
88
-
89
- /** Apply users.afterRead hook to an AdminUser, returning null to filter out */
90
- async function applyUserAfterRead(user: AdminUser, ctx: BackendHookContext): Promise<AdminUser | null> {
91
- if (!hooks?.users?.afterRead) return user;
92
- return hooks.users.afterRead(user, ctx);
93
- }
94
-
95
- /** Apply users.afterRead hook to an array and filter nulls */
96
- async function applyUserAfterReadBatch(users: AdminUser[], ctx: BackendHookContext): Promise<AdminUser[]> {
97
- if (!hooks?.users?.afterRead) return users;
98
- const results = await Promise.all(users.map(u => applyUserAfterRead(u, ctx)));
99
- return results.filter((u): u is AdminUser => u !== null);
100
- }
101
-
102
-
103
-
104
- /** Convert a DB user record + role IDs into the AdminUser API shape */
105
- function toAdminUser(u: { id: string; email: string; displayName?: string | null; photoUrl?: string | null; createdAt?: Date | string; updatedAt?: Date | string }, roles: string[]): AdminUser {
106
- return {
107
- uid: u.id,
108
- email: u.email,
109
- displayName: u.displayName ?? null,
110
- photoURL: u.photoUrl ?? null,
111
- provider: "custom",
112
- roles,
113
- createdAt: u.createdAt instanceof Date ? u.createdAt.toISOString() : (u.createdAt ?? new Date().toISOString()),
114
- updatedAt: u.updatedAt instanceof Date ? u.updatedAt.toISOString() : (u.updatedAt ?? new Date().toISOString())
115
- };
116
- }
117
-
118
- // Attach Rebase error handler to ensure exceptions are correctly formatted
119
- // instead of caught by Hono's default error handler from the sub-router.
120
- router.onError(errorHandler);
121
-
122
- // Apply auth middleware to all routes (service-key-aware when configured)
123
- router.use("/*", createRequireAuth({ serviceKey: config.serviceKey }));
124
-
125
- /**
126
- * POST /admin/bootstrap
127
- *
128
- * One-time endpoint to promote the calling user to admin.
129
- * Guarded by three layers:
130
- * 1. Authentication (handled by middleware above)
131
- * 2. Persistent `bootstrap_completed` flag (when `setBootstrapCompleted` is provided)
132
- * 3. Database check — no existing admin users
133
- *
134
- * Once invoked successfully the persistent flag is set, permanently disabling
135
- * this endpoint even if all admin users are later deleted.
136
- */
137
- router.post("/bootstrap", async (c) => {
138
- const user = c.get("user");
139
- if (!user || typeof user !== "object") {
140
- throw ApiError.unauthorized("Not authenticated");
141
- }
142
-
143
- // ── Guard 1: persistent flag ──────────────────────────────────
144
- if (config.isBootstrapCompleted) {
145
- const alreadyDone = await config.isBootstrapCompleted();
146
- if (alreadyDone) {
147
- throw ApiError.forbidden("Bootstrap has already been completed.", "BOOTSTRAP_COMPLETED");
148
- }
149
- }
150
-
151
- // ── Guard 2: no existing admin users ─────────────────────────
152
- const users = await authRepo.listUsers();
153
- let hasAdmin = false;
154
-
155
- for (const u of users) {
156
- const roles = await authRepo.getUserRoleIds(u.id);
157
- if (roles.includes("admin")) {
158
- hasAdmin = true;
159
- break;
160
- }
161
- }
162
-
163
- if (hasAdmin) {
164
- throw ApiError.forbidden("Admin users already exist. Bootstrap not allowed.", "BOOTSTRAP_COMPLETED");
165
- }
166
-
167
- // ── Promote caller ───────────────────────────────────────────
168
- const userId = "userId" in user ? user.userId : undefined;
169
- if (!userId) {
170
- throw ApiError.unauthorized("User ID not found in auth context");
171
- }
172
- const caller = await authRepo.getUserById(userId);
173
- if (!caller) {
174
- throw ApiError.notFound("Authenticated user does not exist in the database. Please sign out and sign in/register again.", "USER_NOT_FOUND");
175
- }
176
- await authRepo.setUserRoles(userId, ["admin"]);
177
-
178
- // ── Set persistent flag ──────────────────────────────────────
179
- if (config.setBootstrapCompleted) {
180
- await config.setBootstrapCompleted();
181
- }
182
-
183
- return c.json({
184
- success: true,
185
- message: "You are now an admin",
186
- user: {
187
- uid: userId,
188
- roles: ["admin"]
189
- }
190
- });
191
- });
192
-
193
- router.get("/users", requireAdmin, async (c) => {
194
- const limitParam = c.req.query("limit");
195
- const offsetParam = c.req.query("offset");
196
- const search = c.req.query("search");
197
- const orderBy = c.req.query("orderBy");
198
- const orderDir = c.req.query("orderDir") as "asc" | "desc" | undefined;
199
- const hookCtx = buildHookContext(c, "GET");
200
-
201
- const limit = limitParam ? parseInt(limitParam, 10) : 25;
202
- const offset = offsetParam ? parseInt(offsetParam, 10) : 0;
203
-
204
- const result = await authRepo.listUsersPaginated({
205
- limit,
206
- offset,
207
- search: search || undefined,
208
- orderBy: orderBy || undefined,
209
- orderDir: orderDir || undefined,
210
- roleId: c.req.query("role") || undefined
211
- });
212
-
213
- let usersWithRoles: AdminUser[] = await Promise.all(
214
- result.users.map(async (u) => {
215
- const roles = await authRepo.getUserRoleIds(u.id);
216
- return toAdminUser(u, roles);
217
- })
218
- );
219
-
220
- usersWithRoles = await applyUserAfterReadBatch(usersWithRoles, hookCtx);
221
-
222
- return c.json({
223
- users: usersWithRoles,
224
- total: result.total,
225
- limit: result.limit,
226
- offset: result.offset
227
- });
228
- });
229
-
230
- router.get("/users/:userId", requireAdmin, async (c) => {
231
- const userId = c.req.param("userId");
232
- const result = await authRepo.getUserWithRoles(userId);
233
-
234
- if (!result) {
235
- throw ApiError.notFound("User not found");
236
- }
237
-
238
- const hookCtx = buildHookContext(c, "GET");
239
- let adminUser: AdminUser | null = toAdminUser(result.user, result.roles.map(r => r.id));
240
-
241
- adminUser = await applyUserAfterRead(adminUser, hookCtx);
242
- if (!adminUser) {
243
- throw ApiError.notFound("User not found");
244
- }
245
-
246
- return c.json({ user: adminUser });
247
- });
248
-
249
- router.post("/users", requireAdmin, async (c) => {
250
- const body = await c.req.json();
251
- const { password } = body;
252
- let { email, displayName, roles } = body;
253
-
254
- if (!email) {
255
- throw ApiError.badRequest("Email is required", "INVALID_INPUT");
256
- }
257
-
258
- // Apply beforeSave hook
259
- const hookCtx = buildHookContext(c, "POST");
260
- if (hooks?.users?.beforeSave) {
261
- const hooked = await hooks.users.beforeSave({ email, displayName, roles }, hookCtx);
262
- email = hooked.email ?? email;
263
- displayName = hooked.displayName ?? displayName;
264
- roles = hooked.roles ?? roles;
265
- }
266
-
267
- const existing = await authRepo.getUserByEmail(email);
268
- if (existing) {
269
- throw ApiError.conflict("Email already exists", "EMAIL_EXISTS");
270
- }
271
-
272
- // Use provided password or auto-generate one
273
- const clearPassword = password || generateSecurePassword();
274
-
275
- const validation = ops.validatePasswordStrength(clearPassword);
276
- if (!validation.valid) {
277
- throw ApiError.badRequest(validation.errors.join(". "), "WEAK_PASSWORD");
278
- }
279
- const passwordHash = await ops.hashPassword(clearPassword);
280
-
281
- const user = await authRepo.createUser({
282
- email: email.toLowerCase(),
283
- displayName: displayName || null,
284
- passwordHash
285
- });
286
-
287
- if (roles && Array.isArray(roles) && roles.length > 0) {
288
- await authRepo.setUserRoles(user.id, roles);
289
- } else if (config.defaultRole) {
290
- await authRepo.assignDefaultRole(user.id, config.defaultRole);
291
- }
292
-
293
- const userRoles = await authRepo.getUserRoleIds(user.id);
294
-
295
- // Determine if we can send an invitation email
296
- const isEmailConfigured = !!(emailService && emailService.isConfigured());
297
- let invitationSent = false;
298
- let temporaryPassword: string | undefined;
299
-
300
- if (isEmailConfigured && !password) {
301
- // Send invitation email via password-reset token flow
302
- try {
303
- const token = generateSecureToken();
304
- const tokenHash = hashToken(token);
305
- const expiresAt = new Date(Date.now() + 60 * 60 * 1000); // 1 hour
306
-
307
- await authRepo.createPasswordResetToken(user.id, tokenHash, expiresAt);
308
-
309
- const baseUrl = emailConfig?.resetPasswordUrl || "";
310
- const setPasswordUrl = `${baseUrl}/reset-password?token=${token}`;
311
-
312
- const appName = emailConfig?.appName || "Rebase";
313
- const templateFn = emailConfig?.templates?.userInvitation;
314
- const emailContent = templateFn
315
- ? templateFn(setPasswordUrl, { email: user.email,
316
- displayName: user.displayName })
317
- : getUserInvitationTemplate(setPasswordUrl, { email: user.email,
318
- displayName: user.displayName }, appName);
319
-
320
- await emailService!.send({
321
- to: user.email,
322
- subject: emailContent.subject,
323
- html: emailContent.html,
324
- text: emailContent.text
325
- });
326
- invitationSent = true;
327
- } catch (emailError: unknown) {
328
- console.error("Failed to send invitation email:", emailError instanceof Error ? emailError.message : emailError);
329
- // Fall back to returning the temporary password
330
- temporaryPassword = clearPassword;
331
- }
332
- } else if (!password) {
333
- // No email service — return the auto-generated password one-time
334
- temporaryPassword = clearPassword;
335
- }
336
- // If admin provided a password explicitly, don't return it or send email
337
-
338
- const createdAdminUser: AdminUser = toAdminUser(user, userRoles);
339
-
340
- // Fire afterSave hook (fire-and-forget for side-effects)
341
- if (hooks?.users?.afterSave) {
342
- Promise.resolve(hooks.users.afterSave(createdAdminUser, hookCtx)).catch(err => {
343
- console.error("[BackendHooks] users.afterSave error:", err instanceof Error ? err.message : err);
344
- });
345
- }
346
-
347
- return c.json({
348
- user: createdAdminUser,
349
- invitationSent,
350
- ...(temporaryPassword ? { temporaryPassword } : {})
351
- }, 201);
352
- });
353
-
354
- router.post("/users/:userId/reset-password", requireAdmin, async (c) => {
355
- const userId = c.req.param("userId");
356
- const existing = await authRepo.getUserById(userId);
357
- if (!existing) {
358
- throw ApiError.notFound("User not found");
359
- }
360
-
361
- const isEmailConfigured = !!(emailService && emailService.isConfigured());
362
- let invitationSent = false;
363
- let temporaryPassword: string | undefined;
364
-
365
- if (isEmailConfigured) {
366
- try {
367
- const token = generateSecureToken();
368
- const tokenHash = hashToken(token);
369
- const expiresAt = new Date(Date.now() + 60 * 60 * 1000); // 1 hour
370
-
371
- await authRepo.createPasswordResetToken(existing.id, tokenHash, expiresAt);
372
-
373
- const baseUrl = emailConfig?.resetPasswordUrl || "";
374
- const setPasswordUrl = `${baseUrl}/reset-password?token=${token}`;
375
-
376
- const appName = emailConfig?.appName || "Rebase";
377
- const templateFn = emailConfig?.templates?.passwordReset;
378
- const emailContent = templateFn
379
- ? templateFn(setPasswordUrl, { email: existing.email,
380
- displayName: existing.displayName })
381
- : getPasswordResetTemplate(setPasswordUrl, { email: existing.email,
382
- displayName: existing.displayName }, appName);
383
-
384
- await emailService!.send({
385
- to: existing.email,
386
- subject: emailContent.subject,
387
- html: emailContent.html,
388
- text: emailContent.text
389
- });
390
- invitationSent = true;
391
- } catch (emailError: unknown) {
392
- console.error("Failed to send reset email:", emailError instanceof Error ? emailError.message : emailError);
393
- // Fall back to returning the temporary password
394
- const clearPassword = generateSecurePassword();
395
- const passwordHash = await ops.hashPassword(clearPassword);
396
- await authRepo.updatePassword(existing.id, passwordHash);
397
- temporaryPassword = clearPassword;
398
- }
399
- } else {
400
- // No email service — generate password, set it, and return one-time
401
- const clearPassword = generateSecurePassword();
402
- const passwordHash = await ops.hashPassword(clearPassword);
403
- await authRepo.updatePassword(existing.id, passwordHash);
404
- temporaryPassword = clearPassword;
405
- }
406
-
407
- const userRoles = await authRepo.getUserRoleIds(existing.id);
408
-
409
- return c.json({
410
- user: {
411
- uid: existing.id,
412
- email: existing.email,
413
- displayName: existing.displayName,
414
- roles: userRoles
415
- },
416
- invitationSent,
417
- ...(temporaryPassword ? { temporaryPassword } : {})
418
- }, 200);
419
- });
420
-
421
- router.put("/users/:userId", requireAdmin, async (c) => {
422
- const userId = c.req.param("userId");
423
- const body = await c.req.json();
424
- const { password } = body;
425
- let { email, displayName, roles } = body;
426
-
427
- const existing = await authRepo.getUserById(userId);
428
- if (!existing) {
429
- throw ApiError.notFound("User not found");
430
- }
431
-
432
- // Apply beforeSave hook
433
- const hookCtx = buildHookContext(c, "PUT");
434
- if (hooks?.users?.beforeSave) {
435
- const hooked = await hooks.users.beforeSave({ email, displayName, roles }, hookCtx);
436
- email = hooked.email ?? email;
437
- displayName = hooked.displayName ?? displayName;
438
- roles = hooked.roles ?? roles;
439
- }
440
-
441
- const updates: Record<string, unknown> = {};
442
- if (email !== undefined) updates.email = email.toLowerCase();
443
- if (displayName !== undefined) updates.displayName = displayName;
444
-
445
- if (password) {
446
- const validation = ops.validatePasswordStrength(password);
447
- if (!validation.valid) {
448
- throw ApiError.badRequest(validation.errors.join(". "), "WEAK_PASSWORD");
449
- }
450
- updates.passwordHash = await ops.hashPassword(password);
451
- }
452
-
453
- if (Object.keys(updates).length > 0) {
454
- await authRepo.updateUser(userId, updates);
455
- }
456
-
457
- if (roles !== undefined && Array.isArray(roles)) {
458
- const currentRoles = await authRepo.getUserRoleIds(userId);
459
- const wasAdmin = currentRoles.includes("admin");
460
- const willBeAdmin = roles.includes("admin");
461
-
462
- if (wasAdmin && !willBeAdmin) {
463
- const adminUsers = await authRepo.listUsersPaginated({
464
- roleId: "admin",
465
- limit: 1
466
- });
467
- if (adminUsers.total <= 1) {
468
- throw ApiError.forbidden("Cannot demote the last administrator", "LAST_ADMIN");
469
- }
470
- }
471
- await authRepo.setUserRoles(userId, roles);
472
- }
473
-
474
- const result = await authRepo.getUserWithRoles(userId);
475
-
476
- const updatedAdminUser: AdminUser = toAdminUser(result!.user, result!.roles.map(r => r.id));
477
-
478
- // Fire afterSave hook (fire-and-forget)
479
- if (hooks?.users?.afterSave) {
480
- Promise.resolve(hooks.users.afterSave(updatedAdminUser, hookCtx)).catch(err => {
481
- console.error("[BackendHooks] users.afterSave error:", err instanceof Error ? err.message : err);
482
- });
483
- }
484
-
485
- return c.json({ user: updatedAdminUser });
486
- });
487
-
488
- router.delete("/users/:userId", requireAdmin, async (c) => {
489
- const userId = c.req.param("userId");
490
- const user = c.get("user");
491
-
492
- const currentUserId = user && typeof user === "object" && "userId" in user ? user.userId : undefined;
493
- if (currentUserId === userId) {
494
- throw ApiError.badRequest("Cannot delete your own account", "SELF_DELETE");
495
- }
496
-
497
- const existing = await authRepo.getUserById(userId);
498
- if (!existing) {
499
- throw ApiError.notFound("User not found");
500
- }
501
-
502
- const roles = await authRepo.getUserRoleIds(userId);
503
- if (roles.includes("admin")) {
504
- const adminUsers = await authRepo.listUsersPaginated({
505
- roleId: "admin",
506
- limit: 1
507
- });
508
- if (adminUsers.total <= 1) {
509
- throw ApiError.forbidden("Cannot delete the last administrator", "LAST_ADMIN");
510
- }
511
- }
512
-
513
- // Apply beforeDelete hook (throw to abort)
514
- const hookCtx = buildHookContext(c, "DELETE");
515
- if (hooks?.users?.beforeDelete) {
516
- await hooks.users.beforeDelete(userId, hookCtx);
517
- }
518
-
519
- await authRepo.deleteUser(userId);
520
-
521
- // Fire afterDelete hook (fire-and-forget)
522
- if (hooks?.users?.afterDelete) {
523
- Promise.resolve(hooks.users.afterDelete(userId, hookCtx)).catch(err => {
524
- console.error("[BackendHooks] users.afterDelete error:", err instanceof Error ? err.message : err);
525
- });
526
- }
527
-
528
- return c.json({ success: true });
529
- });
530
-
531
-
532
-
533
- return router;
534
- }