@rebasepro/server-core 0.0.1-canary.4d4fb3e

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 (254) 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 +48 -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 +36 -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 +12 -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-BeMqpmfQ.js +239 -0
  78. package/dist/index-BeMqpmfQ.js.map +1 -0
  79. package/dist/index-bl4J3lNb.js +55823 -0
  80. package/dist/index-bl4J3lNb.js.map +1 -0
  81. package/dist/index.es.js +58 -0
  82. package/dist/index.es.js.map +1 -0
  83. package/dist/index.umd.js +56062 -0
  84. package/dist/index.umd.js.map +1 -0
  85. package/dist/server-core/src/api/ast-schema-editor.d.ts +21 -0
  86. package/dist/server-core/src/api/collections_for_test/callbacks_test_collection.d.ts +2 -0
  87. package/dist/server-core/src/api/errors.d.ts +35 -0
  88. package/dist/server-core/src/api/graphql/graphql-schema-generator.d.ts +35 -0
  89. package/dist/server-core/src/api/graphql/index.d.ts +1 -0
  90. package/dist/server-core/src/api/index.d.ts +9 -0
  91. package/dist/server-core/src/api/openapi-generator.d.ts +2 -0
  92. package/dist/server-core/src/api/rest/api-generator.d.ts +64 -0
  93. package/dist/server-core/src/api/rest/index.d.ts +1 -0
  94. package/dist/server-core/src/api/rest/query-parser.d.ts +9 -0
  95. package/dist/server-core/src/api/schema-editor-routes.d.ts +3 -0
  96. package/dist/server-core/src/api/server.d.ts +40 -0
  97. package/dist/server-core/src/api/types.d.ts +90 -0
  98. package/dist/server-core/src/auth/admin-routes.d.ts +7 -0
  99. package/dist/server-core/src/auth/google-oauth.d.ts +20 -0
  100. package/dist/server-core/src/auth/index.d.ts +12 -0
  101. package/dist/server-core/src/auth/interfaces.d.ts +270 -0
  102. package/dist/server-core/src/auth/jwt.d.ts +42 -0
  103. package/dist/server-core/src/auth/middleware.d.ts +56 -0
  104. package/dist/server-core/src/auth/password.d.ts +22 -0
  105. package/dist/server-core/src/auth/rate-limiter.d.ts +31 -0
  106. package/dist/server-core/src/auth/routes.d.ts +17 -0
  107. package/dist/server-core/src/bootstrappers/index.d.ts +0 -0
  108. package/dist/server-core/src/collections/BackendCollectionRegistry.d.ts +13 -0
  109. package/dist/server-core/src/collections/loader.d.ts +5 -0
  110. package/dist/server-core/src/db/interfaces.d.ts +18 -0
  111. package/dist/server-core/src/email/index.d.ts +6 -0
  112. package/dist/server-core/src/email/smtp-email-service.d.ts +25 -0
  113. package/dist/server-core/src/email/templates.d.ts +33 -0
  114. package/dist/server-core/src/email/types.d.ts +110 -0
  115. package/dist/server-core/src/functions/function-loader.d.ts +17 -0
  116. package/dist/server-core/src/functions/function-routes.d.ts +10 -0
  117. package/dist/server-core/src/functions/index.d.ts +3 -0
  118. package/dist/server-core/src/history/history-routes.d.ts +23 -0
  119. package/dist/server-core/src/history/index.d.ts +1 -0
  120. package/dist/server-core/src/index.d.ts +24 -0
  121. package/dist/server-core/src/init.d.ts +49 -0
  122. package/dist/server-core/src/serve-spa.d.ts +30 -0
  123. package/dist/server-core/src/services/driver-registry.d.ts +78 -0
  124. package/dist/server-core/src/storage/LocalStorageController.d.ts +46 -0
  125. package/dist/server-core/src/storage/S3StorageController.d.ts +36 -0
  126. package/dist/server-core/src/storage/index.d.ts +18 -0
  127. package/dist/server-core/src/storage/routes.d.ts +38 -0
  128. package/dist/server-core/src/storage/storage-registry.d.ts +78 -0
  129. package/dist/server-core/src/storage/types.d.ts +91 -0
  130. package/dist/server-core/src/types/index.d.ts +11 -0
  131. package/dist/server-core/src/utils/logging.d.ts +9 -0
  132. package/dist/server-core/src/utils/sql.d.ts +27 -0
  133. package/dist/types/src/controllers/analytics_controller.d.ts +7 -0
  134. package/dist/types/src/controllers/auth.d.ts +117 -0
  135. package/dist/types/src/controllers/client.d.ts +58 -0
  136. package/dist/types/src/controllers/collection_registry.d.ts +44 -0
  137. package/dist/types/src/controllers/customization_controller.d.ts +54 -0
  138. package/dist/types/src/controllers/data.d.ts +141 -0
  139. package/dist/types/src/controllers/data_driver.d.ts +168 -0
  140. package/dist/types/src/controllers/database_admin.d.ts +11 -0
  141. package/dist/types/src/controllers/dialogs_controller.d.ts +36 -0
  142. package/dist/types/src/controllers/effective_role.d.ts +4 -0
  143. package/dist/types/src/controllers/index.d.ts +17 -0
  144. package/dist/types/src/controllers/local_config_persistence.d.ts +20 -0
  145. package/dist/types/src/controllers/navigation.d.ts +213 -0
  146. package/dist/types/src/controllers/registry.d.ts +51 -0
  147. package/dist/types/src/controllers/side_dialogs_controller.d.ts +67 -0
  148. package/dist/types/src/controllers/side_entity_controller.d.ts +89 -0
  149. package/dist/types/src/controllers/snackbar.d.ts +24 -0
  150. package/dist/types/src/controllers/storage.d.ts +173 -0
  151. package/dist/types/src/index.d.ts +4 -0
  152. package/dist/types/src/rebase_context.d.ts +101 -0
  153. package/dist/types/src/types/backend.d.ts +533 -0
  154. package/dist/types/src/types/builders.d.ts +14 -0
  155. package/dist/types/src/types/chips.d.ts +5 -0
  156. package/dist/types/src/types/collections.d.ts +812 -0
  157. package/dist/types/src/types/data_source.d.ts +64 -0
  158. package/dist/types/src/types/entities.d.ts +145 -0
  159. package/dist/types/src/types/entity_actions.d.ts +98 -0
  160. package/dist/types/src/types/entity_callbacks.d.ts +173 -0
  161. package/dist/types/src/types/entity_link_builder.d.ts +7 -0
  162. package/dist/types/src/types/entity_overrides.d.ts +9 -0
  163. package/dist/types/src/types/entity_views.d.ts +61 -0
  164. package/dist/types/src/types/export_import.d.ts +21 -0
  165. package/dist/types/src/types/index.d.ts +22 -0
  166. package/dist/types/src/types/locales.d.ts +4 -0
  167. package/dist/types/src/types/modify_collections.d.ts +5 -0
  168. package/dist/types/src/types/plugins.d.ts +225 -0
  169. package/dist/types/src/types/properties.d.ts +1091 -0
  170. package/dist/types/src/types/property_config.d.ts +70 -0
  171. package/dist/types/src/types/relations.d.ts +336 -0
  172. package/dist/types/src/types/slots.d.ts +228 -0
  173. package/dist/types/src/types/translations.d.ts +826 -0
  174. package/dist/types/src/types/user_management_delegate.d.ts +120 -0
  175. package/dist/types/src/types/websockets.d.ts +78 -0
  176. package/dist/types/src/users/index.d.ts +2 -0
  177. package/dist/types/src/users/roles.d.ts +22 -0
  178. package/dist/types/src/users/user.d.ts +46 -0
  179. package/history_diff.log +385 -0
  180. package/jest.config.cjs +16 -0
  181. package/package.json +86 -0
  182. package/scratch.ts +8 -0
  183. package/src/api/ast-schema-editor.ts +289 -0
  184. package/src/api/collections_for_test/callbacks_test_collection.ts +57 -0
  185. package/src/api/errors.ts +155 -0
  186. package/src/api/graphql/graphql-schema-generator.ts +334 -0
  187. package/src/api/graphql/index.ts +2 -0
  188. package/src/api/index.ts +11 -0
  189. package/src/api/openapi-generator.ts +160 -0
  190. package/src/api/rest/api-generator.ts +466 -0
  191. package/src/api/rest/index.ts +2 -0
  192. package/src/api/rest/query-parser.ts +155 -0
  193. package/src/api/schema-editor-routes.ts +39 -0
  194. package/src/api/server.ts +245 -0
  195. package/src/api/types.ts +90 -0
  196. package/src/auth/admin-routes.ts +488 -0
  197. package/src/auth/google-oauth.ts +60 -0
  198. package/src/auth/index.ts +21 -0
  199. package/src/auth/interfaces.ts +316 -0
  200. package/src/auth/jwt.ts +164 -0
  201. package/src/auth/middleware.ts +235 -0
  202. package/src/auth/password.ts +75 -0
  203. package/src/auth/rate-limiter.ts +129 -0
  204. package/src/auth/routes.ts +730 -0
  205. package/src/bootstrappers/index.ts +1 -0
  206. package/src/collections/BackendCollectionRegistry.ts +20 -0
  207. package/src/collections/loader.ts +49 -0
  208. package/src/db/interfaces.ts +60 -0
  209. package/src/email/index.ts +17 -0
  210. package/src/email/smtp-email-service.ts +88 -0
  211. package/src/email/templates.ts +301 -0
  212. package/src/email/types.ts +112 -0
  213. package/src/functions/function-loader.ts +91 -0
  214. package/src/functions/function-routes.ts +31 -0
  215. package/src/functions/index.ts +3 -0
  216. package/src/history/history-routes.ts +128 -0
  217. package/src/history/index.ts +2 -0
  218. package/src/index.ts +56 -0
  219. package/src/init.ts +309 -0
  220. package/src/serve-spa.ts +81 -0
  221. package/src/services/driver-registry.ts +182 -0
  222. package/src/storage/LocalStorageController.ts +368 -0
  223. package/src/storage/S3StorageController.ts +295 -0
  224. package/src/storage/index.ts +32 -0
  225. package/src/storage/routes.ts +247 -0
  226. package/src/storage/storage-registry.ts +187 -0
  227. package/src/storage/types.ts +122 -0
  228. package/src/types/index.ts +27 -0
  229. package/src/utils/logging.ts +35 -0
  230. package/src/utils/sql.ts +38 -0
  231. package/test/admin-routes.test.ts +591 -0
  232. package/test/api-generator.test.ts +458 -0
  233. package/test/ast-schema-editor.test.ts +61 -0
  234. package/test/auth-middleware-hono.test.ts +321 -0
  235. package/test/auth-routes.test.ts +868 -0
  236. package/test/driver-registry.test.ts +280 -0
  237. package/test/errors-hono.test.ts +133 -0
  238. package/test/errors.test.ts +150 -0
  239. package/test/jwt-security.test.ts +173 -0
  240. package/test/jwt.test.ts +311 -0
  241. package/test/middleware.test.ts +295 -0
  242. package/test/password.test.ts +165 -0
  243. package/test/query-parser.test.ts +258 -0
  244. package/test/rate-limiter.test.ts +102 -0
  245. package/test/storage-local.test.ts +278 -0
  246. package/test/storage-registry.test.ts +280 -0
  247. package/test/storage-routes.test.ts +218 -0
  248. package/test/storage-s3.test.ts +301 -0
  249. package/test-ast.ts +28 -0
  250. package/test_output.txt +1133 -0
  251. package/tsconfig.json +49 -0
  252. package/tsconfig.prod.json +20 -0
  253. package/vite.config.ts +78 -0
  254. package/vite.config.ts.timestamp-1775065397568-8a853255edf6e.mjs +46 -0
