@rebasepro/server-core 0.0.1-canary.09e5ec5

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 (300) hide show
  1. package/LICENSE +6 -0
  2. package/README.md +40 -0
  3. package/build-errors.txt +52 -0
  4. package/coverage/clover.xml +3739 -0
  5. package/coverage/coverage-final.json +31 -0
  6. package/coverage/lcov-report/base.css +224 -0
  7. package/coverage/lcov-report/block-navigation.js +87 -0
  8. package/coverage/lcov-report/favicon.png +0 -0
  9. package/coverage/lcov-report/index.html +266 -0
  10. package/coverage/lcov-report/prettify.css +1 -0
  11. package/coverage/lcov-report/prettify.js +2 -0
  12. package/coverage/lcov-report/sort-arrow-sprite.png +0 -0
  13. package/coverage/lcov-report/sorter.js +210 -0
  14. package/coverage/lcov-report/src/api/ast-schema-editor.ts.html +952 -0
  15. package/coverage/lcov-report/src/api/errors.ts.html +472 -0
  16. package/coverage/lcov-report/src/api/graphql/graphql-schema-generator.ts.html +1069 -0
  17. package/coverage/lcov-report/src/api/graphql/index.html +116 -0
  18. package/coverage/lcov-report/src/api/index.html +176 -0
  19. package/coverage/lcov-report/src/api/openapi-generator.ts.html +565 -0
  20. package/coverage/lcov-report/src/api/rest/api-generator.ts.html +994 -0
  21. package/coverage/lcov-report/src/api/rest/index.html +131 -0
  22. package/coverage/lcov-report/src/api/rest/query-parser.ts.html +550 -0
  23. package/coverage/lcov-report/src/api/schema-editor-routes.ts.html +202 -0
  24. package/coverage/lcov-report/src/api/server.ts.html +823 -0
  25. package/coverage/lcov-report/src/auth/admin-routes.ts.html +973 -0
  26. package/coverage/lcov-report/src/auth/index.html +176 -0
  27. package/coverage/lcov-report/src/auth/jwt.ts.html +574 -0
  28. package/coverage/lcov-report/src/auth/middleware.ts.html +745 -0
  29. package/coverage/lcov-report/src/auth/password.ts.html +310 -0
  30. package/coverage/lcov-report/src/auth/services.ts.html +2074 -0
  31. package/coverage/lcov-report/src/collections/index.html +116 -0
  32. package/coverage/lcov-report/src/collections/loader.ts.html +232 -0
  33. package/coverage/lcov-report/src/db/auth-schema.ts.html +523 -0
  34. package/coverage/lcov-report/src/db/data-transformer.ts.html +1753 -0
  35. package/coverage/lcov-report/src/db/entityService.ts.html +700 -0
  36. package/coverage/lcov-report/src/db/index.html +146 -0
  37. package/coverage/lcov-report/src/db/services/EntityFetchService.ts.html +4048 -0
  38. package/coverage/lcov-report/src/db/services/EntityPersistService.ts.html +883 -0
  39. package/coverage/lcov-report/src/db/services/RelationService.ts.html +3121 -0
  40. package/coverage/lcov-report/src/db/services/entity-helpers.ts.html +442 -0
  41. package/coverage/lcov-report/src/db/services/index.html +176 -0
  42. package/coverage/lcov-report/src/db/services/index.ts.html +124 -0
  43. package/coverage/lcov-report/src/generate-drizzle-schema-logic.ts.html +1960 -0
  44. package/coverage/lcov-report/src/index.html +116 -0
  45. package/coverage/lcov-report/src/services/driver-registry.ts.html +631 -0
  46. package/coverage/lcov-report/src/services/index.html +131 -0
  47. package/coverage/lcov-report/src/services/postgresDataDriver.ts.html +3025 -0
  48. package/coverage/lcov-report/src/storage/LocalStorageController.ts.html +1189 -0
  49. package/coverage/lcov-report/src/storage/S3StorageController.ts.html +970 -0
  50. package/coverage/lcov-report/src/storage/index.html +161 -0
  51. package/coverage/lcov-report/src/storage/storage-registry.ts.html +646 -0
  52. package/coverage/lcov-report/src/storage/types.ts.html +451 -0
  53. package/coverage/lcov-report/src/utils/drizzle-conditions.ts.html +3082 -0
  54. package/coverage/lcov-report/src/utils/index.html +116 -0
  55. package/coverage/lcov.info +7179 -0
  56. package/dist/common/src/collections/CollectionRegistry.d.ts +56 -0
  57. package/dist/common/src/collections/index.d.ts +1 -0
  58. package/dist/common/src/data/buildRebaseData.d.ts +14 -0
  59. package/dist/common/src/index.d.ts +3 -0
  60. package/dist/common/src/util/builders.d.ts +57 -0
  61. package/dist/common/src/util/callbacks.d.ts +6 -0
  62. package/dist/common/src/util/collections.d.ts +11 -0
  63. package/dist/common/src/util/common.d.ts +2 -0
  64. package/dist/common/src/util/conditions.d.ts +26 -0
  65. package/dist/common/src/util/entities.d.ts +58 -0
  66. package/dist/common/src/util/enums.d.ts +3 -0
  67. package/dist/common/src/util/index.d.ts +16 -0
  68. package/dist/common/src/util/navigation_from_path.d.ts +34 -0
  69. package/dist/common/src/util/navigation_utils.d.ts +20 -0
  70. package/dist/common/src/util/parent_references_from_path.d.ts +6 -0
  71. package/dist/common/src/util/paths.d.ts +14 -0
  72. package/dist/common/src/util/permissions.d.ts +5 -0
  73. package/dist/common/src/util/references.d.ts +2 -0
  74. package/dist/common/src/util/relations.d.ts +22 -0
  75. package/dist/common/src/util/resolutions.d.ts +72 -0
  76. package/dist/common/src/util/storage.d.ts +24 -0
  77. package/dist/index-DXVBFp5V.js +37 -0
  78. package/dist/index-DXVBFp5V.js.map +1 -0
  79. package/dist/index.es.js +49934 -0
  80. package/dist/index.es.js.map +1 -0
  81. package/dist/index.umd.js +49968 -0
  82. package/dist/index.umd.js.map +1 -0
  83. package/dist/server-core/src/api/ast-schema-editor.d.ts +21 -0
  84. package/dist/server-core/src/api/collections_for_test/callbacks_test_collection.d.ts +2 -0
  85. package/dist/server-core/src/api/errors.d.ts +35 -0
  86. package/dist/server-core/src/api/graphql/graphql-schema-generator.d.ts +35 -0
  87. package/dist/server-core/src/api/graphql/index.d.ts +1 -0
  88. package/dist/server-core/src/api/index.d.ts +9 -0
  89. package/dist/server-core/src/api/openapi-generator.d.ts +16 -0
  90. package/dist/server-core/src/api/rest/api-generator.d.ts +64 -0
  91. package/dist/server-core/src/api/rest/index.d.ts +1 -0
  92. package/dist/server-core/src/api/rest/query-parser.d.ts +9 -0
  93. package/dist/server-core/src/api/schema-editor-routes.d.ts +3 -0
  94. package/dist/server-core/src/api/server.d.ts +40 -0
  95. package/dist/server-core/src/api/types.d.ts +90 -0
  96. package/dist/server-core/src/auth/admin-routes.d.ts +16 -0
  97. package/dist/server-core/src/auth/apple-oauth.d.ts +30 -0
  98. package/dist/server-core/src/auth/bitbucket-oauth.d.ts +11 -0
  99. package/dist/server-core/src/auth/discord-oauth.d.ts +14 -0
  100. package/dist/server-core/src/auth/facebook-oauth.d.ts +14 -0
  101. package/dist/server-core/src/auth/github-oauth.d.ts +15 -0
  102. package/dist/server-core/src/auth/gitlab-oauth.d.ts +13 -0
  103. package/dist/server-core/src/auth/google-oauth.d.ts +14 -0
  104. package/dist/server-core/src/auth/index.d.ts +23 -0
  105. package/dist/server-core/src/auth/interfaces.d.ts +309 -0
  106. package/dist/server-core/src/auth/jwt.d.ts +43 -0
  107. package/dist/server-core/src/auth/linkedin-oauth.d.ts +18 -0
  108. package/dist/server-core/src/auth/microsoft-oauth.d.ts +16 -0
  109. package/dist/server-core/src/auth/middleware.d.ts +81 -0
  110. package/dist/server-core/src/auth/password.d.ts +22 -0
  111. package/dist/server-core/src/auth/rate-limiter.d.ts +31 -0
  112. package/dist/server-core/src/auth/routes.d.ts +27 -0
  113. package/dist/server-core/src/auth/slack-oauth.d.ts +12 -0
  114. package/dist/server-core/src/auth/spotify-oauth.d.ts +12 -0
  115. package/dist/server-core/src/auth/twitter-oauth.d.ts +18 -0
  116. package/dist/server-core/src/bootstrappers/index.d.ts +0 -0
  117. package/dist/server-core/src/collections/BackendCollectionRegistry.d.ts +13 -0
  118. package/dist/server-core/src/collections/loader.d.ts +5 -0
  119. package/dist/server-core/src/cron/cron-loader.d.ts +17 -0
  120. package/dist/server-core/src/cron/cron-routes.d.ts +14 -0
  121. package/dist/server-core/src/cron/cron-scheduler.d.ts +61 -0
  122. package/dist/server-core/src/cron/cron-store.d.ts +32 -0
  123. package/dist/server-core/src/cron/index.d.ts +6 -0
  124. package/dist/server-core/src/db/interfaces.d.ts +18 -0
  125. package/dist/server-core/src/email/index.d.ts +6 -0
  126. package/dist/server-core/src/email/smtp-email-service.d.ts +25 -0
  127. package/dist/server-core/src/email/templates.d.ts +42 -0
  128. package/dist/server-core/src/email/types.d.ts +107 -0
  129. package/dist/server-core/src/functions/function-loader.d.ts +17 -0
  130. package/dist/server-core/src/functions/function-routes.d.ts +10 -0
  131. package/dist/server-core/src/functions/index.d.ts +3 -0
  132. package/dist/server-core/src/history/history-routes.d.ts +23 -0
  133. package/dist/server-core/src/history/index.d.ts +1 -0
  134. package/dist/server-core/src/index.d.ts +29 -0
  135. package/dist/server-core/src/init.d.ts +159 -0
  136. package/dist/server-core/src/serve-spa.d.ts +30 -0
  137. package/dist/server-core/src/services/driver-registry.d.ts +78 -0
  138. package/dist/server-core/src/singleton.d.ts +35 -0
  139. package/dist/server-core/src/storage/LocalStorageController.d.ts +46 -0
  140. package/dist/server-core/src/storage/S3StorageController.d.ts +36 -0
  141. package/dist/server-core/src/storage/index.d.ts +25 -0
  142. package/dist/server-core/src/storage/routes.d.ts +38 -0
  143. package/dist/server-core/src/storage/storage-registry.d.ts +78 -0
  144. package/dist/server-core/src/storage/types.d.ts +103 -0
  145. package/dist/server-core/src/types/index.d.ts +11 -0
  146. package/dist/server-core/src/utils/dev-port.d.ts +35 -0
  147. package/dist/server-core/src/utils/logger.d.ts +31 -0
  148. package/dist/server-core/src/utils/logging.d.ts +9 -0
  149. package/dist/server-core/src/utils/request-logger.d.ts +19 -0
  150. package/dist/server-core/src/utils/sql.d.ts +27 -0
  151. package/dist/types/src/controllers/analytics_controller.d.ts +7 -0
  152. package/dist/types/src/controllers/auth.d.ts +119 -0
  153. package/dist/types/src/controllers/client.d.ts +170 -0
  154. package/dist/types/src/controllers/collection_registry.d.ts +45 -0
  155. package/dist/types/src/controllers/customization_controller.d.ts +60 -0
  156. package/dist/types/src/controllers/data.d.ts +168 -0
  157. package/dist/types/src/controllers/data_driver.d.ts +160 -0
  158. package/dist/types/src/controllers/database_admin.d.ts +11 -0
  159. package/dist/types/src/controllers/dialogs_controller.d.ts +36 -0
  160. package/dist/types/src/controllers/effective_role.d.ts +4 -0
  161. package/dist/types/src/controllers/email.d.ts +34 -0
  162. package/dist/types/src/controllers/index.d.ts +18 -0
  163. package/dist/types/src/controllers/local_config_persistence.d.ts +20 -0
  164. package/dist/types/src/controllers/navigation.d.ts +213 -0
  165. package/dist/types/src/controllers/registry.d.ts +54 -0
  166. package/dist/types/src/controllers/side_dialogs_controller.d.ts +67 -0
  167. package/dist/types/src/controllers/side_entity_controller.d.ts +90 -0
  168. package/dist/types/src/controllers/snackbar.d.ts +24 -0
  169. package/dist/types/src/controllers/storage.d.ts +171 -0
  170. package/dist/types/src/index.d.ts +4 -0
  171. package/dist/types/src/rebase_context.d.ts +105 -0
  172. package/dist/types/src/types/backend.d.ts +536 -0
  173. package/dist/types/src/types/builders.d.ts +15 -0
  174. package/dist/types/src/types/chips.d.ts +5 -0
  175. package/dist/types/src/types/collections.d.ts +856 -0
  176. package/dist/types/src/types/cron.d.ts +102 -0
  177. package/dist/types/src/types/data_source.d.ts +64 -0
  178. package/dist/types/src/types/entities.d.ts +145 -0
  179. package/dist/types/src/types/entity_actions.d.ts +98 -0
  180. package/dist/types/src/types/entity_callbacks.d.ts +173 -0
  181. package/dist/types/src/types/entity_link_builder.d.ts +7 -0
  182. package/dist/types/src/types/entity_overrides.d.ts +10 -0
  183. package/dist/types/src/types/entity_views.d.ts +61 -0
  184. package/dist/types/src/types/export_import.d.ts +21 -0
  185. package/dist/types/src/types/index.d.ts +23 -0
  186. package/dist/types/src/types/locales.d.ts +4 -0
  187. package/dist/types/src/types/modify_collections.d.ts +5 -0
  188. package/dist/types/src/types/plugins.d.ts +279 -0
  189. package/dist/types/src/types/properties.d.ts +1176 -0
  190. package/dist/types/src/types/property_config.d.ts +70 -0
  191. package/dist/types/src/types/relations.d.ts +336 -0
  192. package/dist/types/src/types/slots.d.ts +252 -0
  193. package/dist/types/src/types/translations.d.ts +870 -0
  194. package/dist/types/src/types/user_management_delegate.d.ts +121 -0
  195. package/dist/types/src/types/websockets.d.ts +78 -0
  196. package/dist/types/src/users/index.d.ts +2 -0
  197. package/dist/types/src/users/roles.d.ts +22 -0
  198. package/dist/types/src/users/user.d.ts +46 -0
  199. package/history_diff.log +385 -0
  200. package/jest.config.cjs +16 -0
  201. package/package.json +86 -0
  202. package/scratch.ts +9 -0
  203. package/src/api/ast-schema-editor.ts +289 -0
  204. package/src/api/collections_for_test/callbacks_test_collection.ts +60 -0
  205. package/src/api/errors.ts +179 -0
  206. package/src/api/graphql/graphql-schema-generator.ts +336 -0
  207. package/src/api/graphql/index.ts +2 -0
  208. package/src/api/index.ts +11 -0
  209. package/src/api/openapi-generator.ts +715 -0
  210. package/src/api/rest/api-generator.ts +472 -0
  211. package/src/api/rest/index.ts +2 -0
  212. package/src/api/rest/query-parser.ts +155 -0
  213. package/src/api/schema-editor-routes.ts +41 -0
  214. package/src/api/server.ts +248 -0
  215. package/src/api/types.ts +90 -0
  216. package/src/auth/admin-routes.ts +529 -0
  217. package/src/auth/apple-oauth.ts +130 -0
  218. package/src/auth/bitbucket-oauth.ts +82 -0
  219. package/src/auth/discord-oauth.ts +83 -0
  220. package/src/auth/facebook-oauth.ts +72 -0
  221. package/src/auth/github-oauth.ts +110 -0
  222. package/src/auth/gitlab-oauth.ts +70 -0
  223. package/src/auth/google-oauth.ts +48 -0
  224. package/src/auth/index.ts +34 -0
  225. package/src/auth/interfaces.ts +363 -0
  226. package/src/auth/jwt.ts +181 -0
  227. package/src/auth/linkedin-oauth.ts +81 -0
  228. package/src/auth/microsoft-oauth.ts +88 -0
  229. package/src/auth/middleware.ts +384 -0
  230. package/src/auth/password.ts +77 -0
  231. package/src/auth/rate-limiter.ts +129 -0
  232. package/src/auth/routes.ts +788 -0
  233. package/src/auth/slack-oauth.ts +71 -0
  234. package/src/auth/spotify-oauth.ts +67 -0
  235. package/src/auth/twitter-oauth.ts +120 -0
  236. package/src/bootstrappers/index.ts +1 -0
  237. package/src/collections/BackendCollectionRegistry.ts +20 -0
  238. package/src/collections/loader.ts +49 -0
  239. package/src/cron/cron-loader.ts +89 -0
  240. package/src/cron/cron-routes.test.ts +265 -0
  241. package/src/cron/cron-routes.ts +85 -0
  242. package/src/cron/cron-scheduler.test.ts +421 -0
  243. package/src/cron/cron-scheduler.ts +413 -0
  244. package/src/cron/cron-store.ts +163 -0
  245. package/src/cron/index.ts +6 -0
  246. package/src/db/interfaces.ts +60 -0
  247. package/src/email/index.ts +18 -0
  248. package/src/email/smtp-email-service.ts +91 -0
  249. package/src/email/templates.ts +388 -0
  250. package/src/email/types.ts +105 -0
  251. package/src/functions/function-loader.ts +119 -0
  252. package/src/functions/function-routes.ts +31 -0
  253. package/src/functions/index.ts +3 -0
  254. package/src/history/history-routes.ts +129 -0
  255. package/src/history/index.ts +2 -0
  256. package/src/index.ts +66 -0
  257. package/src/init.ts +727 -0
  258. package/src/serve-spa.ts +81 -0
  259. package/src/services/driver-registry.ts +182 -0
  260. package/src/singleton.test.ts +28 -0
  261. package/src/singleton.ts +70 -0
  262. package/src/storage/LocalStorageController.ts +365 -0
  263. package/src/storage/S3StorageController.ts +298 -0
  264. package/src/storage/index.ts +43 -0
  265. package/src/storage/routes.ts +264 -0
  266. package/src/storage/storage-registry.ts +187 -0
  267. package/src/storage/types.ts +134 -0
  268. package/src/types/index.ts +27 -0
  269. package/src/utils/dev-port.ts +176 -0
  270. package/src/utils/logger.ts +143 -0
  271. package/src/utils/logging.ts +38 -0
  272. package/src/utils/request-logger.ts +66 -0
  273. package/src/utils/sql.ts +38 -0
  274. package/test/admin-routes.test.ts +640 -0
  275. package/test/api-generator.test.ts +501 -0
  276. package/test/ast-schema-editor.test.ts +63 -0
  277. package/test/auth-middleware-hono.test.ts +556 -0
  278. package/test/auth-routes.test.ts +1047 -0
  279. package/test/driver-registry.test.ts +282 -0
  280. package/test/error-propagation.test.ts +226 -0
  281. package/test/errors-hono.test.ts +133 -0
  282. package/test/errors.test.ts +155 -0
  283. package/test/jwt-security.test.ts +182 -0
  284. package/test/jwt.test.ts +324 -0
  285. package/test/middleware.test.ts +300 -0
  286. package/test/password.test.ts +165 -0
  287. package/test/query-parser.test.ts +263 -0
  288. package/test/rate-limiter.test.ts +102 -0
  289. package/test/safe-compare.test.ts +66 -0
  290. package/test/singleton.test.ts +59 -0
  291. package/test/storage-local.test.ts +271 -0
  292. package/test/storage-registry.test.ts +282 -0
  293. package/test/storage-routes.test.ts +222 -0
  294. package/test/storage-s3.test.ts +304 -0
  295. package/test-ast.ts +28 -0
  296. package/test.ts +6 -0
  297. package/test_output.txt +1133 -0
  298. package/tsconfig.json +49 -0
  299. package/tsconfig.prod.json +20 -0
  300. package/vite.config.ts +80 -0
