@rebasepro/server-core 0.5.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 (279) hide show
  1. package/dist/{server-core/src/api → api}/errors.d.ts +5 -2
  2. package/dist/{server-core/src/api → api}/rest/api-generator.d.ts +8 -2
  3. package/dist/{server-core/src/api → api}/types.d.ts +2 -0
  4. package/dist/auth/admin-user-ops.d.ts +79 -0
  5. package/dist/{server-core/src/auth → auth}/api-keys/index.d.ts +2 -2
  6. package/dist/{server-core/src/auth → auth}/auth-hooks.d.ts +37 -9
  7. package/dist/{server-core/src/auth → auth}/builtin-auth-adapter.d.ts +4 -4
  8. package/dist/{server-core/src/auth → auth}/index.d.ts +5 -2
  9. package/dist/auth/mfa-routes.d.ts +6 -0
  10. package/dist/auth/reset-password-admin.d.ts +29 -0
  11. package/dist/auth/session-routes.d.ts +25 -0
  12. package/dist/backend-CIxN4FVm.js +15 -0
  13. package/dist/backend-CIxN4FVm.js.map +1 -0
  14. package/dist/chunk-Dze3rakg.js +42 -0
  15. package/dist/{server-core/src/cron → cron}/cron-scheduler.d.ts +1 -1
  16. package/dist/dist-CZKP-Xz4.js +832 -0
  17. package/dist/dist-CZKP-Xz4.js.map +1 -0
  18. package/dist/env.d.ts +102 -0
  19. package/dist/from-VbwD7xRf.js +3849 -0
  20. package/dist/from-VbwD7xRf.js.map +1 -0
  21. package/dist/{server-core/src/index.d.ts → index.d.ts} +1 -0
  22. package/dist/index.es.js +60867 -50462
  23. package/dist/index.es.js.map +1 -1
  24. package/dist/index.umd.js +73151 -51298
  25. package/dist/index.umd.js.map +1 -1
  26. package/dist/init/docs.d.ts +4 -0
  27. package/dist/init/health.d.ts +2 -0
  28. package/dist/init/middlewares.d.ts +10 -0
  29. package/dist/init/shutdown.d.ts +11 -0
  30. package/dist/init/storage.d.ts +5 -0
  31. package/dist/{server-core/src/init.d.ts → init.d.ts} +4 -4
  32. package/dist/jwt-DHcQRGC3.js +4168 -0
  33. package/dist/jwt-DHcQRGC3.js.map +1 -0
  34. package/dist/logger-BYU66ENZ.js +94 -0
  35. package/dist/logger-BYU66ENZ.js.map +1 -0
  36. package/dist/ms-BeBSuOXN.js +125 -0
  37. package/dist/ms-BeBSuOXN.js.map +1 -0
  38. package/dist/multipart-parser-CedBDOeC.js +299 -0
  39. package/dist/multipart-parser-CedBDOeC.js.map +1 -0
  40. package/dist/src-COaj0G3P.js +1182 -0
  41. package/dist/src-COaj0G3P.js.map +1 -0
  42. package/dist/{server-core/src/storage → storage}/image-transform.d.ts +3 -1
  43. package/dist/utils/request-id.d.ts +4 -0
  44. package/package.json +29 -29
  45. package/src/api/collections_for_test/callbacks_test_collection.ts +7 -6
  46. package/src/api/errors.ts +75 -19
  47. package/src/api/graphql/graphql-schema-generator.ts +15 -10
  48. package/src/api/logs-routes.ts +5 -2
  49. package/src/api/rest/api-generator-count.test.ts +21 -10
  50. package/src/api/rest/api-generator.ts +83 -24
  51. package/src/api/rest/query-parser.ts +18 -12
  52. package/src/api/server.ts +12 -1
  53. package/src/api/types.ts +2 -0
  54. package/src/auth/adapter-middleware.ts +17 -10
  55. package/src/auth/admin-user-ops.ts +236 -0
  56. package/src/auth/api-keys/api-key-middleware.ts +15 -9
  57. package/src/auth/api-keys/api-key-permission-guard.ts +1 -1
  58. package/src/auth/api-keys/api-key-routes.ts +3 -3
  59. package/src/auth/api-keys/api-key-store.ts +14 -8
  60. package/src/auth/api-keys/index.ts +2 -2
  61. package/src/auth/apple-oauth.ts +4 -3
  62. package/src/auth/auth-hooks.ts +50 -10
  63. package/src/auth/bitbucket-oauth.ts +5 -4
  64. package/src/auth/builtin-auth-adapter.ts +60 -39
  65. package/src/auth/custom-auth-adapter.ts +4 -5
  66. package/src/auth/discord-oauth.ts +5 -4
  67. package/src/auth/facebook-oauth.ts +5 -4
  68. package/src/auth/github-oauth.ts +6 -5
  69. package/src/auth/gitlab-oauth.ts +5 -4
  70. package/src/auth/google-oauth.ts +2 -1
  71. package/src/auth/index.ts +6 -2
  72. package/src/auth/jwt.ts +5 -4
  73. package/src/auth/linkedin-oauth.ts +4 -3
  74. package/src/auth/mfa-routes.ts +299 -0
  75. package/src/auth/mfa.ts +2 -1
  76. package/src/auth/microsoft-oauth.ts +5 -4
  77. package/src/auth/middleware.ts +5 -4
  78. package/src/auth/rate-limiter.ts +1 -1
  79. package/src/auth/reset-password-admin.ts +144 -0
  80. package/src/auth/rls-scope.ts +1 -1
  81. package/src/auth/routes.ts +52 -596
  82. package/src/auth/session-routes.ts +341 -0
  83. package/src/auth/slack-oauth.ts +6 -5
  84. package/src/auth/spotify-oauth.ts +5 -4
  85. package/src/auth/twitter-oauth.ts +4 -3
  86. package/src/collections/loader.ts +5 -4
  87. package/src/cron/cron-scheduler.test.ts +23 -11
  88. package/src/cron/cron-scheduler.ts +14 -8
  89. package/src/cron/cron-store.ts +1 -1
  90. package/src/email/smtp-email-service.ts +3 -2
  91. package/src/env.ts +11 -10
  92. package/src/index.ts +1 -0
  93. package/src/init/docs.ts +47 -0
  94. package/src/init/health.ts +37 -0
  95. package/src/init/middlewares.ts +61 -0
  96. package/src/init/shutdown.ts +56 -0
  97. package/src/init/storage.ts +57 -0
  98. package/src/init.ts +85 -279
  99. package/src/serve-spa.ts +12 -5
  100. package/src/services/driver-registry.ts +4 -3
  101. package/src/storage/S3StorageController.ts +1 -2
  102. package/src/storage/image-transform.ts +25 -10
  103. package/src/storage/routes.ts +30 -11
  104. package/src/storage/storage-registry.ts +4 -3
  105. package/src/storage/tus-handler.ts +12 -12
  106. package/src/utils/request-id.ts +40 -0
  107. package/src/utils/request-logger.ts +6 -0
  108. package/test/api-generator.test.ts +90 -2
  109. package/test/api-key-permission-guard.test.ts +24 -12
  110. package/test/auth-routes.test.ts +5 -3
  111. package/test/backend-hooks-data.test.ts +99 -30
  112. package/test/custom-auth-adapter.test.ts +16 -15
  113. package/test/email-templates.test.ts +10 -5
  114. package/test/env.test.ts +10 -10
  115. package/test/function-loader.test.ts +7 -4
  116. package/test/graphql-schema-generator.test.ts +554 -0
  117. package/test/jwt-security.test.ts +1 -1
  118. package/test/query-parser.test.ts +0 -1
  119. package/test/reset-password-admin.test.ts +113 -0
  120. package/test/singleton.test.ts +5 -5
  121. package/test/smtp-email-service.test.ts +21 -21
  122. package/test/webhook-service.test.ts +22 -11
  123. package/tsconfig.json +2 -0
  124. package/tsconfig.prod.json +3 -0
  125. package/vite.config.ts +5 -5
  126. package/dist/common/src/collections/CollectionRegistry.d.ts +0 -56
  127. package/dist/common/src/collections/default-collections.d.ts +0 -9
  128. package/dist/common/src/collections/index.d.ts +0 -2
  129. package/dist/common/src/data/buildRebaseData.d.ts +0 -14
  130. package/dist/common/src/data/query_builder.d.ts +0 -55
  131. package/dist/common/src/index.d.ts +0 -4
  132. package/dist/common/src/util/builders.d.ts +0 -57
  133. package/dist/common/src/util/callbacks.d.ts +0 -6
  134. package/dist/common/src/util/collections.d.ts +0 -11
  135. package/dist/common/src/util/common.d.ts +0 -2
  136. package/dist/common/src/util/conditions.d.ts +0 -26
  137. package/dist/common/src/util/entities.d.ts +0 -58
  138. package/dist/common/src/util/enums.d.ts +0 -3
  139. package/dist/common/src/util/index.d.ts +0 -16
  140. package/dist/common/src/util/navigation_from_path.d.ts +0 -34
  141. package/dist/common/src/util/navigation_utils.d.ts +0 -20
  142. package/dist/common/src/util/parent_references_from_path.d.ts +0 -6
  143. package/dist/common/src/util/paths.d.ts +0 -14
  144. package/dist/common/src/util/permissions.d.ts +0 -14
  145. package/dist/common/src/util/references.d.ts +0 -2
  146. package/dist/common/src/util/relations.d.ts +0 -22
  147. package/dist/common/src/util/resolutions.d.ts +0 -72
  148. package/dist/common/src/util/storage.d.ts +0 -24
  149. package/dist/index-Cr1D21av.js +0 -49
  150. package/dist/index-Cr1D21av.js.map +0 -1
  151. package/dist/server-core/src/auth/admin-routes.d.ts +0 -27
  152. package/dist/server-core/src/env.d.ts +0 -137
  153. package/dist/types/src/controllers/analytics_controller.d.ts +0 -7
  154. package/dist/types/src/controllers/auth.d.ts +0 -104
  155. package/dist/types/src/controllers/client.d.ts +0 -168
  156. package/dist/types/src/controllers/collection_registry.d.ts +0 -46
  157. package/dist/types/src/controllers/customization_controller.d.ts +0 -60
  158. package/dist/types/src/controllers/data.d.ts +0 -207
  159. package/dist/types/src/controllers/data_driver.d.ts +0 -218
  160. package/dist/types/src/controllers/database_admin.d.ts +0 -11
  161. package/dist/types/src/controllers/dialogs_controller.d.ts +0 -36
  162. package/dist/types/src/controllers/effective_role.d.ts +0 -4
  163. package/dist/types/src/controllers/email.d.ts +0 -36
  164. package/dist/types/src/controllers/index.d.ts +0 -18
  165. package/dist/types/src/controllers/local_config_persistence.d.ts +0 -20
  166. package/dist/types/src/controllers/navigation.d.ts +0 -225
  167. package/dist/types/src/controllers/registry.d.ts +0 -63
  168. package/dist/types/src/controllers/side_dialogs_controller.d.ts +0 -67
  169. package/dist/types/src/controllers/side_entity_controller.d.ts +0 -97
  170. package/dist/types/src/controllers/snackbar.d.ts +0 -24
  171. package/dist/types/src/controllers/storage.d.ts +0 -171
  172. package/dist/types/src/index.d.ts +0 -4
  173. package/dist/types/src/rebase_context.d.ts +0 -122
  174. package/dist/types/src/types/auth_adapter.d.ts +0 -301
  175. package/dist/types/src/types/backend.d.ts +0 -571
  176. package/dist/types/src/types/backend_hooks.d.ts +0 -172
  177. package/dist/types/src/types/builders.d.ts +0 -15
  178. package/dist/types/src/types/chips.d.ts +0 -5
  179. package/dist/types/src/types/collections.d.ts +0 -961
  180. package/dist/types/src/types/component_ref.d.ts +0 -47
  181. package/dist/types/src/types/cron.d.ts +0 -102
  182. package/dist/types/src/types/data_source.d.ts +0 -64
  183. package/dist/types/src/types/database_adapter.d.ts +0 -94
  184. package/dist/types/src/types/entities.d.ts +0 -145
  185. package/dist/types/src/types/entity_actions.d.ts +0 -104
  186. package/dist/types/src/types/entity_callbacks.d.ts +0 -173
  187. package/dist/types/src/types/entity_link_builder.d.ts +0 -7
  188. package/dist/types/src/types/entity_overrides.d.ts +0 -10
  189. package/dist/types/src/types/entity_views.d.ts +0 -87
  190. package/dist/types/src/types/export_import.d.ts +0 -21
  191. package/dist/types/src/types/formex.d.ts +0 -40
  192. package/dist/types/src/types/index.d.ts +0 -28
  193. package/dist/types/src/types/locales.d.ts +0 -4
  194. package/dist/types/src/types/modify_collections.d.ts +0 -5
  195. package/dist/types/src/types/plugins.d.ts +0 -282
  196. package/dist/types/src/types/properties.d.ts +0 -1173
  197. package/dist/types/src/types/property_config.d.ts +0 -74
  198. package/dist/types/src/types/relations.d.ts +0 -336
  199. package/dist/types/src/types/slots.d.ts +0 -262
  200. package/dist/types/src/types/translations.d.ts +0 -900
  201. package/dist/types/src/types/user_management_delegate.d.ts +0 -86
  202. package/dist/types/src/types/websockets.d.ts +0 -78
  203. package/dist/types/src/users/index.d.ts +0 -1
  204. package/dist/types/src/users/user.d.ts +0 -50
  205. package/src/auth/admin-routes.ts +0 -534
  206. package/test/admin-routes.test.ts +0 -544
  207. package/test/backend-hooks-admin.test.ts +0 -388
  208. /package/dist/{server-core/src/api → api}/ast-schema-editor.d.ts +0 -0
  209. /package/dist/{server-core/src/api → api}/collections_for_test/callbacks_test_collection.d.ts +0 -0
  210. /package/dist/{server-core/src/api → api}/graphql/graphql-schema-generator.d.ts +0 -0
  211. /package/dist/{server-core/src/api → api}/graphql/index.d.ts +0 -0
  212. /package/dist/{server-core/src/api → api}/index.d.ts +0 -0
  213. /package/dist/{server-core/src/api → api}/logs-routes.d.ts +0 -0
  214. /package/dist/{server-core/src/api → api}/openapi-generator.d.ts +0 -0
  215. /package/dist/{server-core/src/api → api}/rest/index.d.ts +0 -0
  216. /package/dist/{server-core/src/api → api}/rest/query-parser.d.ts +0 -0
  217. /package/dist/{server-core/src/api → api}/schema-editor-routes.d.ts +0 -0
  218. /package/dist/{server-core/src/api → api}/server.d.ts +0 -0
  219. /package/dist/{server-core/src/auth → auth}/adapter-middleware.d.ts +0 -0
  220. /package/dist/{server-core/src/auth → auth}/api-keys/api-key-middleware.d.ts +0 -0
  221. /package/dist/{server-core/src/auth → auth}/api-keys/api-key-permission-guard.d.ts +0 -0
  222. /package/dist/{server-core/src/auth → auth}/api-keys/api-key-routes.d.ts +0 -0
  223. /package/dist/{server-core/src/auth → auth}/api-keys/api-key-store.d.ts +0 -0
  224. /package/dist/{server-core/src/auth → auth}/api-keys/api-key-types.d.ts +0 -0
  225. /package/dist/{server-core/src/auth → auth}/apple-oauth.d.ts +0 -0
  226. /package/dist/{server-core/src/auth → auth}/bitbucket-oauth.d.ts +0 -0
  227. /package/dist/{server-core/src/auth → auth}/crypto-utils.d.ts +0 -0
  228. /package/dist/{server-core/src/auth → auth}/custom-auth-adapter.d.ts +0 -0
  229. /package/dist/{server-core/src/auth → auth}/discord-oauth.d.ts +0 -0
  230. /package/dist/{server-core/src/auth → auth}/facebook-oauth.d.ts +0 -0
  231. /package/dist/{server-core/src/auth → auth}/github-oauth.d.ts +0 -0
  232. /package/dist/{server-core/src/auth → auth}/gitlab-oauth.d.ts +0 -0
  233. /package/dist/{server-core/src/auth → auth}/google-oauth.d.ts +0 -0
  234. /package/dist/{server-core/src/auth → auth}/interfaces.d.ts +0 -0
  235. /package/dist/{server-core/src/auth → auth}/jwt.d.ts +0 -0
  236. /package/dist/{server-core/src/auth → auth}/linkedin-oauth.d.ts +0 -0
  237. /package/dist/{server-core/src/auth → auth}/mfa.d.ts +0 -0
  238. /package/dist/{server-core/src/auth → auth}/microsoft-oauth.d.ts +0 -0
  239. /package/dist/{server-core/src/auth → auth}/middleware.d.ts +0 -0
  240. /package/dist/{server-core/src/auth → auth}/password.d.ts +0 -0
  241. /package/dist/{server-core/src/auth → auth}/rate-limiter.d.ts +0 -0
  242. /package/dist/{server-core/src/auth → auth}/rls-scope.d.ts +0 -0
  243. /package/dist/{server-core/src/auth → auth}/routes.d.ts +0 -0
  244. /package/dist/{server-core/src/auth → auth}/slack-oauth.d.ts +0 -0
  245. /package/dist/{server-core/src/auth → auth}/spotify-oauth.d.ts +0 -0
  246. /package/dist/{server-core/src/auth → auth}/twitter-oauth.d.ts +0 -0
  247. /package/dist/{server-core/src/collections → collections}/BackendCollectionRegistry.d.ts +0 -0
  248. /package/dist/{server-core/src/collections → collections}/loader.d.ts +0 -0
  249. /package/dist/{server-core/src/cron → cron}/cron-loader.d.ts +0 -0
  250. /package/dist/{server-core/src/cron → cron}/cron-routes.d.ts +0 -0
  251. /package/dist/{server-core/src/cron → cron}/cron-store.d.ts +0 -0
  252. /package/dist/{server-core/src/cron → cron}/index.d.ts +0 -0
  253. /package/dist/{server-core/src/db → db}/interfaces.d.ts +0 -0
  254. /package/dist/{server-core/src/email → email}/index.d.ts +0 -0
  255. /package/dist/{server-core/src/email → email}/smtp-email-service.d.ts +0 -0
  256. /package/dist/{server-core/src/email → email}/templates.d.ts +0 -0
  257. /package/dist/{server-core/src/email → email}/types.d.ts +0 -0
  258. /package/dist/{server-core/src/functions → functions}/function-loader.d.ts +0 -0
  259. /package/dist/{server-core/src/functions → functions}/function-routes.d.ts +0 -0
  260. /package/dist/{server-core/src/functions → functions}/index.d.ts +0 -0
  261. /package/dist/{server-core/src/history → history}/history-routes.d.ts +0 -0
  262. /package/dist/{server-core/src/history → history}/index.d.ts +0 -0
  263. /package/dist/{server-core/src/serve-spa.d.ts → serve-spa.d.ts} +0 -0
  264. /package/dist/{server-core/src/services → services}/driver-registry.d.ts +0 -0
  265. /package/dist/{server-core/src/services → services}/webhook-service.d.ts +0 -0
  266. /package/dist/{server-core/src/singleton.d.ts → singleton.d.ts} +0 -0
  267. /package/dist/{server-core/src/storage → storage}/LocalStorageController.d.ts +0 -0
  268. /package/dist/{server-core/src/storage → storage}/S3StorageController.d.ts +0 -0
  269. /package/dist/{server-core/src/storage → storage}/index.d.ts +0 -0
  270. /package/dist/{server-core/src/storage → storage}/routes.d.ts +0 -0
  271. /package/dist/{server-core/src/storage → storage}/storage-registry.d.ts +0 -0
  272. /package/dist/{server-core/src/storage → storage}/tus-handler.d.ts +0 -0
  273. /package/dist/{server-core/src/storage → storage}/types.d.ts +0 -0
  274. /package/dist/{server-core/src/types → types}/index.d.ts +0 -0
  275. /package/dist/{server-core/src/utils → utils}/dev-port.d.ts +0 -0
  276. /package/dist/{server-core/src/utils → utils}/logger.d.ts +0 -0
  277. /package/dist/{server-core/src/utils → utils}/logging.d.ts +0 -0
  278. /package/dist/{server-core/src/utils → utils}/request-logger.d.ts +0 -0
  279. /package/dist/{server-core/src/utils → utils}/sql.d.ts +0 -0
