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