@portaidentity/server 1.7.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 (742) hide show
  1. package/CHANGELOG.md +11 -0
  2. package/LICENSE +21 -0
  3. package/README.md +33 -0
  4. package/dist/applications/cache.d.ts +64 -0
  5. package/dist/applications/cache.d.ts.map +1 -0
  6. package/dist/applications/cache.js +161 -0
  7. package/dist/applications/cache.js.map +1 -0
  8. package/dist/applications/errors.d.ts +25 -0
  9. package/dist/applications/errors.d.ts.map +1 -0
  10. package/dist/applications/errors.js +31 -0
  11. package/dist/applications/errors.js.map +1 -0
  12. package/dist/applications/index.d.ts +17 -0
  13. package/dist/applications/index.d.ts.map +1 -0
  14. package/dist/applications/index.js +19 -0
  15. package/dist/applications/index.js.map +1 -0
  16. package/dist/applications/repository.d.ts +184 -0
  17. package/dist/applications/repository.d.ts.map +1 -0
  18. package/dist/applications/repository.js +367 -0
  19. package/dist/applications/repository.js.map +1 -0
  20. package/dist/applications/service.d.ts +160 -0
  21. package/dist/applications/service.d.ts.map +1 -0
  22. package/dist/applications/service.js +388 -0
  23. package/dist/applications/service.js.map +1 -0
  24. package/dist/applications/slugs.d.ts +61 -0
  25. package/dist/applications/slugs.d.ts.map +1 -0
  26. package/dist/applications/slugs.js +115 -0
  27. package/dist/applications/slugs.js.map +1 -0
  28. package/dist/applications/types.d.ts +129 -0
  29. package/dist/applications/types.d.ts.map +1 -0
  30. package/dist/applications/types.js +62 -0
  31. package/dist/applications/types.js.map +1 -0
  32. package/dist/auth/csrf.d.ts +71 -0
  33. package/dist/auth/csrf.d.ts.map +1 -0
  34. package/dist/auth/csrf.js +112 -0
  35. package/dist/auth/csrf.js.map +1 -0
  36. package/dist/auth/email-renderer.d.ts +37 -0
  37. package/dist/auth/email-renderer.d.ts.map +1 -0
  38. package/dist/auth/email-renderer.js +116 -0
  39. package/dist/auth/email-renderer.js.map +1 -0
  40. package/dist/auth/email-service.d.ts +154 -0
  41. package/dist/auth/email-service.d.ts.map +1 -0
  42. package/dist/auth/email-service.js +415 -0
  43. package/dist/auth/email-service.js.map +1 -0
  44. package/dist/auth/email-transport.d.ts +65 -0
  45. package/dist/auth/email-transport.d.ts.map +1 -0
  46. package/dist/auth/email-transport.js +82 -0
  47. package/dist/auth/email-transport.js.map +1 -0
  48. package/dist/auth/i18n.d.ts +81 -0
  49. package/dist/auth/i18n.d.ts.map +1 -0
  50. package/dist/auth/i18n.js +395 -0
  51. package/dist/auth/i18n.js.map +1 -0
  52. package/dist/auth/index.d.ts +28 -0
  53. package/dist/auth/index.d.ts.map +1 -0
  54. package/dist/auth/index.js +31 -0
  55. package/dist/auth/index.js.map +1 -0
  56. package/dist/auth/interaction-authority.d.ts +39 -0
  57. package/dist/auth/interaction-authority.d.ts.map +1 -0
  58. package/dist/auth/interaction-authority.js +30 -0
  59. package/dist/auth/interaction-authority.js.map +1 -0
  60. package/dist/auth/magic-link-session.d.ts +81 -0
  61. package/dist/auth/magic-link-session.d.ts.map +1 -0
  62. package/dist/auth/magic-link-session.js +183 -0
  63. package/dist/auth/magic-link-session.js.map +1 -0
  64. package/dist/auth/rate-limiter.d.ts +143 -0
  65. package/dist/auth/rate-limiter.d.ts.map +1 -0
  66. package/dist/auth/rate-limiter.js +220 -0
  67. package/dist/auth/rate-limiter.js.map +1 -0
  68. package/dist/auth/recovery-crypto.d.ts +34 -0
  69. package/dist/auth/recovery-crypto.d.ts.map +1 -0
  70. package/dist/auth/recovery-crypto.js +103 -0
  71. package/dist/auth/recovery-crypto.js.map +1 -0
  72. package/dist/auth/recovery-job-processor.d.ts +24 -0
  73. package/dist/auth/recovery-job-processor.d.ts.map +1 -0
  74. package/dist/auth/recovery-job-processor.js +123 -0
  75. package/dist/auth/recovery-job-processor.js.map +1 -0
  76. package/dist/auth/recovery-job-repository.d.ts +219 -0
  77. package/dist/auth/recovery-job-repository.d.ts.map +1 -0
  78. package/dist/auth/recovery-job-repository.js +340 -0
  79. package/dist/auth/recovery-job-repository.js.map +1 -0
  80. package/dist/auth/recovery-service.d.ts +27 -0
  81. package/dist/auth/recovery-service.d.ts.map +1 -0
  82. package/dist/auth/recovery-service.js +53 -0
  83. package/dist/auth/recovery-service.js.map +1 -0
  84. package/dist/auth/recovery-worker.d.ts +158 -0
  85. package/dist/auth/recovery-worker.d.ts.map +1 -0
  86. package/dist/auth/recovery-worker.js +306 -0
  87. package/dist/auth/recovery-worker.js.map +1 -0
  88. package/dist/auth/template-engine.d.ts +134 -0
  89. package/dist/auth/template-engine.d.ts.map +1 -0
  90. package/dist/auth/template-engine.js +225 -0
  91. package/dist/auth/template-engine.js.map +1 -0
  92. package/dist/auth/token-repository.d.ts +238 -0
  93. package/dist/auth/token-repository.d.ts.map +1 -0
  94. package/dist/auth/token-repository.js +475 -0
  95. package/dist/auth/token-repository.js.map +1 -0
  96. package/dist/auth/tokens.d.ts +45 -0
  97. package/dist/auth/tokens.d.ts.map +1 -0
  98. package/dist/auth/tokens.js +51 -0
  99. package/dist/auth/tokens.js.map +1 -0
  100. package/dist/cli/bootstrap.d.ts +55 -0
  101. package/dist/cli/bootstrap.d.ts.map +1 -0
  102. package/dist/cli/bootstrap.js +90 -0
  103. package/dist/cli/bootstrap.js.map +1 -0
  104. package/dist/cli/commands/health.d.ts +20 -0
  105. package/dist/cli/commands/health.d.ts.map +1 -0
  106. package/dist/cli/commands/health.js +80 -0
  107. package/dist/cli/commands/health.js.map +1 -0
  108. package/dist/cli/commands/init.d.ts +48 -0
  109. package/dist/cli/commands/init.d.ts.map +1 -0
  110. package/dist/cli/commands/init.js +349 -0
  111. package/dist/cli/commands/init.js.map +1 -0
  112. package/dist/cli/commands/migrate.d.ts +19 -0
  113. package/dist/cli/commands/migrate.d.ts.map +1 -0
  114. package/dist/cli/commands/migrate.js +88 -0
  115. package/dist/cli/commands/migrate.js.map +1 -0
  116. package/dist/cli/commands/seed.d.ts +19 -0
  117. package/dist/cli/commands/seed.d.ts.map +1 -0
  118. package/dist/cli/commands/seed.js +71 -0
  119. package/dist/cli/commands/seed.js.map +1 -0
  120. package/dist/cli/commands/user-2fa.d.ts +29 -0
  121. package/dist/cli/commands/user-2fa.d.ts.map +1 -0
  122. package/dist/cli/commands/user-2fa.js +113 -0
  123. package/dist/cli/commands/user-2fa.js.map +1 -0
  124. package/dist/cli/error-handler.d.ts +31 -0
  125. package/dist/cli/error-handler.d.ts.map +1 -0
  126. package/dist/cli/error-handler.js +65 -0
  127. package/dist/cli/error-handler.js.map +1 -0
  128. package/dist/cli/index.d.ts +48 -0
  129. package/dist/cli/index.d.ts.map +1 -0
  130. package/dist/cli/index.js +99 -0
  131. package/dist/cli/index.js.map +1 -0
  132. package/dist/cli/output.d.ts +94 -0
  133. package/dist/cli/output.d.ts.map +1 -0
  134. package/dist/cli/output.js +124 -0
  135. package/dist/cli/output.js.map +1 -0
  136. package/dist/cli/prompt.d.ts +48 -0
  137. package/dist/cli/prompt.d.ts.map +1 -0
  138. package/dist/cli/prompt.js +96 -0
  139. package/dist/cli/prompt.js.map +1 -0
  140. package/dist/clients/cache.d.ts +63 -0
  141. package/dist/clients/cache.d.ts.map +1 -0
  142. package/dist/clients/cache.js +160 -0
  143. package/dist/clients/cache.js.map +1 -0
  144. package/dist/clients/crypto.d.ts +69 -0
  145. package/dist/clients/crypto.d.ts.map +1 -0
  146. package/dist/clients/crypto.js +83 -0
  147. package/dist/clients/crypto.js.map +1 -0
  148. package/dist/clients/errors.d.ts +27 -0
  149. package/dist/clients/errors.d.ts.map +1 -0
  150. package/dist/clients/errors.js +33 -0
  151. package/dist/clients/errors.js.map +1 -0
  152. package/dist/clients/index.d.ts +19 -0
  153. package/dist/clients/index.d.ts.map +1 -0
  154. package/dist/clients/index.js +23 -0
  155. package/dist/clients/index.js.map +1 -0
  156. package/dist/clients/repository.d.ts +161 -0
  157. package/dist/clients/repository.d.ts.map +1 -0
  158. package/dist/clients/repository.js +318 -0
  159. package/dist/clients/repository.js.map +1 -0
  160. package/dist/clients/resolve-login-methods.d.ts +74 -0
  161. package/dist/clients/resolve-login-methods.d.ts.map +1 -0
  162. package/dist/clients/resolve-login-methods.js +68 -0
  163. package/dist/clients/resolve-login-methods.js.map +1 -0
  164. package/dist/clients/secret-repository.d.ts +109 -0
  165. package/dist/clients/secret-repository.d.ts.map +1 -0
  166. package/dist/clients/secret-repository.js +166 -0
  167. package/dist/clients/secret-repository.js.map +1 -0
  168. package/dist/clients/secret-service.d.ts +87 -0
  169. package/dist/clients/secret-service.d.ts.map +1 -0
  170. package/dist/clients/secret-service.js +204 -0
  171. package/dist/clients/secret-service.js.map +1 -0
  172. package/dist/clients/service.d.ts +158 -0
  173. package/dist/clients/service.d.ts.map +1 -0
  174. package/dist/clients/service.js +523 -0
  175. package/dist/clients/service.js.map +1 -0
  176. package/dist/clients/types.d.ts +214 -0
  177. package/dist/clients/types.d.ts.map +1 -0
  178. package/dist/clients/types.js +77 -0
  179. package/dist/clients/types.js.map +1 -0
  180. package/dist/clients/validators.d.ts +87 -0
  181. package/dist/clients/validators.d.ts.map +1 -0
  182. package/dist/clients/validators.js +164 -0
  183. package/dist/clients/validators.js.map +1 -0
  184. package/dist/config/index.d.ts +23 -0
  185. package/dist/config/index.d.ts.map +1 -0
  186. package/dist/config/index.js +52 -0
  187. package/dist/config/index.js.map +1 -0
  188. package/dist/config/schema.d.ts +35 -0
  189. package/dist/config/schema.d.ts.map +1 -0
  190. package/dist/config/schema.js +167 -0
  191. package/dist/config/schema.js.map +1 -0
  192. package/dist/custom-claims/cache.d.ts +48 -0
  193. package/dist/custom-claims/cache.d.ts.map +1 -0
  194. package/dist/custom-claims/cache.js +122 -0
  195. package/dist/custom-claims/cache.js.map +1 -0
  196. package/dist/custom-claims/errors.d.ts +25 -0
  197. package/dist/custom-claims/errors.d.ts.map +1 -0
  198. package/dist/custom-claims/errors.js +31 -0
  199. package/dist/custom-claims/errors.js.map +1 -0
  200. package/dist/custom-claims/index.d.ts +14 -0
  201. package/dist/custom-claims/index.d.ts.map +1 -0
  202. package/dist/custom-claims/index.js +17 -0
  203. package/dist/custom-claims/index.js.map +1 -0
  204. package/dist/custom-claims/repository.d.ts +147 -0
  205. package/dist/custom-claims/repository.d.ts.map +1 -0
  206. package/dist/custom-claims/repository.js +338 -0
  207. package/dist/custom-claims/repository.js.map +1 -0
  208. package/dist/custom-claims/service.d.ts +129 -0
  209. package/dist/custom-claims/service.d.ts.map +1 -0
  210. package/dist/custom-claims/service.js +296 -0
  211. package/dist/custom-claims/service.js.map +1 -0
  212. package/dist/custom-claims/types.d.ts +132 -0
  213. package/dist/custom-claims/types.d.ts.map +1 -0
  214. package/dist/custom-claims/types.js +61 -0
  215. package/dist/custom-claims/types.js.map +1 -0
  216. package/dist/custom-claims/validators.d.ts +61 -0
  217. package/dist/custom-claims/validators.d.ts.map +1 -0
  218. package/dist/custom-claims/validators.js +136 -0
  219. package/dist/custom-claims/validators.js.map +1 -0
  220. package/dist/index.d.ts +14 -0
  221. package/dist/index.d.ts.map +1 -0
  222. package/dist/index.js +91 -0
  223. package/dist/index.js.map +1 -0
  224. package/dist/lib/admin-permissions.d.ts +147 -0
  225. package/dist/lib/admin-permissions.d.ts.map +1 -0
  226. package/dist/lib/admin-permissions.js +261 -0
  227. package/dist/lib/admin-permissions.js.map +1 -0
  228. package/dist/lib/audit-log.d.ts +45 -0
  229. package/dist/lib/audit-log.d.ts.map +1 -0
  230. package/dist/lib/audit-log.js +79 -0
  231. package/dist/lib/audit-log.js.map +1 -0
  232. package/dist/lib/branding-assets.d.ts +47 -0
  233. package/dist/lib/branding-assets.d.ts.map +1 -0
  234. package/dist/lib/branding-assets.js +93 -0
  235. package/dist/lib/branding-assets.js.map +1 -0
  236. package/dist/lib/bulk-operations.d.ts +54 -0
  237. package/dist/lib/bulk-operations.d.ts.map +1 -0
  238. package/dist/lib/bulk-operations.js +201 -0
  239. package/dist/lib/bulk-operations.js.map +1 -0
  240. package/dist/lib/cursor.d.ts +110 -0
  241. package/dist/lib/cursor.d.ts.map +1 -0
  242. package/dist/lib/cursor.js +115 -0
  243. package/dist/lib/cursor.js.map +1 -0
  244. package/dist/lib/data-export.d.ts +67 -0
  245. package/dist/lib/data-export.d.ts.map +1 -0
  246. package/dist/lib/data-export.js +272 -0
  247. package/dist/lib/data-export.js.map +1 -0
  248. package/dist/lib/data-import-plan.d.ts +50 -0
  249. package/dist/lib/data-import-plan.d.ts.map +1 -0
  250. package/dist/lib/data-import-plan.js +235 -0
  251. package/dist/lib/data-import-plan.js.map +1 -0
  252. package/dist/lib/data-import.d.ts +276 -0
  253. package/dist/lib/data-import.d.ts.map +1 -0
  254. package/dist/lib/data-import.js +1694 -0
  255. package/dist/lib/data-import.js.map +1 -0
  256. package/dist/lib/database.d.ts +22 -0
  257. package/dist/lib/database.d.ts.map +1 -0
  258. package/dist/lib/database.js +103 -0
  259. package/dist/lib/database.js.map +1 -0
  260. package/dist/lib/entity-history.d.ts +52 -0
  261. package/dist/lib/entity-history.d.ts.map +1 -0
  262. package/dist/lib/entity-history.js +121 -0
  263. package/dist/lib/entity-history.js.map +1 -0
  264. package/dist/lib/etag.d.ts +92 -0
  265. package/dist/lib/etag.d.ts.map +1 -0
  266. package/dist/lib/etag.js +155 -0
  267. package/dist/lib/etag.js.map +1 -0
  268. package/dist/lib/image-validator.d.ts +44 -0
  269. package/dist/lib/image-validator.d.ts.map +1 -0
  270. package/dist/lib/image-validator.js +204 -0
  271. package/dist/lib/image-validator.js.map +1 -0
  272. package/dist/lib/logger.d.ts +15 -0
  273. package/dist/lib/logger.d.ts.map +1 -0
  274. package/dist/lib/logger.js +84 -0
  275. package/dist/lib/logger.js.map +1 -0
  276. package/dist/lib/migrator.d.ts +14 -0
  277. package/dist/lib/migrator.d.ts.map +1 -0
  278. package/dist/lib/migrator.js +34 -0
  279. package/dist/lib/migrator.js.map +1 -0
  280. package/dist/lib/redis.d.ts +5 -0
  281. package/dist/lib/redis.d.ts.map +1 -0
  282. package/dist/lib/redis.js +29 -0
  283. package/dist/lib/redis.js.map +1 -0
  284. package/dist/lib/runtime-paths.d.ts +38 -0
  285. package/dist/lib/runtime-paths.d.ts.map +1 -0
  286. package/dist/lib/runtime-paths.js +50 -0
  287. package/dist/lib/runtime-paths.js.map +1 -0
  288. package/dist/lib/session-tracking.d.ts +81 -0
  289. package/dist/lib/session-tracking.d.ts.map +1 -0
  290. package/dist/lib/session-tracking.js +145 -0
  291. package/dist/lib/session-tracking.js.map +1 -0
  292. package/dist/lib/signing-key-crypto.d.ts +53 -0
  293. package/dist/lib/signing-key-crypto.d.ts.map +1 -0
  294. package/dist/lib/signing-key-crypto.js +108 -0
  295. package/dist/lib/signing-key-crypto.js.map +1 -0
  296. package/dist/lib/signing-keys.d.ts +141 -0
  297. package/dist/lib/signing-keys.d.ts.map +1 -0
  298. package/dist/lib/signing-keys.js +206 -0
  299. package/dist/lib/signing-keys.js.map +1 -0
  300. package/dist/lib/stats.d.ts +83 -0
  301. package/dist/lib/stats.d.ts.map +1 -0
  302. package/dist/lib/stats.js +187 -0
  303. package/dist/lib/stats.js.map +1 -0
  304. package/dist/lib/super-admin-protection.d.ts +69 -0
  305. package/dist/lib/super-admin-protection.d.ts.map +1 -0
  306. package/dist/lib/super-admin-protection.js +102 -0
  307. package/dist/lib/super-admin-protection.js.map +1 -0
  308. package/dist/lib/system-config.d.ts +79 -0
  309. package/dist/lib/system-config.d.ts.map +1 -0
  310. package/dist/lib/system-config.js +159 -0
  311. package/dist/lib/system-config.js.map +1 -0
  312. package/dist/middleware/admin-auth.d.ts +100 -0
  313. package/dist/middleware/admin-auth.d.ts.map +1 -0
  314. package/dist/middleware/admin-auth.js +241 -0
  315. package/dist/middleware/admin-auth.js.map +1 -0
  316. package/dist/middleware/admin-cors.d.ts +41 -0
  317. package/dist/middleware/admin-cors.d.ts.map +1 -0
  318. package/dist/middleware/admin-cors.js +83 -0
  319. package/dist/middleware/admin-cors.js.map +1 -0
  320. package/dist/middleware/admin-mutation-audit.d.ts +9 -0
  321. package/dist/middleware/admin-mutation-audit.d.ts.map +1 -0
  322. package/dist/middleware/admin-mutation-audit.js +76 -0
  323. package/dist/middleware/admin-mutation-audit.js.map +1 -0
  324. package/dist/middleware/admin-rate-limiter.d.ts +56 -0
  325. package/dist/middleware/admin-rate-limiter.d.ts.map +1 -0
  326. package/dist/middleware/admin-rate-limiter.js +97 -0
  327. package/dist/middleware/admin-rate-limiter.js.map +1 -0
  328. package/dist/middleware/client-secret-hash.d.ts +34 -0
  329. package/dist/middleware/client-secret-hash.d.ts.map +1 -0
  330. package/dist/middleware/client-secret-hash.js +74 -0
  331. package/dist/middleware/client-secret-hash.js.map +1 -0
  332. package/dist/middleware/error-handler.d.ts +3 -0
  333. package/dist/middleware/error-handler.d.ts.map +1 -0
  334. package/dist/middleware/error-handler.js +93 -0
  335. package/dist/middleware/error-handler.js.map +1 -0
  336. package/dist/middleware/health.d.ts +3 -0
  337. package/dist/middleware/health.d.ts.map +1 -0
  338. package/dist/middleware/health.js +35 -0
  339. package/dist/middleware/health.js.map +1 -0
  340. package/dist/middleware/metrics.d.ts +40 -0
  341. package/dist/middleware/metrics.d.ts.map +1 -0
  342. package/dist/middleware/metrics.js +75 -0
  343. package/dist/middleware/metrics.js.map +1 -0
  344. package/dist/middleware/oidc-client-tenant.d.ts +37 -0
  345. package/dist/middleware/oidc-client-tenant.d.ts.map +1 -0
  346. package/dist/middleware/oidc-client-tenant.js +156 -0
  347. package/dist/middleware/oidc-client-tenant.js.map +1 -0
  348. package/dist/middleware/oidc-cors.d.ts +33 -0
  349. package/dist/middleware/oidc-cors.d.ts.map +1 -0
  350. package/dist/middleware/oidc-cors.js +68 -0
  351. package/dist/middleware/oidc-cors.js.map +1 -0
  352. package/dist/middleware/oidc-preflight-cors.d.ts +54 -0
  353. package/dist/middleware/oidc-preflight-cors.d.ts.map +1 -0
  354. package/dist/middleware/oidc-preflight-cors.js +267 -0
  355. package/dist/middleware/oidc-preflight-cors.js.map +1 -0
  356. package/dist/middleware/prompt-login-reset.d.ts +103 -0
  357. package/dist/middleware/prompt-login-reset.d.ts.map +1 -0
  358. package/dist/middleware/prompt-login-reset.js +204 -0
  359. package/dist/middleware/prompt-login-reset.js.map +1 -0
  360. package/dist/middleware/ready.d.ts +23 -0
  361. package/dist/middleware/ready.d.ts.map +1 -0
  362. package/dist/middleware/ready.js +60 -0
  363. package/dist/middleware/ready.js.map +1 -0
  364. package/dist/middleware/request-logger.d.ts +27 -0
  365. package/dist/middleware/request-logger.d.ts.map +1 -0
  366. package/dist/middleware/request-logger.js +46 -0
  367. package/dist/middleware/request-logger.js.map +1 -0
  368. package/dist/middleware/require-permission.d.ts +28 -0
  369. package/dist/middleware/require-permission.d.ts.map +1 -0
  370. package/dist/middleware/require-permission.js +63 -0
  371. package/dist/middleware/require-permission.js.map +1 -0
  372. package/dist/middleware/require-user-organization.d.ts +18 -0
  373. package/dist/middleware/require-user-organization.d.ts.map +1 -0
  374. package/dist/middleware/require-user-organization.js +53 -0
  375. package/dist/middleware/require-user-organization.js.map +1 -0
  376. package/dist/middleware/root-page.d.ts +62 -0
  377. package/dist/middleware/root-page.d.ts.map +1 -0
  378. package/dist/middleware/root-page.js +134 -0
  379. package/dist/middleware/root-page.js.map +1 -0
  380. package/dist/middleware/security-headers.d.ts +98 -0
  381. package/dist/middleware/security-headers.d.ts.map +1 -0
  382. package/dist/middleware/security-headers.js +159 -0
  383. package/dist/middleware/security-headers.js.map +1 -0
  384. package/dist/middleware/tenant-resolver.d.ts +37 -0
  385. package/dist/middleware/tenant-resolver.d.ts.map +1 -0
  386. package/dist/middleware/tenant-resolver.js +71 -0
  387. package/dist/middleware/tenant-resolver.js.map +1 -0
  388. package/dist/middleware/token-rate-limiter.d.ts +89 -0
  389. package/dist/middleware/token-rate-limiter.d.ts.map +1 -0
  390. package/dist/middleware/token-rate-limiter.js +189 -0
  391. package/dist/middleware/token-rate-limiter.js.map +1 -0
  392. package/dist/oidc/account-finder.d.ts +52 -0
  393. package/dist/oidc/account-finder.d.ts.map +1 -0
  394. package/dist/oidc/account-finder.js +134 -0
  395. package/dist/oidc/account-finder.js.map +1 -0
  396. package/dist/oidc/adapter-factory.d.ts +90 -0
  397. package/dist/oidc/adapter-factory.d.ts.map +1 -0
  398. package/dist/oidc/adapter-factory.js +165 -0
  399. package/dist/oidc/adapter-factory.js.map +1 -0
  400. package/dist/oidc/configuration.d.ts +155 -0
  401. package/dist/oidc/configuration.d.ts.map +1 -0
  402. package/dist/oidc/configuration.js +531 -0
  403. package/dist/oidc/configuration.js.map +1 -0
  404. package/dist/oidc/postgres-adapter.d.ts +159 -0
  405. package/dist/oidc/postgres-adapter.d.ts.map +1 -0
  406. package/dist/oidc/postgres-adapter.js +222 -0
  407. package/dist/oidc/postgres-adapter.js.map +1 -0
  408. package/dist/oidc/protocol-security-observer.d.ts +38 -0
  409. package/dist/oidc/protocol-security-observer.d.ts.map +1 -0
  410. package/dist/oidc/protocol-security-observer.js +93 -0
  411. package/dist/oidc/protocol-security-observer.js.map +1 -0
  412. package/dist/oidc/provider.d.ts +37 -0
  413. package/dist/oidc/provider.d.ts.map +1 -0
  414. package/dist/oidc/provider.js +71 -0
  415. package/dist/oidc/provider.js.map +1 -0
  416. package/dist/oidc/redis-adapter.d.ts +140 -0
  417. package/dist/oidc/redis-adapter.d.ts.map +1 -0
  418. package/dist/oidc/redis-adapter.js +344 -0
  419. package/dist/oidc/redis-adapter.js.map +1 -0
  420. package/dist/organizations/cache.d.ts +60 -0
  421. package/dist/organizations/cache.d.ts.map +1 -0
  422. package/dist/organizations/cache.js +157 -0
  423. package/dist/organizations/cache.js.map +1 -0
  424. package/dist/organizations/errors.d.ts +25 -0
  425. package/dist/organizations/errors.d.ts.map +1 -0
  426. package/dist/organizations/errors.js +31 -0
  427. package/dist/organizations/errors.js.map +1 -0
  428. package/dist/organizations/index.d.ts +15 -0
  429. package/dist/organizations/index.d.ts.map +1 -0
  430. package/dist/organizations/index.js +15 -0
  431. package/dist/organizations/index.js.map +1 -0
  432. package/dist/organizations/repository.d.ts +183 -0
  433. package/dist/organizations/repository.d.ts.map +1 -0
  434. package/dist/organizations/repository.js +354 -0
  435. package/dist/organizations/repository.js.map +1 -0
  436. package/dist/organizations/service.d.ts +168 -0
  437. package/dist/organizations/service.d.ts.map +1 -0
  438. package/dist/organizations/service.js +511 -0
  439. package/dist/organizations/service.js.map +1 -0
  440. package/dist/organizations/slugs.d.ts +57 -0
  441. package/dist/organizations/slugs.d.ts.map +1 -0
  442. package/dist/organizations/slugs.js +130 -0
  443. package/dist/organizations/slugs.js.map +1 -0
  444. package/dist/organizations/types.d.ts +146 -0
  445. package/dist/organizations/types.d.ts.map +1 -0
  446. package/dist/organizations/types.js +49 -0
  447. package/dist/organizations/types.js.map +1 -0
  448. package/dist/rbac/cache.d.ts +110 -0
  449. package/dist/rbac/cache.d.ts.map +1 -0
  450. package/dist/rbac/cache.js +271 -0
  451. package/dist/rbac/cache.js.map +1 -0
  452. package/dist/rbac/errors.d.ts +34 -0
  453. package/dist/rbac/errors.d.ts.map +1 -0
  454. package/dist/rbac/errors.js +43 -0
  455. package/dist/rbac/errors.js.map +1 -0
  456. package/dist/rbac/index.d.ts +17 -0
  457. package/dist/rbac/index.d.ts.map +1 -0
  458. package/dist/rbac/index.js +21 -0
  459. package/dist/rbac/index.js.map +1 -0
  460. package/dist/rbac/mapping-repository.d.ts +121 -0
  461. package/dist/rbac/mapping-repository.d.ts.map +1 -0
  462. package/dist/rbac/mapping-repository.js +224 -0
  463. package/dist/rbac/mapping-repository.js.map +1 -0
  464. package/dist/rbac/permission-repository.d.ts +100 -0
  465. package/dist/rbac/permission-repository.d.ts.map +1 -0
  466. package/dist/rbac/permission-repository.js +190 -0
  467. package/dist/rbac/permission-repository.js.map +1 -0
  468. package/dist/rbac/permission-service.d.ts +85 -0
  469. package/dist/rbac/permission-service.d.ts.map +1 -0
  470. package/dist/rbac/permission-service.js +179 -0
  471. package/dist/rbac/permission-service.js.map +1 -0
  472. package/dist/rbac/role-repository.d.ts +104 -0
  473. package/dist/rbac/role-repository.d.ts.map +1 -0
  474. package/dist/rbac/role-repository.js +202 -0
  475. package/dist/rbac/role-repository.js.map +1 -0
  476. package/dist/rbac/role-service.d.ts +111 -0
  477. package/dist/rbac/role-service.d.ts.map +1 -0
  478. package/dist/rbac/role-service.js +256 -0
  479. package/dist/rbac/role-service.js.map +1 -0
  480. package/dist/rbac/slugs.d.ts +116 -0
  481. package/dist/rbac/slugs.d.ts.map +1 -0
  482. package/dist/rbac/slugs.js +178 -0
  483. package/dist/rbac/slugs.js.map +1 -0
  484. package/dist/rbac/types.d.ts +187 -0
  485. package/dist/rbac/types.d.ts.map +1 -0
  486. package/dist/rbac/types.js +75 -0
  487. package/dist/rbac/types.js.map +1 -0
  488. package/dist/rbac/user-role-service.d.ts +101 -0
  489. package/dist/rbac/user-role-service.d.ts.map +1 -0
  490. package/dist/rbac/user-role-service.js +166 -0
  491. package/dist/rbac/user-role-service.js.map +1 -0
  492. package/dist/routes/applications.d.ts +40 -0
  493. package/dist/routes/applications.d.ts.map +1 -0
  494. package/dist/routes/applications.js +284 -0
  495. package/dist/routes/applications.js.map +1 -0
  496. package/dist/routes/audit.d.ts +31 -0
  497. package/dist/routes/audit.d.ts.map +1 -0
  498. package/dist/routes/audit.js +140 -0
  499. package/dist/routes/audit.js.map +1 -0
  500. package/dist/routes/branding.d.ts +17 -0
  501. package/dist/routes/branding.d.ts.map +1 -0
  502. package/dist/routes/branding.js +104 -0
  503. package/dist/routes/branding.js.map +1 -0
  504. package/dist/routes/bulk.d.ts +37 -0
  505. package/dist/routes/bulk.d.ts.map +1 -0
  506. package/dist/routes/bulk.js +94 -0
  507. package/dist/routes/bulk.js.map +1 -0
  508. package/dist/routes/clients.d.ts +44 -0
  509. package/dist/routes/clients.d.ts.map +1 -0
  510. package/dist/routes/clients.js +353 -0
  511. package/dist/routes/clients.js.map +1 -0
  512. package/dist/routes/config.d.ts +35 -0
  513. package/dist/routes/config.d.ts.map +1 -0
  514. package/dist/routes/config.js +115 -0
  515. package/dist/routes/config.js.map +1 -0
  516. package/dist/routes/custom-claims.d.ts +38 -0
  517. package/dist/routes/custom-claims.d.ts.map +1 -0
  518. package/dist/routes/custom-claims.js +208 -0
  519. package/dist/routes/custom-claims.js.map +1 -0
  520. package/dist/routes/exports.d.ts +21 -0
  521. package/dist/routes/exports.d.ts.map +1 -0
  522. package/dist/routes/exports.js +98 -0
  523. package/dist/routes/exports.js.map +1 -0
  524. package/dist/routes/imports.d.ts +18 -0
  525. package/dist/routes/imports.d.ts.map +1 -0
  526. package/dist/routes/imports.js +72 -0
  527. package/dist/routes/imports.js.map +1 -0
  528. package/dist/routes/interactions.d.ts +111 -0
  529. package/dist/routes/interactions.d.ts.map +1 -0
  530. package/dist/routes/interactions.js +1074 -0
  531. package/dist/routes/interactions.js.map +1 -0
  532. package/dist/routes/invitation.d.ts +31 -0
  533. package/dist/routes/invitation.d.ts.map +1 -0
  534. package/dist/routes/invitation.js +342 -0
  535. package/dist/routes/invitation.js.map +1 -0
  536. package/dist/routes/keys.d.ts +28 -0
  537. package/dist/routes/keys.d.ts.map +1 -0
  538. package/dist/routes/keys.js +91 -0
  539. package/dist/routes/keys.js.map +1 -0
  540. package/dist/routes/magic-link.d.ts +43 -0
  541. package/dist/routes/magic-link.d.ts.map +1 -0
  542. package/dist/routes/magic-link.js +264 -0
  543. package/dist/routes/magic-link.js.map +1 -0
  544. package/dist/routes/organizations.d.ts +38 -0
  545. package/dist/routes/organizations.d.ts.map +1 -0
  546. package/dist/routes/organizations.js +347 -0
  547. package/dist/routes/organizations.js.map +1 -0
  548. package/dist/routes/password-reset.d.ts +65 -0
  549. package/dist/routes/password-reset.d.ts.map +1 -0
  550. package/dist/routes/password-reset.js +493 -0
  551. package/dist/routes/password-reset.js.map +1 -0
  552. package/dist/routes/permissions.d.ts +33 -0
  553. package/dist/routes/permissions.d.ts.map +1 -0
  554. package/dist/routes/permissions.js +166 -0
  555. package/dist/routes/permissions.js.map +1 -0
  556. package/dist/routes/roles.d.ts +37 -0
  557. package/dist/routes/roles.d.ts.map +1 -0
  558. package/dist/routes/roles.js +222 -0
  559. package/dist/routes/roles.js.map +1 -0
  560. package/dist/routes/sessions.d.ts +37 -0
  561. package/dist/routes/sessions.d.ts.map +1 -0
  562. package/dist/routes/sessions.js +121 -0
  563. package/dist/routes/sessions.js.map +1 -0
  564. package/dist/routes/stats.d.ts +28 -0
  565. package/dist/routes/stats.d.ts.map +1 -0
  566. package/dist/routes/stats.js +65 -0
  567. package/dist/routes/stats.js.map +1 -0
  568. package/dist/routes/two-factor-admin.d.ts +54 -0
  569. package/dist/routes/two-factor-admin.d.ts.map +1 -0
  570. package/dist/routes/two-factor-admin.js +344 -0
  571. package/dist/routes/two-factor-admin.js.map +1 -0
  572. package/dist/routes/two-factor.d.ts +31 -0
  573. package/dist/routes/two-factor.d.ts.map +1 -0
  574. package/dist/routes/two-factor.js +595 -0
  575. package/dist/routes/two-factor.js.map +1 -0
  576. package/dist/routes/user-roles.d.ts +33 -0
  577. package/dist/routes/user-roles.d.ts.map +1 -0
  578. package/dist/routes/user-roles.js +129 -0
  579. package/dist/routes/user-roles.js.map +1 -0
  580. package/dist/routes/users.d.ts +61 -0
  581. package/dist/routes/users.d.ts.map +1 -0
  582. package/dist/routes/users.js +868 -0
  583. package/dist/routes/users.js.map +1 -0
  584. package/dist/security/decision-context.d.ts +52 -0
  585. package/dist/security/decision-context.d.ts.map +1 -0
  586. package/dist/security/decision-context.js +150 -0
  587. package/dist/security/decision-context.js.map +1 -0
  588. package/dist/security/decision-event.d.ts +128 -0
  589. package/dist/security/decision-event.d.ts.map +1 -0
  590. package/dist/security/decision-event.js +225 -0
  591. package/dist/security/decision-event.js.map +1 -0
  592. package/dist/security/transport-decision.d.ts +6 -0
  593. package/dist/security/transport-decision.d.ts.map +1 -0
  594. package/dist/security/transport-decision.js +37 -0
  595. package/dist/security/transport-decision.js.map +1 -0
  596. package/dist/server.d.ts +50 -0
  597. package/dist/server.d.ts.map +1 -0
  598. package/dist/server.js +491 -0
  599. package/dist/server.js.map +1 -0
  600. package/dist/two-factor/cache.d.ts +47 -0
  601. package/dist/two-factor/cache.d.ts.map +1 -0
  602. package/dist/two-factor/cache.js +122 -0
  603. package/dist/two-factor/cache.js.map +1 -0
  604. package/dist/two-factor/crypto.d.ts +41 -0
  605. package/dist/two-factor/crypto.d.ts.map +1 -0
  606. package/dist/two-factor/crypto.js +94 -0
  607. package/dist/two-factor/crypto.js.map +1 -0
  608. package/dist/two-factor/errors.d.ts +82 -0
  609. package/dist/two-factor/errors.d.ts.map +1 -0
  610. package/dist/two-factor/errors.js +112 -0
  611. package/dist/two-factor/errors.js.map +1 -0
  612. package/dist/two-factor/index.d.ts +18 -0
  613. package/dist/two-factor/index.d.ts.map +1 -0
  614. package/dist/two-factor/index.js +25 -0
  615. package/dist/two-factor/index.js.map +1 -0
  616. package/dist/two-factor/otp.d.ts +43 -0
  617. package/dist/two-factor/otp.d.ts.map +1 -0
  618. package/dist/two-factor/otp.js +57 -0
  619. package/dist/two-factor/otp.js.map +1 -0
  620. package/dist/two-factor/recovery.d.ts +45 -0
  621. package/dist/two-factor/recovery.d.ts.map +1 -0
  622. package/dist/two-factor/recovery.js +104 -0
  623. package/dist/two-factor/recovery.js.map +1 -0
  624. package/dist/two-factor/repository.d.ts +156 -0
  625. package/dist/two-factor/repository.d.ts.map +1 -0
  626. package/dist/two-factor/repository.js +251 -0
  627. package/dist/two-factor/repository.js.map +1 -0
  628. package/dist/two-factor/service.d.ts +232 -0
  629. package/dist/two-factor/service.d.ts.map +1 -0
  630. package/dist/two-factor/service.js +599 -0
  631. package/dist/two-factor/service.js.map +1 -0
  632. package/dist/two-factor/totp.d.ts +57 -0
  633. package/dist/two-factor/totp.d.ts.map +1 -0
  634. package/dist/two-factor/totp.js +96 -0
  635. package/dist/two-factor/totp.js.map +1 -0
  636. package/dist/two-factor/types.d.ts +152 -0
  637. package/dist/two-factor/types.d.ts.map +1 -0
  638. package/dist/two-factor/types.js +64 -0
  639. package/dist/two-factor/types.js.map +1 -0
  640. package/dist/users/cache.d.ts +49 -0
  641. package/dist/users/cache.d.ts.map +1 -0
  642. package/dist/users/cache.js +140 -0
  643. package/dist/users/cache.js.map +1 -0
  644. package/dist/users/claims.d.ts +67 -0
  645. package/dist/users/claims.d.ts.map +1 -0
  646. package/dist/users/claims.js +104 -0
  647. package/dist/users/claims.js.map +1 -0
  648. package/dist/users/errors.d.ts +25 -0
  649. package/dist/users/errors.d.ts.map +1 -0
  650. package/dist/users/errors.js +31 -0
  651. package/dist/users/errors.js.map +1 -0
  652. package/dist/users/gdpr.d.ts +110 -0
  653. package/dist/users/gdpr.d.ts.map +1 -0
  654. package/dist/users/gdpr.js +211 -0
  655. package/dist/users/gdpr.js.map +1 -0
  656. package/dist/users/index.d.ts +20 -0
  657. package/dist/users/index.d.ts.map +1 -0
  658. package/dist/users/index.js +19 -0
  659. package/dist/users/index.js.map +1 -0
  660. package/dist/users/password.d.ts +85 -0
  661. package/dist/users/password.d.ts.map +1 -0
  662. package/dist/users/password.js +138 -0
  663. package/dist/users/password.js.map +1 -0
  664. package/dist/users/repository.d.ts +263 -0
  665. package/dist/users/repository.d.ts.map +1 -0
  666. package/dist/users/repository.js +499 -0
  667. package/dist/users/repository.js.map +1 -0
  668. package/dist/users/service.d.ts +299 -0
  669. package/dist/users/service.d.ts.map +1 -0
  670. package/dist/users/service.js +698 -0
  671. package/dist/users/service.js.map +1 -0
  672. package/dist/users/types.d.ts +220 -0
  673. package/dist/users/types.d.ts.map +1 -0
  674. package/dist/users/types.js +77 -0
  675. package/dist/users/types.js.map +1 -0
  676. package/locales/default/en/common.json +10 -0
  677. package/locales/default/en/consent.json +13 -0
  678. package/locales/default/en/emails.json +17 -0
  679. package/locales/default/en/errors.json +21 -0
  680. package/locales/default/en/forgot-password.json +11 -0
  681. package/locales/default/en/invitation.json +18 -0
  682. package/locales/default/en/login.json +17 -0
  683. package/locales/default/en/logout.json +9 -0
  684. package/locales/default/en/magic-link.json +12 -0
  685. package/locales/default/en/reset-password.json +16 -0
  686. package/locales/default/en/two-factor.json +29 -0
  687. package/migrations/.gitkeep +0 -0
  688. package/migrations/001_extensions.sql +22 -0
  689. package/migrations/002_organizations.sql +43 -0
  690. package/migrations/003_applications.sql +50 -0
  691. package/migrations/004_clients.sql +74 -0
  692. package/migrations/005_users.sql +119 -0
  693. package/migrations/006_roles_permissions.sql +80 -0
  694. package/migrations/007_custom_claims.sql +55 -0
  695. package/migrations/008_config.sql +51 -0
  696. package/migrations/009_audit_log.sql +31 -0
  697. package/migrations/010_oidc_adapter.sql +34 -0
  698. package/migrations/011_seed.sql +53 -0
  699. package/migrations/012_two_factor.sql +88 -0
  700. package/migrations/013_client_secret_sha256.sql +22 -0
  701. package/migrations/014_login_methods.sql +34 -0
  702. package/migrations/015_signing_key_encryption.sql +20 -0
  703. package/migrations/016_failed_login_count.sql +27 -0
  704. package/migrations/017_audit_retention.sql +11 -0
  705. package/migrations/018_admin_api_enhancements.sql +78 -0
  706. package/migrations/019_invitation_details.sql +27 -0
  707. package/migrations/020_two_factor_admin_permission.sql +108 -0
  708. package/migrations/021_auth_recovery_jobs.sql +74 -0
  709. package/migrations/022_recovery_artifact_ownership.sql +25 -0
  710. package/migrations/023_magic_link_authority.sql +39 -0
  711. package/package.json +106 -0
  712. package/templates/default/emails/invitation.hbs +39 -0
  713. package/templates/default/emails/invitation.txt.hbs +18 -0
  714. package/templates/default/emails/magic-link.hbs +30 -0
  715. package/templates/default/emails/magic-link.txt.hbs +11 -0
  716. package/templates/default/emails/otp-code.hbs +31 -0
  717. package/templates/default/emails/otp-code.txt.hbs +15 -0
  718. package/templates/default/emails/password-changed.hbs +24 -0
  719. package/templates/default/emails/password-changed.txt.hbs +7 -0
  720. package/templates/default/emails/password-reset.hbs +30 -0
  721. package/templates/default/emails/password-reset.txt.hbs +11 -0
  722. package/templates/default/emails/welcome.hbs +25 -0
  723. package/templates/default/emails/welcome.txt.hbs +9 -0
  724. package/templates/default/layouts/main.hbs +49 -0
  725. package/templates/default/pages/accept-invite.hbs +25 -0
  726. package/templates/default/pages/consent.hbs +27 -0
  727. package/templates/default/pages/error.hbs +15 -0
  728. package/templates/default/pages/forgot-password.hbs +21 -0
  729. package/templates/default/pages/invite-expired.hbs +6 -0
  730. package/templates/default/pages/invite-success.hbs +9 -0
  731. package/templates/default/pages/login.hbs +108 -0
  732. package/templates/default/pages/logout-success.hbs +5 -0
  733. package/templates/default/pages/logout.hbs +24 -0
  734. package/templates/default/pages/magic-link-sent.hbs +10 -0
  735. package/templates/default/pages/magic-link-success.hbs +17 -0
  736. package/templates/default/pages/reset-password.hbs +20 -0
  737. package/templates/default/pages/reset-success.hbs +9 -0
  738. package/templates/default/pages/two-factor-setup.hbs +65 -0
  739. package/templates/default/pages/two-factor-verify.hbs +77 -0
  740. package/templates/default/partials/flash-messages.hbs +10 -0
  741. package/templates/default/partials/footer.hbs +4 -0
  742. package/templates/default/partials/header.hbs +8 -0