@@ -0,0 +1,236 @@
1
+ /**
2
+ * Admin User Operations
3
+ *
4
+ * Shared utilities and orchestration for admin-initiated user management
5
+ * (user creation via REST API, password reset via admin panel).
6
+ *
7
+ * Hook resolution order:
8
+ * 1. Collection-level hook (`auth.onCreateUser` on the collection) — closest to the data
9
+ * 2. Backend-level hook (`AuthHooks.onAdminCreateUser`) — global override
10
+ * 3. Built-in default — framework fallback
11
+ */
12
+
13
+ import { randomBytes, createHash, randomInt } from "node:crypto";
14
+ import type { AuthRepository } from "./interfaces";
15
+ import type { EmailService, EmailConfig } from "../email";
16
+ import type { ResolvedAuthHooks } from "./auth-hooks";
17
+ import type { AuthCollectionConfig, AuthCollectionContext } from "@rebasepro/types";
18
+ import { getPasswordResetTemplate } from "../email/templates";
19
+ import { logger } from "../utils/logger";
20
+
21
+ // ─── Shared Crypto Utilities ────────────────────────────────────────────────
22
+
23
+ /**
24
+ * Generate a cryptographically secure random password that meets strength requirements.
25
+ *
26
+ * 16 characters, guaranteed at least one uppercase, one lowercase, one digit.
27
+ * Ambiguous characters (0, O, 1, l, I) are excluded.
28
+ */
29
+ export function generateSecurePassword(): string {
30
+ const upper = "ABCDEFGHJKLMNPQRSTUVWXYZ";
31
+ const lower = "abcdefghjkmnpqrstuvwxyz";
32
+ const digits = "23456789";
33
+ const all = upper + lower + digits;
34
+
35
+ const pick = (chars: string) => chars[randomInt(chars.length)];
36
+ const parts = [pick(upper), pick(lower), pick(digits)];
37
+
38
+ for (let i = parts.length; i < 16; i++) {
39
+ parts.push(pick(all));
40
+ }
41
+
42
+ // Shuffle
43
+ for (let i = parts.length - 1; i > 0; i--) {
44
+ const j = randomInt(i + 1);
45
+ [parts[i], parts[j]] = [parts[j], parts[i]];
46
+ }
47
+ return parts.join("");
48
+ }
49
+
50
+ /**
51
+ * Generate a cryptographically secure random token (80 hex characters).
52
+ */
53
+ export function generateSecureToken(): string {
54
+ return randomBytes(40).toString("hex");
55
+ }
56
+
57
+ /**
58
+ * Hash a token for database storage using SHA-256.
59
+ */
60
+ export function hashToken(token: string): string {
61
+ return createHash("sha256").update(token).digest("hex");
62
+ }
63
+
64
+ // ─── Admin User Creation ────────────────────────────────────────────────────
65
+
66
+ /**
67
+ * Context needed by admin user creation / password reset operations.
68
+ */
69
+ export interface AdminUserContext {
70
+ authRepo: AuthRepository;
71
+ emailService?: EmailService;
72
+ emailConfig?: EmailConfig;
73
+ resolvedHooks: ResolvedAuthHooks;
74
+ /** The parsed auth config from the collection (if `auth` is an object, not just `true`). */
75
+ collectionAuthConfig?: AuthCollectionConfig;
76
+ }
77
+
78
+ /**
79
+ * Result of preparing user values for admin-initiated creation.
80
+ */
81
+ export interface AdminUserPrepareResult {
82
+ /** Values ready for `driver.saveEntity()`. */
83
+ values: Record<string, unknown>;
84
+ /** The cleartext password (for returning to admin or sending via email). */
85
+ clearPassword?: string;
86
+ /** Whether the hook already handled the invitation email. */
87
+ hookHandledEmail: boolean;
88
+ /** Whether an invitation was sent (only relevant when hookHandledEmail is true). */
89
+ invitationSent: boolean;
90
+ }
91
+
92
+ /**
93
+ * Build the `AuthCollectionContext` facade from server internals.
94
+ *
95
+ * This is the simplified context exposed to collection-level auth hooks,
96
+ * keeping them decoupled from internal interfaces like `AuthRepository`.
97
+ */
98
+ function buildCollectionContext(ctx: AdminUserContext): AuthCollectionContext {
99
+ const isEmailConfigured = !!(ctx.emailService && ctx.emailService.isConfigured());
100
+
101
+ return {
102
+ hashPassword: (password: string) => ctx.resolvedHooks.hashPassword(password),
103
+ sendEmail: isEmailConfigured
104
+ ? (options) => ctx.emailService!.send(options)
105
+ : undefined,
106
+ emailConfigured: isEmailConfigured,
107
+ appName: ctx.emailConfig?.appName || "Rebase",
108
+ resetPasswordUrl: ctx.emailConfig?.resetPasswordUrl || ""
109
+ };
110
+ }
111
+
112
+ /**
113
+ * Prepare user values for an admin-initiated user creation.
114
+ *
115
+ * Resolution order:
116
+ * 1. Collection-level `auth.onCreateUser` — closest to the data
117
+ * 2. Backend-level `AuthHooks.onAdminCreateUser` — global override
118
+ * 3. Built-in default — generate password → hash → normalize email
119
+ *
120
+ * The caller is responsible for persisting (via `driver.saveEntity()`).
121
+ */
122
+ export async function prepareAdminUserValues(
123
+ body: Record<string, unknown>,
124
+ ctx: AdminUserContext
125
+ ): Promise<AdminUserPrepareResult> {
126
+ const { resolvedHooks, collectionAuthConfig } = ctx;
127
+
128
+ // 1. Collection-level hook (closest to the data)
129
+ if (collectionAuthConfig?.onCreateUser) {
130
+ const collectionCtx = buildCollectionContext(ctx);
131
+ const hookResult = await collectionAuthConfig.onCreateUser(body, collectionCtx);
132
+ return {
133
+ values: hookResult.values,
134
+ clearPassword: hookResult.temporaryPassword,
135
+ hookHandledEmail: true,
136
+ invitationSent: hookResult.invitationSent ?? false
137
+ };
138
+ }
139
+
140
+ // 2. Backend-level hook (global override)
141
+ if (resolvedHooks.onAdminCreateUser) {
142
+ const hookResult = await resolvedHooks.onAdminCreateUser(body, {
143
+ authRepo: ctx.authRepo,
144
+ emailService: ctx.emailService,
145
+ emailConfig: ctx.emailConfig,
146
+ hashPassword: (password: string) => resolvedHooks.hashPassword(password)
147
+ });
148
+ return {
149
+ values: hookResult.values,
150
+ clearPassword: hookResult.temporaryPassword,
151
+ hookHandledEmail: true,
152
+ invitationSent: hookResult.invitationSent ?? false
153
+ };
154
+ }
155
+
156
+ // 3. Built-in default
157
+ const password = body.password as string | undefined;
158
+ const clearPassword = password || generateSecurePassword();
159
+ const passwordHash = await resolvedHooks.hashPassword(clearPassword);
160
+
161
+ const values = { ...body };
162
+ values.passwordHash = passwordHash;
163
+ if (values.email) {
164
+ values.email = (values.email as string).toLowerCase();
165
+ }
166
+ values.emailVerified = true;
167
+ delete values.password;
168
+
169
+ return {
170
+ values,
171
+ clearPassword: password ? undefined : clearPassword,
172
+ hookHandledEmail: false,
173
+ invitationSent: false
174
+ };
175
+ }
176
+
177
+ /**
178
+ * Handle post-creation work for admin-created users.
179
+ *
180
+ * Sends an invitation email (password-reset link) if email is configured
181
+ * and no explicit password was provided. Falls back to returning the
182
+ * temporary password if email fails or is not configured.
183
+ */
184
+ export async function finalizeAdminUserCreation(
185
+ entity: { id: string; values: Record<string, unknown> },
186
+ clearPassword: string | undefined,
187
+ ctx: AdminUserContext
188
+ ): Promise<{
189
+ temporaryPassword?: string;
190
+ invitationSent: boolean;
191
+ }> {
192
+ // If an explicit password was provided (clearPassword is undefined), nothing to do
193
+ if (!clearPassword) {
194
+ return { invitationSent: false };
195
+ }
196
+
197
+ const isEmailConfigured = !!(ctx.emailService && ctx.emailService.isConfigured());
198
+
199
+ if (isEmailConfigured) {
200
+ try {
201
+ const token = generateSecureToken();
202
+ const tokenHash = hashToken(token);
203
+ const expiresAt = new Date(Date.now() + 24 * 60 * 60 * 1000); // 24 hours
204
+
205
+ await ctx.authRepo.createPasswordResetToken(entity.id, tokenHash, expiresAt);
206
+
207
+ const baseUrl = ctx.emailConfig?.resetPasswordUrl || "";
208
+ const setPasswordUrl = `${baseUrl}/reset-password?token=${token}`;
209
+
210
+ const appName = ctx.emailConfig?.appName || "Rebase";
211
+ const templateFn = ctx.emailConfig?.templates?.passwordReset;
212
+ const emailContent = templateFn
213
+ ? templateFn(setPasswordUrl, { email: entity.values.email as string,
214
+ displayName: entity.values.displayName as string })
215
+ : getPasswordResetTemplate(setPasswordUrl, { email: entity.values.email as string,
216
+ displayName: entity.values.displayName as string }, appName);
217
+
218
+ await ctx.emailService!.send({
219
+ to: entity.values.email as string,
220
+ subject: emailContent.subject,
221
+ html: emailContent.html,
222
+ text: emailContent.text
223
+ });
224
+ return { invitationSent: true };
225
+ } catch (emailError: unknown) {
226
+ logger.error("Failed to send reset email", { error: emailError instanceof Error ? emailError.message : emailError });
227
+ // Fall back to returning the temporary password
228
+ return { temporaryPassword: clearPassword,
229
+ invitationSent: false };
230
+ }
231
+ }
232
+
233
+ // No email service — return the temporary password
234
+ return { temporaryPassword: clearPassword,
235
+ invitationSent: false };
236
+ }
@@ -20,6 +20,7 @@ import type { HonoEnv } from "../../api/types";
20
20
  import type { ApiKeyStore } from "./api-key-store";
