@rebasepro/server-core 0.4.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (282) hide show
  1. package/README.md +62 -25
  2. package/dist/{server-core/src/api → api}/errors.d.ts +20 -2
  3. package/dist/{server-core/src/api → api}/rest/api-generator.d.ts +8 -2
  4. package/dist/{server-core/src/api → api}/types.d.ts +2 -0
  5. package/dist/auth/admin-user-ops.d.ts +79 -0
  6. package/dist/{server-core/src/auth → auth}/api-keys/index.d.ts +2 -2
  7. package/dist/{server-core/src/auth → auth}/auth-hooks.d.ts +37 -9
  8. package/dist/{server-core/src/auth → auth}/builtin-auth-adapter.d.ts +4 -4
  9. package/dist/{server-core/src/auth → auth}/index.d.ts +5 -2
  10. package/dist/{server-core/src/auth → auth}/jwt.d.ts +10 -0
  11. package/dist/auth/mfa-routes.d.ts +6 -0
  12. package/dist/auth/reset-password-admin.d.ts +29 -0
  13. package/dist/auth/session-routes.d.ts +25 -0
  14. package/dist/backend-CIxN4FVm.js +15 -0
  15. package/dist/backend-CIxN4FVm.js.map +1 -0
  16. package/dist/chunk-Dze3rakg.js +42 -0
  17. package/dist/{server-core/src/cron → cron}/cron-scheduler.d.ts +1 -1
  18. package/dist/dist-CZKP-Xz4.js +832 -0
  19. package/dist/dist-CZKP-Xz4.js.map +1 -0
  20. package/dist/env.d.ts +102 -0
  21. package/dist/from-VbwD7xRf.js +3849 -0
  22. package/dist/from-VbwD7xRf.js.map +1 -0
  23. package/dist/{server-core/src/index.d.ts → index.d.ts} +1 -0
  24. package/dist/index.es.js +60871 -50424
  25. package/dist/index.es.js.map +1 -1
  26. package/dist/index.umd.js +73151 -51256
  27. package/dist/index.umd.js.map +1 -1
  28. package/dist/init/docs.d.ts +4 -0
  29. package/dist/init/health.d.ts +2 -0
  30. package/dist/init/middlewares.d.ts +10 -0
  31. package/dist/init/shutdown.d.ts +11 -0
  32. package/dist/init/storage.d.ts +5 -0
  33. package/dist/{server-core/src/init.d.ts → init.d.ts} +4 -4
  34. package/dist/jwt-DHcQRGC3.js +4168 -0
  35. package/dist/jwt-DHcQRGC3.js.map +1 -0
  36. package/dist/logger-BYU66ENZ.js +94 -0
  37. package/dist/logger-BYU66ENZ.js.map +1 -0
  38. package/dist/ms-BeBSuOXN.js +125 -0
  39. package/dist/ms-BeBSuOXN.js.map +1 -0
  40. package/dist/multipart-parser-CedBDOeC.js +299 -0
  41. package/dist/multipart-parser-CedBDOeC.js.map +1 -0
  42. package/dist/src-COaj0G3P.js +1182 -0
  43. package/dist/src-COaj0G3P.js.map +1 -0
  44. package/dist/{server-core/src/storage → storage}/image-transform.d.ts +3 -1
  45. package/dist/utils/request-id.d.ts +4 -0
  46. package/package.json +29 -29
  47. package/src/api/collections_for_test/callbacks_test_collection.ts +7 -6
  48. package/src/api/errors.ts +95 -20
  49. package/src/api/graphql/graphql-schema-generator.ts +15 -10
  50. package/src/api/logs-routes.ts +5 -2
  51. package/src/api/openapi-generator.ts +1 -1
  52. package/src/api/rest/api-generator-count.test.ts +21 -10
  53. package/src/api/rest/api-generator.ts +151 -36
  54. package/src/api/rest/query-parser.ts +18 -12
  55. package/src/api/server.ts +13 -2
  56. package/src/api/types.ts +2 -0
  57. package/src/auth/adapter-middleware.ts +17 -10
  58. package/src/auth/admin-user-ops.ts +236 -0
  59. package/src/auth/api-keys/api-key-middleware.ts +15 -9
  60. package/src/auth/api-keys/api-key-permission-guard.ts +1 -1
  61. package/src/auth/api-keys/api-key-routes.ts +3 -3
  62. package/src/auth/api-keys/api-key-store.ts +14 -8
  63. package/src/auth/api-keys/index.ts +2 -2
  64. package/src/auth/apple-oauth.ts +4 -3
  65. package/src/auth/auth-hooks.ts +50 -10
  66. package/src/auth/bitbucket-oauth.ts +5 -4
  67. package/src/auth/builtin-auth-adapter.ts +64 -54
  68. package/src/auth/custom-auth-adapter.ts +4 -5
  69. package/src/auth/discord-oauth.ts +5 -4
  70. package/src/auth/facebook-oauth.ts +5 -4
  71. package/src/auth/github-oauth.ts +6 -5
  72. package/src/auth/gitlab-oauth.ts +5 -4
  73. package/src/auth/google-oauth.ts +2 -1
  74. package/src/auth/index.ts +6 -2
  75. package/src/auth/jwt.ts +15 -4
  76. package/src/auth/linkedin-oauth.ts +4 -3
  77. package/src/auth/mfa-routes.ts +299 -0
  78. package/src/auth/mfa.ts +2 -1
  79. package/src/auth/microsoft-oauth.ts +5 -4
  80. package/src/auth/middleware.ts +5 -4
  81. package/src/auth/rate-limiter.ts +1 -1
  82. package/src/auth/reset-password-admin.ts +144 -0
  83. package/src/auth/rls-scope.ts +1 -1
  84. package/src/auth/routes.ts +52 -600
  85. package/src/auth/session-routes.ts +341 -0
  86. package/src/auth/slack-oauth.ts +6 -5
  87. package/src/auth/spotify-oauth.ts +5 -4
  88. package/src/auth/twitter-oauth.ts +4 -3
  89. package/src/collections/loader.ts +5 -4
  90. package/src/cron/cron-scheduler.test.ts +23 -11
  91. package/src/cron/cron-scheduler.ts +14 -8
  92. package/src/cron/cron-store.ts +1 -1
  93. package/src/email/smtp-email-service.ts +3 -2
  94. package/src/env.ts +11 -10
  95. package/src/index.ts +1 -0
  96. package/src/init/docs.ts +47 -0
  97. package/src/init/health.ts +37 -0
  98. package/src/init/middlewares.ts +61 -0
  99. package/src/init/shutdown.ts +56 -0
  100. package/src/init/storage.ts +57 -0
  101. package/src/init.ts +93 -295
  102. package/src/serve-spa.ts +12 -5
  103. package/src/services/driver-registry.ts +4 -3
  104. package/src/storage/S3StorageController.ts +1 -2
  105. package/src/storage/image-transform.ts +25 -10
  106. package/src/storage/routes.ts +30 -11
  107. package/src/storage/storage-registry.ts +4 -3
  108. package/src/storage/tus-handler.ts +12 -12
  109. package/src/utils/request-id.ts +40 -0
  110. package/src/utils/request-logger.ts +6 -0
  111. package/test/api-generator.test.ts +90 -2
  112. package/test/api-key-permission-guard.test.ts +24 -12
  113. package/test/auth-routes.test.ts +5 -3
  114. package/test/backend-hooks-data.test.ts +99 -30
  115. package/test/custom-auth-adapter.test.ts +16 -15
  116. package/test/email-templates.test.ts +10 -5
  117. package/test/env.test.ts +10 -10
  118. package/test/function-loader.test.ts +7 -4
  119. package/test/graphql-schema-generator.test.ts +554 -0
  120. package/test/jwt-security.test.ts +1 -1
  121. package/test/query-parser.test.ts +0 -1
  122. package/test/reset-password-admin.test.ts +113 -0
  123. package/test/singleton.test.ts +5 -5
  124. package/test/smtp-email-service.test.ts +21 -21
  125. package/test/webhook-service.test.ts +22 -11
  126. package/tsconfig.json +2 -0
  127. package/tsconfig.prod.json +3 -0
  128. package/vite.config.ts +5 -5
  129. package/dist/common/src/collections/CollectionRegistry.d.ts +0 -56
  130. package/dist/common/src/collections/default-collections.d.ts +0 -9
  131. package/dist/common/src/collections/index.d.ts +0 -2
  132. package/dist/common/src/data/buildRebaseData.d.ts +0 -14
  133. package/dist/common/src/data/query_builder.d.ts +0 -55
  134. package/dist/common/src/index.d.ts +0 -4
  135. package/dist/common/src/util/builders.d.ts +0 -57
  136. package/dist/common/src/util/callbacks.d.ts +0 -6
  137. package/dist/common/src/util/collections.d.ts +0 -11
  138. package/dist/common/src/util/common.d.ts +0 -2
  139. package/dist/common/src/util/conditions.d.ts +0 -26
  140. package/dist/common/src/util/entities.d.ts +0 -58
  141. package/dist/common/src/util/enums.d.ts +0 -3
  142. package/dist/common/src/util/index.d.ts +0 -16
  143. package/dist/common/src/util/navigation_from_path.d.ts +0 -34
  144. package/dist/common/src/util/navigation_utils.d.ts +0 -20
  145. package/dist/common/src/util/parent_references_from_path.d.ts +0 -6
  146. package/dist/common/src/util/paths.d.ts +0 -14
  147. package/dist/common/src/util/permissions.d.ts +0 -6
  148. package/dist/common/src/util/references.d.ts +0 -2
  149. package/dist/common/src/util/relations.d.ts +0 -22
  150. package/dist/common/src/util/resolutions.d.ts +0 -72
  151. package/dist/common/src/util/storage.d.ts +0 -24
  152. package/dist/index-Cr1D21av.js +0 -49
  153. package/dist/index-Cr1D21av.js.map +0 -1
  154. package/dist/server-core/src/auth/admin-routes.d.ts +0 -27
  155. package/dist/server-core/src/env.d.ts +0 -137
  156. package/dist/types/src/controllers/analytics_controller.d.ts +0 -7
  157. package/dist/types/src/controllers/auth.d.ts +0 -104
  158. package/dist/types/src/controllers/client.d.ts +0 -168
  159. package/dist/types/src/controllers/collection_registry.d.ts +0 -46
  160. package/dist/types/src/controllers/customization_controller.d.ts +0 -60
  161. package/dist/types/src/controllers/data.d.ts +0 -207
  162. package/dist/types/src/controllers/data_driver.d.ts +0 -218
  163. package/dist/types/src/controllers/database_admin.d.ts +0 -11
  164. package/dist/types/src/controllers/dialogs_controller.d.ts +0 -36
  165. package/dist/types/src/controllers/effective_role.d.ts +0 -4
  166. package/dist/types/src/controllers/email.d.ts +0 -36
  167. package/dist/types/src/controllers/index.d.ts +0 -18
  168. package/dist/types/src/controllers/local_config_persistence.d.ts +0 -20
  169. package/dist/types/src/controllers/navigation.d.ts +0 -225
  170. package/dist/types/src/controllers/registry.d.ts +0 -63
  171. package/dist/types/src/controllers/side_dialogs_controller.d.ts +0 -67
  172. package/dist/types/src/controllers/side_entity_controller.d.ts +0 -97
  173. package/dist/types/src/controllers/snackbar.d.ts +0 -24
  174. package/dist/types/src/controllers/storage.d.ts +0 -171
  175. package/dist/types/src/index.d.ts +0 -4
  176. package/dist/types/src/rebase_context.d.ts +0 -122
  177. package/dist/types/src/types/auth_adapter.d.ts +0 -301
  178. package/dist/types/src/types/backend.d.ts +0 -536
  179. package/dist/types/src/types/backend_hooks.d.ts +0 -172
  180. package/dist/types/src/types/builders.d.ts +0 -15
  181. package/dist/types/src/types/chips.d.ts +0 -5
  182. package/dist/types/src/types/collections.d.ts +0 -941
  183. package/dist/types/src/types/component_ref.d.ts +0 -47
  184. package/dist/types/src/types/cron.d.ts +0 -102
  185. package/dist/types/src/types/data_source.d.ts +0 -64
  186. package/dist/types/src/types/database_adapter.d.ts +0 -94
  187. package/dist/types/src/types/entities.d.ts +0 -145
  188. package/dist/types/src/types/entity_actions.d.ts +0 -104
  189. package/dist/types/src/types/entity_callbacks.d.ts +0 -173
  190. package/dist/types/src/types/entity_link_builder.d.ts +0 -7
  191. package/dist/types/src/types/entity_overrides.d.ts +0 -10
  192. package/dist/types/src/types/entity_views.d.ts +0 -87
  193. package/dist/types/src/types/export_import.d.ts +0 -21
  194. package/dist/types/src/types/formex.d.ts +0 -40
  195. package/dist/types/src/types/index.d.ts +0 -28
  196. package/dist/types/src/types/locales.d.ts +0 -4
  197. package/dist/types/src/types/modify_collections.d.ts +0 -5
  198. package/dist/types/src/types/plugins.d.ts +0 -282
  199. package/dist/types/src/types/properties.d.ts +0 -1181
  200. package/dist/types/src/types/property_config.d.ts +0 -74
  201. package/dist/types/src/types/relations.d.ts +0 -336
  202. package/dist/types/src/types/slots.d.ts +0 -262
  203. package/dist/types/src/types/translations.d.ts +0 -900
  204. package/dist/types/src/types/user_management_delegate.d.ts +0 -86
  205. package/dist/types/src/types/websockets.d.ts +0 -78
  206. package/dist/types/src/users/index.d.ts +0 -1
  207. package/dist/types/src/users/user.d.ts +0 -50
  208. package/src/auth/admin-routes.ts +0 -534
  209. package/test/admin-routes.test.ts +0 -544
  210. package/test/backend-hooks-admin.test.ts +0 -388
  211. package/test.ts +0 -6
  212. /package/dist/{server-core/src/api → api}/ast-schema-editor.d.ts +0 -0
  213. /package/dist/{server-core/src/api → api}/collections_for_test/callbacks_test_collection.d.ts +0 -0
  214. /package/dist/{server-core/src/api → api}/graphql/graphql-schema-generator.d.ts +0 -0
  215. /package/dist/{server-core/src/api → api}/graphql/index.d.ts +0 -0
  216. /package/dist/{server-core/src/api → api}/index.d.ts +0 -0
  217. /package/dist/{server-core/src/api → api}/logs-routes.d.ts +0 -0
  218. /package/dist/{server-core/src/api → api}/openapi-generator.d.ts +0 -0
  219. /package/dist/{server-core/src/api → api}/rest/index.d.ts +0 -0
  220. /package/dist/{server-core/src/api → api}/rest/query-parser.d.ts +0 -0
  221. /package/dist/{server-core/src/api → api}/schema-editor-routes.d.ts +0 -0
  222. /package/dist/{server-core/src/api → api}/server.d.ts +0 -0
  223. /package/dist/{server-core/src/auth → auth}/adapter-middleware.d.ts +0 -0
  224. /package/dist/{server-core/src/auth → auth}/api-keys/api-key-middleware.d.ts +0 -0
  225. /package/dist/{server-core/src/auth → auth}/api-keys/api-key-permission-guard.d.ts +0 -0
  226. /package/dist/{server-core/src/auth → auth}/api-keys/api-key-routes.d.ts +0 -0
  227. /package/dist/{server-core/src/auth → auth}/api-keys/api-key-store.d.ts +0 -0
  228. /package/dist/{server-core/src/auth → auth}/api-keys/api-key-types.d.ts +0 -0
  229. /package/dist/{server-core/src/auth → auth}/apple-oauth.d.ts +0 -0
  230. /package/dist/{server-core/src/auth → auth}/bitbucket-oauth.d.ts +0 -0
  231. /package/dist/{server-core/src/auth → auth}/crypto-utils.d.ts +0 -0
  232. /package/dist/{server-core/src/auth → auth}/custom-auth-adapter.d.ts +0 -0
  233. /package/dist/{server-core/src/auth → auth}/discord-oauth.d.ts +0 -0
  234. /package/dist/{server-core/src/auth → auth}/facebook-oauth.d.ts +0 -0
  235. /package/dist/{server-core/src/auth → auth}/github-oauth.d.ts +0 -0
  236. /package/dist/{server-core/src/auth → auth}/gitlab-oauth.d.ts +0 -0
  237. /package/dist/{server-core/src/auth → auth}/google-oauth.d.ts +0 -0
  238. /package/dist/{server-core/src/auth → auth}/interfaces.d.ts +0 -0
  239. /package/dist/{server-core/src/auth → auth}/linkedin-oauth.d.ts +0 -0
  240. /package/dist/{server-core/src/auth → auth}/mfa.d.ts +0 -0
  241. /package/dist/{server-core/src/auth → auth}/microsoft-oauth.d.ts +0 -0
  242. /package/dist/{server-core/src/auth → auth}/middleware.d.ts +0 -0
  243. /package/dist/{server-core/src/auth → auth}/password.d.ts +0 -0
  244. /package/dist/{server-core/src/auth → auth}/rate-limiter.d.ts +0 -0
  245. /package/dist/{server-core/src/auth → auth}/rls-scope.d.ts +0 -0
  246. /package/dist/{server-core/src/auth → auth}/routes.d.ts +0 -0
  247. /package/dist/{server-core/src/auth → auth}/slack-oauth.d.ts +0 -0
  248. /package/dist/{server-core/src/auth → auth}/spotify-oauth.d.ts +0 -0
  249. /package/dist/{server-core/src/auth → auth}/twitter-oauth.d.ts +0 -0
  250. /package/dist/{server-core/src/collections → collections}/BackendCollectionRegistry.d.ts +0 -0
  251. /package/dist/{server-core/src/collections → collections}/loader.d.ts +0 -0
  252. /package/dist/{server-core/src/cron → cron}/cron-loader.d.ts +0 -0
  253. /package/dist/{server-core/src/cron → cron}/cron-routes.d.ts +0 -0
  254. /package/dist/{server-core/src/cron → cron}/cron-store.d.ts +0 -0
  255. /package/dist/{server-core/src/cron → cron}/index.d.ts +0 -0
  256. /package/dist/{server-core/src/db → db}/interfaces.d.ts +0 -0
  257. /package/dist/{server-core/src/email → email}/index.d.ts +0 -0
  258. /package/dist/{server-core/src/email → email}/smtp-email-service.d.ts +0 -0
  259. /package/dist/{server-core/src/email → email}/templates.d.ts +0 -0
  260. /package/dist/{server-core/src/email → email}/types.d.ts +0 -0
  261. /package/dist/{server-core/src/functions → functions}/function-loader.d.ts +0 -0
  262. /package/dist/{server-core/src/functions → functions}/function-routes.d.ts +0 -0
  263. /package/dist/{server-core/src/functions → functions}/index.d.ts +0 -0
  264. /package/dist/{server-core/src/history → history}/history-routes.d.ts +0 -0
  265. /package/dist/{server-core/src/history → history}/index.d.ts +0 -0
  266. /package/dist/{server-core/src/serve-spa.d.ts → serve-spa.d.ts} +0 -0
  267. /package/dist/{server-core/src/services → services}/driver-registry.d.ts +0 -0
  268. /package/dist/{server-core/src/services → services}/webhook-service.d.ts +0 -0
  269. /package/dist/{server-core/src/singleton.d.ts → singleton.d.ts} +0 -0
  270. /package/dist/{server-core/src/storage → storage}/LocalStorageController.d.ts +0 -0
  271. /package/dist/{server-core/src/storage → storage}/S3StorageController.d.ts +0 -0
  272. /package/dist/{server-core/src/storage → storage}/index.d.ts +0 -0
  273. /package/dist/{server-core/src/storage → storage}/routes.d.ts +0 -0
  274. /package/dist/{server-core/src/storage → storage}/storage-registry.d.ts +0 -0
  275. /package/dist/{server-core/src/storage → storage}/tus-handler.d.ts +0 -0
  276. /package/dist/{server-core/src/storage → storage}/types.d.ts +0 -0
  277. /package/dist/{server-core/src/types → types}/index.d.ts +0 -0
  278. /package/dist/{server-core/src/utils → utils}/dev-port.d.ts +0 -0
  279. /package/dist/{server-core/src/utils → utils}/logger.d.ts +0 -0
  280. /package/dist/{server-core/src/utils → utils}/logging.d.ts +0 -0
  281. /package/dist/{server-core/src/utils → utils}/request-logger.d.ts +0 -0
  282. /package/dist/{server-core/src/utils → utils}/sql.d.ts +0 -0