@@ -0,0 +1,529 @@
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 { hashPassword, validatePasswordStrength } from "./password";
6
+ import { AuthModuleConfig } from "./routes";
7
+
8
+ interface AdminRouteOptions extends AuthModuleConfig {
9
+ serviceKey?: string;
10
+ /**
11
+ * Callback to persistently mark bootstrap as completed.
12
+ * Invoked after the first admin user is promoted via POST /admin/bootstrap.
13
+ */
14
+ setBootstrapCompleted?: () => Promise<void>;
15
+ }
16
+ import { HonoEnv } from "../api/types";
17
+ import { randomBytes, createHash } from "crypto";
18
+ import { getUserInvitationTemplate, getPasswordResetTemplate } from "../email/templates";
19
+
20
+ /**
21
+ * Generate a cryptographically secure random password that meets strength requirements.
22
+ */
23
+ function generateSecurePassword(): string {
24
+ const upper = "ABCDEFGHJKLMNPQRSTUVWXYZ";
25
+ const lower = "abcdefghjkmnpqrstuvwxyz";
26
+ const digits = "23456789";
27
+ const all = upper + lower + digits;
28
+
29
+ // Guarantee at least one of each required class
30
+ const pick = (chars: string) => chars[Math.floor(Math.random() * chars.length)];
31
+ const parts = [pick(upper), pick(lower), pick(digits)];
32
+
33
+ // Fill remaining with random chars (16 total)
34
+ for (let i = parts.length; i < 16; i++) {
35
+ parts.push(pick(all));
36
+ }
37
+
38
+ // Shuffle
39
+ for (let i = parts.length - 1; i > 0; i--) {
40
+ const j = Math.floor(Math.random() * (i + 1));
41
+ [parts[i], parts[j]] = [parts[j], parts[i]];
42
+ }
43
+ return parts.join("");
44
+ }
45
+
46
+ /**
47
+ * Generate a secure random token
48
+ */
49
+ function generateSecureToken(): string {
50
+ return randomBytes(40).toString("hex");
51
+ }
52
+
53
+ /**
54
+ * Hash a token for database storage
55
+ */
56
+ function hashToken(token: string): string {
57
+ return createHash("sha256").update(token).digest("hex");
58
+ }
59
+
60
+ /**
61
+ * Create admin routes for user and role management
62
+ */
63
+ export function createAdminRoutes(config: AdminRouteOptions): Hono<HonoEnv> {
64
+ const router = new Hono<HonoEnv>();
65
+ const authRepo = config.authRepo;
66
+ const { emailService, emailConfig } = config;
67
+
68
+ // Attach Rebase error handler to ensure exceptions are correctly formatted
69
+ // instead of caught by Hono's default error handler from the sub-router.
70
+ router.onError(errorHandler);
71
+
72
+ // Apply auth middleware to all routes (service-key-aware when configured)
73
+ router.use("/*", createRequireAuth({ serviceKey: config.serviceKey }));
74
+
75
+ /**
76
+ * POST /admin/bootstrap
77
+ *
78
+ * One-time endpoint to promote the calling user to admin.
79
+ * Guarded by three layers:
80
+ * 1. Authentication (handled by middleware above)
81
+ * 2. Persistent `bootstrap_completed` flag (when `setBootstrapCompleted` is provided)
82
+ * 3. Database check — no existing admin users
83
+ *
84
+ * Once invoked successfully the persistent flag is set, permanently disabling
85
+ * this endpoint even if all admin users are later deleted.
86
+ */
87
+ router.post("/bootstrap", async (c) => {
88
+ const user = c.get("user");
89
+ if (!user || typeof user !== "object") {
90
+ throw ApiError.unauthorized("Not authenticated");
91
+ }
92
+
93
+ // ── Guard 1: persistent flag ──────────────────────────────────
94
+ if (config.isBootstrapCompleted) {
95
+ const alreadyDone = await config.isBootstrapCompleted();
96
+ if (alreadyDone) {
97
+ throw ApiError.forbidden("Bootstrap has already been completed.", "BOOTSTRAP_COMPLETED");
98
+ }
99
+ }
100
+
101
+ // ── Guard 2: no existing admin users ─────────────────────────
102
+ const users = await authRepo.listUsers();
103
+ let hasAdmin = false;
104
+
105
+ for (const u of users) {
106
+ const roles = await authRepo.getUserRoleIds(u.id);
107
+ if (roles.includes("admin")) {
108
+ hasAdmin = true;
109
+ break;
110
+ }
111
+ }
112
+
113
+ if (hasAdmin) {
114
+ throw ApiError.forbidden("Admin users already exist. Bootstrap not allowed.", "BOOTSTRAP_COMPLETED");
115
+ }
116
+
117
+ // ── Promote caller ───────────────────────────────────────────
118
+ const userId = "userId" in user ? user.userId : undefined;
119
+ if (!userId) {
120
+ throw ApiError.unauthorized("User ID not found in auth context");
121
+ }
122
+ await authRepo.setUserRoles(userId, ["admin"]);
123
+
124
+ // ── Set persistent flag ──────────────────────────────────────
125
+ if (config.setBootstrapCompleted) {
126
+ await config.setBootstrapCompleted();
127
+ }
128
+
129
+ return c.json({
130
+ success: true,
131
+ message: "You are now an admin",
132
+ user: {
133
+ uid: userId,
134
+ roles: ["admin"]
135
+ }
136
+ });
137
+ });
138
+
139
+ router.get("/users", requireAdmin, async (c) => {
140
+ const limitParam = c.req.query("limit");
141
+ const offsetParam = c.req.query("offset");
142
+ const search = c.req.query("search");
143
+ const orderBy = c.req.query("orderBy");
144
+ const orderDir = c.req.query("orderDir") as "asc" | "desc" | undefined;
145
+
146
+ // If pagination params are provided, use the paginated path
147
+ if (limitParam !== undefined || search) {
148
+ const limit = limitParam ? parseInt(limitParam, 10) : 25;
149
+ const offset = offsetParam ? parseInt(offsetParam, 10) : 0;
150
+
151
+ const result = await authRepo.listUsersPaginated({
152
+ limit,
153
+ offset,
154
+ search: search || undefined,
155
+ orderBy: orderBy || undefined,
156
+ orderDir: orderDir || undefined,
157
+ roleId: c.req.query("role") || undefined
158
+ });
159
+
160
+ const usersWithRoles = await Promise.all(
161
+ result.users.map(async (u) => {
162
+ const roles = await authRepo.getUserRoleIds(u.id);
163
+ return {
164
+ uid: u.id,
165
+ email: u.email,
166
+ displayName: u.displayName,
167
+ photoURL: u.photoUrl,
168
+ roles,
169
+ createdAt: u.createdAt,
170
+ updatedAt: u.updatedAt
171
+ };
172
+ })
173
+ );
174
+
175
+ return c.json({
176
+ users: usersWithRoles,
177
+ total: result.total,
178
+ limit: result.limit,
179
+ offset: result.offset
180
+ });
181
+ }
182
+
183
+ // Legacy: return all users (no pagination)
184
+ const users = await authRepo.listUsers();
185
+ const usersWithRoles = await Promise.all(
186
+ users.map(async (u) => {
187
+ const roles = await authRepo.getUserRoleIds(u.id);
188
+ return {
189
+ uid: u.id,
190
+ email: u.email,
191
+ displayName: u.displayName,
192
+ photoURL: u.photoUrl,
193
+ roles,
194
+ createdAt: u.createdAt,
195
+ updatedAt: u.updatedAt
196
+ };
197
+ })
198
+ );
199
+ return c.json({ users: usersWithRoles });
200
+ });
201
+
202
+ router.get("/users/:userId", requireAdmin, async (c) => {
203
+ const userId = c.req.param("userId");
204
+ const result = await authRepo.getUserWithRoles(userId);
205
+
206
+ if (!result) {
207
+ throw ApiError.notFound("User not found");
208
+ }
209
+
210
+ return c.json({
211
+ user: {
212
+ uid: result.user.id,
213
+ email: result.user.email,
214
+ displayName: result.user.displayName,
215
+ photoURL: result.user.photoUrl,
216
+ roles: result.roles.map(r => r.id),
217
+ createdAt: result.user.createdAt,
218
+ updatedAt: result.user.updatedAt
219
+ }
220
+ });
221
+ });
222
+
223
+ router.post("/users", requireAdmin, async (c) => {
224
+ const body = await c.req.json();
225
+ const { email, displayName, password, roles } = body;
226
+
227
+ if (!email) {
228
+ throw ApiError.badRequest("Email is required", "INVALID_INPUT");
229
+ }
230
+
231
+ const existing = await authRepo.getUserByEmail(email);
232
+ if (existing) {
233
+ throw ApiError.conflict("Email already exists", "EMAIL_EXISTS");
234
+ }
235
+
236
+ // Use provided password or auto-generate one
237
+ const clearPassword = password || generateSecurePassword();
238
+
239
+ const validation = validatePasswordStrength(clearPassword);
240
+ if (!validation.valid) {
241
+ throw ApiError.badRequest(validation.errors.join(". "), "WEAK_PASSWORD");
242
+ }
243
+ const passwordHash = await hashPassword(clearPassword);
244
+
245
+ const user = await authRepo.createUser({
246
+ email: email.toLowerCase(),
247
+ displayName: displayName || null,
248
+ passwordHash
249
+ });
250
+
251
+ if (roles && Array.isArray(roles) && roles.length > 0) {
252
+ await authRepo.setUserRoles(user.id, roles);
253
+ } else if (config.defaultRole) {
254
+ await authRepo.assignDefaultRole(user.id, config.defaultRole);
255
+ }
256
+
257
+ const userRoles = await authRepo.getUserRoleIds(user.id);
258
+
259
+ // Determine if we can send an invitation email
260
+ const isEmailConfigured = !!(emailService && emailService.isConfigured());
261
+ let invitationSent = false;
262
+ let temporaryPassword: string | undefined;
263
+
264
+ if (isEmailConfigured && !password) {
265
+ // Send invitation email via password-reset token flow
266
+ try {
267
+ const token = generateSecureToken();
268
+ const tokenHash = hashToken(token);
269
+ const expiresAt = new Date(Date.now() + 60 * 60 * 1000); // 1 hour
270
+
271
+ await authRepo.createPasswordResetToken(user.id, tokenHash, expiresAt);
272
+
273
+ const baseUrl = emailConfig?.resetPasswordUrl || "";
274
+ const setPasswordUrl = `${baseUrl}/reset-password?token=${token}`;
275
+
276
+ const appName = emailConfig?.appName || "Rebase";
277
+ const templateFn = emailConfig?.templates?.userInvitation;
278
+ const emailContent = templateFn
279
+ ? templateFn(setPasswordUrl, { email: user.email,
280
+ displayName: user.displayName })
281
+ : getUserInvitationTemplate(setPasswordUrl, { email: user.email,
282
+ displayName: user.displayName }, appName);
283
+
284
+ await emailService!.send({
285
+ to: user.email,
286
+ subject: emailContent.subject,
287
+ html: emailContent.html,
288
+ text: emailContent.text
289
+ });
290
+ invitationSent = true;
291
+ } catch (emailError: unknown) {
292
+ console.error("Failed to send invitation email:", emailError instanceof Error ? emailError.message : emailError);
293
+ // Fall back to returning the temporary password
294
+ temporaryPassword = clearPassword;
295
+ }
296
+ } else if (!password) {
297
+ // No email service — return the auto-generated password one-time
298
+ temporaryPassword = clearPassword;
299
+ }
300
+ // If admin provided a password explicitly, don't return it or send email
301
+
302
+ return c.json({
303
+ user: {
304
+ uid: user.id,
305
+ email: user.email,
306
+ displayName: user.displayName,
307
+ roles: userRoles
308
+ },
309
+ invitationSent,
310
+ ...(temporaryPassword ? { temporaryPassword } : {})
311
+ }, 201);
312
+ });
313
+
314
+ router.post("/users/:userId/reset-password", requireAdmin, async (c) => {
315
+ const userId = c.req.param("userId");
316
+ const existing = await authRepo.getUserById(userId);
317
+ if (!existing) {
318
+ throw ApiError.notFound("User not found");
319
+ }
320
+
321
+ const isEmailConfigured = !!(emailService && emailService.isConfigured());
322
+ let invitationSent = false;
323
+ let temporaryPassword: string | undefined;
324
+
325
+ if (isEmailConfigured) {
326
+ try {
327
+ const token = generateSecureToken();
328
+ const tokenHash = hashToken(token);
329
+ const expiresAt = new Date(Date.now() + 60 * 60 * 1000); // 1 hour
330
+
331
+ await authRepo.createPasswordResetToken(existing.id, tokenHash, expiresAt);
332
+
333
+ const baseUrl = emailConfig?.resetPasswordUrl || "";
334
+ const setPasswordUrl = `${baseUrl}/reset-password?token=${token}`;
335
+
336
+ const appName = emailConfig?.appName || "Rebase";
337
+ const templateFn = emailConfig?.templates?.passwordReset;
338
+ const emailContent = templateFn
339
+ ? templateFn(setPasswordUrl, { email: existing.email,
340
+ displayName: existing.displayName })
341
+ : getPasswordResetTemplate(setPasswordUrl, { email: existing.email,
342
+ displayName: existing.displayName }, appName);
343
+
344
+ await emailService!.send({
345
+ to: existing.email,
346
+ subject: emailContent.subject,
347
+ html: emailContent.html,
348
+ text: emailContent.text
349
+ });
350
+ invitationSent = true;
351
+ } catch (emailError: unknown) {
352
+ console.error("Failed to send reset email:", emailError instanceof Error ? emailError.message : emailError);
353
+ // Fall back to returning the temporary password
354
+ const clearPassword = generateSecurePassword();
355
+ const passwordHash = await hashPassword(clearPassword);
356
+ await authRepo.updatePassword(existing.id, passwordHash);
357
+ temporaryPassword = clearPassword;
358
+ }
359
+ } else {
360
+ // No email service — generate password, set it, and return one-time
361
+ const clearPassword = generateSecurePassword();
362
+ const passwordHash = await hashPassword(clearPassword);
363
+ await authRepo.updatePassword(existing.id, passwordHash);
364
+ temporaryPassword = clearPassword;
365
+ }
366
+
367
+ const userRoles = await authRepo.getUserRoleIds(existing.id);
368
+
369
+ return c.json({
370
+ user: {
371
+ uid: existing.id,
372
+ email: existing.email,
373
+ displayName: existing.displayName,
374
+ roles: userRoles
375
+ },
376
+ invitationSent,
377
+ ...(temporaryPassword ? { temporaryPassword } : {})
378
+ }, 200);
379
+ });
380
+
381
+ router.put("/users/:userId", requireAdmin, async (c) => {
382
+ const userId = c.req.param("userId");
383
+ const body = await c.req.json();
384
+ const { email, displayName, password, roles } = body;
385
+
386
+ const existing = await authRepo.getUserById(userId);
387
+ if (!existing) {
388
+ throw ApiError.notFound("User not found");
389
+ }
390
+
391
+ const updates: Record<string, unknown> = {};
392
+ if (email !== undefined) updates.email = email.toLowerCase();
393
+ if (displayName !== undefined) updates.displayName = displayName;
394
+
395
+ if (password) {
396
+ const validation = validatePasswordStrength(password);
397
+ if (!validation.valid) {
398
+ throw ApiError.badRequest(validation.errors.join(". "), "WEAK_PASSWORD");
399
+ }
400
+ updates.passwordHash = await hashPassword(password);
401
+ }
402
+
403
+ if (Object.keys(updates).length > 0) {
404
+ await authRepo.updateUser(userId, updates);
405
+ }
406
+
407
+ if (roles !== undefined && Array.isArray(roles)) {
408
+ await authRepo.setUserRoles(userId, roles);
409
+ }
410
+
411
+ const result = await authRepo.getUserWithRoles(userId);
412
+
413
+ return c.json({
414
+ user: {
415
+ uid: result!.user.id,
416
+ email: result!.user.email,
417
+ displayName: result!.user.displayName,
418
+ roles: result!.roles.map(r => r.id)
419
+ }
420
+ });
421
+ });
422
+
423
+ router.delete("/users/:userId", requireAdmin, async (c) => {
424
+ const userId = c.req.param("userId");
425
+ const user = c.get("user");
426
+
427
+ const currentUserId = user && typeof user === "object" && "userId" in user ? user.userId : undefined;
428
+ if (currentUserId === userId) {
429
+ throw ApiError.badRequest("Cannot delete your own account", "SELF_DELETE");
430
+ }
431
+
432
+ const existing = await authRepo.getUserById(userId);
433
+ if (!existing) {
434
+ throw ApiError.notFound("User not found");
435
+ }
436
+
437
+ await authRepo.deleteUser(userId);
438
+
439
+ return c.json({ success: true });
440
+ });
441
+
442
+ router.get("/roles", requireAdmin, async (c) => {
443
+ const roles = await authRepo.listRoles();
444
+
445
+ return c.json({
446
+ roles: roles.map(r => ({
447
+ id: r.id,
448
+ name: r.name,
449
+ isAdmin: r.isAdmin,
450
+ defaultPermissions: r.defaultPermissions,
451
+ config: r.config
452
+ }))
453
+ });
454
+ });
455
+
456
+ router.get("/roles/:roleId", requireAdmin, async (c) => {
457
+ const roleId = c.req.param("roleId");
458
+ const role = await authRepo.getRoleById(roleId);
459
+
460
+ if (!role) {
461
+ throw ApiError.notFound("Role not found");
462
+ }
463
+
464
+ return c.json({ role });
465
+ });
466
+
467
+ router.post("/roles", requireAdmin, async (c) => {
468
+ const body = await c.req.json();
469
+ const { id, name, isAdmin, defaultPermissions, config } = body;
470
+
471
+ if (!id || !name) {
472
+ throw ApiError.badRequest("Role ID and name are required", "INVALID_INPUT");
473
+ }
474
+
475
+ const existing = await authRepo.getRoleById(id);
476
+ if (existing) {
477
+ throw ApiError.conflict("Role already exists", "ROLE_EXISTS");
478
+ }
479
+
480
+ const role = await authRepo.createRole({
481
+ id,
482
+ name,
483
+ isAdmin: isAdmin ?? false,
484
+ defaultPermissions: defaultPermissions ?? null,
485
+ config: config ?? null
486
+ });
487
+
488
+ return c.json({ role }, 201);
489
+ });
490
+
491
+ router.put("/roles/:roleId", requireAdmin, async (c) => {
492
+ const roleId = c.req.param("roleId");
493
+ const body = await c.req.json();
494
+ const { name, isAdmin, defaultPermissions, config } = body;
495
+
496
+ const existing = await authRepo.getRoleById(roleId);
497
+ if (!existing) {
498
+ throw ApiError.notFound("Role not found");
499
+ }
500
+
501
+ const role = await authRepo.updateRole(roleId, {
502
+ name,
503
+ isAdmin,
504
+ defaultPermissions,
505
+ config
506
+ });
507
+
508
+ return c.json({ role });
509
+ });
510
+
511
+ router.delete("/roles/:roleId", requireAdmin, async (c) => {
512
+ const roleId = c.req.param("roleId");
513
+
514
+ if (["admin", "editor", "viewer"].includes(roleId)) {
515
+ throw ApiError.badRequest("Cannot delete built-in roles", "BUILTIN_ROLE");
516
+ }
517
+
518
+ const existing = await authRepo.getRoleById(roleId);
519
+ if (!existing) {
520
+ throw ApiError.notFound("Role not found");
521
+ }
522
+
523
+ await authRepo.deleteRole(roleId);
524
+
525
+ return c.json({ success: true });
526
+ });
527
+
528
+ return router;
529
+ }
@@ -0,0 +1,130 @@
1
+ import type { OAuthProvider, OAuthProviderProfile } from "./interfaces";
2
+ import { z } from "zod";
3
+ import { createPrivateKey } from "crypto";
4
+ import { SignJWT } from "jose";
5
+
6
+ /**
7
+ * Creates an Apple Sign In OAuth Provider integration.
8
+ *
9
+ * Apple requires a client secret that is a signed JWT, regenerated on each
10
+ * token exchange (valid up to 6 months). This provider handles that automatically.
11
+ *
12
+ * Required Apple Developer configuration:
13
+ * - Services ID (clientId)
14
+ * - Key ID from the private key registered with Apple
15
+ * - Team ID from Apple Developer account
16
+ * - Private key (.p8 file contents) downloaded from Apple Developer portal
17
+ */
18
+ export function createAppleProvider(config: {
19
+ clientId: string;
20
+ teamId: string;
21
+ keyId: string;
22
+ /** The raw PEM contents of the .p8 private key file */
23
+ privateKey: string;
24
+ }): OAuthProvider<{
25
+ code: string;
26
+ redirectUri: string;
27
+ user?: { name?: { firstName?: string; lastName?: string }; email?: string };
28
+ }> {
29
+ /**
30
+ * Generate a client_secret JWT signed with the Apple private key.
31
+ * Apple requires this instead of a static client_secret.
32
+ */
33
+ async function generateClientSecret(): Promise<string> {
34
+ const key = createPrivateKey({
35
+ key: config.privateKey,
36
+ format: "pem"
37
+ });
38
+
39
+ const now = Math.floor(Date.now() / 1000);
40
+
41
+ return new SignJWT({})
42
+ .setProtectedHeader({ alg: "ES256",
43
+ kid: config.keyId })
44
+ .setIssuer(config.teamId)
45
+ .setIssuedAt(now)
46
+ .setExpirationTime(now + 86400 * 180) // 6 months max
47
+ .setAudience("https://appleid.apple.com")
48
+ .setSubject(config.clientId)
49
+ .sign(key);
50
+ }
51
+
52
+ return {
53
+ id: "apple",
54
+ schema: z.object({
55
+ code: z.string().min(1, "Auth code is required"),
56
+ redirectUri: z.string().url("Valid redirect URI is required"),
57
+ /** Apple sends user info only on first authorization; the frontend must forward it. */
58
+ user: z.object({
59
+ name: z.object({
60
+ firstName: z.string().optional(),
61
+ lastName: z.string().optional()
62
+ }).optional(),
63
+ email: z.string().email().optional()
64
+ }).optional()
65
+ }),
66
+ verify: async (payload: {
67
+ code: string;
68
+ redirectUri: string;
69
+ user?: { name?: { firstName?: string; lastName?: string }; email?: string };
70
+ }): Promise<OAuthProviderProfile | null> => {
71
+ try {
72
+ const clientSecret = await generateClientSecret();
73
+
74
+ // Exchange code for tokens
75
+ const tokenResponse = await fetch("https://appleid.apple.com/auth/token", {
76
+ method: "POST",
77
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
78
+ body: new URLSearchParams({
79
+ client_id: config.clientId,
80
+ client_secret: clientSecret,
81
+ code: payload.code,
82
+ grant_type: "authorization_code",
83
+ redirect_uri: payload.redirectUri
84
+ })
85
+ });
86
+
87
+ if (!tokenResponse.ok) {
88
+ console.error("Failed to get Apple access token:", await tokenResponse.text());
89
+ return null;
90
+ }
91
+
92
+ const tokenData = await tokenResponse.json() as { id_token: string };
93
+
94
+ // Decode the id_token (JWT) to get user info.
95
+ // Apple's id_token is a standard JWT — we only need the payload.
96
+ const [, payloadB64] = tokenData.id_token.split(".");
97
+ const decoded = JSON.parse(Buffer.from(payloadB64, "base64url").toString("utf8")) as {
98
+ sub: string;
99
+ email?: string;
100
+ email_verified?: string | boolean;
101
+ };
102
+
103
+ // Apple only sends the user's name on the FIRST authorization.
104
+ // Subsequent logins only have the id_token. The frontend should pass
105
+ // the user object from the first auth for us to capture the name.
106
+ const email = decoded.email || payload.user?.email;
107
+ if (!email) {
108
+ console.error("Apple user has no email");
109
+ return null;
110
+ }
111
+
112
+ let displayName: string | null = null;
113
+ if (payload.user?.name) {
114
+ const parts = [payload.user.name.firstName, payload.user.name.lastName].filter(Boolean);
115
+ displayName = parts.length > 0 ? parts.join(" ") : null;
116
+ }
117
+
118
+ return {
119
+ providerId: decoded.sub,
120
+ email,
121
+ displayName,
122
+ photoUrl: null // Apple does not provide a profile photo
123
+ };
124
+ } catch (error) {
125
+ console.error("Apple OAuth error:", error);
126
+ return null;
127
+ }
128
+ }
129
+ };
130
+ }