21
21
  import type { ApiKeyMasked } from "./api-key-types";
22
22
  import { scopeDataDriver } from "../rls-scope";
23
+ import { logger } from "../../utils/logger";
23
24
 
24
25
  /**
25
26
  * Check whether a token looks like a Rebase API key.
@@ -55,7 +56,7 @@ export interface ApiKeyAuthOptions {
55
56
  export async function validateApiKey(
56
57
  c: Context<HonoEnv>,
57
58
  token: string,
58
- options: ApiKeyAuthOptions,
59
+ options: ApiKeyAuthOptions
59
60
  ): Promise<Response | true> {
60
61
  const { store, driver } = options;
61
62
 
@@ -64,27 +65,31 @@ export async function validateApiKey(
64
65
 
65
66
  if (!apiKey) {
66
67
  return c.json({
67
- error: { message: "Invalid API key", code: "UNAUTHORIZED" },
68
+ error: { message: "Invalid API key",
69
+ code: "UNAUTHORIZED" }
68
70
  }, 401);
69
71
  }
70
72
 
71
73
  // Check revocation
72
74
  if (apiKey.revoked_at) {
73
75
  return c.json({
74
- error: { message: "API key has been revoked", code: "UNAUTHORIZED" },
76
+ error: { message: "API key has been revoked",
77
+ code: "UNAUTHORIZED" }
75
78
  }, 401);
76
79
  }
77
80
 
78
81
  // Check expiration
79
82
  if (apiKey.expires_at && new Date(apiKey.expires_at) < new Date()) {
80
83
  return c.json({
81
- error: { message: "API key has expired", code: "UNAUTHORIZED" },
84
+ error: { message: "API key has expired",
85
+ code: "UNAUTHORIZED" }
82
86
  }, 401);
83
87
  }
84
88
 
85
89
  // Set user identity — API keys represent service accounts
86
90
  const userId = `api-key:${apiKey.id}`;
87
- c.set("user", { userId, roles: [] });
91
+ c.set("user", { userId,
92
+ roles: [] });
88
93
 
89
94
  // Expose masked key metadata for downstream permission checks
90
95
  const masked: ApiKeyMasked = {
@@ -98,7 +103,7 @@ export async function validateApiKey(
98
103
  updated_at: apiKey.updated_at,
99
104
  last_used_at: apiKey.last_used_at,
100
105
  expires_at: apiKey.expires_at,
101
- revoked_at: apiKey.revoked_at,
106
+ revoked_at: apiKey.revoked_at
102
107
  };
103
108
  // Store apiKey in the context for permission checking in api-generator
104
109
  c.set("apiKey", masked);
@@ -107,13 +112,14 @@ export async function validateApiKey(
107
112
  try {
108
113
  const scopedDriver = await scopeDataDriver(driver, {
109
114
  uid: userId,
110
- roles: ["service"],
115
+ roles: ["service"]
111
116
  });
112
117
  c.set("driver", scopedDriver);
113
118
  } catch (error) {
114
- console.error("[AUTH] RLS scoping failed for API key:", error);
119
+ logger.error("[AUTH] RLS scoping failed for API key", { error: error });
115
120
  return c.json({
116
- error: { message: "Internal authentication error", code: "INTERNAL_ERROR" },
121
+ error: { message: "Internal authentication error",
122
+ code: "INTERNAL_ERROR" }
117
123
  }, 500);
118
124
  }
119
125
 
@@ -52,7 +52,7 @@ export function httpMethodToOperation(method: string): ApiKeyOperation {
52
52
  export function isOperationAllowed(
53
53
  permissions: ApiKeyPermission[],
54
54
  collection: string,
55
- operation: ApiKeyOperation,
55
+ operation: ApiKeyOperation
56
56
  ): boolean {
57
57
  for (const perm of permissions) {
58
58
  const collectionMatch = perm.collection === "*" || perm.collection === collection;
@@ -68,7 +68,7 @@ export function createApiKeyRoutes(options: ApiKeyRouteOptions): Hono<HonoEnv> {
68
68
  if (!validatePermissions(permissions)) {
69
69
  throw ApiError.badRequest(
70
70
  "Permissions must be an array of { collection: string, operations: ('read' | 'write' | 'delete')[] }",
71
- "INVALID_INPUT",
71
+ "INVALID_INPUT"
72
72
  );
73
73
  }
74
74
 
@@ -97,7 +97,7 @@ export function createApiKeyRoutes(options: ApiKeyRouteOptions): Hono<HonoEnv> {
97
97
  name: name.trim(),
98
98
  permissions: permissions as ApiKeyPermission[],
99
99
  rate_limit: (rate_limit as number | null) ?? null,
100
- expires_at: (expires_at as string | null) ?? null,
100
+ expires_at: (expires_at as string | null) ?? null
101
101
  };
102
102
 
103
103
  const keyWithSecret = await store.createApiKey(request, createdBy);
@@ -134,7 +134,7 @@ export function createApiKeyRoutes(options: ApiKeyRouteOptions): Hono<HonoEnv> {
134
134
  if (!validatePermissions(permissions)) {
135
135
  throw ApiError.badRequest(
136
136
  "Permissions must be an array of { collection: string, operations: ('read' | 'write' | 'delete')[] }",
137
- "INVALID_INPUT",
137
+ "INVALID_INPUT"
138
138
  );
139
139
  }
140
140
  }
@@ -15,9 +15,10 @@ import { logger } from "../../utils/logger";
15
15
  import type {
16
16
  ApiKey,
17
17
  ApiKeyMasked,
18
+ ApiKeyPermission,
18
19
  ApiKeyWithSecret,
19
20
  CreateApiKeyRequest,
20
- UpdateApiKeyRequest,
21
+ UpdateApiKeyRequest
21
22
  } from "./api-key-types";
22
23
 
23
24
  const TABLE = "rebase.api_keys";
@@ -64,7 +65,7 @@ function toMasked(row: ApiKey): ApiKeyMasked {
64
65
  updated_at: row.updated_at,
65
66
  last_used_at: row.last_used_at,
66
67
  expires_at: row.expires_at,
67
- revoked_at: row.revoked_at,
68
+ revoked_at: row.revoked_at
68
69
  };
69
70
  }
70
71
 
@@ -72,9 +73,14 @@ function toMasked(row: ApiKey): ApiKeyMasked {
72
73
  * Parse a raw DB row into the typed `ApiKey` shape.
73
74
  */