@@ -0,0 +1,488 @@
1
+ import { Hono } from "hono";
2
+ import { ApiError } from "../api/errors";
3
+ import type { AuthRepository } from "./interfaces";
4
+ import { requireAuth, requireAdmin } from "./middleware";
5
+ import { hashPassword, validatePasswordStrength } from "./password";
6
+ import { AuthModuleConfig } from "./routes";
7
+ import { HonoEnv } from "../api/types";
8
+ import { randomBytes, createHash } from "crypto";
9
+ import { getUserInvitationTemplate, getPasswordResetTemplate } from "../email/templates";
10
+
11
+ /**
12
+ * Generate a cryptographically secure random password that meets strength requirements.
13
+ */
14
+ function generateSecurePassword(): string {
15
+ const upper = "ABCDEFGHJKLMNPQRSTUVWXYZ";
16
+ const lower = "abcdefghjkmnpqrstuvwxyz";
17
+ const digits = "23456789";
18
+ const all = upper + lower + digits;
19
+
20
+ // Guarantee at least one of each required class
21
+ const pick = (chars: string) => chars[Math.floor(Math.random() * chars.length)];
22
+ const parts = [pick(upper), pick(lower), pick(digits)];
23
+
24
+ // Fill remaining with random chars (16 total)
25
+ for (let i = parts.length; i < 16; i++) {
26
+ parts.push(pick(all));
27
+ }
28
+
29
+ // Shuffle
30
+ for (let i = parts.length - 1; i > 0; i--) {
31
+ const j = Math.floor(Math.random() * (i + 1));
32
+ [parts[i], parts[j]] = [parts[j], parts[i]];
33
+ }
34
+ return parts.join("");
35
+ }
36
+
37
+ /**
38
+ * Generate a secure random token
39
+ */
40
+ function generateSecureToken(): string {
41
+ return randomBytes(40).toString("hex");
42
+ }
43
+
44
+ /**
45
+ * Hash a token for database storage
46
+ */
47
+ function hashToken(token: string): string {
48
+ return createHash("sha256").update(token).digest("hex");
49
+ }
50
+
51
+ /**
52
+ * Create admin routes for user and role management
53
+ */
54
+ export function createAdminRoutes(config: AuthModuleConfig): Hono<HonoEnv> {
55
+ const router = new Hono<HonoEnv>();
56
+ const authRepo = config.authRepo;
57
+ const { emailService, emailConfig } = config;
58
+
59
+ // Apply auth middleware to all routes
60
+ router.use("/*", requireAuth);
61
+
62
+ router.post("/bootstrap", async (c) => {
63
+ const user = c.get("user");
64
+ if (!user || typeof user !== "object") {
65
+ throw ApiError.unauthorized("Not authenticated");
66
+ }
67
+
68
+ const users = await authRepo.listUsers();
69
+ let hasAdmin = false;
70
+
71
+ for (const u of users) {
72
+ const roles = await authRepo.getUserRoleIds(u.id);
73
+ if (roles.includes("admin")) {
74
+ hasAdmin = true;
75
+ break;
76
+ }
77
+ }
78
+
79
+ if (hasAdmin) {
80
+ throw ApiError.forbidden("Admin users already exist. Bootstrap not allowed.");
81
+ }
82
+
83
+ const userId = "userId" in user ? user.userId : undefined;
84
+ if (!userId) {
85
+ throw ApiError.unauthorized("User ID not found in auth context");
86
+ }
87
+ await authRepo.setUserRoles(userId, ["admin"]);
88
+
89
+ return c.json({
90
+ success: true,
91
+ message: "You are now an admin",
92
+ user: {
93
+ uid: userId,
94
+ roles: ["admin"]
95
+ }
96
+ });
97
+ });
98
+
99
+ router.get("/users", requireAdmin, async (c) => {
100
+ const limitParam = c.req.query("limit");
101
+ const offsetParam = c.req.query("offset");
102
+ const search = c.req.query("search");
103
+ const orderBy = c.req.query("orderBy");
104
+ const orderDir = c.req.query("orderDir") as "asc" | "desc" | undefined;
105
+
106
+ // If pagination params are provided, use the paginated path
107
+ if (limitParam !== undefined || search) {
108
+ const limit = limitParam ? parseInt(limitParam, 10) : 25;
109
+ const offset = offsetParam ? parseInt(offsetParam, 10) : 0;
110
+
111
+ const result = await authRepo.listUsersPaginated({
112
+ limit,
113
+ offset,
114
+ search: search || undefined,
115
+ orderBy: orderBy || undefined,
116
+ orderDir: orderDir || undefined
117
+ });
118
+
119
+ const usersWithRoles = await Promise.all(
120
+ result.users.map(async (u) => {
121
+ const roles = await authRepo.getUserRoleIds(u.id);
122
+ return {
123
+ uid: u.id,
124
+ email: u.email,
125
+ displayName: u.displayName,
126
+ photoURL: u.photoUrl,
127
+ provider: u.provider,
128
+ roles,
129
+ createdAt: u.createdAt,
130
+ updatedAt: u.updatedAt
131
+ };
132
+ })
133
+ );
134
+
135
+ return c.json({
136
+ users: usersWithRoles,
137
+ total: result.total,
138
+ limit: result.limit,
139
+ offset: result.offset
140
+ });
141
+ }
142
+
143
+ // Legacy: return all users (no pagination)
144
+ const users = await authRepo.listUsers();
145
+ const usersWithRoles = await Promise.all(
146
+ users.map(async (u) => {
147
+ const roles = await authRepo.getUserRoleIds(u.id);
148
+ return {
149
+ uid: u.id,
150
+ email: u.email,
151
+ displayName: u.displayName,
152
+ photoURL: u.photoUrl,
153
+ provider: u.provider,
154
+ roles,
155
+ createdAt: u.createdAt,
156
+ updatedAt: u.updatedAt
157
+ };
158
+ })
159
+ );
160
+ return c.json({ users: usersWithRoles });
161
+ });
162
+
163
+ router.get("/users/:userId", requireAdmin, async (c) => {
164
+ const userId = c.req.param("userId");
165
+ const result = await authRepo.getUserWithRoles(userId);
166
+
167
+ if (!result) {
168
+ throw ApiError.notFound("User not found");
169
+ }
170
+
171
+ return c.json({
172
+ user: {
173
+ uid: result.user.id,
174
+ email: result.user.email,
175
+ displayName: result.user.displayName,
176
+ photoURL: result.user.photoUrl,
177
+ provider: result.user.provider,
178
+ roles: result.roles.map(r => r.id),
179
+ createdAt: result.user.createdAt,
180
+ updatedAt: result.user.updatedAt
181
+ }
182
+ });
183
+ });
184
+
185
+ router.post("/users", requireAdmin, async (c) => {
186
+ const body = await c.req.json();
187
+ const { email, displayName, password, roles } = body;
188
+
189
+ if (!email) {
190
+ throw ApiError.badRequest("Email is required", "INVALID_INPUT");
191
+ }
192
+
193
+ const existing = await authRepo.getUserByEmail(email);
194
+ if (existing) {
195
+ throw ApiError.conflict("Email already exists", "EMAIL_EXISTS");
196
+ }
197
+
198
+ // Use provided password or auto-generate one
199
+ const clearPassword = password || generateSecurePassword();
200
+
201
+ const validation = validatePasswordStrength(clearPassword);
202
+ if (!validation.valid) {
203
+ throw ApiError.badRequest(validation.errors.join(". "), "WEAK_PASSWORD");
204
+ }
205
+ const passwordHash = await hashPassword(clearPassword);
206
+
207
+ const user = await authRepo.createUser({
208
+ email: email.toLowerCase(),
209
+ displayName: displayName || null,
210
+ passwordHash,
211
+ provider: "email"
212
+ });
213
+
214
+ if (roles && Array.isArray(roles) && roles.length > 0) {
215
+ await authRepo.setUserRoles(user.id, roles);
216
+ } else if (config.defaultRole) {
217
+ await authRepo.assignDefaultRole(user.id, config.defaultRole);
218
+ }
219
+
220
+ const userRoles = await authRepo.getUserRoleIds(user.id);
221
+
222
+ // Determine if we can send an invitation email
223
+ const isEmailConfigured = !!(emailService && emailService.isConfigured());
224
+ let invitationSent = false;
225
+ let temporaryPassword: string | undefined;
226
+
227
+ if (isEmailConfigured && !password) {
228
+ // Send invitation email via password-reset token flow
229
+ try {
230
+ const token = generateSecureToken();
231
+ const tokenHash = hashToken(token);
232
+ const expiresAt = new Date(Date.now() + 60 * 60 * 1000); // 1 hour
233
+
234
+ await authRepo.createPasswordResetToken(user.id, tokenHash, expiresAt);
235
+
236
+ const baseUrl = emailConfig?.resetPasswordUrl || "";
237
+ const setPasswordUrl = `${baseUrl}/reset-password?token=${token}`;
238
+
239
+ const appName = emailConfig?.appName || "Rebase";
240
+ const templateFn = emailConfig?.templates?.userInvitation;
241
+ const emailContent = templateFn
242
+ ? templateFn(setPasswordUrl, { email: user.email, displayName: user.displayName })
243
+ : getUserInvitationTemplate(setPasswordUrl, { email: user.email, displayName: user.displayName }, appName);
244
+
245
+ await emailService!.send({
246
+ to: user.email,
247
+ subject: emailContent.subject,
248
+ html: emailContent.html,
249
+ text: emailContent.text
250
+ });
251
+ invitationSent = true;
252
+ } catch (emailError: unknown) {
253
+ console.error("Failed to send invitation email:", emailError instanceof Error ? emailError.message : emailError);
254
+ // Fall back to returning the temporary password
255
+ temporaryPassword = clearPassword;
256
+ }
257
+ } else if (!password) {
258
+ // No email service — return the auto-generated password one-time
259
+ temporaryPassword = clearPassword;
260
+ }
261
+ // If admin provided a password explicitly, don't return it or send email
262
+
263
+ return c.json({
264
+ user: {
265
+ uid: user.id,
266
+ email: user.email,
267
+ displayName: user.displayName,
268
+ roles: userRoles
269
+ },
270
+ invitationSent,
271
+ ...(temporaryPassword ? { temporaryPassword } : {})
272
+ }, 201);
273
+ });
274
+
275
+ router.post("/users/:userId/reset-password", requireAdmin, async (c) => {
276
+ const userId = c.req.param("userId");
277
+ const existing = await authRepo.getUserById(userId);
278
+ if (!existing) {
279
+ throw ApiError.notFound("User not found");
280
+ }
281
+
282
+ const isEmailConfigured = !!(emailService && emailService.isConfigured());
283
+ let invitationSent = false;
284
+ let temporaryPassword: string | undefined;
285
+
286
+ if (isEmailConfigured) {
287
+ try {
288
+ const token = generateSecureToken();
289
+ const tokenHash = hashToken(token);
290
+ const expiresAt = new Date(Date.now() + 60 * 60 * 1000); // 1 hour
291
+
292
+ await authRepo.createPasswordResetToken(existing.id, tokenHash, expiresAt);
293
+
294
+ const baseUrl = emailConfig?.resetPasswordUrl || "";
295
+ const setPasswordUrl = `${baseUrl}/reset-password?token=${token}`;
296
+
297
+ const appName = emailConfig?.appName || "Rebase";
298
+ const templateFn = emailConfig?.templates?.passwordReset;
299
+ const emailContent = templateFn
300
+ ? templateFn(setPasswordUrl, { email: existing.email, displayName: existing.displayName })
301
+ : getPasswordResetTemplate(setPasswordUrl, { email: existing.email, displayName: existing.displayName }, appName);
302
+
303
+ await emailService!.send({
304
+ to: existing.email,
305
+ subject: emailContent.subject,
306
+ html: emailContent.html,
307
+ text: emailContent.text
308
+ });
309
+ invitationSent = true;
310
+ } catch (emailError: unknown) {
311
+ console.error("Failed to send reset email:", emailError instanceof Error ? emailError.message : emailError);
312
+ // Fall back to returning the temporary password
313
+ const clearPassword = generateSecurePassword();
314
+ const passwordHash = await hashPassword(clearPassword);
315
+ await authRepo.updatePassword(existing.id, passwordHash);
316
+ temporaryPassword = clearPassword;
317
+ }
318
+ } else {
319
+ // No email service — generate password, set it, and return one-time
320
+ const clearPassword = generateSecurePassword();
321
+ const passwordHash = await hashPassword(clearPassword);
322
+ await authRepo.updatePassword(existing.id, passwordHash);
323
+ temporaryPassword = clearPassword;
324
+ }
325
+
326
+ const userRoles = await authRepo.getUserRoleIds(existing.id);
327
+
328
+ return c.json({
329
+ user: {
330
+ uid: existing.id,
331
+ email: existing.email,
332
+ displayName: existing.displayName,
333
+ roles: userRoles
334
+ },
335
+ invitationSent,
336
+ ...(temporaryPassword ? { temporaryPassword } : {})
337
+ }, 200);
338
+ });
339
+
340
+ router.put("/users/:userId", requireAdmin, async (c) => {
341
+ const userId = c.req.param("userId");
342
+ const body = await c.req.json();
343
+ const { email, displayName, password, roles } = body;
344
+
345
+ const existing = await authRepo.getUserById(userId);
346
+ if (!existing) {
347
+ throw ApiError.notFound("User not found");
348
+ }
349
+
350
+ const updates: Record<string, unknown> = {};
351
+ if (email !== undefined) updates.email = email.toLowerCase();
352
+ if (displayName !== undefined) updates.displayName = displayName;
353
+
354
+ if (password) {
355
+ const validation = validatePasswordStrength(password);
356
+ if (!validation.valid) {
357
+ throw ApiError.badRequest(validation.errors.join(". "), "WEAK_PASSWORD");
358
+ }
359
+ updates.passwordHash = await hashPassword(password);
360
+ }
361
+
362
+ if (Object.keys(updates).length > 0) {
363
+ await authRepo.updateUser(userId, updates);
364
+ }
365
+
366
+ if (roles !== undefined && Array.isArray(roles)) {
367
+ await authRepo.setUserRoles(userId, roles);
368
+ }
369
+
370
+ const result = await authRepo.getUserWithRoles(userId);
371
+
372
+ return c.json({
373
+ user: {
374
+ uid: result!.user.id,
375
+ email: result!.user.email,
376
+ displayName: result!.user.displayName,
377
+ roles: result!.roles.map(r => r.id)
378
+ }
379
+ });
380
+ });
381
+
382
+ router.delete("/users/:userId", requireAdmin, async (c) => {
383
+ const userId = c.req.param("userId");
384
+ const user = c.get("user");
385
+
386
+ const currentUserId = user && typeof user === "object" && "userId" in user ? user.userId : undefined;
387
+ if (currentUserId === userId) {
388
+ throw ApiError.badRequest("Cannot delete your own account", "SELF_DELETE");
389
+ }
390
+
391
+ const existing = await authRepo.getUserById(userId);
392
+ if (!existing) {
393
+ throw ApiError.notFound("User not found");
394
+ }
395
+
396
+ await authRepo.deleteUser(userId);
397
+
398
+ return c.json({ success: true });
399
+ });
400
+
401
+ router.get("/roles", requireAdmin, async (c) => {
402
+ const roles = await authRepo.listRoles();
403
+
404
+ return c.json({
405
+ roles: roles.map(r => ({
406
+ id: r.id,
407
+ name: r.name,
408
+ isAdmin: r.isAdmin,
409
+ defaultPermissions: r.defaultPermissions,
410
+ config: r.config
411
+ }))
412
+ });
413
+ });
414
+
415
+ router.get("/roles/:roleId", requireAdmin, async (c) => {
416
+ const roleId = c.req.param("roleId");
417
+ const role = await authRepo.getRoleById(roleId);
418
+
419
+ if (!role) {
420
+ throw ApiError.notFound("Role not found");
421
+ }
422
+
423
+ return c.json({ role });
424
+ });
425
+
426
+ router.post("/roles", requireAdmin, async (c) => {
427
+ const body = await c.req.json();
428
+ const { id, name, isAdmin, defaultPermissions, config } = body;
429
+
430
+ if (!id || !name) {
431
+ throw ApiError.badRequest("Role ID and name are required", "INVALID_INPUT");
432
+ }
433
+
434
+ const existing = await authRepo.getRoleById(id);
435
+ if (existing) {
436
+ throw ApiError.conflict("Role already exists", "ROLE_EXISTS");
437
+ }
438
+
439
+ const role = await authRepo.createRole({
440
+ id,
441
+ name,
442
+ isAdmin: isAdmin ?? false,
443
+ defaultPermissions: defaultPermissions ?? null,
444
+ config: config ?? null
445
+ });
446
+
447
+ return c.json({ role }, 201);
448
+ });
449
+
450
+ router.put("/roles/:roleId", requireAdmin, async (c) => {
451
+ const roleId = c.req.param("roleId");
452
+ const body = await c.req.json();
453
+ const { name, isAdmin, defaultPermissions, config } = body;
454
+
455
+ const existing = await authRepo.getRoleById(roleId);
456
+ if (!existing) {
457
+ throw ApiError.notFound("Role not found");
458
+ }
459
+
460
+ const role = await authRepo.updateRole(roleId, {
461
+ name,
462
+ isAdmin,
463
+ defaultPermissions,
464
+ config
465
+ });
466
+
467
+ return c.json({ role });
468
+ });
469
+
470
+ router.delete("/roles/:roleId", requireAdmin, async (c) => {
471
+ const roleId = c.req.param("roleId");
472
+
473
+ if (["admin", "editor", "viewer"].includes(roleId)) {
474
+ throw ApiError.badRequest("Cannot delete built-in roles", "BUILTIN_ROLE");
475
+ }
476
+
477
+ const existing = await authRepo.getRoleById(roleId);
478
+ if (!existing) {
479
+ throw ApiError.notFound("Role not found");
480
+ }
481
+
482
+ await authRepo.deleteRole(roleId);
483
+
484
+ return c.json({ success: true });
485
+ });
486
+
487
+ return router;
488
+ }
@@ -0,0 +1,60 @@
1
+ import { OAuth2Client } from "google-auth-library/build/src/index.js";
2
+
3
+ export interface GoogleUserInfo {
4
+ googleId: string;
5
+ email: string;
6
+ displayName: string | null;
7
+ photoUrl: string | null;
8
+ emailVerified: boolean;
9
+ }
10
+
11
+ let googleClient: OAuth2Client | null = null;
12
+ let configuredClientId: string | null = null;
13
+
14
+ /**
15
+ * Configure Google OAuth - call this during initialization
16
+ */
17
+ export function configureGoogleOAuth(clientId: string): void {
18
+ configuredClientId = clientId;
19
+ googleClient = new OAuth2Client(clientId);
20
+ }
21
+
22
+ /**
23
+ * Verify a Google ID token and extract user information
24
+ * @param idToken The ID token from Google Sign-In on the frontend
25
+ */
26
+ export async function verifyGoogleIdToken(idToken: string): Promise<GoogleUserInfo | null> {
27
+ if (!googleClient || !configuredClientId) {
28
+ throw new Error("Google OAuth not configured. Call configureGoogleOAuth() first.");
29
+ }
30
+
31
+ try {
32
+ const ticket = await googleClient.verifyIdToken({
33
+ idToken,
34
+ audience: configuredClientId
35
+ });
36
+
37
+ const payload = ticket.getPayload();
38
+ if (!payload) {
39
+ return null;
40
+ }
41
+
42
+ return {
43
+ googleId: payload.sub,
44
+ email: payload.email || "",
45
+ displayName: payload.name || null,
46
+ photoUrl: payload.picture || null,
47
+ emailVerified: payload.email_verified || false
48
+ };
49
+ } catch (error) {
50
+ console.error("Failed to verify Google ID token:", error);
51
+ return null;
52
+ }
53
+ }
54
+
55
+ /**
56
+ * Check if Google OAuth is configured
57
+ */
58
+ export function isGoogleOAuthConfigured(): boolean {
59
+ return googleClient !== null && configuredClientId !== null;
60
+ }
@@ -0,0 +1,21 @@
1
+ // Auth module exports
2
+ export { configureJwt, generateAccessToken, verifyAccessToken, generateRefreshToken, hashRefreshToken, getRefreshTokenExpiry, getAccessTokenExpiry } from "./jwt";
3
+ export type { JwtConfig, AccessTokenPayload } from "./jwt";
4
+
5
+ export { hashPassword, verifyPassword, validatePasswordStrength } from "./password";
6
+ export type { PasswordValidationResult } from "./password";
7
+
8
+ export { configureGoogleOAuth, verifyGoogleIdToken, isGoogleOAuthConfigured } from "./google-oauth";
9
+ export type { GoogleUserInfo } from "./google-oauth";
10
+
11
+ export { requireAuth, requireAdmin, optionalAuth, extractUserFromToken, createAuthMiddleware } from "./middleware";
12
+ export type { AuthMiddlewareOptions, AuthResult } from "./middleware";
13
+
14
+
15
+ export { createAuthRoutes } from "./routes";
16
+ export type { AuthModuleConfig } from "./routes";
17
+
18
+ export { createAdminRoutes } from "./admin-routes";
19
+
20
+
21
+ export { createRateLimiter, defaultAuthLimiter, strictAuthLimiter } from "./rate-limiter";