@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,788 @@
1
+ import { Hono } from "hono";
2
+ import { ApiError, errorHandler } from "../api/errors";
3
+ import { randomBytes, createHash } from "crypto";
4
+ import type { AuthRepository, OAuthProvider } from "./interfaces";
5
+ import { generateAccessToken, generateRefreshToken, hashRefreshToken, getRefreshTokenExpiry, getAccessTokenExpiry } from "./jwt";
6
+ import { hashPassword, verifyPassword, validatePasswordStrength } from "./password";
7
+ import { requireAuth } from "./middleware";
8
+ import { EmailService, EmailConfig } from "../email";
9
+ import { getPasswordResetTemplate, getEmailVerificationTemplate, getWelcomeEmailTemplate } from "../email/templates";
10
+ import { HonoEnv } from "../api/types";
11
+ import { defaultAuthLimiter, strictAuthLimiter } from "./rate-limiter";
12
+ import { z } from "zod";
13
+
14
+ /**
15
+ * Shared configuration for auth and admin route factories.
16
+ */
17
+ export interface AuthModuleConfig {
18
+ authRepo: AuthRepository;
19
+ emailService?: EmailService;
20
+ emailConfig?: EmailConfig;
21
+ /** Allow new user registration (default: false). */
22
+ allowRegistration?: boolean;
23
+ /** Default role ID to assign to new users (default: none). Must NOT be "admin". */
24
+ defaultRole?: string;
25
+ /** Optional array of OAuth providers */
26
+ oauthProviders?: OAuthProvider[];
27
+ /** When true, blocks all self-registration regardless of `allowRegistration`. */
28
+ disableSelfRegistration?: boolean;
29
+ /**
30
+ * Callback that checks if bootstrap has already been completed.
31
+ * Used by GET /auth/config to report `needsSetup` status.
32
+ * When not provided, falls back to checking if any users exist.
33
+ */
34
+ isBootstrapCompleted?: () => Promise<boolean>;
35
+ }
36
+
37
+ /**
38
+ * Helper to build standard auth response output
39
+ */
40
+ function buildAuthResponse(
41
+ user: { id: string; email: string; displayName?: string | null; photoUrl?: string | null },
42
+ roleIds: string[],
43
+ accessToken: string,
44
+ refreshToken: string
45
+ ) {
46
+ return {
47
+ user: {
48
+ uid: user.id,
49
+ email: user.email,
50
+ displayName: user.displayName ?? null,
51
+ photoURL: user.photoUrl ?? null,
52
+ roles: roleIds
53
+ },
54
+ tokens: {
55
+ accessToken,
56
+ refreshToken,
57
+ accessTokenExpiresAt: getAccessTokenExpiry()
58
+ }
59
+ };
60
+ }
61
+
62
+ /**
63
+ * Generate a secure random token
64
+ */
65
+ function generateSecureToken(): string {
66
+ return randomBytes(40).toString("hex");
67
+ }
68
+
69
+ /**
70
+ * Hash a token for database storage
71
+ */
72
+ function hashToken(token: string): string {
73
+ return createHash("sha256").update(token).digest("hex");
74
+ }
75
+
76
+ /**
77
+ * Get password reset token expiry (1 hour from now)
78
+ */
79
+ function getPasswordResetExpiry(): Date {
80
+ return new Date(Date.now() + 60 * 60 * 1000); // 1 hour
81
+ }
82
+
83
+ export function createAuthRoutes(config: AuthModuleConfig): Hono<HonoEnv> {
84
+ if (config.defaultRole === "admin") {
85
+ throw new Error("CRITICAL SECURITY ERROR: defaultRole cannot be 'admin'. Administrative privilege escalation via registration is strictly forbidden. Use the POST /admin/bootstrap endpoint to promote the initial administrator.");
86
+ }
87
+
88
+ const router = new Hono<HonoEnv>();
89
+
90
+ // Attach Rebase error handler to ensure ApiError exceptions are correctly
91
+ // formatted instead of caught by Hono's default error handler.
92
+ // Hono's onError does NOT propagate from parent to child routers.
93
+ router.onError(errorHandler);
94
+
95
+ const authRepo = config.authRepo;
96
+ const { emailService, emailConfig, allowRegistration = false } = config;
97
+
98
+ // ── Zod input schemas ──────────────────────────────────────────────
99
+ const registerSchema = z.object({
100
+ email: z.string().email("Invalid email address").max(255),
101
+ password: z.string().min(1, "Password is required").max(128),
102
+ displayName: z.string().max(255).optional()
103
+ });
104
+ const loginSchema = z.object({
105
+ email: z.string().email("Invalid email address").max(255),
106
+ password: z.string().min(1, "Password is required").max(128)
107
+ });
108
+ const forgotPasswordSchema = z.object({
109
+ email: z.string().email("Invalid email address").max(255)
110
+ });
111
+ const resetPasswordSchema = z.object({
112
+ token: z.string().min(1, "Token is required"),
113
+ password: z.string().min(1, "Password is required").max(128)
114
+ });
115
+ const changePasswordSchema = z.object({
116
+ oldPassword: z.string().min(1, "Old password is required").max(128),
117
+ newPassword: z.string().min(1, "New password is required").max(128)
118
+ });
119
+ const refreshSchema = z.object({
120
+ refreshToken: z.string().min(1, "Refresh token is required")
121
+ });
122
+ const logoutSchema = z.object({
123
+ refreshToken: z.string().optional()
124
+ });
125
+ const updateProfileSchema = z.object({
126
+ displayName: z.string().max(255).optional(),
127
+ photoURL: z.string().url().max(2048).optional()
128
+ });
129
+
130
+ /** Parse a Zod schema against the request body, throwing ApiError on failure */
131
+ function parseBody<T>(schema: z.ZodSchema<T>, body: unknown): T {
132
+ const result = schema.safeParse(body);
133
+ if (!result.success) {
134
+ const messages = result.error.errors.map(e => `${e.path.join(".")}: ${e.message}`).join(". ");
135
+ throw ApiError.badRequest(messages, "INVALID_INPUT");
136
+ }
137
+ return result.data;
138
+ }
139
+
140
+ /**
141
+ * Check if email service is configured
142
+ */
143
+ function isEmailConfigured(): boolean {
144
+ return !!(emailService && emailService.isConfigured());
145
+ }
146
+
147
+ /**
148
+ * Check if registration is allowed.
149
+ * Registration is only allowed when explicitly enabled via `allowRegistration`.
150
+ * First-user bootstrap must use POST /admin/bootstrap instead.
151
+ */
152
+ function isRegistrationAllowed(): boolean {
153
+ if (config.disableSelfRegistration) return false;
154
+ return !!allowRegistration;
155
+ }
156
+
157
+ /**
158
+ * Send welcome email to a newly registered user (fire-and-forget).
159
+ */
160
+ function sendWelcomeEmail(user: { email: string; displayName?: string | null }) {
161
+ if (!isEmailConfigured()) return;
162
+ const appName = emailConfig?.appName || "Rebase";
163
+ const loginUrl = emailConfig?.resetPasswordUrl || ""; // reuse base URL → the login / app page
164
+ const templateFn = emailConfig?.templates?.welcomeEmail;
165
+ const emailContent = templateFn
166
+ ? templateFn(user, appName)
167
+ : getWelcomeEmailTemplate(user, appName, loginUrl ? `${loginUrl}/app` : undefined);
168
+
169
+ emailService!.send({
170
+ to: user.email,
171
+ subject: emailContent.subject,
172
+ html: emailContent.html,
173
+ text: emailContent.text
174
+ }).catch(err => {
175
+ console.error("Failed to send welcome email:", err instanceof Error ? err.message : err);
176
+ });
177
+ }
178
+
179
+ /**
180
+ * Helper to generate and store session tokens
181
+ */
182
+ async function createSessionAndTokens(userId: string, userAgent: string, ipAddress: string) {
183
+ const roles = await authRepo.getUserRoles(userId);
184
+ const roleIds = roles.map(r => r.id);
185
+ const accessToken = generateAccessToken(userId, roleIds);
186
+ const refreshToken = generateRefreshToken();
187
+
188
+ await authRepo.createRefreshToken(
189
+ userId,
190
+ hashRefreshToken(refreshToken),
191
+ getRefreshTokenExpiry(),
192
+ userAgent,
193
+ ipAddress
194
+ );
195
+
196
+ return { roleIds, accessToken, refreshToken };
197
+ }
198
+
199
+ /**
200
+ * POST /auth/register
201
+ * Create a new account with email/password
202
+ */
203
+ router.post("/register", defaultAuthLimiter, async (c) => {
204
+ const { email, password, displayName } = parseBody(registerSchema, await c.req.json());
205
+
206
+ // Hard kill switch — blocks registration regardless of allowRegistration
207
+ if (config.disableSelfRegistration) {
208
+ throw ApiError.forbidden("Registration is disabled", "REGISTRATION_DISABLED");
209
+ }
210
+
211
+ // Check if registration is allowed (no bypass for empty databases)
212
+ if (!isRegistrationAllowed()) {
213
+ throw ApiError.forbidden("Registration is disabled", "REGISTRATION_DISABLED");
214
+ }
215
+
216
+ // Validate password strength
217
+ const passwordValidation = validatePasswordStrength(password);
218
+ if (!passwordValidation.valid) {
219
+ throw ApiError.badRequest(passwordValidation.errors.join(". "), "WEAK_PASSWORD");
220
+ }
221
+
222
+ // Check if email already exists
223
+ const existingUser = await authRepo.getUserByEmail(email);
224
+ if (existingUser) {
225
+ throw ApiError.conflict("Email already registered", "EMAIL_EXISTS");
226
+ }
227
+
228
+ // Create user
229
+ const passwordHash = await hashPassword(password);
230
+ const user = await authRepo.createUser({
231
+ email: email.toLowerCase(),
232
+ passwordHash,
233
+ displayName: displayName || undefined
234
+ });
235
+
236
+ // Assign configured default role (never auto-assign admin via registration)
237
+ if (config.defaultRole) {
238
+ await authRepo.assignDefaultRole(user.id, config.defaultRole);
239
+ }
240
+
241
+ const { roleIds, accessToken, refreshToken } = await createSessionAndTokens(
242
+ user.id,
243
+ c.req.header("user-agent") || "unknown",
244
+ c.req.header("x-forwarded-for") || "unknown"
245
+ );
246
+
247
+ // Send welcome email (fire-and-forget, don't block registration)
248
+ sendWelcomeEmail({ email: user.email,
249
+ displayName: user.displayName });
250
+
251
+ return c.json(buildAuthResponse(user, roleIds, accessToken, refreshToken), 201);
252
+ });
253
+
254
+ /**
255
+ * POST /auth/login
256
+ * Login with email/password
257
+ */
258
+ router.post("/login", defaultAuthLimiter, async (c) => {
259
+ const { email, password } = parseBody(loginSchema, await c.req.json());
260
+
261
+ const user = await authRepo.getUserByEmail(email);
262
+ if (!user) {
263
+ throw ApiError.unauthorized("Invalid email or password", "INVALID_CREDENTIALS");
264
+ }
265
+
266
+ if (!user.passwordHash) {
267
+ throw ApiError.unauthorized("Invalid email or password", "INVALID_CREDENTIALS");
268
+ }
269
+
270
+ const isValidPassword = await verifyPassword(password, user.passwordHash);
271
+ if (!isValidPassword) {
272
+ throw ApiError.unauthorized("Invalid email or password", "INVALID_CREDENTIALS");
273
+ }
274
+
275
+ const { roleIds, accessToken, refreshToken } = await createSessionAndTokens(
276
+ user.id,
277
+ c.req.header("user-agent") || "unknown",
278
+ c.req.header("x-forwarded-for") || "unknown"
279
+ );
280
+
281
+ return c.json(buildAuthResponse(user, roleIds, accessToken, refreshToken));
282
+ });
283
+
284
+ /**
285
+ * Dynamically mount OAuth provider routes
286
+ */
287
+ if (config.oauthProviders && config.oauthProviders.length > 0) {
288
+ for (const provider of config.oauthProviders) {
289
+ router.post(`/${provider.id}`, defaultAuthLimiter, async (c) => {
290
+ const payload = parseBody(provider.schema, await c.req.json());
291
+
292
+ const externalUser = await provider.verify(payload);
293
+ if (!externalUser) {
294
+ throw ApiError.unauthorized(`Invalid ${provider.id} credentials`, "INVALID_TOKEN");
295
+ }
296
+
297
+ // Find or create user
298
+ let user = await authRepo.getUserByIdentity(provider.id, externalUser.providerId);
299
+
300
+ if (!user) {
301
+ // Check if email exists (link accounts)
302
+ user = await authRepo.getUserByEmail(externalUser.email);
303
+
304
+ if (user) {
305
+ // Link Provider to existing account
306
+ await authRepo.linkUserIdentity(user.id, provider.id, externalUser.providerId, { email: externalUser.email });
307
+
308
+ // Optional: Update profile info from external provider if empty
309
+ await authRepo.updateUser(user.id, {
310
+ displayName: user.displayName || externalUser.displayName || undefined,
311
+ photoUrl: user.photoUrl || externalUser.photoUrl || undefined
312
+ });
313
+ } else {
314
+ // Create new user
315
+ user = await authRepo.createUser({
316
+ email: externalUser.email.toLowerCase(),
317
+ displayName: externalUser.displayName || undefined,
318
+ photoUrl: externalUser.photoUrl || undefined
319
+ });
320
+
321
+ await authRepo.linkUserIdentity(user.id, provider.id, externalUser.providerId, { email: externalUser.email });
322
+
323
+ // Assign configured default role (never auto-assign admin via registration)
324
+ if (config.defaultRole) {
325
+ await authRepo.assignDefaultRole(user.id, config.defaultRole);
326
+ }
327
+
328
+ // Send welcome email for new OAuth users (fire-and-forget)
329
+ sendWelcomeEmail({ email: user.email,
330
+ displayName: user.displayName });
331
+ }
332
+ } else {
333
+ // Update profile info from external provider
334
+ await authRepo.updateUser(user.id, {
335
+ displayName: externalUser.displayName || user.displayName || undefined,
336
+ photoUrl: externalUser.photoUrl || user.photoUrl || undefined
337
+ });
338
+ }
339
+
340
+ const { roleIds, accessToken, refreshToken } = await createSessionAndTokens(
341
+ user.id,
342
+ c.req.header("user-agent") || "unknown",
343
+ c.req.header("x-forwarded-for") || "unknown"
344
+ );
345
+
346
+ return c.json(buildAuthResponse(user, roleIds, accessToken, refreshToken));
347
+ });
348
+ }
349
+ }
350
+
351
+ /**
352
+ * POST /auth/forgot-password
353
+ * Request password reset email
354
+ */
355
+ router.post("/forgot-password", strictAuthLimiter, async (c) => {
356
+ const { email } = parseBody(forgotPasswordSchema, await c.req.json());
357
+
358
+ // Check if email service is configured
359
+ if (!isEmailConfigured()) {
360
+ throw ApiError.serviceUnavailable("Email service not configured. Password reset is not available.", "EMAIL_NOT_CONFIGURED");
361
+ }
362
+
363
+ // Always return success (security: don't reveal if email exists)
364
+ // But only send email if user exists
365
+ const user = await authRepo.getUserByEmail(email);
366
+
367
+ if (user) {
368
+ // Generate reset token
369
+ const token = generateSecureToken();
370
+ const tokenHash = hashToken(token);
371
+ const expiresAt = getPasswordResetExpiry();
372
+
373
+ await authRepo.createPasswordResetToken(user.id, tokenHash, expiresAt);
374
+
375
+ // Build reset URL
376
+ const baseUrl = emailConfig?.resetPasswordUrl || "";
377
+ const resetUrl = `${baseUrl}/reset-password?token=${token}`;
378
+
379
+ // Get email template
380
+ const appName = emailConfig?.appName || "Rebase";
381
+ const templateFn = emailConfig?.templates?.passwordReset;
382
+ const emailContent = templateFn
383
+ ? templateFn(resetUrl, { email: user.email,
384
+ displayName: user.displayName })
385
+ : getPasswordResetTemplate(resetUrl, { email: user.email,
386
+ displayName: user.displayName }, appName);
387
+
388
+ // Send email
389
+ try {
390
+ await emailService!.send({
391
+ to: user.email,
392
+ subject: emailContent.subject,
393
+ html: emailContent.html,
394
+ text: emailContent.text
395
+ });
396
+ } catch (emailError: unknown) {
397
+ console.error("Failed to send password reset email:", emailError instanceof Error ? emailError.message : emailError);
398
+ // Don't reveal email sending failure to client
399
+ }
400
+ }
401
+
402
+ // Always return success
403
+ return c.json({
404
+ success: true,
405
+ message: "If an account with that email exists, a password reset link has been sent."
406
+ });
407
+ });
408
+
409
+ /**
410
+ * POST /auth/reset-password
411
+ * Reset password using token
412
+ */
413
+ router.post("/reset-password", strictAuthLimiter, async (c) => {
414
+ const { token, password } = parseBody(resetPasswordSchema, await c.req.json());
415
+
416
+ // Validate password strength
417
+ const passwordValidation = validatePasswordStrength(password);
418
+ if (!passwordValidation.valid) {
419
+ throw ApiError.badRequest(passwordValidation.errors.join(". "), "WEAK_PASSWORD");
420
+ }
421
+
422
+ // Find valid token
423
+ const tokenHash = hashToken(token);
424
+ const storedToken = await authRepo.findValidPasswordResetToken(tokenHash);
425
+
426
+ if (!storedToken) {
427
+ throw ApiError.badRequest("Invalid or expired reset token", "INVALID_TOKEN");
428
+ }
429
+
430
+ // Update password
431
+ const passwordHash = await hashPassword(password);
432
+ await authRepo.updatePassword(storedToken.userId, passwordHash);
433
+
434
+ // Mark token as used
435
+ await authRepo.markPasswordResetTokenUsed(tokenHash);
436
+
437
+ // Invalidate all refresh tokens (security: log out all sessions)
438
+ await authRepo.deleteAllRefreshTokensForUser(storedToken.userId);
439
+
440
+ return c.json({ success: true,
441
+ message: "Password has been reset successfully" });
442
+ });
443
+
444
+ /**
445
+ * POST /auth/change-password
446
+ * Change password for authenticated user
447
+ */
448
+ router.post("/change-password", requireAuth, async (c) => {
449
+ const userCtx = c.get("user") as { userId: string; roles?: string[] } | undefined;
450
+ if (!userCtx) {
451
+ throw ApiError.unauthorized("Not authenticated");
452
+ }
453
+
454
+ const { oldPassword, newPassword } = parseBody(changePasswordSchema, await c.req.json());
455
+
456
+ // Get user
457
+ const user = await authRepo.getUserById(userCtx.userId);
458
+ if (!user || !user.passwordHash) {
459
+ throw ApiError.badRequest("Cannot change password for this account", "INVALID_ACCOUNT");
460
+ }
461
+
462
+ // Verify old password
463
+ const isValidOldPassword = await verifyPassword(oldPassword, user.passwordHash);
464
+ if (!isValidOldPassword) {
465
+ throw ApiError.unauthorized("Current password is incorrect", "INVALID_CREDENTIALS");
466
+ }
467
+
468
+ // Validate new password strength
469
+ const passwordValidation = validatePasswordStrength(newPassword);
470
+ if (!passwordValidation.valid) {
471
+ throw ApiError.badRequest(passwordValidation.errors.join(". "), "WEAK_PASSWORD");
472
+ }
473
+
474
+ // Update password
475
+ const passwordHash = await hashPassword(newPassword);
476
+ await authRepo.updatePassword(user.id, passwordHash);
477
+
478
+ // Invalidate all refresh tokens (security: log out all sessions)
479
+ await authRepo.deleteAllRefreshTokensForUser(user.id);
480
+
481
+ return c.json({ success: true,
482
+ message: "Password has been changed successfully" });
483
+ });
484
+
485
+ /**
486
+ * POST /auth/send-verification
487
+ * Send email verification link (authenticated)
488
+ */
489
+ router.post("/send-verification", requireAuth, async (c) => {
490
+ const userCtx = c.get("user") as { userId: string; roles?: string[] } | undefined;
491
+ if (!userCtx) {
492
+ throw ApiError.unauthorized("Not authenticated");
493
+ }
494
+
495
+ // Check if email service is configured
496
+ if (!isEmailConfigured()) {
497
+ throw ApiError.serviceUnavailable("Email service not configured. Email verification is not available.", "EMAIL_NOT_CONFIGURED");
498
+ }
499
+
500
+ const user = await authRepo.getUserById(userCtx.userId);
501
+ if (!user) {
502
+ throw ApiError.notFound("User not found");
503
+ }
504
+
505
+ if (user.emailVerified) {
506
+ throw ApiError.badRequest("Email is already verified", "ALREADY_VERIFIED");
507
+ }
508
+
509
+ // Generate verification token
510
+ const token = generateSecureToken();
511
+
512
+ // Store hashed token in user record (raw token goes in the email URL)
513
+ await authRepo.setVerificationToken(user.id, hashToken(token));
514
+
515
+ // Build verification URL
516
+ const baseUrl = emailConfig?.verifyEmailUrl || "";
517
+ const verifyUrl = `${baseUrl}/verify-email?token=${token}`;
518
+
519
+ // Get email template
520
+ const appName = emailConfig?.appName || "Rebase";
521
+ const templateFn = emailConfig?.templates?.emailVerification;
522
+ const emailContent = templateFn
523
+ ? templateFn(verifyUrl, { email: user.email,
524
+ displayName: user.displayName })
525
+ : getEmailVerificationTemplate(verifyUrl, { email: user.email,
526
+ displayName: user.displayName }, appName);
527
+
528
+ // Send email
529
+ await emailService!.send({
530
+ to: user.email,
531
+ subject: emailContent.subject,
532
+ html: emailContent.html,
533
+ text: emailContent.text
534
+ });
535
+
536
+ return c.json({ success: true,
537
+ message: "Verification email sent" });
538
+ });
539
+
540
+ /**
541
+ * GET /auth/verify-email
542
+ * Verify email address using token
543
+ */
544
+ router.get("/verify-email", async (c) => {
545
+ const token = c.req.query("token");
546
+
547
+ if (!token) {
548
+ throw ApiError.badRequest("Verification token is required", "INVALID_INPUT");
549
+ }
550
+
551
+ // Find user by hashed verification token
552
+ const user = await authRepo.getUserByVerificationToken(hashToken(token));
553
+ if (!user) {
554
+ throw ApiError.badRequest("Invalid or expired verification token", "INVALID_TOKEN");
555
+ }
556
+
557
+ // Mark email as verified
558
+ await authRepo.setEmailVerified(user.id, true);
559
+
560
+ return c.json({ success: true,
561
+ message: "Email verified successfully" });
562
+ });
563
+
564
+ /**
565
+ * POST /auth/refresh
566
+ * Refresh access token using refresh token
567
+ */
568
+ router.post("/refresh", async (c) => {
569
+ const { refreshToken } = parseBody(refreshSchema, await c.req.json());
570
+
571
+ const tokenHash = hashRefreshToken(refreshToken);
572
+ const storedToken = await authRepo.findRefreshTokenByHash(tokenHash);
573
+
574
+ if (!storedToken) {
575
+ throw ApiError.unauthorized("Invalid refresh token", "INVALID_TOKEN");
576
+ }
577
+
578
+ if (new Date() > storedToken.expiresAt) {
579
+ await authRepo.deleteRefreshToken(tokenHash);
580
+ throw ApiError.unauthorized("Refresh token expired", "TOKEN_EXPIRED");
581
+ }
582
+
583
+ // Generate new tokens
584
+ const roles = await authRepo.getUserRoles(storedToken.userId);
585
+ const roleIds = roles.map(r => r.id);
586
+
587
+ const newAccessToken = generateAccessToken(storedToken.userId, roleIds);
588
+ const newRefreshToken = generateRefreshToken();
589
+
590
+ // Rotate refresh token (delete old, create new)
591
+ const userAgent = c.req.header("user-agent") || "unknown";
592
+ const ipAddress = c.req.header("x-forwarded-for") || "unknown";
593
+
594
+ await authRepo.deleteRefreshToken(tokenHash);
595
+ await authRepo.createRefreshToken(
596
+ storedToken.userId,
597
+ hashRefreshToken(newRefreshToken),
598
+ getRefreshTokenExpiry(),
599
+ userAgent,
600
+ ipAddress
601
+ );
602
+
603
+ return c.json({
604
+ tokens: {
605
+ accessToken: newAccessToken,
606
+ refreshToken: newRefreshToken,
607
+ accessTokenExpiresAt: getAccessTokenExpiry()
608
+ }
609
+ });
610
+ });
611
+
612
+ /**
613
+ * POST /auth/logout
614
+ * Invalidate refresh token
615
+ */
616
+ router.post("/logout", async (c) => {
617
+ const { refreshToken } = parseBody(logoutSchema, await c.req.json());
618
+
619
+ if (refreshToken) {
620
+ const tokenHash = hashRefreshToken(refreshToken);
621
+ await authRepo.deleteRefreshToken(tokenHash);
622
+ }
623
+
624
+ return c.json({ success: true });
625
+ });
626
+
627
+ /**
628
+ * GET /auth/sessions
629
+ * Get active refresh tokens (sessions) for the current user
630
+ */
631
+ router.get("/sessions", requireAuth, async (c) => {
632
+ const userCtx = c.get("user") as { userId: string; roles?: string[] } | undefined;
633
+ if (!userCtx) {
634
+ throw ApiError.unauthorized("Not authenticated");
635
+ }
636
+
637
+ const currentRefreshToken = c.req.header("x-refresh-token") as string;
638
+ const currentTokenHash = currentRefreshToken ? hashRefreshToken(currentRefreshToken) : null;
639
+
640
+ const sessions = await authRepo.listRefreshTokensForUser(userCtx.userId);
641
+
642
+ const mappedSessions = sessions.map(s => ({
643
+ id: s.id,
644
+ userAgent: s.userAgent,
645
+ ipAddress: s.ipAddress,
646
+ createdAt: s.createdAt,
647
+ isCurrentSession: currentTokenHash ? s.tokenHash === currentTokenHash : false
648
+ }));
649
+
650
+ return c.json({ sessions: mappedSessions });
651
+ });
652
+
653
+ /**
654
+ * DELETE /auth/sessions
655
+ * Delete all refresh tokens for the current user (remote logout every device)
656
+ */
657
+ router.delete("/sessions", requireAuth, async (c) => {
658
+ const userCtx = c.get("user") as { userId: string; roles?: string[] } | undefined;
659
+ if (!userCtx) {
660
+ throw ApiError.unauthorized("Not authenticated");
661
+ }
662
+
663
+ await authRepo.deleteAllRefreshTokensForUser(userCtx.userId);
664
+ return c.json({ success: true,
665
+ message: "All sessions revoked successfully" });
666
+ });
667
+
668
+ /**
669
+ * DELETE /auth/sessions/:id
670
+ * Delete a specific refresh token (remote logout)
671
+ */
672
+ router.delete("/sessions/:id", requireAuth, async (c) => {
673
+ const userCtx = c.get("user") as { userId: string; roles?: string[] } | undefined;
674
+ if (!userCtx) {
675
+ throw ApiError.unauthorized("Not authenticated");
676
+ }
677
+
678
+ const id = c.req.param("id");
679
+ if (!id) {
680
+ throw ApiError.badRequest("Session ID is required", "INVALID_INPUT");
681
+ }
682
+
683
+ await authRepo.deleteRefreshTokenById(id, userCtx.userId);
684
+ return c.json({ success: true,
685
+ message: "Session revoked successfully" });
686
+ });
687
+
688
+ /**
689
+ * GET /auth/me
690
+ * Get current authenticated user
691
+ */
692
+ router.get("/me", requireAuth, async (c) => {
693
+ const userCtx = c.get("user") as { userId: string; roles?: string[] } | undefined;
694
+ if (!userCtx) {
695
+ throw ApiError.unauthorized("Not authenticated");
696
+ }
697
+
698
+ const result = await authRepo.getUserWithRoles(userCtx.userId);
699
+ if (!result) {
700
+ throw ApiError.notFound("User not found");
701
+ }
702
+
703
+ return c.json({
704
+ user: {
705
+ uid: result.user.id,
706
+ email: result.user.email,
707
+ displayName: result.user.displayName,
708
+ photoURL: result.user.photoUrl,
709
+ emailVerified: result.user.emailVerified,
710
+ roles: result.roles.map(r => r.id)
711
+ }
712
+ });
713
+ });
714
+
715
+ /**
716
+ * PATCH /auth/me
717
+ * Update current authenticated user profile
718
+ */
719
+ router.patch("/me", requireAuth, async (c) => {
720
+ const userCtx = c.get("user") as { userId: string; roles?: string[] } | undefined;
721
+ if (!userCtx) {
722
+ throw ApiError.unauthorized("Not authenticated");
723
+ }
724
+
725
+ const { displayName, photoURL } = parseBody(updateProfileSchema, await c.req.json());
726
+
727
+ const updatedUser = await authRepo.updateUser(userCtx.userId, {
728
+ displayName: displayName !== undefined ? displayName : undefined,
729
+ photoUrl: photoURL !== undefined ? photoURL : undefined
730
+ });
731
+
732
+ if (!updatedUser) {
733
+ throw ApiError.notFound("User not found");
734
+ }
735
+
736
+ const result = await authRepo.getUserWithRoles(userCtx.userId);
737
+ if (!result) {
738
+ throw ApiError.notFound("User not found");
739
+ }
740
+
741
+ return c.json({
742
+ user: {
743
+ uid: result.user.id,
744
+ email: result.user.email,
745
+ displayName: result.user.displayName,
746
+ photoURL: result.user.photoUrl,
747
+ emailVerified: result.user.emailVerified,
748
+ roles: result.roles.map(r => r.id)
749
+ }
750
+ });
751
+ });
752
+
753
+ /**
754
+ * GET /auth/config
755
+ * Get public auth configuration (for frontend to know what's available)
756
+ */
757
+ router.get("/config", defaultAuthLimiter, async (c) => {
758
+ // Determine if setup is needed using the persistent bootstrap flag
759
+ // when available, falling back to user-count check for backward compat.
760
+ let needsSetup: boolean;
761
+ if (config.isBootstrapCompleted) {
762
+ needsSetup = !(await config.isBootstrapCompleted());
763
+ } else {
764
+ const allUsers = await authRepo.listUsers();
765
+ needsSetup = allUsers.length === 0;
766
+ }
767
+
768
+ // Registration is allowed when explicitly enabled OR during initial setup
769
+ const registrationAllowed = needsSetup || !!allowRegistration;
770
+
771
+ // Build a dynamic map of enabled providers for frontend discovery.
772
+ // Also maintain legacy boolean fields for backward compatibility.
773
+ const enabledProviders = (config.oauthProviders || []).map(p => p.id);
774
+
775
+ return c.json({
776
+ needsSetup,
777
+ registrationEnabled: registrationAllowed,
778
+ // Legacy fields (kept for backward compat)
779
+ googleEnabled: enabledProviders.includes("google"),
780
+ linkedinEnabled: enabledProviders.includes("linkedin"),
781
+ emailServiceEnabled: isEmailConfigured(),
782
+ // New: complete list of available OAuth providers
783
+ enabledProviders
784
+ });
785
+ });
786
+
787
+ return router;
788
+ }