74
75
  function rowToApiKey(row: Record<string, unknown>): ApiKey {
75
- const permissions = typeof row.permissions === "string"
76
- ? JSON.parse(row.permissions)
77
- : (row.permissions ?? []);
76
+ let permissions = (row.permissions ?? []) as ApiKeyPermission[];
77
+ if (typeof row.permissions === "string") {
78
+ try {
79
+ permissions = JSON.parse(row.permissions) as ApiKeyPermission[];
80
+ } catch {
81
+ permissions = [];
82
+ }
83
+ }
78
84
 
79
85
  return {
80
86
  id: row.id as string,
@@ -90,7 +96,7 @@ function rowToApiKey(row: Record<string, unknown>): ApiKey {
90
96
  updated_at: new Date(row.updated_at as string).toISOString(),
91
97
  last_used_at: row.last_used_at ? new Date(row.last_used_at as string).toISOString() : null,
92
98
  expires_at: row.expires_at ? new Date(row.expires_at as string).toISOString() : null,
93
- revoked_at: row.revoked_at ? new Date(row.revoked_at as string).toISOString() : null,
99
+ revoked_at: row.revoked_at ? new Date(row.revoked_at as string).toISOString() : null
94
100
  };
95
101
  }
96
102
 
@@ -212,7 +218,7 @@ export function createApiKeyStore(driver: DataDriver): ApiKeyStore | undefined {
212
218
  const apiKey = rowToApiKey(rows[0]);
213
219
  return {
214
220
  ...toMasked(apiKey),
215
- key: plaintext,
221
+ key: plaintext
216
222
  };
217
223
  },
218
224
 
@@ -312,6 +318,6 @@ export function createApiKeyStore(driver: DataDriver): ApiKeyStore | undefined {
312
318
  // Non-blocking — don't fail requests because of a usage timestamp update
313
319
  logger.error("[api-key-store] Failed to update last_used_at", { error: err });
314
320
  }
315
- },
321
+ }
316
322
  };