@@ -1,544 +0,0 @@
1
- /**
2
- * Admin Routes — Integration Tests (Hono)
3
- *
4
- * Tests the full Hono request → admin route handler → JSON response cycle.
5
- * Replaces the previous Express-based mock tests that only tested service calls.
6
- */
7
-
8
- import { Hono } from "hono";
9
- import type { HonoEnv } from "../src/api/types";
10
- import { errorHandler } from "../src/api/errors";
11
- import { createAdminRoutes } from "../src/auth/admin-routes";
12
- import { configureJwt, generateAccessToken } from "../src/auth/jwt";
13
- import type { AuthModuleConfig } from "../src/auth/routes";
14
-
15
- // ── Mocks ───────────────────────────────────────────────────────────────────
16
-
17
- jest.mock("../src/auth/password");
18
-
19
- import { UserService, RoleService } from "../src/auth/services";
20
- import { hashPassword, validatePasswordStrength } from "../src/auth/password";
21
-
22
- // ── Helpers ─────────────────────────────────────────────────────────────────
23
-
24
- const TEST_SECRET = "admin-test-secret-key-that-is-definitely-32-chars-long!!!!!";
25
-
26
- function mockUser(overrides: Partial<{ id: string; email: string; displayName: string | null; photoUrl: string | null; provider: string }> = {}) {
27
- return {
28
- id: overrides.id ?? "user-1",
29
- email: overrides.email ?? "test@example.com",
30
- passwordHash: "salt:hash",
31
- displayName: overrides.displayName ?? "Test User",
32
- photoUrl: overrides.photoUrl ?? null,
33
- emailVerified: false,
34
- emailVerificationToken: null,
35
- emailVerificationSentAt: null,
36
- createdAt: new Date("2024-01-01"),
37
- updatedAt: new Date("2024-01-01")
38
- };
39
- }
40
-
41
- function mockRole(id: string, isAdmin = false) {
42
- return { id,
43
- name: id.charAt(0).toUpperCase() + id.slice(1),
44
- isAdmin,
45
- defaultPermissions: null,
46
- collectionPermissions: null };
47
- }
48
-
49
-
50
- let mockAuthRepo: jest.Mocked<any>;
51
-
52
- function createApp(opts: { defaultRole?: string } = {}) {
53
- mockAuthRepo = {
54
- getUserByEmail: jest.fn().mockResolvedValue(null),
55
- getUserByIdentity: jest.fn().mockResolvedValue(null),
56
- linkUserIdentity: jest.fn().mockResolvedValue(undefined),
57
- getUserIdentities: jest.fn().mockResolvedValue([]),
58
- getUserById: jest.fn().mockImplementation((id) => Promise.resolve(mockUser({ id }))),
59
- createUser: jest.fn().mockImplementation((data) =>
60
- Promise.resolve(mockUser({ email: data.email,
61
- displayName: data.displayName,
62
- passwordHash: data.passwordHash }))
63
- ),
64
- listUsers: jest.fn().mockResolvedValue([]),
65
- listUsersPaginated: jest.fn().mockResolvedValue({ users: [], total: 0, limit: 25, offset: 0 }),
66
- getUserRoles: jest.fn().mockResolvedValue([mockRole("editor")]),
67
- getUserRoleIds: jest.fn().mockResolvedValue(["editor"]),
68
- assignDefaultRole: jest.fn().mockResolvedValue(undefined),
69
- setUserRoles: jest.fn().mockResolvedValue(undefined),
70
- updateUser: jest.fn().mockImplementation((id, data) =>
71
- Promise.resolve(mockUser({ id,
72
- ...data }))
73
- ),
74
- deleteUser: jest.fn().mockResolvedValue(undefined),
75
- updatePassword: jest.fn().mockResolvedValue(undefined),
76
- setEmailVerified: jest.fn().mockResolvedValue(undefined),
77
- setVerificationToken: jest.fn().mockResolvedValue(undefined),
78
- getUserByVerificationToken: jest.fn().mockResolvedValue(null),
79
- getUserWithRoles: jest.fn().mockImplementation(async (userId) => {
80
- const user = mockUser({ id: userId });
81
- return { user,
82
- roles: [mockRole("editor")] };
83
- }),
84
- createRefreshToken: jest.fn().mockResolvedValue(undefined),
85
- findRefreshTokenByHash: jest.fn().mockResolvedValue(null),
86
- deleteRefreshToken: jest.fn().mockResolvedValue(undefined),
87
- deleteAllRefreshTokensForUser: jest.fn().mockResolvedValue(undefined),
88
- listRefreshTokensForUser: jest.fn().mockResolvedValue([]),
89
- deleteRefreshTokenById: jest.fn().mockResolvedValue(undefined),
90
- createPasswordResetToken: jest.fn().mockResolvedValue(undefined),
91
- findValidPasswordResetToken: jest.fn().mockResolvedValue(null),
92
- markPasswordResetTokenUsed: jest.fn().mockResolvedValue(undefined),
93
- deleteExpiredPasswordResetTokens: jest.fn().mockResolvedValue(undefined),
94
- listRoles: jest.fn().mockResolvedValue([]),
95
- getRoleById: jest.fn().mockResolvedValue(null),
96
- createRole: jest.fn().mockImplementation(r => Promise.resolve({ id: r.id,
97
- name: r.name,
98
- isAdmin: r.isAdmin || false,
99
- defaultPermissions: null,
100
- collectionPermissions: null })),
101
- updateRole: jest.fn().mockImplementation((id, r) => Promise.resolve({ id,
102
- name: r.name,
103
- isAdmin: r.isAdmin || false,
104
- defaultPermissions: null,
105
- collectionPermissions: null })),
106
- deleteRole: jest.fn().mockResolvedValue(undefined)
107
- } as unknown as jest.Mocked<AuthRepository>;
108
-
109
-
110
- // Password mocks
111
- (validatePasswordStrength as jest.Mock).mockReturnValue({ valid: true,
112
- errors: [] });
113
- (hashPassword as jest.Mock).mockResolvedValue("hashed-pw");
114
-
115
- const config: AuthModuleConfig = {
116
- authRepo: mockAuthRepo,
117
- defaultRole: opts.defaultRole
118
- };
119
-
120
- const app = new Hono<HonoEnv>();
121
- app.onError(errorHandler);
122
- app.route("/admin", createAdminRoutes(config));
123
- return app;
124
- }
125
-
126
- function adminAuth(userId = "admin-1") {
127
- return { Authorization: `Bearer ${generateAccessToken(userId, ["admin"])}` };
128
- }
129
-
130
- function editorAuth(userId = "editor-1") {
131
- return { Authorization: `Bearer ${generateAccessToken(userId, ["editor"])}` };
132
- }
133
-
134
- function json(body: Record<string, unknown>) {
135
- return {
136
- method: "POST" as const,
137
- headers: { "Content-Type": "application/json" },
138
- body: JSON.stringify(body)
139
- };
140
- }
141
-
142
- // ═══════════════════════════════════════════════════════════════════════════
143
- // TESTS
144
- // ═══════════════════════════════════════════════════════════════════════════
145
-
146
- describe("Admin Routes (Integration)", () => {
147
- beforeAll(() => {
148
- configureJwt({ secret: TEST_SECRET,
149
- accessExpiresIn: "1h" });
150
- });
151
-
152
- beforeEach(() => {
153
- jest.clearAllMocks();
154
- });
155
-
156
- // ── Auth barriers ───────────────────────────────────────────────────
157
- describe("Authorization", () => {
158
- it("returns 401 for unauthenticated requests", async () => {
159
- const app = createApp();
160
- const res = await app.request("/admin/users");
161
- expect(res.status).toBe(401);
162
- });
163
-
164
- it("returns 403 for non-admin users on admin-only endpoints", async () => {
165
- const app = createApp();
166
- const res = await app.request("/admin/users", {
167
- headers: { ...editorAuth() }
168
- });
169
- expect(res.status).toBe(403);
170
- });
171
-
172
- it("allows admin users through", async () => {
173
- const app = createApp();
174
- const res = await app.request("/admin/users", {
175
- headers: { ...adminAuth() }
176
- });
177
- expect(res.status).toBe(200);
178
- });
179
-
180
- it("allows schema-admin users through", async () => {
181
- const app = createApp();
182
- const res = await app.request("/admin/users", {
183
- headers: { Authorization: `Bearer ${generateAccessToken("sa-1", ["schema-admin"])}` }
184
- });
185
- expect(res.status).toBe(200);
186
- });
187
- });
188
-
189
- // ── Bootstrap ───────────────────────────────────────────────────────
190
- describe("POST /admin/bootstrap", () => {
191
- it("promotes current user to admin when no admins exist", async () => {
192
- const app = createApp();
193
- mockAuthRepo.listUsers.mockResolvedValueOnce([mockUser({ id: "user-1" })]);
194
- mockAuthRepo.getUserRoleIds.mockResolvedValueOnce(["editor"]); // no admin
195
-
196
- const res = await app.request("/admin/bootstrap", {
197
- method: "POST",
198
- headers: { ...adminAuth("user-1") }
199
- });
200
- expect(res.status).toBe(200);
201
- const body = await res.json() as any;
202
- expect(body.user.roles).toContain("admin");
203
- expect(mockAuthRepo.setUserRoles).toHaveBeenCalledWith("user-1", ["admin"]);
204
- });
205
-
206
- it("returns 403 when admin already exists", async () => {
207
- const app = createApp();
208
- const adminUser = mockUser({ id: "existing-admin" });
209
- mockAuthRepo.listUsers.mockResolvedValueOnce([adminUser]);
210
- mockAuthRepo.getUserRoleIds.mockResolvedValueOnce(["admin"]); // admin exists
211
-
212
- const res = await app.request("/admin/bootstrap", {
213
- method: "POST",
214
- headers: { ...adminAuth("user-1") }
215
- });
216
- expect(res.status).toBe(403);
217
- const body = await res.json() as any;
218
- expect(body.error.message).toContain("Admin users already exist");
219
- });
220
- });
221
-
222
- // ── User CRUD ───────────────────────────────────────────────────────
223
- describe("User Management", () => {
224
- describe("GET /admin/users", () => {
225
- it("returns list of users with roles", async () => {
226
- const app = createApp();
227
- mockAuthRepo.listUsersPaginated.mockResolvedValueOnce({
228
- users: [
229
- mockUser({ id: "u1",
230
- email: "a@test.com" }),
231
- mockUser({ id: "u2",
232
- email: "b@test.com" })
233
- ],
234
- total: 2,
235
- limit: 25,
236
- offset: 0
237
- });
238
- mockAuthRepo.getUserRoleIds
239
- .mockResolvedValueOnce(["admin"])
240
- .mockResolvedValueOnce(["editor"]);
241
-
242
- const res = await app.request("/admin/users", { headers: { ...adminAuth() } });
243
- expect(res.status).toBe(200);
244
- const body = await res.json() as any;
245
- expect(body.users).toHaveLength(2);
246
- expect(body.users[0].roles).toContain("admin");
247
- expect(body.users[1].roles).toContain("editor");
248
- });
249
- });
250
-
251
- describe("GET /admin/users/:userId", () => {
252
- it("returns user with roles", async () => {
253
- const app = createApp();
254
- mockAuthRepo.getUserWithRoles.mockResolvedValueOnce({
255
- user: mockUser({ id: "u1" }),
256
- roles: [mockRole("editor"), mockRole("viewer")]
257
- });
258
-
259
- const res = await app.request("/admin/users/u1", { headers: { ...adminAuth() } });
260
- expect(res.status).toBe(200);
261
- const body = await res.json() as any;
262
- expect(body.user.uid).toBe("u1");
263
- expect(body.user.roles).toEqual(["editor", "viewer"]);
264
- });
265
-
266
- it("returns 404 for non-existent user", async () => {
267
- const app = createApp();
268
- mockAuthRepo.getUserWithRoles.mockResolvedValueOnce(null);
269
-
270
- const res = await app.request("/admin/users/missing", { headers: { ...adminAuth() } });
271
- expect(res.status).toBe(404);
272
- });
273
- });
274
-
275
- describe("POST /admin/users", () => {
276
- it("creates a new user", async () => {
277
- const app = createApp();
278
-
279
- const res = await app.request("/admin/users", {
280
- ...json({ email: "new@test.com",
281
- displayName: "New",
282
- password: "StrongPass1",
283
- roles: ["editor"] }),
284
- headers: { ...json({}).headers,
285
- ...adminAuth() }
286
- });
287
- expect(res.status).toBe(201);
288
- const body = await res.json() as any;
289
- expect(body.user.email).toBe("new@test.com");
290
- expect(mockAuthRepo.createUser).toHaveBeenCalledWith(expect.objectContaining({
291
- email: "new@test.com"
292
- }));
293
- });
294
-
295
- it("hashes password when provided", async () => {
296
- const app = createApp();
297
-
298
- await app.request("/admin/users", {
299
- ...json({ email: "pw@test.com",
300
- password: "StrongPass1" }),
301
- headers: { ...json({}).headers,
302
- ...adminAuth() }
303
- });
304
- expect(hashPassword).toHaveBeenCalledWith("StrongPass1");
305
- });
306
-
307
- it("returns 400 for missing email", async () => {
308
- const app = createApp();
309
-
310
- const res = await app.request("/admin/users", {
311
- ...json({ displayName: "No Email" }),
312
- headers: { ...json({}).headers,
313
- ...adminAuth() }
314
- });
315
- expect(res.status).toBe(400);
316
- });
317
-
318
- it("returns 409 when email already exists", async () => {
319
- const app = createApp();
320
- mockAuthRepo.getUserByEmail.mockResolvedValueOnce(mockUser());
321
-
322
- const res = await app.request("/admin/users", {
323
- ...json({ email: "existing@test.com" }),
324
- headers: { ...json({}).headers,
325
- ...adminAuth() }
326
- });
327
- expect(res.status).toBe(409);
328
- const body = await res.json() as any;
329
- expect(body.error.code).toBe("EMAIL_EXISTS");
330
- });
331
-
332
- it("returns 400 for weak password", async () => {
333
- const app = createApp();
334
- (validatePasswordStrength as jest.Mock).mockReturnValueOnce({ valid: false,
335
- errors: ["Too short"] });
336
-
337
- const res = await app.request("/admin/users", {
338
- ...json({ email: "weak@test.com",
339
- password: "weak" }),
340
- headers: { ...json({}).headers,
341
- ...adminAuth() }
342
- });
343
- expect(res.status).toBe(400);
344
- const body = await res.json() as any;
345
- expect(body.error.code).toBe("WEAK_PASSWORD");
346
- });
347
-
348
- it("assigns configured default role when no roles specified", async () => {
349
- const app = createApp({ defaultRole: "editor" });
350
-
351
- await app.request("/admin/users", {
352
- ...json({ email: "norole@test.com" }),
353
- headers: { ...json({}).headers,
354
- ...adminAuth() }
355
- });
356
- expect(mockAuthRepo.assignDefaultRole).toHaveBeenCalledWith(expect.any(String), "editor");
357
- });
358
-
359
- it("does not assign a default role when not configured", async () => {
360
- const app = createApp();
361
-
362
- await app.request("/admin/users", {
363
- ...json({ email: "nodefault@test.com" }),
364
- headers: { ...json({}).headers,
365
- ...adminAuth() }
366
- });
367
- expect(mockAuthRepo.assignDefaultRole).not.toHaveBeenCalled();
368
- });
369
-
370
- it("assigns specified roles", async () => {
371
- const app = createApp();
372
-
373
- await app.request("/admin/users", {
374
- ...json({ email: "withroles@test.com",
375
- roles: ["admin", "editor"] }),
376
- headers: { ...json({}).headers,
377
- ...adminAuth() }
378
- });
379
- expect(mockAuthRepo.setUserRoles).toHaveBeenCalledWith(expect.any(String), ["admin", "editor"]);
380
- });
381
- });
382
-
383
- describe("PUT /admin/users/:userId", () => {
384
- it("updates user profile", async () => {
385
- const app = createApp();
386
- mockAuthRepo.getUserById.mockResolvedValueOnce(mockUser({ id: "u1" }));
387
- mockAuthRepo.getUserWithRoles.mockResolvedValueOnce({
388
- user: mockUser({ id: "u1",
389
- displayName: "Updated" }),
390
- roles: [mockRole("editor")]
391
- });
392
-
393
- const res = await app.request("/admin/users/u1", {
394
- method: "PUT",
395
- headers: { "Content-Type": "application/json",
396
- ...adminAuth() },
397
- body: JSON.stringify({ displayName: "Updated" })
398
- });
399
- expect(res.status).toBe(200);
400
- const body = await res.json() as any;
401
- expect(body.user.displayName).toBe("Updated");
402
- });
403
-
404
- it("updates roles when specified", async () => {
405
- const app = createApp();
406
- mockAuthRepo.getUserById.mockResolvedValueOnce(mockUser({ id: "u1" }));
407
- mockAuthRepo.getUserRoleIds.mockResolvedValueOnce(["admin"]);
408
- mockAuthRepo.listUsersPaginated.mockResolvedValueOnce({ total: 2 });
409
- mockAuthRepo.getUserWithRoles.mockResolvedValueOnce({
410
- user: mockUser({ id: "u1" }),
411
- roles: [mockRole("admin")]
412
- });
413
-
414
- await app.request("/admin/users/u1", {
415
- method: "PUT",
416
- headers: { "Content-Type": "application/json",
417
- ...adminAuth() },
418
- body: JSON.stringify({ roles: ["admin"] })
419
- });
420
- expect(mockAuthRepo.setUserRoles).toHaveBeenCalledWith("u1", ["admin"]);
421
- });
422
-
423
- it("prevents demoting the last admin", async () => {
424
- const app = createApp();
425
- mockAuthRepo.getUserById.mockResolvedValueOnce(mockUser({ id: "u1" }));
426
- mockAuthRepo.getUserRoleIds.mockResolvedValueOnce(["admin"]);
427
- mockAuthRepo.listUsersPaginated.mockResolvedValueOnce({ total: 1 });
428
-
429
- const res = await app.request("/admin/users/u1", {
430
- method: "PUT",
431
- headers: { "Content-Type": "application/json",
432
- ...adminAuth() },
433
- body: JSON.stringify({ roles: ["editor"] })
434
- });
435
- expect(res.status).toBe(403);
436
- const body = await res.json() as any;
437
- expect(body.error.code).toBe("LAST_ADMIN");
438
- expect(mockAuthRepo.setUserRoles).not.toHaveBeenCalled();
439
- });
440
-
441
- it("allows demoting an admin if there are other admins", async () => {
442
- const app = createApp();
443
- mockAuthRepo.getUserById.mockResolvedValueOnce(mockUser({ id: "u1" }));
444
- mockAuthRepo.getUserRoleIds.mockResolvedValueOnce(["admin"]);
445
- mockAuthRepo.listUsersPaginated.mockResolvedValueOnce({ total: 2 });
446
- mockAuthRepo.getUserWithRoles.mockResolvedValueOnce({
447
- user: mockUser({ id: "u1" }),
448
- roles: [mockRole("editor")]
449
- });
450
-
451
- const res = await app.request("/admin/users/u1", {
452
- method: "PUT",
453
- headers: { "Content-Type": "application/json",
454
- ...adminAuth() },
455
- body: JSON.stringify({ roles: ["editor"] })
456
- });
457
- expect(res.status).toBe(200);
458
- expect(mockAuthRepo.setUserRoles).toHaveBeenCalledWith("u1", ["editor"]);
459
- });
460
-
461
- it("returns 404 for non-existent user", async () => {
462
- const app = createApp();
463
- mockAuthRepo.getUserById.mockResolvedValueOnce(null);
464
-
465
- const res = await app.request("/admin/users/missing", {
466
- method: "PUT",
467
- headers: { "Content-Type": "application/json",
468
- ...adminAuth() },
469
- body: JSON.stringify({ displayName: "Updated" })
470
- });
471
- expect(res.status).toBe(404);
472
- });
473
- });
474
-
475
- describe("DELETE /admin/users/:userId", () => {
476
- it("deletes a user", async () => {
477
- const app = createApp();
478
- mockAuthRepo.getUserById.mockResolvedValueOnce(mockUser({ id: "u1" }));
479
-
480
- const res = await app.request("/admin/users/u1", {
481
- method: "DELETE",
482
- headers: { ...adminAuth("admin-1") }
483
- });
484
- expect(res.status).toBe(200);
485
- expect(mockAuthRepo.deleteUser).toHaveBeenCalledWith("u1");
486
- });
487
-
488
- it("prevents self-deletion", async () => {
489
- const app = createApp();
490
-
491
- const res = await app.request("/admin/users/admin-1", {
492
- method: "DELETE",
493
- headers: { ...adminAuth("admin-1") }
494
- });
495
- expect(res.status).toBe(400);
496
- const body = await res.json() as any;
497
- expect(body.error.code).toBe("SELF_DELETE");
498
- });
499
-
500
- it("prevents last-admin deletion", async () => {
501
- const app = createApp();
502
- mockAuthRepo.getUserById.mockResolvedValueOnce(mockUser({ id: "u2" }));
503
- mockAuthRepo.getUserRoleIds.mockResolvedValueOnce(["admin"]);
504
- mockAuthRepo.listUsersPaginated.mockResolvedValueOnce({ total: 1 });
505
-
506
- const res = await app.request("/admin/users/u2", {
507
- method: "DELETE",
508
- headers: { ...adminAuth("admin-1") }
509
- });
510
- expect(res.status).toBe(403);
511
- const body = await res.json() as any;
512
- expect(body.error.code).toBe("LAST_ADMIN");
513
- expect(mockAuthRepo.deleteUser).not.toHaveBeenCalled();
514
- });
515
-
516
- it("allows deleting an admin if there are other admins", async () => {
517
- const app = createApp();
518
- mockAuthRepo.getUserById.mockResolvedValueOnce(mockUser({ id: "u2" }));
519
- mockAuthRepo.getUserRoleIds.mockResolvedValueOnce(["admin"]);
520
- mockAuthRepo.listUsersPaginated.mockResolvedValueOnce({ total: 2 });
521
-
522
- const res = await app.request("/admin/users/u2", {
523
- method: "DELETE",
524
- headers: { ...adminAuth("admin-1") }
525
- });
526
- expect(res.status).toBe(200);
527
- expect(mockAuthRepo.deleteUser).toHaveBeenCalledWith("u2");
528
- });
529
-
530
- it("returns 404 for non-existent user", async () => {
531
- const app = createApp();
532
- mockAuthRepo.getUserById.mockResolvedValueOnce(null);
533
-
534
- const res = await app.request("/admin/users/missing", {
535
- method: "DELETE",
536
- headers: { ...adminAuth() }
537
- });
538
- expect(res.status).toBe(404);
539
- });
540
- });
541
- });
542
-
543
-
544
- });