@@ -0,0 +1,868 @@
1
+ /**
2
+ * User management API routes.
3
+ *
4
+ * All routes are under `/api/admin/organizations/:orgId/users` and
5
+ * require admin authorization with granular permissions. User routes
6
+ * are nested under an organization context because users are always
7
+ * scoped to an org.
8
+ *
9
+ * Route structure:
10
+ * POST / — Create a user in the organization
11
+ * GET / — List users (paginated, searchable)
12
+ * GET /:userId — Get user by ID
13
+ * PUT /:userId — Update user profile
14
+ * POST /:userId/deactivate — Deactivate (active → inactive)
15
+ * POST /:userId/reactivate — Reactivate (inactive → active)
16
+ * POST /:userId/suspend — Suspend (active → suspended)
17
+ * POST /:userId/unsuspend — Unsuspend (suspended → active)
18
+ * POST /:userId/lock — Lock (active → locked)
19
+ * POST /:userId/unlock — Unlock (locked → active)
20
+ * POST /:userId/password — Set/change password
21
+ * DELETE /:userId/password — Clear password (passwordless)
22
+ * POST /:userId/verify-email — Mark email as verified
23
+ * GET /:userId/export — GDPR data export (Article 20)
24
+ * POST /:userId/purge — GDPR data purge (Article 17)
25
+ *
26
+ * Error mapping:
27
+ * UserNotFoundError → 404
28
+ * UserValidationError → 400
29
+ * SuperAdminProtectionError → 403
30
+ * ZodError → 400 with validation details
31
+ */
32
+ import Router from '@koa/router';
33
+ import { z } from 'zod';
34
+ import { renderInvitationEmail, sendInvitationEmail } from '../auth/email-service.js';
35
+ import { insertInvitationToken, invalidateUserTokens } from '../auth/token-repository.js';
36
+ import { generateToken } from '../auth/tokens.js';
37
+ import { config } from '../config/index.js';
38
+ import { ADMIN_PERMISSIONS } from '../lib/admin-permissions.js';
39
+ import { writeAuditLog } from '../lib/audit-log.js';
40
+ import { afterDatabaseCommit, getPool } from '../lib/database.js';
41
+ import { getEntityHistory } from '../lib/entity-history.js';
42
+ import { checkIfMatch, setETagHeader } from '../lib/etag.js';
43
+ import { guardSuperAdmin, SuperAdminProtectionError } from '../lib/super-admin-protection.js';
44
+ import { requireAdminAuth } from '../middleware/admin-auth.js';
45
+ import { requirePermission } from '../middleware/require-permission.js';
46
+ import { requireUserOrganization } from '../middleware/require-user-organization.js';
47
+ import { getOrganizationById } from '../organizations/service.js';
48
+ import { UserNotFoundError, UserValidationError } from '../users/errors.js';
49
+ import { exportUserData, purgeUserData } from '../users/gdpr.js';
50
+ import * as userService from '../users/service.js';
51
+ // ---------------------------------------------------------------------------
52
+ // Validation schemas
53
+ // ---------------------------------------------------------------------------
54
+ /** Schema for creating a new user */
55
+ const createUserSchema = z.object({
56
+ email: z.string().email().max(255),
57
+ password: z.string().min(8).max(128).optional(),
58
+ givenName: z.string().max(255).optional(),
59
+ familyName: z.string().max(255).optional(),
60
+ middleName: z.string().max(255).optional(),
61
+ nickname: z.string().max(255).optional(),
62
+ preferredUsername: z.string().max(255).optional(),
63
+ profileUrl: z.string().url().optional(),
64
+ pictureUrl: z.string().url().optional(),
65
+ websiteUrl: z.string().url().optional(),
66
+ gender: z.string().max(50).optional(),
67
+ birthdate: z
68
+ .string()
69
+ .regex(/^\d{4}-\d{2}-\d{2}$/)
70
+ .optional(),
71
+ zoneinfo: z.string().max(50).optional(),
72
+ locale: z.string().max(10).optional(),
73
+ phoneNumber: z.string().max(50).optional(),
74
+ phoneNumberVerified: z.boolean().optional(),
75
+ address: z
76
+ .object({
77
+ street: z.string().nullable().optional(),
78
+ locality: z.string().max(255).nullable().optional(),
79
+ region: z.string().max(255).nullable().optional(),
80
+ postalCode: z.string().max(20).nullable().optional(),
81
+ country: z.string().length(2).nullable().optional(),
82
+ })
83
+ .optional(),
84
+ });
85
+ /** Schema for updating a user (all fields optional, nullable for clearing) */
86
+ const updateUserSchema = z.object({
87
+ givenName: z.string().max(255).nullable().optional(),
88
+ familyName: z.string().max(255).nullable().optional(),
89
+ middleName: z.string().max(255).nullable().optional(),
90
+ nickname: z.string().max(255).nullable().optional(),
91
+ preferredUsername: z.string().max(255).nullable().optional(),
92
+ profileUrl: z.string().url().nullable().optional(),
93
+ pictureUrl: z.string().url().nullable().optional(),
94
+ websiteUrl: z.string().url().nullable().optional(),
95
+ gender: z.string().max(50).nullable().optional(),
96
+ birthdate: z
97
+ .string()
98
+ .regex(/^\d{4}-\d{2}-\d{2}$/)
99
+ .nullable()
100
+ .optional(),
101
+ zoneinfo: z.string().max(50).nullable().optional(),
102
+ locale: z.string().max(10).nullable().optional(),
103
+ phoneNumber: z.string().max(50).nullable().optional(),
104
+ phoneNumberVerified: z.boolean().optional(),
105
+ address: z
106
+ .object({
107
+ street: z.string().nullable().optional(),
108
+ locality: z.string().max(255).nullable().optional(),
109
+ region: z.string().max(255).nullable().optional(),
110
+ postalCode: z.string().max(20).nullable().optional(),
111
+ country: z.string().length(2).nullable().optional(),
112
+ })
113
+ .nullable()
114
+ .optional(),
115
+ });
116
+ /** Schema for listing users with pagination */
117
+ const listUsersSchema = z.object({
118
+ page: z.coerce.number().int().min(1).default(1),
119
+ pageSize: z.coerce.number().int().min(1).max(100).default(20),
120
+ status: z.enum(['active', 'inactive', 'suspended', 'locked']).optional(),
121
+ search: z.string().max(255).optional(),
122
+ sortBy: z
123
+ .enum(['email', 'given_name', 'family_name', 'created_at', 'last_login_at'])
124
+ .default('created_at'),
125
+ sortOrder: z.enum(['asc', 'desc']).default('desc'),
126
+ });
127
+ /** Schema for cursor-based listing (keyset pagination) */
128
+ const listUsersCursorSchema = z.object({
129
+ cursor: z.string().optional(),
130
+ limit: z.coerce.number().int().min(1).max(100).default(25),
131
+ status: z.enum(['active', 'inactive', 'suspended', 'locked']).optional(),
132
+ search: z.string().max(255).optional(),
133
+ sortBy: z
134
+ .enum(['email', 'given_name', 'family_name', 'created_at', 'last_login_at'])
135
+ .default('created_at'),
136
+ sortOrder: z.enum(['asc', 'desc']).default('desc'),
137
+ });
138
+ /** Schema for setting a password */
139
+ const setPasswordSchema = z.object({
140
+ password: z.string().min(8).max(128),
141
+ });
142
+ /** Schema for locking a user (reason required) */
143
+ const lockUserSchema = z.object({
144
+ reason: z.string().min(1).max(500),
145
+ });
146
+ /** Schema for suspending a user (reason optional) */
147
+ const suspendUserSchema = z.object({
148
+ reason: z.string().max(500).optional(),
149
+ });
150
+ // ---------------------------------------------------------------------------
151
+ // Error handler helper
152
+ // ---------------------------------------------------------------------------
153
+ /**
154
+ * Handle domain errors and map them to HTTP responses.
155
+ * Unknown errors are re-thrown for the global error handler.
156
+ */
157
+ function handleError(ctx, err) {
158
+ if (err instanceof SuperAdminProtectionError) {
159
+ ctx.status = 403;
160
+ ctx.body = { error: 'Forbidden', message: 'The requested operation is not permitted' };
161
+ return undefined;
162
+ }
163
+ if (err instanceof UserNotFoundError) {
164
+ ctx.throw(404, 'User not found');
165
+ }
166
+ if (err instanceof UserValidationError) {
167
+ ctx.throw(400, 'User request is invalid');
168
+ }
169
+ if (err instanceof z.ZodError) {
170
+ ctx.status = 400;
171
+ ctx.body = { error: 'User request is invalid' };
172
+ return undefined;
173
+ }
174
+ throw err;
175
+ }
176
+ // ---------------------------------------------------------------------------
177
+ // Router factory
178
+ // ---------------------------------------------------------------------------
179
+ /**
180
+ * Create the user management router.
181
+ *
182
+ * All routes require admin authorization with granular permissions.
183
+ * Users are always scoped to an organization via the :orgId URL parameter.
184
+ * Destructive operations on the super-admin user are blocked by
185
+ * guardSuperAdmin() checks.
186
+ *
187
+ * Prefix: /api/admin/organizations/:orgId/users
188
+ *
189
+ * @returns Configured Koa Router
190
+ */
191
+ export function createUserRouter() {
192
+ const router = new Router({ prefix: '/api/admin/organizations/:orgId/users' });
193
+ // All routes require admin authentication
194
+ router.use(requireAdminAuth());
195
+ // -------------------------------------------------------------------------
196
+ // POST / — Create user
197
+ // -------------------------------------------------------------------------
198
+ router.post('/', requirePermission(ADMIN_PERMISSIONS.USER_CREATE), async (ctx) => {
199
+ try {
200
+ const body = createUserSchema.parse(ctx.request.body);
201
+ const user = await userService.createUser({
202
+ organizationId: ctx.params.orgId,
203
+ ...body,
204
+ });
205
+ ctx.status = 201;
206
+ ctx.body = { data: user };
207
+ }
208
+ catch (err) {
209
+ handleError(ctx, err);
210
+ }
211
+ });
212
+ // -------------------------------------------------------------------------
213
+ // GET / — List users (paginated)
214
+ // -------------------------------------------------------------------------
215
+ router.get('/', requirePermission(ADMIN_PERMISSIONS.USER_READ), async (ctx) => {
216
+ try {
217
+ // Cursor-based pagination when `cursor` or `limit` param is present
218
+ if (ctx.query.cursor !== undefined || ctx.query.limit !== undefined) {
219
+ const query = listUsersCursorSchema.parse(ctx.query);
220
+ const result = await userService.listUsersCursor({
221
+ organizationId: ctx.params.orgId,
222
+ ...query,
223
+ });
224
+ ctx.body = result;
225
+ return;
226
+ }
227
+ // Default: offset-based pagination (backward compatible)
228
+ const query = listUsersSchema.parse(ctx.query);
229
+ const result = await userService.listUsersByOrganization({
230
+ organizationId: ctx.params.orgId,
231
+ ...query,
232
+ });
233
+ ctx.body = result;
234
+ }
235
+ catch (err) {
236
+ handleError(ctx, err);
237
+ }
238
+ });
239
+ // -------------------------------------------------------------------------
240
+ // GET /:userId — Get user by ID
241
+ // -------------------------------------------------------------------------
242
+ router.get('/:userId', requirePermission(ADMIN_PERMISSIONS.USER_READ), requireUserOrganization(), async (ctx) => {
243
+ const user = await userService.getUserById(ctx.params.userId);
244
+ if (!user) {
245
+ ctx.throw(404, 'User not found');
246
+ return; // unreachable — keeps TS narrowing happy
247
+ }
248
+ setETagHeader(ctx, 'user', user.id, user.updatedAt);
249
+ ctx.body = { data: user };
250
+ });
251
+ // -------------------------------------------------------------------------
252
+ // PUT /:userId — Update user profile
253
+ // -------------------------------------------------------------------------
254
+ router.put('/:userId', requirePermission(ADMIN_PERMISSIONS.USER_UPDATE), requireUserOrganization(), async (ctx) => {
255
+ try {
256
+ const body = updateUserSchema.parse(ctx.request.body);
257
+ // Check If-Match for optimistic concurrency (optional — backward compatible)
258
+ const current = await userService.getUserById(ctx.params.userId);
259
+ if (current && !checkIfMatch(ctx, 'user', current.id, current.updatedAt, current))
260
+ return;
261
+ // Convert null address to undefined — Zod allows null for clearing,
262
+ // but UpdateUserInput uses undefined to mean "no change"
263
+ const input = { ...body, address: body.address ?? undefined };
264
+ const user = await userService.updateUser(ctx.params.userId, input);
265
+ setETagHeader(ctx, 'user', user.id, user.updatedAt);
266
+ ctx.body = { data: user };
267
+ }
268
+ catch (err) {
269
+ handleError(ctx, err);
270
+ }
271
+ });
272
+ // -------------------------------------------------------------------------
273
+ // POST /:userId/deactivate — Deactivate user
274
+ // Protected: super-admin user cannot be deactivated
275
+ // -------------------------------------------------------------------------
276
+ router.post('/:userId/deactivate', requirePermission(ADMIN_PERMISSIONS.USER_SUSPEND), requireUserOrganization(), async (ctx) => {
277
+ try {
278
+ await guardSuperAdmin(ctx.params.userId, 'deactivate');
279
+ await userService.deactivateUser(ctx.params.userId);
280
+ ctx.status = 204;
281
+ }
282
+ catch (err) {
283
+ handleError(ctx, err);
284
+ }
285
+ });
286
+ // -------------------------------------------------------------------------
287
+ // POST /:userId/reactivate — Reactivate user
288
+ // -------------------------------------------------------------------------
289
+ router.post('/:userId/reactivate', requirePermission(ADMIN_PERMISSIONS.USER_SUSPEND), requireUserOrganization(), async (ctx) => {
290
+ try {
291
+ await userService.reactivateUser(ctx.params.userId);
292
+ ctx.status = 204;
293
+ }
294
+ catch (err) {
295
+ handleError(ctx, err);
296
+ }
297
+ });
298
+ // -------------------------------------------------------------------------
299
+ // POST /:userId/suspend — Suspend user
300
+ // Protected: super-admin user cannot be suspended
301
+ // -------------------------------------------------------------------------
302
+ router.post('/:userId/suspend', requirePermission(ADMIN_PERMISSIONS.USER_SUSPEND), requireUserOrganization(), async (ctx) => {
303
+ try {
304
+ await guardSuperAdmin(ctx.params.userId, 'suspend');
305
+ const body = suspendUserSchema.parse(ctx.request.body ?? {});
306
+ await userService.suspendUser(ctx.params.userId, body.reason);
307
+ ctx.status = 204;
308
+ }
309
+ catch (err) {
310
+ handleError(ctx, err);
311
+ }
312
+ });
313
+ // -------------------------------------------------------------------------
314
+ // POST /:userId/unsuspend — Unsuspend user
315
+ // -------------------------------------------------------------------------
316
+ router.post('/:userId/unsuspend', requirePermission(ADMIN_PERMISSIONS.USER_SUSPEND), requireUserOrganization(), async (ctx) => {
317
+ try {
318
+ await userService.unsuspendUser(ctx.params.userId);
319
+ ctx.status = 204;
320
+ }
321
+ catch (err) {
322
+ handleError(ctx, err);
323
+ }
324
+ });
325
+ // -------------------------------------------------------------------------
326
+ // POST /:userId/lock — Lock user
327
+ // Protected: super-admin user cannot be locked
328
+ // -------------------------------------------------------------------------
329
+ router.post('/:userId/lock', requirePermission(ADMIN_PERMISSIONS.USER_SUSPEND), requireUserOrganization(), async (ctx) => {
330
+ try {
331
+ await guardSuperAdmin(ctx.params.userId, 'lock');
332
+ const body = lockUserSchema.parse(ctx.request.body);
333
+ await userService.lockUser(ctx.params.userId, body.reason);
334
+ ctx.status = 204;
335
+ }
336
+ catch (err) {
337
+ handleError(ctx, err);
338
+ }
339
+ });
340
+ // -------------------------------------------------------------------------
341
+ // POST /:userId/unlock — Unlock user
342
+ // -------------------------------------------------------------------------
343
+ router.post('/:userId/unlock', requirePermission(ADMIN_PERMISSIONS.USER_SUSPEND), requireUserOrganization(), async (ctx) => {
344
+ try {
345
+ await userService.unlockUser(ctx.params.userId);
346
+ ctx.status = 204;
347
+ }
348
+ catch (err) {
349
+ handleError(ctx, err);
350
+ }
351
+ });
352
+ // -------------------------------------------------------------------------
353
+ // POST /:userId/password — Set password
354
+ // -------------------------------------------------------------------------
355
+ router.post('/:userId/password', requirePermission(ADMIN_PERMISSIONS.USER_UPDATE), requireUserOrganization(), async (ctx) => {
356
+ try {
357
+ const body = setPasswordSchema.parse(ctx.request.body);
358
+ await userService.setUserPassword(ctx.params.userId, body.password);
359
+ ctx.status = 204;
360
+ }
361
+ catch (err) {
362
+ handleError(ctx, err);
363
+ }
364
+ });
365
+ // -------------------------------------------------------------------------
366
+ // DELETE /:userId/password — Clear password (convert to passwordless)
367
+ // -------------------------------------------------------------------------
368
+ router.delete('/:userId/password', requirePermission(ADMIN_PERMISSIONS.USER_UPDATE), requireUserOrganization(), async (ctx) => {
369
+ try {
370
+ await userService.clearUserPassword(ctx.params.userId);
371
+ ctx.status = 204;
372
+ }
373
+ catch (err) {
374
+ handleError(ctx, err);
375
+ }
376
+ });
377
+ // -------------------------------------------------------------------------
378
+ // POST /:userId/verify-email — Mark email as verified
379
+ // -------------------------------------------------------------------------
380
+ router.post('/:userId/verify-email', requirePermission(ADMIN_PERMISSIONS.USER_UPDATE), requireUserOrganization(), async (ctx) => {
381
+ try {
382
+ await userService.markEmailVerified(ctx.params.userId);
383
+ ctx.status = 204;
384
+ }
385
+ catch (err) {
386
+ handleError(ctx, err);
387
+ }
388
+ });
389
+ // -------------------------------------------------------------------------
390
+ // GET /:userId/export — GDPR data export (Article 20)
391
+ // -------------------------------------------------------------------------
392
+ router.get('/:userId/export', requirePermission(ADMIN_PERMISSIONS.USER_READ), requireUserOrganization(), async (ctx) => {
393
+ const user = await userService.getUserById(ctx.params.userId);
394
+ if (!user) {
395
+ return ctx.throw(404, 'User not found');
396
+ }
397
+ const exportData = await exportUserData(user);
398
+ ctx.body = { data: exportData };
399
+ });
400
+ // -------------------------------------------------------------------------
401
+ // POST /:userId/purge — GDPR data purge (Article 17)
402
+ //
403
+ // Requires X-Confirm-Purge: true header for safety.
404
+ // Irreversibly anonymizes user data and deletes related records.
405
+ // Protected: super-admin user cannot be purged
406
+ // -------------------------------------------------------------------------
407
+ router.post('/:userId/purge', requirePermission(ADMIN_PERMISSIONS.USER_ARCHIVE), requireUserOrganization(), async (ctx) => {
408
+ // Require explicit confirmation via header OR request body
409
+ const confirmHeader = ctx.get('X-Confirm-Purge');
410
+ const confirmBody = ctx.request.body?.confirmPurge;
411
+ if (confirmHeader !== 'true' && confirmBody !== true) {
412
+ ctx.status = 400;
413
+ ctx.body = {
414
+ error: 'Purge requires confirmation',
415
+ message: 'Set X-Confirm-Purge: true header or send { "confirmPurge": true } in body',
416
+ };
417
+ return;
418
+ }
419
+ const user = await userService.getUserById(ctx.params.userId);
420
+ if (!user) {
421
+ return ctx.throw(404, 'User not found');
422
+ }
423
+ try {
424
+ // Guard: super-admin user cannot be purged
425
+ await guardSuperAdmin(ctx.params.userId, 'delete');
426
+ // Use the admin user's ID as the actor for the audit trail
427
+ const actorId = ctx.state.adminUser?.id ?? 'system';
428
+ const result = await purgeUserData(user, actorId);
429
+ ctx.body = { data: result };
430
+ }
431
+ catch (err) {
432
+ if (err instanceof SuperAdminProtectionError) {
433
+ ctx.status = 403;
434
+ ctx.body = { error: 'The requested operation is not permitted' };
435
+ return;
436
+ }
437
+ if (err instanceof Error && err.message.includes('super-admin')) {
438
+ ctx.status = 403;
439
+ ctx.body = { error: 'The requested operation is not permitted' };
440
+ return;
441
+ }
442
+ throw err;
443
+ }
444
+ });
445
+ // -------------------------------------------------------------------------
446
+ // GET /:userId/history — User change history
447
+ // -------------------------------------------------------------------------
448
+ router.get('/:userId/history', requirePermission(ADMIN_PERMISSIONS.USER_READ), requireUserOrganization(), async (ctx) => {
449
+ const { limit, after, event_type } = ctx.query;
450
+ const result = await getEntityHistory('user', ctx.params.userId, {
451
+ limit: limit ? parseInt(limit, 10) : undefined,
452
+ after: after || undefined,
453
+ eventTypePrefix: event_type || undefined,
454
+ });
455
+ ctx.body = result;
456
+ });
457
+ // -------------------------------------------------------------------------
458
+ // POST /invite — Send invitation to a new or existing user
459
+ //
460
+ // Enhanced invitation with optional personal message, role/claim
461
+ // pre-assignment, and inviter tracking. Creates the user if they
462
+ // don't exist, generates an invitation token with pre-assignment
463
+ // details, and sends the invitation email.
464
+ // -------------------------------------------------------------------------
465
+ router.post('/invite', requirePermission(ADMIN_PERMISSIONS.USER_INVITE), async (ctx) => {
466
+ try {
467
+ const body = inviteUserSchema.parse(ctx.request.body);
468
+ const orgId = ctx.params.orgId;
469
+ const adminUser = ctx.state.adminUser;
470
+ // Resolve the organization for branding and slug
471
+ const org = await getOrganizationById(orgId);
472
+ if (!org) {
473
+ ctx.throw(404, 'Organization not found');
474
+ return;
475
+ }
476
+ // Validate referenced applicationIds, roleIds, claimDefinitionIds exist
477
+ if (body.roles?.length || body.claims?.length) {
478
+ await validatePreAssignments(orgId, body.roles, body.claims);
479
+ }
480
+ // Find or create the user
481
+ let user = await userService.getUserByEmail(orgId, body.email);
482
+ let created = false;
483
+ if (!user) {
484
+ user = await userService.createUser({
485
+ organizationId: orgId,
486
+ email: body.email,
487
+ givenName: body.givenName,
488
+ familyName: body.familyName,
489
+ });
490
+ created = true;
491
+ }
492
+ // Invalidate any previous pending invitation tokens for this user
493
+ await invalidateUserTokens('invitation_tokens', user.id);
494
+ // Generate a new invitation token
495
+ const { plaintext, hash } = generateToken();
496
+ const expiresAt = new Date(Date.now() + 7 * 24 * 60 * 60 * 1000); // 7 days
497
+ // Build inviter display name
498
+ const inviterName = adminUser.givenName
499
+ ? adminUser.familyName
500
+ ? `${adminUser.givenName} ${adminUser.familyName}`
501
+ : adminUser.givenName
502
+ : (adminUser.email ?? 'Admin');
503
+ // Store pre-assignment details in the token
504
+ const details = {};
505
+ if (body.personalMessage)
506
+ details.personalMessage = body.personalMessage;
507
+ if (body.roles?.length)
508
+ details.roles = body.roles;
509
+ if (body.claims?.length)
510
+ details.claims = body.claims;
511
+ details.inviterName = inviterName;
512
+ await insertInvitationToken(user.id, hash, expiresAt, Object.keys(details).length > 0 ? details : null, adminUser.id);
513
+ // Build the invitation URL. Prefix the trusted, configured issuerBaseUrl
514
+ // (never a request header) so the email link is absolute and includes
515
+ // the server origin — matching the password-reset / magic-link pattern.
516
+ const inviteUrl = `${config.issuerBaseUrl}/${org.slug}/auth/accept-invite/${plaintext}`;
517
+ // Send the invitation email
518
+ const emailOptions = {};
519
+ if (body.personalMessage)
520
+ emailOptions.personalMessage = body.personalMessage;
521
+ emailOptions.inviterName = inviterName;
522
+ await afterDatabaseCommit(() => sendInvitationEmail({
523
+ id: user.id,
524
+ email: user.email,
525
+ givenName: user.givenName,
526
+ familyName: user.familyName,
527
+ }, {
528
+ id: org.id,
529
+ slug: org.slug,
530
+ brandingLogoUrl: org.brandingLogoUrl,
531
+ brandingPrimaryColor: org.brandingPrimaryColor,
532
+ brandingCompanyName: org.brandingCompanyName,
533
+ }, inviteUrl, body.locale ?? org.defaultLocale ?? 'en', emailOptions));
534
+ // Audit log the invitation
535
+ writeAuditLog({
536
+ organizationId: orgId,
537
+ userId: user.id,
538
+ actorId: adminUser.id,
539
+ eventType: 'user.invited',
540
+ eventCategory: 'admin',
541
+ description: 'User invitation created',
542
+ metadata: {
543
+ hasPersonalMessage: !!body.personalMessage,
544
+ preAssignedRoles: body.roles?.length ?? 0,
545
+ preAssignedClaims: body.claims?.length ?? 0,
546
+ },
547
+ });
548
+ ctx.status = created ? 201 : 200;
549
+ ctx.body = {
550
+ data: {
551
+ userId: user.id,
552
+ email: user.email,
553
+ created,
554
+ invitationSent: true,
555
+ expiresAt: expiresAt.toISOString(),
556
+ },
557
+ };
558
+ }
559
+ catch (err) {
560
+ handleError(ctx, err);
561
+ }
562
+ });
563
+ // -------------------------------------------------------------------------
564
+ // POST /invite/preview — Preview invitation email without sending
565
+ //
566
+ // Renders the invitation email with the provided parameters and returns
567
+ // the HTML, plain text, and subject line for admin review.
568
+ // -------------------------------------------------------------------------
569
+ router.post('/invite/preview', requirePermission(ADMIN_PERMISSIONS.USER_INVITE), async (ctx) => {
570
+ try {
571
+ const body = invitePreviewSchema.parse(ctx.request.body);
572
+ const orgId = ctx.params.orgId;
573
+ const adminUser = ctx.state.adminUser;
574
+ // Resolve the organization for branding
575
+ const org = await getOrganizationById(orgId);
576
+ if (!org) {
577
+ ctx.throw(404, 'Organization not found');
578
+ return;
579
+ }
580
+ // Build inviter display name
581
+ const inviterName = adminUser.givenName
582
+ ? adminUser.familyName
583
+ ? `${adminUser.givenName} ${adminUser.familyName}`
584
+ : adminUser.givenName
585
+ : (adminUser.email ?? 'Admin');
586
+ // Build a mock user for the preview
587
+ const previewUser = {
588
+ id: '00000000-0000-0000-0000-000000000000',
589
+ email: body.email,
590
+ givenName: body.givenName ?? null,
591
+ familyName: body.familyName ?? null,
592
+ };
593
+ // Render the invitation email (without sending)
594
+ const emailOptions = {};
595
+ if (body.personalMessage)
596
+ emailOptions.personalMessage = body.personalMessage;
597
+ emailOptions.inviterName = inviterName;
598
+ const result = await renderInvitationEmail(previewUser, {
599
+ id: org.id,
600
+ slug: org.slug,
601
+ brandingLogoUrl: org.brandingLogoUrl,
602
+ brandingPrimaryColor: org.brandingPrimaryColor,
603
+ brandingCompanyName: org.brandingCompanyName,
604
+ },
605
+ // Absolute preview URL — same trusted issuerBaseUrl prefix as the real invite.
606
+ `${config.issuerBaseUrl}/${org.slug}/auth/accept-invite/PREVIEW_TOKEN`, body.locale ?? org.defaultLocale ?? 'en', emailOptions);
607
+ ctx.body = { data: result };
608
+ }
609
+ catch (err) {
610
+ handleError(ctx, err);
611
+ }
612
+ });
613
+ return router;
614
+ }
615
+ // ---------------------------------------------------------------------------
616
+ // Invitation schemas
617
+ // ---------------------------------------------------------------------------
618
+ /** Schema for the enhanced invitation request */
619
+ const inviteUserSchema = z.object({
620
+ email: z.string().email(),
621
+ givenName: z.string().min(1).max(255).optional(),
622
+ familyName: z.string().min(1).max(255).optional(),
623
+ personalMessage: z.string().max(500).optional(),
624
+ roles: z
625
+ .array(z.object({
626
+ applicationId: z.string().uuid(),
627
+ roleId: z.string().uuid(),
628
+ }))
629
+ .optional(),
630
+ claims: z
631
+ .array(z.object({
632
+ applicationId: z.string().uuid(),
633
+ claimDefinitionId: z.string().uuid(),
634
+ value: z.unknown(),
635
+ }))
636
+ .optional(),
637
+ locale: z.string().max(10).optional(),
638
+ });
639
+ /** Schema for the invitation preview request */
640
+ const invitePreviewSchema = z.object({
641
+ email: z.string().email(),
642
+ givenName: z.string().min(1).max(255).optional(),
643
+ familyName: z.string().min(1).max(255).optional(),
644
+ personalMessage: z.string().max(500).optional(),
645
+ locale: z.string().max(10).optional(),
646
+ });
647
+ // ---------------------------------------------------------------------------
648
+ // Pre-assignment validation helpers
649
+ // ---------------------------------------------------------------------------
650
+ /**
651
+ * Validate that referenced roles and claims exist in the database.
652
+ *
653
+ * Checks that all applicationIds are valid within the org, all roleIds
654
+ * belong to their specified application, and all claimDefinitionIds
655
+ * belong to their specified application. Throws a ZodError-like 400
656
+ * on validation failure.
657
+ *
658
+ * @param orgId - Organization ID for scoping
659
+ * @param roles - Array of role pre-assignments to validate
660
+ * @param claims - Array of claim pre-assignments to validate
661
+ * @throws Error with 400 status if any reference is invalid
662
+ */
663
+ async function validatePreAssignments(orgId, roles, claims) {
664
+ const pool = getPool();
665
+ const errors = [];
666
+ // Collect unique applicationIds from both roles and claims
667
+ const appIds = new Set();
668
+ roles?.forEach((r) => appIds.add(r.applicationId));
669
+ claims?.forEach((c) => appIds.add(c.applicationId));
670
+ // Verify all applications exist within the org
671
+ if (appIds.size > 0) {
672
+ const appResult = await pool.query(`SELECT id FROM applications WHERE id = ANY($1) AND organization_id = $2`, [Array.from(appIds), orgId]);
673
+ const foundIds = new Set(appResult.rows.map((r) => r.id));
674
+ for (const appId of appIds) {
675
+ if (!foundIds.has(appId)) {
676
+ errors.push(`Application ${appId} not found in this organization`);
677
+ }
678
+ }
679
+ }
680
+ // Verify all roles exist within their application
681
+ if (roles?.length) {
682
+ for (const role of roles) {
683
+ const result = await pool.query(`SELECT id FROM roles WHERE id = $1 AND application_id = $2`, [role.roleId, role.applicationId]);
684
+ if (result.rows.length === 0) {
685
+ errors.push(`Role ${role.roleId} not found in application ${role.applicationId}`);
686
+ }
687
+ }
688
+ }
689
+ // Verify all claim definitions exist within their application
690
+ if (claims?.length) {
691
+ for (const claim of claims) {
692
+ const result = await pool.query(`SELECT id FROM claim_definitions WHERE id = $1 AND application_id = $2`, [claim.claimDefinitionId, claim.applicationId]);
693
+ if (result.rows.length === 0) {
694
+ errors.push(`Claim definition ${claim.claimDefinitionId} not found in application ${claim.applicationId}`);
695
+ }
696
+ }
697
+ }
698
+ if (errors.length > 0) {
699
+ const err = new Error(`Pre-assignment validation failed: ${errors.join('; ')}`);
700
+ err.status = 400;
701
+ throw err;
702
+ }
703
+ }
704
+ // ---------------------------------------------------------------------------
705
+ // Standalone user router (non-org-scoped)
706
+ // ---------------------------------------------------------------------------
707
+ /**
708
+ * Create a standalone user admin router.
709
+ *
710
+ * Provides direct user access by userId at `/api/admin/users/:userId/*`
711
+ * without requiring the organizationId in the URL path. This is used by
712
+ * the Admin GUI SPA for user detail views and mutations where the user
713
+ * detail page only has the userId (from the URL) and not the orgId.
714
+ *
715
+ * The handlers delegate to the same user service functions as the
716
+ * org-scoped router. All routes require admin authentication and
717
+ * granular permissions.
718
+ *
719
+ * Prefix: /api/admin/users
720
+ */
721
+ export function createStandaloneUserRouter() {
722
+ const router = new Router({ prefix: '/api/admin/users' });
723
+ router.use(requireAdminAuth());
724
+ // GET /:userId — Get user by ID
725
+ router.get('/:userId', requirePermission(ADMIN_PERMISSIONS.USER_READ), async (ctx) => {
726
+ const user = await userService.getUserById(ctx.params.userId);
727
+ if (!user) {
728
+ ctx.throw(404, 'User not found');
729
+ return;
730
+ }
731
+ setETagHeader(ctx, 'user', user.id, user.updatedAt);
732
+ ctx.body = { data: user };
733
+ });
734
+ // PUT /:userId — Update user profile
735
+ router.put('/:userId', requirePermission(ADMIN_PERMISSIONS.USER_UPDATE), async (ctx) => {
736
+ try {
737
+ const body = updateUserSchema.parse(ctx.request.body);
738
+ const current = await userService.getUserById(ctx.params.userId);
739
+ if (current && !checkIfMatch(ctx, 'user', current.id, current.updatedAt, current))
740
+ return;
741
+ const input = { ...body, address: body.address ?? undefined };
742
+ const user = await userService.updateUser(ctx.params.userId, input);
743
+ setETagHeader(ctx, 'user', user.id, user.updatedAt);
744
+ ctx.body = { data: user };
745
+ }
746
+ catch (err) {
747
+ handleError(ctx, err);
748
+ }
749
+ });
750
+ // POST /:userId/deactivate — Deactivate user
751
+ router.post('/:userId/deactivate', requirePermission(ADMIN_PERMISSIONS.USER_SUSPEND), async (ctx) => {
752
+ try {
753
+ await guardSuperAdmin(ctx.params.userId, 'deactivate');
754
+ await userService.deactivateUser(ctx.params.userId);
755
+ ctx.status = 204;
756
+ }
757
+ catch (err) {
758
+ handleError(ctx, err);
759
+ }
760
+ });
761
+ // POST /:userId/reactivate — Reactivate user (inactive → active)
762
+ router.post('/:userId/reactivate', requirePermission(ADMIN_PERMISSIONS.USER_SUSPEND), async (ctx) => {
763
+ try {
764
+ await userService.reactivateUser(ctx.params.userId);
765
+ ctx.status = 204;
766
+ }
767
+ catch (err) {
768
+ handleError(ctx, err);
769
+ }
770
+ });
771
+ // POST /:userId/activate — Alias for reactivate (SPA compatibility)
772
+ router.post('/:userId/activate', requirePermission(ADMIN_PERMISSIONS.USER_SUSPEND), async (ctx) => {
773
+ try {
774
+ await userService.reactivateUser(ctx.params.userId);
775
+ ctx.status = 204;
776
+ }
777
+ catch (err) {
778
+ handleError(ctx, err);
779
+ }
780
+ });
781
+ // POST /:userId/suspend — Suspend user
782
+ router.post('/:userId/suspend', requirePermission(ADMIN_PERMISSIONS.USER_SUSPEND), async (ctx) => {
783
+ try {
784
+ await guardSuperAdmin(ctx.params.userId, 'suspend');
785
+ const body = suspendUserSchema.parse(ctx.request.body ?? {});
786
+ await userService.suspendUser(ctx.params.userId, body.reason);
787
+ ctx.status = 204;
788
+ }
789
+ catch (err) {
790
+ handleError(ctx, err);
791
+ }
792
+ });
793
+ // POST /:userId/unsuspend — Unsuspend user
794
+ router.post('/:userId/unsuspend', requirePermission(ADMIN_PERMISSIONS.USER_SUSPEND), async (ctx) => {
795
+ try {
796
+ await userService.unsuspendUser(ctx.params.userId);
797
+ ctx.status = 204;
798
+ }
799
+ catch (err) {
800
+ handleError(ctx, err);
801
+ }
802
+ });
803
+ // POST /:userId/lock — Lock user
804
+ router.post('/:userId/lock', requirePermission(ADMIN_PERMISSIONS.USER_SUSPEND), async (ctx) => {
805
+ try {
806
+ await guardSuperAdmin(ctx.params.userId, 'lock');
807
+ const body = lockUserSchema.parse(ctx.request.body);
808
+ await userService.lockUser(ctx.params.userId, body.reason);
809
+ ctx.status = 204;
810
+ }
811
+ catch (err) {
812
+ handleError(ctx, err);
813
+ }
814
+ });
815
+ // POST /:userId/unlock — Unlock user
816
+ router.post('/:userId/unlock', requirePermission(ADMIN_PERMISSIONS.USER_SUSPEND), async (ctx) => {
817
+ try {
818
+ await userService.unlockUser(ctx.params.userId);
819
+ ctx.status = 204;
820
+ }
821
+ catch (err) {
822
+ handleError(ctx, err);
823
+ }
824
+ });
825
+ // POST /:userId/password — Set password
826
+ router.post('/:userId/password', requirePermission(ADMIN_PERMISSIONS.USER_UPDATE), async (ctx) => {
827
+ try {
828
+ const body = setPasswordSchema.parse(ctx.request.body);
829
+ await userService.setUserPassword(ctx.params.userId, body.password);
830
+ ctx.status = 204;
831
+ }
832
+ catch (err) {
833
+ handleError(ctx, err);
834
+ }
835
+ });
836
+ // DELETE /:userId/password — Clear password
837
+ router.delete('/:userId/password', requirePermission(ADMIN_PERMISSIONS.USER_UPDATE), async (ctx) => {
838
+ try {
839
+ await userService.clearUserPassword(ctx.params.userId);
840
+ ctx.status = 204;
841
+ }
842
+ catch (err) {
843
+ handleError(ctx, err);
844
+ }
845
+ });
846
+ // POST /:userId/verify-email — Mark email as verified
847
+ router.post('/:userId/verify-email', requirePermission(ADMIN_PERMISSIONS.USER_UPDATE), async (ctx) => {
848
+ try {
849
+ await userService.markEmailVerified(ctx.params.userId);
850
+ ctx.status = 204;
851
+ }
852
+ catch (err) {
853
+ handleError(ctx, err);
854
+ }
855
+ });
856
+ // GET /:userId/history — Entity change history
857
+ router.get('/:userId/history', requirePermission(ADMIN_PERMISSIONS.USER_READ), async (ctx) => {
858
+ try {
859
+ const history = await getEntityHistory('user', ctx.params.userId);
860
+ ctx.body = { data: history };
861
+ }
862
+ catch (err) {
863
+ handleError(ctx, err);
864
+ }
865
+ });
866
+ return router;
867
+ }
868
+ //# sourceMappingURL=users.js.map