317
323
  }
@@ -14,7 +14,7 @@ export type {
14
14
  ApiKeyPermission,
15
15
  ApiKeyWithSecret,
16
16
  CreateApiKeyRequest,
17
- UpdateApiKeyRequest,
17
+ UpdateApiKeyRequest
18
18
  } from "./api-key-types";
19
19
 
20
20
  // Store
@@ -28,7 +28,7 @@ export type { ApiKeyAuthOptions } from "./api-key-middleware";
28
28
  // Permission guard
29
29
  export {
30
30
  httpMethodToOperation,
31
- isOperationAllowed,
31
+ isOperationAllowed
32
32
  } from "./api-key-permission-guard";
33
33
  export type { ApiKeyOperation } from "./api-key-permission-guard";
34
34
 
@@ -1,6 +1,7 @@
1
1
  import type { OAuthProvider, OAuthProviderProfile } from "./interfaces";
2
2
  import { z } from "zod";
3
3
  import jwt from "jsonwebtoken";
4
+ import { logger } from "../utils/logger";
4
5
  /**
5
6
  * Creates an Apple Sign In OAuth Provider integration.
6
7
  *
@@ -75,7 +76,7 @@ export function createAppleProvider(config: {
75
76
  });
76
77
 
77
78
  if (!tokenResponse.ok) {
78
- console.error("Failed to get Apple access token:", await tokenResponse.text());
79
+ logger.error("Failed to get Apple access token", { detail: await tokenResponse.text() });
79
80
  return null;
80
81
  }
81
82
 
@@ -95,7 +96,7 @@ export function createAppleProvider(config: {
95
96
  // the user object from the first auth for us to capture the name.
96
97
  const email = decoded.email || payload.user?.email;
97
98
  if (!email) {
98
- console.error("Apple user has no email");
99
+ logger.error("Apple user has no email");
99
100
  return null;
100
101
  }
101
102
 
@@ -112,7 +113,7 @@ export function createAppleProvider(config: {
112
113
  photoUrl: null // Apple does not provide a profile photo
113
114
  };
114
115
  } catch (error) {
115
- console.error("Apple OAuth error:", error);
116
+ logger.error("Apple OAuth error", { error: error });
116
117
  return null;
117
118
  }
118
119
  }
@@ -44,6 +44,7 @@ import {
44
44
  } from "./password";
45
45
  import type { PasswordValidationResult } from "./password";
46
46
  import type { AuthRepository, UserData, CreateUserData } from "./interfaces";
47
+ import type { EmailService, EmailConfig } from "../email";
47
48
 
48
49
  /**
49
50
  * Authentication method identifier for lifecycle hooks.
@@ -205,28 +206,67 @@ export interface AuthHooks {
205
206
  * This is fire-and-forget — errors are logged but do not fail the request.
206
207
  */
207
208
  afterUserDelete?(userId: string): Promise<void>;
209
+
210
+ /**
211
+ * Optional hook to customize or override the default user creation flow via the admin panel/REST API.
212
+ * When provided, this replaces the built-in password generation, hashing, and invitation email logic.
213
+ */
214
+ onAdminCreateUser?(
215
+ values: Record<string, unknown>,
216
+ ctx: {
217
+ authRepo: AuthRepository;
218
+ emailService?: EmailService;
219
+ emailConfig?: EmailConfig;
220
+ hashPassword: (password: string) => Promise<string>;
221
+ }
222
+ ): Promise<{
223
+ values: Record<string, unknown>;
224
+ temporaryPassword?: string;
225
+ invitationSent?: boolean;
226
+ }>;
227
+
228
+ /**
229
+ * Optional hook to customize or override the default password reset flow via the admin panel.
230
+ * When provided, this replaces the built-in password reset token generation, hashing, and email logic.
231
+ */
232
+ onAdminResetPassword?(
233
+ userId: string,
234
+ ctx: {
235
+ authRepo: AuthRepository;
236
+ emailService?: EmailService;
237
+ emailConfig?: EmailConfig;
238
+ }
239
+ ): Promise<{
240
+ temporaryPassword?: string;
241
+ invitationSent?: boolean;
242
+ }>;
208
243
  }
209
244
 
210
245
  /**
211
- * Resolved auth operationsevery method is guaranteed to exist.
246
+ * Resolved auth hookspassword operations are guaranteed to exist,
247
+ * all other hooks are passed through as-is (optional).
248
+ *
212
249
  * Created by `resolveAuthHooks()` which merges user hooks
213
250
  * with built-in defaults.
251
+ *
252
+ * Consumers should use the resolved object exclusively —
253
+ * never access the raw `AuthHooks` directly.
214
254
  */
215
- export interface ResolvedAuthOperations {
216
- hashPassword(password: string): Promise<string>;
217
- verifyPassword(password: string, storedHash: string): Promise<boolean>;
218
- validatePasswordStrength(password: string): PasswordValidationResult;
219
- }
255
+ export type ResolvedAuthHooks =
256
+ Required<Pick<AuthHooks, "hashPassword" | "verifyPassword" | "validatePasswordStrength">>
257
+ & Omit<AuthHooks, "hashPassword" | "verifyPassword" | "validatePasswordStrength">;
220
258
 
221
259
  /**
222
260
  * Merge user-provided hooks with the built-in defaults to produce
223
- * a complete set of resolved operations.
261
+ * a complete set of resolved hooks.
224
262
  *
225
263
  * This is the single point where defaults are applied — all consumers
226
- * call this once and use the resolved operations throughout.
264
+ * call this once and use the resolved hooks throughout.
227
265
  */
228
- export function resolveAuthHooks(hooks?: AuthHooks): ResolvedAuthOperations {
266
+ export function resolveAuthHooks(hooks?: AuthHooks): ResolvedAuthHooks {
229
267
  return {
268
+ ...hooks,
269
+
230
270
  hashPassword: hooks?.hashPassword
231
271
  ?? defaultHashPassword,
232
272
 
@@ -234,6 +274,6 @@ export function resolveAuthHooks(hooks?: AuthHooks): ResolvedAuthOperations {
234
274
  ?? defaultVerifyPassword,
235
275
 
236
276
  validatePasswordStrength: hooks?.validatePasswordStrength
237
- ?? defaultValidatePasswordStrength,
277
+ ?? defaultValidatePasswordStrength
238
278
  };
239
279
  }
@@ -1,5 +1,6 @@
1
1
  import type { OAuthProvider, OAuthProviderProfile } from "./interfaces";
2
2
  import { z } from "zod";
3
+ import { logger } from "../utils/logger";
3
4
 
4
5
  /**
5
6
  * Creates a Bitbucket OAuth Provider integration (OAuth 2.0 consumer).
@@ -29,7 +30,7 @@ export function createBitbucketProvider(config: { clientId: string; clientSecret
29
30
  });
30
31
 
31
32
  if (!tokenResponse.ok) {
32
- console.error("Failed to get Bitbucket access token:", await tokenResponse.text());
33
+ logger.error("Failed to get Bitbucket access token", { detail: await tokenResponse.text() });
33
34
  return null;
34
35
  }
35
36
 
@@ -41,7 +42,7 @@ export function createBitbucketProvider(config: { clientId: string; clientSecret
41
42
  });
42
43
 
43
44
  if (!profileResponse.ok) {
44
- console.error("Failed to get Bitbucket user info:", await profileResponse.text());
45
+ logger.error("Failed to get Bitbucket user info", { detail: await profileResponse.text() });
45
46
  return null;
46
47
  }
47
48
 
@@ -65,7 +66,7 @@ export function createBitbucketProvider(config: { clientId: string; clientSecret
65
66
  email = primary?.email || null;
66
67
  }
67
68
 
68
- if (!email) { console.error("Bitbucket user has no verified email"); return null; }
69
+ if (!email) { logger.error("Bitbucket user has no verified email"); return null; }
69
70
 
70
71
  return {
71
72
  providerId: p.uuid,
@@ -74,7 +75,7 @@ export function createBitbucketProvider(config: { clientId: string; clientSecret
74
75
  photoUrl: p.links?.avatar?.href || null
75
76
  };
76
77
  } catch (error) {
77
- console.error("Bitbucket OAuth error:", error);
78
+ logger.error("Bitbucket OAuth error", { error: error });
78
79
  return null;
79
80
  }
80
81
  }