@digitaldefiance/node-express-suite 1.0.22 → 1.0.23

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 (633) hide show
  1. package/README.md +4 -0
  2. package/package.json +27 -32
  3. package/src/application-base.ts +492 -0
  4. package/src/application.ts +254 -0
  5. package/src/backup-code.ts +336 -0
  6. package/src/constants.ts +69 -0
  7. package/src/controllers/base.ts +440 -0
  8. package/{dist/controllers/index.d.ts → src/controllers/index.ts} +0 -1
  9. package/src/controllers/user.ts +1451 -0
  10. package/src/decorators/base-controller.ts +61 -0
  11. package/src/decorators/controller.ts +109 -0
  12. package/{dist/decorators/index.d.ts → src/decorators/index.ts} +0 -1
  13. package/src/decorators/zod-validation.ts +57 -0
  14. package/src/defaults.ts +94 -0
  15. package/src/documents/base.ts +7 -0
  16. package/src/documents/email-token.ts +14 -0
  17. package/{dist/documents/index.d.ts → src/documents/index.ts} +0 -1
  18. package/{dist/documents/mnemonic.d.ts → src/documents/mnemonic.ts} +5 -2
  19. package/{dist/documents/role.d.ts → src/documents/role.ts} +5 -2
  20. package/src/documents/used-direct-login-token.ts +7 -0
  21. package/{dist/documents/user-role.d.ts → src/documents/user-role.ts} +5 -2
  22. package/{dist/documents/user.d.ts → src/documents/user.ts} +4 -2
  23. package/src/enumerations/base-model-name.ts +41 -0
  24. package/{dist/enumerations/index.d.ts → src/enumerations/index.ts} +0 -1
  25. package/src/enumerations/length-encoding-type.ts +6 -0
  26. package/src/enumerations/schema-collection.ts +33 -0
  27. package/src/enumerations/symmetric-error-type.ts +4 -0
  28. package/src/environment.ts +770 -0
  29. package/src/errors/express-validation.ts +21 -0
  30. package/{dist/errors/index.d.ts → src/errors/index.ts} +0 -1
  31. package/src/errors/invalid-backup-code-version.ts +14 -0
  32. package/src/errors/invalid-jwt-token.ts +10 -0
  33. package/src/errors/invalid-model.ts +11 -0
  34. package/src/errors/invalid-new-password.ts +18 -0
  35. package/src/errors/invalid-password.ts +13 -0
  36. package/src/errors/missing-validated-data.ts +36 -0
  37. package/src/errors/mnemonic-or-password-required.ts +12 -0
  38. package/src/errors/model-not-registered.ts +11 -0
  39. package/src/errors/mongoose-validation.ts +34 -0
  40. package/src/errors/symmetric.ts +41 -0
  41. package/src/errors/token-expired.ts +10 -0
  42. package/src/get-language.ts +53 -0
  43. package/src/get-timezone.ts +45 -0
  44. package/{dist/index.d.ts → src/index.ts} +3 -2
  45. package/{dist/interfaces/api-error-response.d.ts → src/interfaces/api-error-response.ts} +2 -2
  46. package/src/interfaces/api-express-validation-error-response.ts +8 -0
  47. package/src/interfaces/api-message-response.ts +3 -0
  48. package/{dist/interfaces/api-mongo-validation-error-response.d.ts → src/interfaces/api-mongo-validation-error-response.ts} +2 -2
  49. package/{dist/interfaces/api-responses/backup-codes-response.d.ts → src/interfaces/api-responses/backup-codes-response.ts} +2 -2
  50. package/{dist/interfaces/api-responses/challenge-response.d.ts → src/interfaces/api-responses/challenge-response.ts} +3 -3
  51. package/{dist/interfaces/api-responses/code-count-response.d.ts → src/interfaces/api-responses/code-count-response.ts} +2 -2
  52. package/{dist/interfaces/api-responses/index.d.ts → src/interfaces/api-responses/index.ts} +0 -1
  53. package/{dist/interfaces/api-responses/login-response.d.ts → src/interfaces/api-responses/login-response.ts} +4 -4
  54. package/{dist/interfaces/api-responses/mnemonic-response.d.ts → src/interfaces/api-responses/mnemonic-response.ts} +2 -2
  55. package/{dist/interfaces/api-responses/registration-response.d.ts → src/interfaces/api-responses/registration-response.ts} +3 -3
  56. package/{dist/interfaces/api-responses/request-user-response.d.ts → src/interfaces/api-responses/request-user-response.ts} +2 -2
  57. package/{dist/interfaces/application.d.ts → src/interfaces/application.ts} +7 -7
  58. package/src/interfaces/backend-objects/email-token.ts +11 -0
  59. package/{dist/interfaces/backend-objects/index.d.ts → src/interfaces/backend-objects/index.ts} +0 -1
  60. package/{dist/interfaces/backend-objects/request-user.d.ts → src/interfaces/backend-objects/request-user.ts} +7 -2
  61. package/{dist/interfaces/backend-objects/role.d.ts → src/interfaces/backend-objects/role.ts} +1 -1
  62. package/src/interfaces/backend-objects/user.ts +9 -0
  63. package/src/interfaces/checksum-config.ts +4 -0
  64. package/src/interfaces/checksum-consts.ts +13 -0
  65. package/{dist/interfaces/constants.d.ts → src/interfaces/constants.ts} +5 -5
  66. package/src/interfaces/create-user-basics.ts +17 -0
  67. package/src/interfaces/csp-config.ts +35 -0
  68. package/src/interfaces/deep-partial.ts +3 -0
  69. package/{dist/interfaces/discriminator-collections.d.ts → src/interfaces/discriminator-collections.ts} +3 -3
  70. package/src/interfaces/email-service.ts +8 -0
  71. package/src/interfaces/environment-mongo.ts +76 -0
  72. package/src/interfaces/environment.ts +181 -0
  73. package/src/interfaces/failable-result.ts +6 -0
  74. package/src/interfaces/fec-consts.ts +4 -0
  75. package/src/interfaces/handleable-error-options.ts +6 -0
  76. package/{dist/interfaces/index.d.ts → src/interfaces/index.ts} +0 -1
  77. package/src/interfaces/jwt-consts.ts +23 -0
  78. package/src/interfaces/jwt-sign-response.ts +19 -0
  79. package/src/interfaces/mongo-errors.ts +5 -0
  80. package/src/interfaces/request-user.ts +50 -0
  81. package/src/interfaces/required-string-keys.ts +26 -0
  82. package/src/interfaces/schema.ts +31 -0
  83. package/src/interfaces/server-init-result.ts +37 -0
  84. package/src/interfaces/status-code-response.ts +7 -0
  85. package/src/interfaces/symmetric-encryption-results.d.ts +5 -0
  86. package/src/interfaces/symmetric-encryption-results.d.ts.map +1 -0
  87. package/src/interfaces/symmetric-encryption-results.js.map +1 -0
  88. package/src/interfaces/symmetric-encryption-results.ts +4 -0
  89. package/{dist/interfaces/token-response.d.ts → src/interfaces/token-response.ts} +2 -2
  90. package/src/middlewares/authenticate-crypto.ts +243 -0
  91. package/src/middlewares/authenticate-token.ts +152 -0
  92. package/src/middlewares/cleanup-crypto.ts +40 -0
  93. package/{dist/middlewares/index.d.ts → src/middlewares/index.ts} +0 -1
  94. package/src/middlewares/set-global-context-language.ts +24 -0
  95. package/src/middlewares.ts +120 -0
  96. package/src/model-registry.ts +75 -0
  97. package/src/models/email-token.ts +19 -0
  98. package/{dist/models/index.d.ts → src/models/index.ts} +0 -1
  99. package/src/models/mnemonic.ts +19 -0
  100. package/src/models/role.ts +19 -0
  101. package/src/models/used-direct-login-token.ts +23 -0
  102. package/src/models/user-role.ts +17 -0
  103. package/src/models/user.ts +19 -0
  104. package/src/registry/email-service-registry.ts +24 -0
  105. package/{dist/registry/index.d.ts → src/registry/index.ts} +0 -1
  106. package/src/routers/api.ts +151 -0
  107. package/src/routers/app.ts +258 -0
  108. package/src/routers/base.ts +17 -0
  109. package/{dist/routers/index.d.ts → src/routers/index.ts} +0 -1
  110. package/src/schemas/email-token.ts +91 -0
  111. package/{dist/schemas/index.d.ts → src/schemas/index.ts} +1 -2
  112. package/src/schemas/mnemonic.ts +37 -0
  113. package/src/schemas/role.ts +127 -0
  114. package/src/schemas/schema.ts +140 -0
  115. package/src/schemas/used-direct-login-token.ts +38 -0
  116. package/src/schemas/user-role.ts +75 -0
  117. package/src/schemas/user.ts +202 -0
  118. package/src/services/backup-code.ts +316 -0
  119. package/src/services/base.ts +33 -0
  120. package/src/services/checksum.ts +161 -0
  121. package/src/services/crc.ts +213 -0
  122. package/src/services/database-initialization.ts +1479 -0
  123. package/src/services/db-init-cache.d.ts +16 -0
  124. package/src/services/direct-login-token.ts +62 -0
  125. package/src/services/fec-usage-example.ts +102 -0
  126. package/src/services/fec.ts +296 -0
  127. package/{dist/services/index.d.ts → src/services/index.ts} +0 -1
  128. package/src/services/jwt.ts +134 -0
  129. package/src/services/key-wrapping.ts +434 -0
  130. package/src/services/mnemonic.ts +167 -0
  131. package/src/services/request-user.ts +62 -0
  132. package/src/services/role.ts +396 -0
  133. package/src/services/symmetric.ts +139 -0
  134. package/src/services/system-user.ts +82 -0
  135. package/src/services/user.ts +2137 -0
  136. package/src/services/xor.ts +34 -0
  137. package/src/types.d.ts +44 -0
  138. package/src/types.ts +128 -0
  139. package/src/utils.ts +1022 -0
  140. package/dist/application-base.d.ts +0 -112
  141. package/dist/application-base.d.ts.map +0 -1
  142. package/dist/application-base.js +0 -301
  143. package/dist/application-base.js.map +0 -1
  144. package/dist/application.d.ts +0 -23
  145. package/dist/application.d.ts.map +0 -1
  146. package/dist/application.js +0 -126
  147. package/dist/application.js.map +0 -1
  148. package/dist/backup-code.d.ts +0 -67
  149. package/dist/backup-code.d.ts.map +0 -1
  150. package/dist/backup-code.js +0 -270
  151. package/dist/backup-code.js.map +0 -1
  152. package/dist/constants.d.ts +0 -16
  153. package/dist/constants.d.ts.map +0 -1
  154. package/dist/constants.js +0 -54
  155. package/dist/constants.js.map +0 -1
  156. package/dist/controllers/base.d.ts +0 -63
  157. package/dist/controllers/base.d.ts.map +0 -1
  158. package/dist/controllers/base.js +0 -269
  159. package/dist/controllers/base.js.map +0 -1
  160. package/dist/controllers/index.d.ts.map +0 -1
  161. package/dist/controllers/index.js +0 -19
  162. package/dist/controllers/index.js.map +0 -1
  163. package/dist/controllers/user.d.ts +0 -45
  164. package/dist/controllers/user.d.ts.map +0 -1
  165. package/dist/controllers/user.js +0 -750
  166. package/dist/controllers/user.js.map +0 -1
  167. package/dist/decorators/base-controller.d.ts +0 -14
  168. package/dist/decorators/base-controller.d.ts.map +0 -1
  169. package/dist/decorators/base-controller.js +0 -49
  170. package/dist/decorators/base-controller.js.map +0 -1
  171. package/dist/decorators/controller.d.ts +0 -32
  172. package/dist/decorators/controller.d.ts.map +0 -1
  173. package/dist/decorators/controller.js +0 -67
  174. package/dist/decorators/controller.js.map +0 -1
  175. package/dist/decorators/index.d.ts.map +0 -1
  176. package/dist/decorators/index.js +0 -20
  177. package/dist/decorators/index.js.map +0 -1
  178. package/dist/decorators/zod-validation.d.ts +0 -5
  179. package/dist/decorators/zod-validation.d.ts.map +0 -1
  180. package/dist/decorators/zod-validation.js +0 -47
  181. package/dist/decorators/zod-validation.js.map +0 -1
  182. package/dist/defaults.d.ts +0 -7
  183. package/dist/defaults.d.ts.map +0 -1
  184. package/dist/defaults.js +0 -83
  185. package/dist/defaults.js.map +0 -1
  186. package/dist/documents/base.d.ts +0 -3
  187. package/dist/documents/base.d.ts.map +0 -1
  188. package/dist/documents/base.js +0 -3
  189. package/dist/documents/base.js.map +0 -1
  190. package/dist/documents/email-token.d.ts +0 -8
  191. package/dist/documents/email-token.d.ts.map +0 -1
  192. package/dist/documents/email-token.js +0 -3
  193. package/dist/documents/email-token.js.map +0 -1
  194. package/dist/documents/index.d.ts.map +0 -1
  195. package/dist/documents/index.js +0 -3
  196. package/dist/documents/index.js.map +0 -1
  197. package/dist/documents/mnemonic.d.ts.map +0 -1
  198. package/dist/documents/mnemonic.js +0 -3
  199. package/dist/documents/mnemonic.js.map +0 -1
  200. package/dist/documents/role.d.ts.map +0 -1
  201. package/dist/documents/role.js +0 -3
  202. package/dist/documents/role.js.map +0 -1
  203. package/dist/documents/used-direct-login-token.d.ts +0 -5
  204. package/dist/documents/used-direct-login-token.d.ts.map +0 -1
  205. package/dist/documents/used-direct-login-token.js +0 -3
  206. package/dist/documents/used-direct-login-token.js.map +0 -1
  207. package/dist/documents/user-role.d.ts.map +0 -1
  208. package/dist/documents/user-role.js +0 -3
  209. package/dist/documents/user-role.js.map +0 -1
  210. package/dist/documents/user.d.ts.map +0 -1
  211. package/dist/documents/user.js +0 -3
  212. package/dist/documents/user.js.map +0 -1
  213. package/dist/enumerations/base-model-name.d.ts +0 -38
  214. package/dist/enumerations/base-model-name.d.ts.map +0 -1
  215. package/dist/enumerations/base-model-name.js +0 -34
  216. package/dist/enumerations/base-model-name.js.map +0 -1
  217. package/dist/enumerations/index.d.ts.map +0 -1
  218. package/dist/enumerations/index.js +0 -21
  219. package/dist/enumerations/index.js.map +0 -1
  220. package/dist/enumerations/length-encoding-type.d.ts +0 -7
  221. package/dist/enumerations/length-encoding-type.d.ts.map +0 -1
  222. package/dist/enumerations/length-encoding-type.js +0 -11
  223. package/dist/enumerations/length-encoding-type.js.map +0 -1
  224. package/dist/enumerations/schema-collection.d.ts +0 -34
  225. package/dist/enumerations/schema-collection.d.ts.map +0 -1
  226. package/dist/enumerations/schema-collection.js +0 -38
  227. package/dist/enumerations/schema-collection.js.map +0 -1
  228. package/dist/enumerations/symmetric-error-type.d.ts +0 -5
  229. package/dist/enumerations/symmetric-error-type.d.ts.map +0 -1
  230. package/dist/enumerations/symmetric-error-type.js +0 -9
  231. package/dist/enumerations/symmetric-error-type.js.map +0 -1
  232. package/dist/environment.d.ts +0 -189
  233. package/dist/environment.d.ts.map +0 -1
  234. package/dist/environment.js +0 -618
  235. package/dist/environment.js.map +0 -1
  236. package/dist/errors/express-validation.d.ts +0 -9
  237. package/dist/errors/express-validation.d.ts.map +0 -1
  238. package/dist/errors/express-validation.js +0 -17
  239. package/dist/errors/express-validation.js.map +0 -1
  240. package/dist/errors/index.d.ts.map +0 -1
  241. package/dist/errors/index.js +0 -29
  242. package/dist/errors/index.js.map +0 -1
  243. package/dist/errors/invalid-backup-code-version.d.ts +0 -6
  244. package/dist/errors/invalid-backup-code-version.d.ts.map +0 -1
  245. package/dist/errors/invalid-backup-code-version.js +0 -14
  246. package/dist/errors/invalid-backup-code-version.js.map +0 -1
  247. package/dist/errors/invalid-jwt-token.d.ts +0 -5
  248. package/dist/errors/invalid-jwt-token.d.ts.map +0 -1
  249. package/dist/errors/invalid-jwt-token.js +0 -11
  250. package/dist/errors/invalid-jwt-token.js.map +0 -1
  251. package/dist/errors/invalid-model.d.ts +0 -6
  252. package/dist/errors/invalid-model.d.ts.map +0 -1
  253. package/dist/errors/invalid-model.js +0 -13
  254. package/dist/errors/invalid-model.js.map +0 -1
  255. package/dist/errors/invalid-new-password.d.ts +0 -5
  256. package/dist/errors/invalid-new-password.d.ts.map +0 -1
  257. package/dist/errors/invalid-new-password.js +0 -14
  258. package/dist/errors/invalid-new-password.js.map +0 -1
  259. package/dist/errors/invalid-password.d.ts +0 -5
  260. package/dist/errors/invalid-password.d.ts.map +0 -1
  261. package/dist/errors/invalid-password.js +0 -14
  262. package/dist/errors/invalid-password.js.map +0 -1
  263. package/dist/errors/missing-validated-data.d.ts +0 -7
  264. package/dist/errors/missing-validated-data.d.ts.map +0 -1
  265. package/dist/errors/missing-validated-data.js +0 -34
  266. package/dist/errors/missing-validated-data.js.map +0 -1
  267. package/dist/errors/mnemonic-or-password-required.d.ts +0 -5
  268. package/dist/errors/mnemonic-or-password-required.d.ts.map +0 -1
  269. package/dist/errors/mnemonic-or-password-required.js +0 -13
  270. package/dist/errors/mnemonic-or-password-required.js.map +0 -1
  271. package/dist/errors/model-not-registered.d.ts +0 -5
  272. package/dist/errors/model-not-registered.d.ts.map +0 -1
  273. package/dist/errors/model-not-registered.js +0 -12
  274. package/dist/errors/model-not-registered.js.map +0 -1
  275. package/dist/errors/mongoose-validation.d.ts +0 -11
  276. package/dist/errors/mongoose-validation.d.ts.map +0 -1
  277. package/dist/errors/mongoose-validation.js +0 -16
  278. package/dist/errors/mongoose-validation.js.map +0 -1
  279. package/dist/errors/symmetric.d.ts +0 -8
  280. package/dist/errors/symmetric.d.ts.map +0 -1
  281. package/dist/errors/symmetric.js +0 -23
  282. package/dist/errors/symmetric.js.map +0 -1
  283. package/dist/errors/token-expired.d.ts +0 -5
  284. package/dist/errors/token-expired.d.ts.map +0 -1
  285. package/dist/errors/token-expired.js +0 -11
  286. package/dist/errors/token-expired.js.map +0 -1
  287. package/dist/get-language.d.ts +0 -2
  288. package/dist/get-language.d.ts.map +0 -1
  289. package/dist/get-language.js +0 -30
  290. package/dist/get-language.js.map +0 -1
  291. package/dist/get-timezone.d.ts +0 -3
  292. package/dist/get-timezone.d.ts.map +0 -1
  293. package/dist/get-timezone.js +0 -31
  294. package/dist/get-timezone.js.map +0 -1
  295. package/dist/index.d.ts.map +0 -1
  296. package/dist/index.js +0 -40
  297. package/dist/index.js.map +0 -1
  298. package/dist/interfaces/api-error-response.d.ts.map +0 -1
  299. package/dist/interfaces/api-error-response.js +0 -3
  300. package/dist/interfaces/api-error-response.js.map +0 -1
  301. package/dist/interfaces/api-express-validation-error-response.d.ts +0 -7
  302. package/dist/interfaces/api-express-validation-error-response.d.ts.map +0 -1
  303. package/dist/interfaces/api-express-validation-error-response.js +0 -3
  304. package/dist/interfaces/api-express-validation-error-response.js.map +0 -1
  305. package/dist/interfaces/api-message-response.d.ts +0 -4
  306. package/dist/interfaces/api-message-response.d.ts.map +0 -1
  307. package/dist/interfaces/api-message-response.js +0 -3
  308. package/dist/interfaces/api-message-response.js.map +0 -1
  309. package/dist/interfaces/api-mongo-validation-error-response.d.ts.map +0 -1
  310. package/dist/interfaces/api-mongo-validation-error-response.js +0 -3
  311. package/dist/interfaces/api-mongo-validation-error-response.js.map +0 -1
  312. package/dist/interfaces/api-responses/backup-codes-response.d.ts.map +0 -1
  313. package/dist/interfaces/api-responses/backup-codes-response.js +0 -3
  314. package/dist/interfaces/api-responses/backup-codes-response.js.map +0 -1
  315. package/dist/interfaces/api-responses/challenge-response.d.ts.map +0 -1
  316. package/dist/interfaces/api-responses/challenge-response.js +0 -3
  317. package/dist/interfaces/api-responses/challenge-response.js.map +0 -1
  318. package/dist/interfaces/api-responses/code-count-response.d.ts.map +0 -1
  319. package/dist/interfaces/api-responses/code-count-response.js +0 -3
  320. package/dist/interfaces/api-responses/code-count-response.js.map +0 -1
  321. package/dist/interfaces/api-responses/index.d.ts.map +0 -1
  322. package/dist/interfaces/api-responses/index.js +0 -24
  323. package/dist/interfaces/api-responses/index.js.map +0 -1
  324. package/dist/interfaces/api-responses/login-response.d.ts.map +0 -1
  325. package/dist/interfaces/api-responses/login-response.js +0 -3
  326. package/dist/interfaces/api-responses/login-response.js.map +0 -1
  327. package/dist/interfaces/api-responses/mnemonic-response.d.ts.map +0 -1
  328. package/dist/interfaces/api-responses/mnemonic-response.js +0 -3
  329. package/dist/interfaces/api-responses/mnemonic-response.js.map +0 -1
  330. package/dist/interfaces/api-responses/registration-response.d.ts.map +0 -1
  331. package/dist/interfaces/api-responses/registration-response.js +0 -3
  332. package/dist/interfaces/api-responses/registration-response.js.map +0 -1
  333. package/dist/interfaces/api-responses/request-user-response.d.ts.map +0 -1
  334. package/dist/interfaces/api-responses/request-user-response.js +0 -3
  335. package/dist/interfaces/api-responses/request-user-response.js.map +0 -1
  336. package/dist/interfaces/application.d.ts.map +0 -1
  337. package/dist/interfaces/application.js +0 -3
  338. package/dist/interfaces/application.js.map +0 -1
  339. package/dist/interfaces/backend-objects/email-token.d.ts +0 -4
  340. package/dist/interfaces/backend-objects/email-token.d.ts.map +0 -1
  341. package/dist/interfaces/backend-objects/email-token.js +0 -3
  342. package/dist/interfaces/backend-objects/email-token.js.map +0 -1
  343. package/dist/interfaces/backend-objects/index.d.ts.map +0 -1
  344. package/dist/interfaces/backend-objects/index.js +0 -21
  345. package/dist/interfaces/backend-objects/index.js.map +0 -1
  346. package/dist/interfaces/backend-objects/request-user.d.ts.map +0 -1
  347. package/dist/interfaces/backend-objects/request-user.js +0 -3
  348. package/dist/interfaces/backend-objects/request-user.js.map +0 -1
  349. package/dist/interfaces/backend-objects/role.d.ts.map +0 -1
  350. package/dist/interfaces/backend-objects/role.js +0 -3
  351. package/dist/interfaces/backend-objects/role.js.map +0 -1
  352. package/dist/interfaces/backend-objects/user.d.ts +0 -4
  353. package/dist/interfaces/backend-objects/user.d.ts.map +0 -1
  354. package/dist/interfaces/backend-objects/user.js +0 -3
  355. package/dist/interfaces/backend-objects/user.js.map +0 -1
  356. package/dist/interfaces/checksum-config.d.ts +0 -5
  357. package/dist/interfaces/checksum-config.d.ts.map +0 -1
  358. package/dist/interfaces/checksum-config.js +0 -3
  359. package/dist/interfaces/checksum-config.js.map +0 -1
  360. package/dist/interfaces/checksum-consts.d.ts +0 -11
  361. package/dist/interfaces/checksum-consts.d.ts.map +0 -1
  362. package/dist/interfaces/checksum-consts.js +0 -3
  363. package/dist/interfaces/checksum-consts.js.map +0 -1
  364. package/dist/interfaces/constants.d.ts.map +0 -1
  365. package/dist/interfaces/constants.js +0 -3
  366. package/dist/interfaces/constants.js.map +0 -1
  367. package/dist/interfaces/create-user-basics.d.ts +0 -18
  368. package/dist/interfaces/create-user-basics.d.ts.map +0 -1
  369. package/dist/interfaces/create-user-basics.js +0 -3
  370. package/dist/interfaces/create-user-basics.js.map +0 -1
  371. package/dist/interfaces/csp-config.d.ts +0 -14
  372. package/dist/interfaces/csp-config.d.ts.map +0 -1
  373. package/dist/interfaces/csp-config.js +0 -3
  374. package/dist/interfaces/csp-config.js.map +0 -1
  375. package/dist/interfaces/deep-partial.d.ts +0 -4
  376. package/dist/interfaces/deep-partial.d.ts.map +0 -1
  377. package/dist/interfaces/deep-partial.js +0 -3
  378. package/dist/interfaces/deep-partial.js.map +0 -1
  379. package/dist/interfaces/discriminator-collections.d.ts.map +0 -1
  380. package/dist/interfaces/discriminator-collections.js +0 -3
  381. package/dist/interfaces/discriminator-collections.js.map +0 -1
  382. package/dist/interfaces/email-service.d.ts +0 -4
  383. package/dist/interfaces/email-service.d.ts.map +0 -1
  384. package/dist/interfaces/email-service.js +0 -3
  385. package/dist/interfaces/email-service.js.map +0 -1
  386. package/dist/interfaces/environment-mongo.d.ts +0 -76
  387. package/dist/interfaces/environment-mongo.d.ts.map +0 -1
  388. package/dist/interfaces/environment-mongo.js +0 -3
  389. package/dist/interfaces/environment-mongo.js.map +0 -1
  390. package/dist/interfaces/environment.d.ts +0 -181
  391. package/dist/interfaces/environment.d.ts.map +0 -1
  392. package/dist/interfaces/environment.js +0 -3
  393. package/dist/interfaces/environment.js.map +0 -1
  394. package/dist/interfaces/failable-result.d.ts +0 -7
  395. package/dist/interfaces/failable-result.d.ts.map +0 -1
  396. package/dist/interfaces/failable-result.js +0 -3
  397. package/dist/interfaces/failable-result.js.map +0 -1
  398. package/dist/interfaces/fec-consts.d.ts +0 -5
  399. package/dist/interfaces/fec-consts.d.ts.map +0 -1
  400. package/dist/interfaces/fec-consts.js +0 -3
  401. package/dist/interfaces/fec-consts.js.map +0 -1
  402. package/dist/interfaces/handleable-error-options.d.ts +0 -7
  403. package/dist/interfaces/handleable-error-options.d.ts.map +0 -1
  404. package/dist/interfaces/handleable-error-options.js +0 -3
  405. package/dist/interfaces/handleable-error-options.js.map +0 -1
  406. package/dist/interfaces/index.d.ts.map +0 -1
  407. package/dist/interfaces/index.js +0 -46
  408. package/dist/interfaces/index.js.map +0 -1
  409. package/dist/interfaces/jwt-consts.d.ts +0 -11
  410. package/dist/interfaces/jwt-consts.d.ts.map +0 -1
  411. package/dist/interfaces/jwt-consts.js +0 -3
  412. package/dist/interfaces/jwt-consts.js.map +0 -1
  413. package/dist/interfaces/jwt-sign-response.d.ts +0 -11
  414. package/dist/interfaces/jwt-sign-response.d.ts.map +0 -1
  415. package/dist/interfaces/jwt-sign-response.js +0 -3
  416. package/dist/interfaces/jwt-sign-response.js.map +0 -1
  417. package/dist/interfaces/mongo-errors.d.ts +0 -5
  418. package/dist/interfaces/mongo-errors.d.ts.map +0 -1
  419. package/dist/interfaces/mongo-errors.js +0 -3
  420. package/dist/interfaces/mongo-errors.js.map +0 -1
  421. package/dist/interfaces/request-user.d.ts +0 -42
  422. package/dist/interfaces/request-user.d.ts.map +0 -1
  423. package/dist/interfaces/request-user.js +0 -3
  424. package/dist/interfaces/request-user.js.map +0 -1
  425. package/dist/interfaces/required-string-keys.d.ts +0 -22
  426. package/dist/interfaces/required-string-keys.d.ts.map +0 -1
  427. package/dist/interfaces/required-string-keys.js +0 -3
  428. package/dist/interfaces/required-string-keys.js.map +0 -1
  429. package/dist/interfaces/schema.d.ts +0 -29
  430. package/dist/interfaces/schema.d.ts.map +0 -1
  431. package/dist/interfaces/schema.js +0 -3
  432. package/dist/interfaces/schema.js.map +0 -1
  433. package/dist/interfaces/server-init-result.d.ts +0 -35
  434. package/dist/interfaces/server-init-result.d.ts.map +0 -1
  435. package/dist/interfaces/server-init-result.js +0 -3
  436. package/dist/interfaces/server-init-result.js.map +0 -1
  437. package/dist/interfaces/status-code-response.d.ts +0 -7
  438. package/dist/interfaces/status-code-response.d.ts.map +0 -1
  439. package/dist/interfaces/status-code-response.js +0 -3
  440. package/dist/interfaces/status-code-response.js.map +0 -1
  441. package/dist/interfaces/symmetric-encryption-results.d.ts +0 -5
  442. package/dist/interfaces/symmetric-encryption-results.d.ts.map +0 -1
  443. package/dist/interfaces/symmetric-encryption-results.js.map +0 -1
  444. package/dist/interfaces/token-response.d.ts.map +0 -1
  445. package/dist/interfaces/token-response.js +0 -3
  446. package/dist/interfaces/token-response.js.map +0 -1
  447. package/dist/middlewares/authenticate-crypto.d.ts +0 -13
  448. package/dist/middlewares/authenticate-crypto.d.ts.map +0 -1
  449. package/dist/middlewares/authenticate-crypto.js +0 -146
  450. package/dist/middlewares/authenticate-crypto.js.map +0 -1
  451. package/dist/middlewares/authenticate-token.d.ts +0 -24
  452. package/dist/middlewares/authenticate-token.d.ts.map +0 -1
  453. package/dist/middlewares/authenticate-token.js +0 -102
  454. package/dist/middlewares/authenticate-token.js.map +0 -1
  455. package/dist/middlewares/cleanup-crypto.d.ts +0 -7
  456. package/dist/middlewares/cleanup-crypto.d.ts.map +0 -1
  457. package/dist/middlewares/cleanup-crypto.js +0 -32
  458. package/dist/middlewares/cleanup-crypto.js.map +0 -1
  459. package/dist/middlewares/index.d.ts.map +0 -1
  460. package/dist/middlewares/index.js +0 -21
  461. package/dist/middlewares/index.js.map +0 -1
  462. package/dist/middlewares/set-global-context-language.d.ts +0 -3
  463. package/dist/middlewares/set-global-context-language.d.ts.map +0 -1
  464. package/dist/middlewares/set-global-context-language.js +0 -14
  465. package/dist/middlewares/set-global-context-language.js.map +0 -1
  466. package/dist/middlewares.d.ts +0 -18
  467. package/dist/middlewares.d.ts.map +0 -1
  468. package/dist/middlewares.js +0 -76
  469. package/dist/middlewares.js.map +0 -1
  470. package/dist/model-registry.d.ts +0 -23
  471. package/dist/model-registry.d.ts.map +0 -1
  472. package/dist/model-registry.js +0 -47
  473. package/dist/model-registry.js.map +0 -1
  474. package/dist/models/email-token.d.ts +0 -11
  475. package/dist/models/email-token.d.ts.map +0 -1
  476. package/dist/models/email-token.js +0 -11
  477. package/dist/models/email-token.js.map +0 -1
  478. package/dist/models/index.d.ts.map +0 -1
  479. package/dist/models/index.js +0 -23
  480. package/dist/models/index.js.map +0 -1
  481. package/dist/models/mnemonic.d.ts +0 -11
  482. package/dist/models/mnemonic.d.ts.map +0 -1
  483. package/dist/models/mnemonic.js +0 -11
  484. package/dist/models/mnemonic.js.map +0 -1
  485. package/dist/models/role.d.ts +0 -11
  486. package/dist/models/role.d.ts.map +0 -1
  487. package/dist/models/role.js +0 -11
  488. package/dist/models/role.js.map +0 -1
  489. package/dist/models/used-direct-login-token.d.ts +0 -11
  490. package/dist/models/used-direct-login-token.d.ts.map +0 -1
  491. package/dist/models/used-direct-login-token.js +0 -11
  492. package/dist/models/used-direct-login-token.js.map +0 -1
  493. package/dist/models/user-role.d.ts +0 -6
  494. package/dist/models/user-role.d.ts.map +0 -1
  495. package/dist/models/user-role.js +0 -10
  496. package/dist/models/user-role.js.map +0 -1
  497. package/dist/models/user.d.ts +0 -7
  498. package/dist/models/user.d.ts.map +0 -1
  499. package/dist/models/user.js +0 -11
  500. package/dist/models/user.js.map +0 -1
  501. package/dist/registry/email-service-registry.d.ts +0 -9
  502. package/dist/registry/email-service-registry.d.ts.map +0 -1
  503. package/dist/registry/email-service-registry.js +0 -17
  504. package/dist/registry/email-service-registry.js.map +0 -1
  505. package/dist/registry/index.d.ts.map +0 -1
  506. package/dist/registry/index.js +0 -6
  507. package/dist/registry/index.js.map +0 -1
  508. package/dist/routers/api.d.ts +0 -27
  509. package/dist/routers/api.d.ts.map +0 -1
  510. package/dist/routers/api.js +0 -44
  511. package/dist/routers/api.js.map +0 -1
  512. package/dist/routers/app.d.ts +0 -28
  513. package/dist/routers/app.d.ts.map +0 -1
  514. package/dist/routers/app.js +0 -182
  515. package/dist/routers/app.js.map +0 -1
  516. package/dist/routers/base.d.ts +0 -12
  517. package/dist/routers/base.d.ts.map +0 -1
  518. package/dist/routers/base.js +0 -12
  519. package/dist/routers/base.js.map +0 -1
  520. package/dist/routers/index.d.ts.map +0 -1
  521. package/dist/routers/index.js +0 -20
  522. package/dist/routers/index.js.map +0 -1
  523. package/dist/schemas/email-token.d.ts +0 -38
  524. package/dist/schemas/email-token.d.ts.map +0 -1
  525. package/dist/schemas/email-token.js +0 -56
  526. package/dist/schemas/email-token.js.map +0 -1
  527. package/dist/schemas/index.d.ts.map +0 -1
  528. package/dist/schemas/index.js +0 -24
  529. package/dist/schemas/index.js.map +0 -1
  530. package/dist/schemas/mnemonic.d.ts +0 -20
  531. package/dist/schemas/mnemonic.d.ts.map +0 -1
  532. package/dist/schemas/mnemonic.js +0 -30
  533. package/dist/schemas/mnemonic.js.map +0 -1
  534. package/dist/schemas/role.d.ts +0 -32
  535. package/dist/schemas/role.d.ts.map +0 -1
  536. package/dist/schemas/role.js +0 -86
  537. package/dist/schemas/role.js.map +0 -1
  538. package/dist/schemas/schema.d.ts +0 -40
  539. package/dist/schemas/schema.d.ts.map +0 -1
  540. package/dist/schemas/schema.js +0 -64
  541. package/dist/schemas/schema.js.map +0 -1
  542. package/dist/schemas/used-direct-login-token.d.ts +0 -27
  543. package/dist/schemas/used-direct-login-token.d.ts.map +0 -1
  544. package/dist/schemas/used-direct-login-token.js +0 -23
  545. package/dist/schemas/used-direct-login-token.js.map +0 -1
  546. package/dist/schemas/user-role.d.ts +0 -29
  547. package/dist/schemas/user-role.d.ts.map +0 -1
  548. package/dist/schemas/user-role.js +0 -54
  549. package/dist/schemas/user-role.js.map +0 -1
  550. package/dist/schemas/user.d.ts +0 -21
  551. package/dist/schemas/user.d.ts.map +0 -1
  552. package/dist/schemas/user.js +0 -178
  553. package/dist/schemas/user.js.map +0 -1
  554. package/dist/services/backup-code.d.ts +0 -78
  555. package/dist/services/backup-code.d.ts.map +0 -1
  556. package/dist/services/backup-code.js +0 -180
  557. package/dist/services/backup-code.js.map +0 -1
  558. package/dist/services/base.d.ts +0 -13
  559. package/dist/services/base.d.ts.map +0 -1
  560. package/dist/services/base.js +0 -14
  561. package/dist/services/base.js.map +0 -1
  562. package/dist/services/checksum.d.ts +0 -67
  563. package/dist/services/checksum.d.ts.map +0 -1
  564. package/dist/services/checksum.js +0 -175
  565. package/dist/services/checksum.js.map +0 -1
  566. package/dist/services/crc.d.ts +0 -87
  567. package/dist/services/crc.d.ts.map +0 -1
  568. package/dist/services/crc.js +0 -198
  569. package/dist/services/crc.js.map +0 -1
  570. package/dist/services/database-initialization.d.ts +0 -105
  571. package/dist/services/database-initialization.d.ts.map +0 -1
  572. package/dist/services/database-initialization.js +0 -779
  573. package/dist/services/database-initialization.js.map +0 -1
  574. package/dist/services/direct-login-token.d.ts +0 -9
  575. package/dist/services/direct-login-token.d.ts.map +0 -1
  576. package/dist/services/direct-login-token.js +0 -41
  577. package/dist/services/direct-login-token.js.map +0 -1
  578. package/dist/services/fec-usage-example.d.ts +0 -38
  579. package/dist/services/fec-usage-example.d.ts.map +0 -1
  580. package/dist/services/fec-usage-example.js +0 -77
  581. package/dist/services/fec-usage-example.js.map +0 -1
  582. package/dist/services/fec.d.ts +0 -46
  583. package/dist/services/fec.d.ts.map +0 -1
  584. package/dist/services/fec.js +0 -192
  585. package/dist/services/fec.js.map +0 -1
  586. package/dist/services/index.d.ts.map +0 -1
  587. package/dist/services/index.js +0 -35
  588. package/dist/services/index.js.map +0 -1
  589. package/dist/services/jwt.d.ts +0 -33
  590. package/dist/services/jwt.d.ts.map +0 -1
  591. package/dist/services/jwt.js +0 -90
  592. package/dist/services/jwt.js.map +0 -1
  593. package/dist/services/key-wrapping.d.ts +0 -60
  594. package/dist/services/key-wrapping.d.ts.map +0 -1
  595. package/dist/services/key-wrapping.js +0 -311
  596. package/dist/services/key-wrapping.js.map +0 -1
  597. package/dist/services/mnemonic.d.ts +0 -61
  598. package/dist/services/mnemonic.d.ts.map +0 -1
  599. package/dist/services/mnemonic.js +0 -112
  600. package/dist/services/mnemonic.js.map +0 -1
  601. package/dist/services/request-user.d.ts +0 -20
  602. package/dist/services/request-user.d.ts.map +0 -1
  603. package/dist/services/request-user.js +0 -50
  604. package/dist/services/request-user.js.map +0 -1
  605. package/dist/services/role.d.ts +0 -88
  606. package/dist/services/role.d.ts.map +0 -1
  607. package/dist/services/role.js +0 -263
  608. package/dist/services/role.js.map +0 -1
  609. package/dist/services/symmetric.d.ts +0 -42
  610. package/dist/services/symmetric.d.ts.map +0 -1
  611. package/dist/services/symmetric.js +0 -101
  612. package/dist/services/symmetric.js.map +0 -1
  613. package/dist/services/system-user.d.ts +0 -17
  614. package/dist/services/system-user.d.ts.map +0 -1
  615. package/dist/services/system-user.js +0 -46
  616. package/dist/services/system-user.js.map +0 -1
  617. package/dist/services/user.d.ts +0 -320
  618. package/dist/services/user.d.ts.map +0 -1
  619. package/dist/services/user.js +0 -1373
  620. package/dist/services/user.js.map +0 -1
  621. package/dist/services/xor.d.ts +0 -24
  622. package/dist/services/xor.d.ts.map +0 -1
  623. package/dist/services/xor.js +0 -37
  624. package/dist/services/xor.js.map +0 -1
  625. package/dist/types.d.ts +0 -70
  626. package/dist/types.d.ts.map +0 -1
  627. package/dist/types.js +0 -14
  628. package/dist/types.js.map +0 -1
  629. package/dist/utils.d.ts +0 -202
  630. package/dist/utils.d.ts.map +0 -1
  631. package/dist/utils.js +0 -786
  632. package/dist/utils.js.map +0 -1
  633. /package/{dist → src}/interfaces/symmetric-encryption-results.js +0 -0
@@ -0,0 +1,2137 @@
1
+ import {
2
+ Constants as EciesConstants,
3
+ EmailString,
4
+ getEciesI18nEngine,
5
+ IECIESConfig,
6
+ InvalidEmailError,
7
+ InvalidEmailErrorType,
8
+ MemberType,
9
+ SecureBuffer,
10
+ SecureString,
11
+ SignatureString,
12
+ } from '@digitaldefiance/ecies-lib';
13
+ import {
14
+ Member as BackendMember,
15
+ ECIESService,
16
+ SignatureBuffer,
17
+ } from '@digitaldefiance/node-ecies-lib';
18
+ import {
19
+ AccountLockedError,
20
+ AccountStatus,
21
+ AccountStatusError,
22
+ DefaultLanguageCode,
23
+ EmailInUseError,
24
+ EmailTokenExpiredError,
25
+ EmailTokenFailedToSendError,
26
+ EmailTokenSentTooRecentlyError,
27
+ EmailTokenType,
28
+ EmailTokenUsedOrInvalidError,
29
+ EmailVerifiedError,
30
+ getSuiteCoreTranslation,
31
+ IBackupCode,
32
+ InvalidChallengeResponseError,
33
+ InvalidCredentialsError,
34
+ InvalidUsernameError,
35
+ IRequestUserDTO,
36
+ ITokenRole,
37
+ IUserBase,
38
+ IUserDTO,
39
+ LoginChallengeExpiredError,
40
+ PendingEmailVerificationError,
41
+ PrivateKeyRequiredError,
42
+ Role,
43
+ SuiteCoreStringKey,
44
+ TranslatableSuiteError,
45
+ TranslatableSuiteHandleableError,
46
+ UsernameInUseError,
47
+ UsernameOrEmailRequiredError,
48
+ UserNotFoundError,
49
+ } from '@digitaldefiance/suite-core-lib';
50
+ import { Wallet } from '@ethereumjs/wallet';
51
+ import { randomBytes } from 'crypto';
52
+ import { ObjectId } from 'mongodb';
53
+ import { ClientSession, Document, ProjectionType, Types } from 'mongoose';
54
+ import validator from 'validator';
55
+ import { BackupCode } from '../backup-code';
56
+ import { IBaseDocument } from '../documents';
57
+ import { IEmailTokenDocument } from '../documents/email-token';
58
+ import { IMnemonicDocument } from '../documents/mnemonic';
59
+ import { IUserDocument } from '../documents/user';
60
+ import { BaseModelName } from '../enumerations/base-model-name';
61
+ import { Environment } from '../environment';
62
+ import { InvalidNewPasswordError } from '../errors';
63
+ import { MongooseValidationError } from '../errors/mongoose-validation';
64
+ import { ICreateUserBasics } from '../interfaces';
65
+ import { IApplication } from '../interfaces/application';
66
+ import { IUserBackendObject } from '../interfaces/backend-objects/user';
67
+ import { IConstants } from '../interfaces/constants';
68
+ import { IEmailService } from '../interfaces/email-service';
69
+ import { ModelRegistry } from '../model-registry';
70
+ import { debugLog } from '../utils';
71
+ import { BackupCodeService } from './backup-code';
72
+ import { BaseService } from './base';
73
+ import { DirectLoginTokenService } from './direct-login-token';
74
+ import { KeyWrappingService } from './key-wrapping';
75
+ import { MnemonicService } from './mnemonic';
76
+ import { RequestUserService } from './request-user';
77
+ import { RoleService } from './role';
78
+ import { SystemUserService } from './system-user';
79
+
80
+ type ProjectionObject = Record<string, 0 | 1 | -1 | boolean>;
81
+
82
+ export class UserService<
83
+ T,
84
+ I extends Types.ObjectId | string,
85
+ D extends Date,
86
+ S extends string,
87
+ A extends string,
88
+ TEnvironment extends Environment = Environment,
89
+ TConstants extends IConstants = IConstants,
90
+ TBaseDocument extends IBaseDocument<T, I> = IBaseDocument<T, I>,
91
+ TUser extends IUserBase<I, D, S, A> = IUserBase<I, D, S, A>,
92
+ TTokenRole extends ITokenRole<I, D> = ITokenRole<I, D>,
93
+ TApplication extends IApplication<
94
+ T,
95
+ I,
96
+ TBaseDocument,
97
+ TEnvironment,
98
+ TConstants
99
+ > = IApplication<T, I, TBaseDocument, TEnvironment, TConstants>,
100
+ > extends BaseService {
101
+ protected readonly roleService: RoleService<I, D, TTokenRole>;
102
+ protected readonly eciesService: ECIESService;
103
+ protected readonly keyWrappingService: KeyWrappingService;
104
+ protected readonly mnemonicService: MnemonicService;
105
+ protected readonly emailService: IEmailService;
106
+ protected readonly backupCodeService: BackupCodeService<
107
+ I,
108
+ D,
109
+ TTokenRole,
110
+ TApplication
111
+ >;
112
+ protected readonly serverUrl: string;
113
+ protected readonly disableEmailSend: boolean;
114
+
115
+ constructor(
116
+ application: IApplication<T, I, TBaseDocument, TEnvironment, TConstants>,
117
+ roleService: RoleService<I, D, TTokenRole>,
118
+ emailService: IEmailService,
119
+ keyWrappingService: KeyWrappingService,
120
+ backupCodeService: BackupCodeService<I, D, TTokenRole, TApplication>,
121
+ ) {
122
+ super(application);
123
+ this.roleService = roleService;
124
+ this.emailService = emailService;
125
+ this.keyWrappingService = keyWrappingService;
126
+ this.backupCodeService = backupCodeService;
127
+ this.serverUrl = application.environment.serverUrl;
128
+ this.disableEmailSend = application.environment.disableEmailSend;
129
+ const config: IECIESConfig = {
130
+ curveName: this.application.constants.ECIES.CURVE_NAME,
131
+ primaryKeyDerivationPath:
132
+ this.application.constants.ECIES.PRIMARY_KEY_DERIVATION_PATH,
133
+ mnemonicStrength: this.application.constants.ECIES.MNEMONIC_STRENGTH,
134
+ symmetricAlgorithm:
135
+ this.application.constants.ECIES.SYMMETRIC_ALGORITHM_CONFIGURATION,
136
+ symmetricKeyBits: this.application.constants.ECIES.SYMMETRIC.KEY_BITS,
137
+ symmetricKeyMode: this.application.constants.ECIES.SYMMETRIC.MODE,
138
+ };
139
+ this.eciesService = new ECIESService(config);
140
+ const mnemonicModel =
141
+ ModelRegistry.instance.getTypedModel<IMnemonicDocument>(
142
+ BaseModelName.Mnemonic,
143
+ );
144
+ this.mnemonicService = new MnemonicService(
145
+ mnemonicModel,
146
+ application.environment.mnemonicHmacSecret,
147
+ this.keyWrappingService,
148
+ );
149
+ }
150
+
151
+ /**
152
+ * Given a User Document, make a User DTO
153
+ * @param user a User Document
154
+ * @returns An IUserDTO
155
+ */
156
+ public static userToUserDTO(
157
+ user: IUserDocument | Record<string, unknown>,
158
+ ): IUserDTO {
159
+ return {
160
+ ...(user instanceof Document ? user.toObject() : user),
161
+ _id: (user._id instanceof Types.ObjectId
162
+ ? user._id.toString()
163
+ : user._id) as string,
164
+ createdBy: (user.createdBy instanceof Date
165
+ ? user.createdBy.toString()
166
+ : user.createdBy) as string,
167
+ updatedBy: (user.updatedBy instanceof Date
168
+ ? user.updatedBy.toString()
169
+ : user.updatedBy) as string,
170
+ ...(user.lastLogin
171
+ ? {
172
+ lastLogin: (user.lastLogin instanceof Date
173
+ ? user.lastLogin.toString()
174
+ : user.lastLogin) as string,
175
+ }
176
+ : {}),
177
+ ...(user.deletedBy
178
+ ? {
179
+ deletedBy: (user.deletedBy instanceof Date
180
+ ? user.deletedBy.toString()
181
+ : user.deletedBy) as string,
182
+ }
183
+ : {}),
184
+ } as IUserDTO;
185
+ }
186
+
187
+ /**
188
+ * Given a User DTO, reconstitute ids and dates
189
+ * @param user a User DTO
190
+ * @returns An IUserBackendObject
191
+ */
192
+ public hydrateUserDTOToBackend(user: IUserDTO): IUserBackendObject<string> {
193
+ return {
194
+ ...user,
195
+ _id: new ObjectId(user._id),
196
+ ...(user.lastLogin ? { lastLogin: new Date(user.lastLogin) } : {}),
197
+ createdAt: new Date(user.createdAt),
198
+ createdBy: new ObjectId(user.createdBy),
199
+ updatedAt: new Date(user.updatedAt),
200
+ updatedBy: new ObjectId(user.updatedBy),
201
+ ...(user.deletedAt ? { deletedAt: new Date(user.deletedAt) } : {}),
202
+ ...(user.deletedBy
203
+ ? {
204
+ deletedBy: new ObjectId(user.deletedBy),
205
+ }
206
+ : {}),
207
+ ...(user.mnemonicId ? { mnemonicId: new ObjectId(user.mnemonicId) } : {}),
208
+ } as IUserBackendObject<string>;
209
+ }
210
+
211
+ /**
212
+ * Create a new email token to send to the user for email verification
213
+ * @param userDoc The user to create the email token for
214
+ * @param type The type of email token to create
215
+ * @param session The session to use for the query
216
+ * @returns The email token document
217
+ */
218
+ public async createEmailToken(
219
+ userDoc: IUserDocument,
220
+ type: EmailTokenType,
221
+ session?: ClientSession,
222
+ ): Promise<IEmailTokenDocument> {
223
+ const EmailTokenModel =
224
+ ModelRegistry.instance.getTypedModel<IEmailTokenDocument>(
225
+ BaseModelName.EmailToken,
226
+ );
227
+
228
+ // If we already have a session, use it directly to avoid nested transactions
229
+ if (session) {
230
+ const now = new Date();
231
+ const tokenData = {
232
+ userId: userDoc._id,
233
+ type: type,
234
+ email: userDoc.email,
235
+ token: randomBytes(
236
+ this.application.constants.EmailTokenLength,
237
+ ).toString('hex'),
238
+ createdAt: now,
239
+ updatedAt: now,
240
+ expiresAt: new Date(
241
+ now.getTime() + this.application.constants.EmailTokenExpiration,
242
+ ),
243
+ };
244
+
245
+ // Use findOneAndUpdate with upsert to avoid duplicate key errors
246
+ const emailToken = await EmailTokenModel.findOneAndUpdate(
247
+ {
248
+ userId: userDoc._id,
249
+ email: userDoc.email,
250
+ type: type,
251
+ },
252
+ tokenData,
253
+ {
254
+ upsert: true,
255
+ new: true,
256
+ session,
257
+ },
258
+ );
259
+
260
+ if (!emailToken) {
261
+ throw new TranslatableSuiteError(
262
+ SuiteCoreStringKey.Error_FailedToCreateEmailToken,
263
+ );
264
+ }
265
+ return emailToken;
266
+ }
267
+
268
+ // Only create a new transaction if no session is provided
269
+ return await this.withTransaction<IEmailTokenDocument>(
270
+ async (sess: ClientSession | undefined): Promise<IEmailTokenDocument> => {
271
+ const now = new Date();
272
+ const tokenData = {
273
+ userId: userDoc._id,
274
+ type: type,
275
+ email: userDoc.email,
276
+ token: randomBytes(
277
+ this.application.constants.EmailTokenLength,
278
+ ).toString('hex'),
279
+ createdAt: now,
280
+ updatedAt: now,
281
+ expiresAt: new Date(
282
+ now.getTime() + this.application.constants.EmailTokenExpiration,
283
+ ),
284
+ };
285
+
286
+ // Use findOneAndUpdate with upsert to avoid duplicate key errors
287
+ const emailToken = await EmailTokenModel.findOneAndUpdate(
288
+ {
289
+ userId: userDoc._id,
290
+ email: userDoc.email,
291
+ type: type,
292
+ },
293
+ tokenData,
294
+ {
295
+ upsert: true,
296
+ new: true,
297
+ session: sess,
298
+ },
299
+ );
300
+
301
+ if (!emailToken) {
302
+ throw new TranslatableSuiteError(
303
+ SuiteCoreStringKey.Error_FailedToCreateEmailToken,
304
+ );
305
+ }
306
+ return emailToken;
307
+ },
308
+ undefined,
309
+ {
310
+ timeoutMs: this.application.environment.mongo.transactionTimeout,
311
+ retryAttempts: 2,
312
+ },
313
+ );
314
+ }
315
+
316
+ /**
317
+ * Create and send an email token to the user for email verification
318
+ * @param user The user to send the email token to
319
+ * @param type The type of email token to send
320
+ * @param session The session to use for the query
321
+ * @returns The email token document
322
+ */
323
+ public async createAndSendEmailToken(
324
+ user: IUserDocument,
325
+ type: EmailTokenType = EmailTokenType.AccountVerification,
326
+ session?: ClientSession,
327
+ debug = false,
328
+ ): Promise<IEmailTokenDocument> {
329
+ const emailToken = await this.createEmailToken(user, type, session);
330
+ try {
331
+ await this.sendEmailToken(emailToken, session, debug);
332
+ } catch (error) {
333
+ // keep parity with previous behavior: continue returning token even if email send fails
334
+ }
335
+ return emailToken;
336
+ }
337
+
338
+ /**
339
+ * Create and send an email token directly within an existing transaction
340
+ * @param user The user to send the email token to
341
+ * @param type The type of email token to send
342
+ * @param session The session to use for the query (required)
343
+ * @param debug Whether to enable debug logging
344
+ * @returns The email token document
345
+ */
346
+ public async createAndSendEmailTokenDirect(
347
+ user: IUserDocument,
348
+ type: EmailTokenType = EmailTokenType.AccountVerification,
349
+ session: ClientSession,
350
+ debug = false,
351
+ ): Promise<IEmailTokenDocument> {
352
+ const EmailTokenModel =
353
+ ModelRegistry.instance.getTypedModel<IEmailTokenDocument>(
354
+ BaseModelName.EmailToken,
355
+ );
356
+
357
+ // Create token directly within the existing session using upsert
358
+ const now = new Date();
359
+ const tokenData = {
360
+ userId: user._id,
361
+ type: type,
362
+ email: user.email,
363
+ token: randomBytes(this.application.constants.EmailTokenLength).toString(
364
+ 'hex',
365
+ ),
366
+ createdAt: now,
367
+ updatedAt: now,
368
+ expiresAt: new Date(
369
+ now.getTime() + this.application.constants.EmailTokenExpiration,
370
+ ),
371
+ };
372
+
373
+ // Use findOneAndUpdate with upsert to avoid duplicate key errors
374
+ const emailToken = await EmailTokenModel.findOneAndUpdate(
375
+ {
376
+ userId: user._id,
377
+ email: user.email,
378
+ type: type,
379
+ },
380
+ tokenData,
381
+ {
382
+ upsert: true,
383
+ new: true,
384
+ session,
385
+ },
386
+ );
387
+
388
+ if (!emailToken) {
389
+ throw new TranslatableSuiteError(
390
+ SuiteCoreStringKey.Error_FailedToCreateEmailToken,
391
+ );
392
+ }
393
+
394
+ try {
395
+ await this.sendEmailToken(emailToken, session, debug);
396
+ } catch (error) {
397
+ // Ignore email send errors in direct token creation
398
+ }
399
+
400
+ return emailToken;
401
+ }
402
+
403
+ /**
404
+ * Send an email token to the user for email verification
405
+ * @param emailToken The email token to send
406
+ * @param session The session to use for the query
407
+ * @returns void
408
+ */
409
+ public async sendEmailToken(
410
+ emailToken: IEmailTokenDocument,
411
+ session?: ClientSession,
412
+ debug = false,
413
+ ): Promise<void> {
414
+ if (this.disableEmailSend) {
415
+ debugLog(debug, 'log', 'Email sending disabled for testing');
416
+ // Still update lastSent and expiration to keep token valid during tests
417
+ emailToken.lastSent = new Date();
418
+ emailToken.expiresAt = new Date(
419
+ Date.now() + this.application.constants.EmailTokenExpiration,
420
+ );
421
+ await emailToken.save({ session });
422
+ return;
423
+ }
424
+
425
+ if (
426
+ emailToken.lastSent &&
427
+ emailToken.lastSent.getTime() +
428
+ this.application.constants.EmailTokenResendInterval >
429
+ Date.now()
430
+ ) {
431
+ throw new EmailTokenSentTooRecentlyError(emailToken.lastSent);
432
+ }
433
+
434
+ let subjectString: SuiteCoreStringKey;
435
+ let bodyString: SuiteCoreStringKey;
436
+ let url: string;
437
+ switch (emailToken.type) {
438
+ case EmailTokenType.AccountVerification:
439
+ subjectString = SuiteCoreStringKey.Email_ConfirmationSubjectTemplate;
440
+ bodyString = SuiteCoreStringKey.Email_ConfirmationBody;
441
+ url = `${this.serverUrl}/verify-email?token=${emailToken.token}`;
442
+ break;
443
+ case EmailTokenType.PasswordReset:
444
+ subjectString = SuiteCoreStringKey.Email_ResetPasswordSubjectTemplate;
445
+ bodyString = SuiteCoreStringKey.Email_ResetPasswordBody;
446
+ url = `${this.serverUrl}/forgot-password?token=${emailToken.token}`;
447
+ break;
448
+ case EmailTokenType.LoginRequest:
449
+ subjectString = SuiteCoreStringKey.Email_LoginRequestSubjectTemplate;
450
+ bodyString = SuiteCoreStringKey.Email_LoginRequestBody;
451
+ url = `${this.serverUrl}/challenge?token=${emailToken.token}`;
452
+ break;
453
+ case EmailTokenType.MnemonicRecoveryRequest:
454
+ case EmailTokenType.PrivateKeyRequest:
455
+ default:
456
+ throw new Error('Invalid email token type');
457
+ }
458
+ const emailSubject = getSuiteCoreTranslation(subjectString);
459
+ const emailText = `${getSuiteCoreTranslation(bodyString)}\r\n\r\n${url}`;
460
+ const emailHtml = `<p>${getSuiteCoreTranslation(
461
+ bodyString,
462
+ )}</p><br/><p><a href="${url}">${url}</a></p><p>${getSuiteCoreTranslation(
463
+ SuiteCoreStringKey.Email_LinkExpiresInTemplate,
464
+ )}</p>`;
465
+
466
+ try {
467
+ // Use the EmailService to send the email
468
+ await this.emailService.sendEmail(
469
+ emailToken.email,
470
+ emailSubject,
471
+ emailText,
472
+ emailHtml,
473
+ );
474
+
475
+ // update last sent/expiration
476
+ emailToken.lastSent = new Date();
477
+ emailToken.expiresAt = new Date(
478
+ Date.now() + this.application.constants.EmailTokenExpiration,
479
+ );
480
+ await emailToken.save({ session });
481
+ } catch (error) {
482
+ throw new EmailTokenFailedToSendError(emailToken.type);
483
+ }
484
+ }
485
+
486
+ /**
487
+ * Find a user by email or username and enforce account status checks
488
+ * @param email Optional email
489
+ * @param username Optional username
490
+ * @param session Optional mongoose session
491
+ * @throws UsernameOrEmailRequiredError if neither provided
492
+ * @throws InvalidCredentialsError if not found or deleted
493
+ * @throws AccountLockedError | PendingEmailVerificationError | AccountStatusError per status
494
+ */
495
+ public async findUser(
496
+ email?: string,
497
+ username?: string,
498
+ session?: ClientSession,
499
+ ): Promise<IUserDocument> {
500
+ if (!email && !username) {
501
+ throw new UsernameOrEmailRequiredError();
502
+ }
503
+ const UserModel = ModelRegistry.instance.getTypedModel<IUserDocument>(
504
+ BaseModelName.User,
505
+ );
506
+ let userDoc: IUserDocument | null = null;
507
+
508
+ try {
509
+ if (email) {
510
+ userDoc = await UserModel.findOne({
511
+ email: email.toLowerCase(),
512
+ })
513
+ .session(session ?? null)
514
+ .exec();
515
+ } else if (username) {
516
+ userDoc = await UserModel.findOne({ username })
517
+ .collation({ locale: 'en', strength: 2 })
518
+ .session(session ?? null)
519
+ .exec();
520
+ }
521
+ } catch (error) {
522
+ // Database error in findUser - convert to InvalidCredentialsError for security
523
+ throw new InvalidCredentialsError();
524
+ }
525
+
526
+ if (!userDoc || userDoc.deletedAt) {
527
+ if (email) {
528
+ const engine = getEciesI18nEngine();
529
+ throw new InvalidEmailError(InvalidEmailErrorType.Missing, engine);
530
+ }
531
+ throw new InvalidUsernameError();
532
+ }
533
+
534
+ switch (userDoc.accountStatus) {
535
+ case AccountStatus.Active:
536
+ break;
537
+ case AccountStatus.AdminLock:
538
+ throw new AccountLockedError();
539
+ case AccountStatus.PendingEmailVerification:
540
+ throw new PendingEmailVerificationError();
541
+ default:
542
+ throw new AccountStatusError(userDoc.accountStatus);
543
+ }
544
+
545
+ return userDoc as IUserDocument;
546
+ }
547
+
548
+ /**
549
+ * Finds a user record by ID
550
+ * @param userId The user ID
551
+ * @param throwIfNotActive Whether to throw if the user is inactive
552
+ * @param session The active session, if present
553
+ * @returns The user document
554
+ */
555
+ public async findUserById(
556
+ userId: Types.ObjectId,
557
+ throwIfNotActive: boolean,
558
+ session?: ClientSession,
559
+ select?: ProjectionType<IUserDocument>,
560
+ ): Promise<IUserDocument> {
561
+ const UserModel = ModelRegistry.instance.getTypedModel<IUserDocument>(
562
+ BaseModelName.User,
563
+ );
564
+ const baseQuery = UserModel.findById(userId).session(session ?? null);
565
+ if (select) {
566
+ // Always include fields needed for status checks
567
+ const merged = this.ensureRequiredFieldsInProjection(select, [
568
+ 'deletedAt',
569
+ 'accountStatus',
570
+ ]);
571
+ baseQuery.select(merged);
572
+ }
573
+ const userDoc = (await baseQuery.exec()) as IUserDocument | null;
574
+ if (!userDoc || userDoc.deletedAt) {
575
+ throw new UserNotFoundError();
576
+ }
577
+ if (throwIfNotActive) {
578
+ switch (userDoc.accountStatus) {
579
+ case AccountStatus.Active:
580
+ break;
581
+ case AccountStatus.AdminLock:
582
+ throw new AccountLockedError();
583
+ case AccountStatus.PendingEmailVerification:
584
+ throw new PendingEmailVerificationError();
585
+ default:
586
+ throw new AccountStatusError(userDoc.accountStatus);
587
+ }
588
+ }
589
+ return userDoc;
590
+ }
591
+
592
+ /**
593
+ * Ensure required fields are present in a projection for queries that rely on them.
594
+ * Supports string and object-style projections. For inclusion projections, adds fields.
595
+ * For exclusion projections, ensures required fields are not excluded.
596
+ */
597
+ private ensureRequiredFieldsInProjection(
598
+ select: ProjectionType<IUserDocument>,
599
+ required: string[],
600
+ ): ProjectionType<IUserDocument> {
601
+ if (typeof select === 'string') {
602
+ const parts = select
603
+ .split(/\s+/)
604
+ .map((s) => s.trim())
605
+ .filter(Boolean);
606
+ const exclusions = new Set(
607
+ parts.filter((p) => p.startsWith('-')).map((p) => p.slice(1)),
608
+ );
609
+ // Remove exclusions on required fields
610
+ for (const r of required) {
611
+ exclusions.delete(r);
612
+ }
613
+ const cleaned = parts.filter((p) => !p.startsWith('-'));
614
+ for (const r of required) {
615
+ if (!cleaned.includes(r)) cleaned.push(r);
616
+ }
617
+ const result = [...cleaned, ...[...exclusions].map((r) => `-${r}`)];
618
+ return result.join(' ');
619
+ }
620
+ if (select && typeof select === 'object') {
621
+ const proj: ProjectionObject = { ...(select as ProjectionObject) };
622
+ const values = Object.values(proj);
623
+ const hasInclusions = values.some((v) => v === 1 || v === true);
624
+ if (hasInclusions) {
625
+ for (const r of required) {
626
+ proj[r] = 1;
627
+ }
628
+ } else {
629
+ const keysToRemove = required.filter(
630
+ (r) => proj[r] === 0 || proj[r] === false || proj[r] === -1,
631
+ );
632
+ keysToRemove.forEach((key) => delete proj[key]);
633
+ }
634
+ return proj as ProjectionType<IUserDocument>;
635
+ }
636
+ return select;
637
+ }
638
+
639
+ /**
640
+ * Fill in the default values to a user object
641
+ * @param newUser The user object to fill in
642
+ * @param createdBy The user ID of the user creating the new user
643
+ * @returns The filled in user
644
+ */
645
+ public fillUserDefaults(
646
+ newUser: ICreateUserBasics,
647
+ createdBy: Types.ObjectId,
648
+ backupCodes: Array<IBackupCode>,
649
+ encryptedMnemonic: string,
650
+ userId?: Types.ObjectId,
651
+ ): IUserBackendObject<string> {
652
+ return {
653
+ ...(userId ? { _id: userId } : {}),
654
+ timezone: 'UTC',
655
+ ...newUser,
656
+ email: newUser.email.toLowerCase(),
657
+ emailVerified: false,
658
+ accountStatus: AccountStatus.PendingEmailVerification,
659
+ duressPasswords: [],
660
+ siteLanguage: DefaultLanguageCode,
661
+ publicKey: '',
662
+ backupCodes,
663
+ mnemonicRecovery: encryptedMnemonic,
664
+ directChallenge: false,
665
+ createdAt: new Date(),
666
+ createdBy: createdBy,
667
+ updatedAt: new Date(),
668
+ updatedBy: createdBy,
669
+ } as IUserBackendObject<string>;
670
+ }
671
+
672
+ /**
673
+ * Create a new user document from an IUser and unhashed password
674
+ * @param newUser The user object
675
+ * @returns The new user document
676
+ */
677
+ public async makeUserDoc(newUser: TUser): Promise<IUserDocument> {
678
+ const UserModel = ModelRegistry.instance.getTypedModel<IUserDocument>(
679
+ BaseModelName.User,
680
+ );
681
+
682
+ const newUserDoc: IUserDocument = new UserModel(newUser);
683
+
684
+ const validationError = newUserDoc.validateSync();
685
+ if (validationError) {
686
+ throw new MongooseValidationError(validationError.errors);
687
+ }
688
+
689
+ return newUserDoc;
690
+ }
691
+
692
+ /**
693
+ * Create a new user.
694
+ * Do not set createdBy to a new (non-existing) ObjectId unless you also set newUserId to it.
695
+ * If newUserId is not set, one will be generated.
696
+ * @param systemUser The system user performing the operation
697
+ * @param userData Username, email, password in a ICreateUserBasics object
698
+ * @param createdBy The user id of the user creating the user
699
+ * @param newUserId the user id of the new user object- usually the createdBy user id.
700
+ * @param session The session to use for the query
701
+ * @param debug Whether to log debug information
702
+ * @param password The password to use for the new user (optional, if not provided, mnemonic will be used)
703
+ * @returns The new user document
704
+ */
705
+ public async newUser(
706
+ systemUser: BackendMember,
707
+ userData: ICreateUserBasics,
708
+ createdBy?: Types.ObjectId,
709
+ newUserId?: Types.ObjectId,
710
+ session?: ClientSession,
711
+ debug = false,
712
+ password?: string,
713
+ ): Promise<{
714
+ user: IUserDocument;
715
+ mnemonic: string;
716
+ backupCodes: Array<string>;
717
+ password?: string;
718
+ }> {
719
+ const _newUserId = newUserId ?? new Types.ObjectId();
720
+ if (!this.application.constants.UsernameRegex.test(userData.username)) {
721
+ throw new InvalidUsernameError();
722
+ }
723
+ if (password && !this.application.constants.PasswordRegex.test(password)) {
724
+ throw new InvalidNewPasswordError();
725
+ }
726
+
727
+ const UserModel = ModelRegistry.instance.getTypedModel<IUserDocument>(
728
+ BaseModelName.User,
729
+ );
730
+ return await this.withTransaction<{
731
+ user: IUserDocument;
732
+ backupCodes: Array<string>;
733
+ mnemonic: string;
734
+ }>(
735
+ async (sess: ClientSession | undefined) => {
736
+ const existingEmail: IUserDocument | null = await UserModel.findOne({
737
+ email: userData.email.toLowerCase(),
738
+ }).session(sess ?? null);
739
+ if (existingEmail) {
740
+ throw new EmailInUseError();
741
+ }
742
+ const existingUsername: IUserDocument | null = await UserModel.findOne({
743
+ username: { $regex: new RegExp(`^${userData.username}$`, 'i') },
744
+ }).session(sess ?? null);
745
+ if (existingUsername) {
746
+ throw new UsernameInUseError();
747
+ }
748
+
749
+ let mnemonic: SecureString | undefined;
750
+ let member: BackendMember | undefined;
751
+ while (!mnemonic || !member) {
752
+ try {
753
+ const { member: newMember, mnemonic: newMnemonic } =
754
+ BackendMember.newMember(
755
+ this.eciesService,
756
+ MemberType.User,
757
+ userData.username,
758
+ new EmailString(userData.email),
759
+ undefined,
760
+ createdBy,
761
+ );
762
+ // make sure the new mnemonic is not already in the database
763
+
764
+ const mnemonicExists = await this.mnemonicService.mnemonicExists(
765
+ newMnemonic,
766
+ sess,
767
+ );
768
+ if (!mnemonicExists) {
769
+ member = newMember;
770
+ mnemonic = newMnemonic;
771
+ }
772
+ } catch {
773
+ // If we fail to create a new member, we will retry until we succeed.
774
+ // This is to ensure that we do not end up with duplicate mnemonics.
775
+ debugLog(
776
+ debug,
777
+ 'warn',
778
+ 'Failed to create a new member, retrying...',
779
+ );
780
+ }
781
+ }
782
+
783
+ const backupCodes = BackupCode.generateBackupCodes();
784
+ const encryptedBackupCodes = await BackupCode.encryptBackupCodes(
785
+ member,
786
+ systemUser,
787
+ backupCodes,
788
+ );
789
+ const encryptedMnemonic = member
790
+ .encryptData(Buffer.from(mnemonic.value ?? '', 'utf-8'))
791
+ .toString('hex');
792
+
793
+ const newUserDoc = new UserModel({
794
+ ...this.fillUserDefaults(
795
+ userData,
796
+ createdBy ?? _newUserId,
797
+ encryptedBackupCodes,
798
+ encryptedMnemonic,
799
+ _newUserId,
800
+ ),
801
+ publicKey: member.publicKey.toString('hex'),
802
+ });
803
+
804
+ const validationError = newUserDoc.validateSync();
805
+ if (validationError) {
806
+ throw new MongooseValidationError(validationError.errors);
807
+ }
808
+
809
+ // Always add HMAC-only mnemonic doc
810
+ const newMnemonicDoc = await this.mnemonicService.addMnemonic(
811
+ mnemonic,
812
+ sess,
813
+ );
814
+ if (newMnemonicDoc) {
815
+ newUserDoc.mnemonicId = newMnemonicDoc._id;
816
+ }
817
+
818
+ // If password provided, wrap the ECIES private key with the password (Option B)
819
+ if (password) {
820
+ const passwordSecure = new SecureString(password);
821
+ try {
822
+ const priv = new SecureBuffer(member.privateKey!.value);
823
+ try {
824
+ const wrapped = this.keyWrappingService.wrapSecret(
825
+ priv,
826
+ passwordSecure,
827
+ );
828
+ newUserDoc.passwordWrappedPrivateKey = wrapped;
829
+ } finally {
830
+ priv.dispose();
831
+ }
832
+ } finally {
833
+ passwordSecure.dispose();
834
+ }
835
+ }
836
+
837
+ const savedUserDoc = await newUserDoc.save({ session: sess });
838
+
839
+ const memberRoleId = await this.roleService.getRoleIdByName(
840
+ this.application.constants.MemberRole as Role,
841
+ sess,
842
+ );
843
+
844
+ if (!memberRoleId) {
845
+ throw new TranslatableSuiteError(
846
+ SuiteCoreStringKey.Error_FailedToLookupRoleTemplate,
847
+ {
848
+ ROLE: getSuiteCoreTranslation(SuiteCoreStringKey.Common_Member),
849
+ },
850
+ );
851
+ }
852
+
853
+ await this.roleService.addUserToRole(
854
+ memberRoleId,
855
+ savedUserDoc._id,
856
+ _newUserId,
857
+ sess,
858
+ );
859
+
860
+ return {
861
+ user: savedUserDoc,
862
+ mnemonic: mnemonic.value ?? '',
863
+ backupCodes: backupCodes.map((code: BackupCode) => code.value ?? ''),
864
+ ...(password ? { password } : {}),
865
+ };
866
+ },
867
+ session,
868
+ {
869
+ timeoutMs: this.application.environment.mongo.transactionTimeout * 10,
870
+ },
871
+ );
872
+ }
873
+
874
+ /**
875
+ * Get the backup codes for a user.
876
+ * Requires the user not be deleted or inactive
877
+ */
878
+ public async getEncryptedUserBackupCodes(
879
+ userId: Types.ObjectId,
880
+ session?: ClientSession,
881
+ ): Promise<Array<IBackupCode>> {
882
+ const userWithCodes = await this.findUserById(userId, true, session, {
883
+ backupCodes: 1,
884
+ });
885
+ return userWithCodes.backupCodes;
886
+ }
887
+
888
+ /**
889
+ * Resets the given user's backup codes
890
+ * @param backupUser The user to generate codes for
891
+ * @param session The current session, if any
892
+ * @returns A promise of an array of backup codes
893
+ */
894
+ public async resetUserBackupCodes(
895
+ backupUser: BackendMember,
896
+ systemUser: BackendMember,
897
+ session?: ClientSession,
898
+ ): Promise<Array<BackupCode>> {
899
+ if (!backupUser.hasPrivateKey) {
900
+ throw new PrivateKeyRequiredError();
901
+ }
902
+ const backupCodes = BackupCode.generateBackupCodes();
903
+ const encryptedBackupCodes = await BackupCode.encryptBackupCodes(
904
+ backupUser,
905
+ systemUser,
906
+ backupCodes,
907
+ );
908
+ const UserModel = ModelRegistry.instance.get('User')?.model;
909
+ return await this.withTransaction<Array<BackupCode>>(
910
+ async (sess: ClientSession | undefined) => {
911
+ await UserModel.updateOne(
912
+ { _id: backupUser.id },
913
+ { $set: { backupCodes: encryptedBackupCodes } },
914
+ { session: sess },
915
+ );
916
+ return backupCodes;
917
+ },
918
+ session,
919
+ {
920
+ timeoutMs: this.application.environment.mongo.transactionTimeout,
921
+ },
922
+ );
923
+ }
924
+
925
+ /**
926
+ * Recover a user's mnemonic from an encrypted mnemonic
927
+ * @param user The user whose mnemonic to recover
928
+ * @param encryptedMnemonic The encrypted mnemonic
929
+ * @returns The recovered mnemonic
930
+ */
931
+ public recoverMnemonic(
932
+ user: BackendMember,
933
+ encryptedMnemonic: string,
934
+ ): SecureString {
935
+ if (!encryptedMnemonic) {
936
+ throw new TranslatableSuiteHandleableError(
937
+ SuiteCoreStringKey.MnemonicRecovery_Missing,
938
+ undefined,
939
+ undefined,
940
+ {
941
+ statusCode: 400,
942
+ },
943
+ );
944
+ }
945
+
946
+ return new SecureString(
947
+ user.decryptData(Buffer.from(encryptedMnemonic, 'hex')).toString('utf-8'),
948
+ );
949
+ }
950
+
951
+ /**
952
+ * Make a Member from a user document and optional private key
953
+ * @param userDoc The user document
954
+ * @param privateKey Optional private key to load the wallet
955
+ * @param publicKey Optional public key to override the userDoc public key
956
+ * @param session The current session, if any
957
+ * @returns A promise containing the created Member
958
+ */
959
+ public async makeUserFromUserDoc(
960
+ userDoc: IUserDocument,
961
+ privateKey?: SecureBuffer,
962
+ publicKey?: Buffer,
963
+ mnemonic?: SecureString,
964
+ wallet?: Wallet,
965
+ session?: ClientSession,
966
+ ): Promise<BackendMember> {
967
+ const memberType = await this.roleService.getMemberType(userDoc, session);
968
+ const user = new BackendMember(
969
+ this.eciesService,
970
+ memberType,
971
+ userDoc.username,
972
+ new EmailString(userDoc.email),
973
+ publicKey ?? Buffer.from(userDoc.publicKey, 'hex'),
974
+ privateKey,
975
+ wallet,
976
+ userDoc._id,
977
+ new Date(userDoc.createdAt),
978
+ new Date(userDoc.updatedAt),
979
+ userDoc.createdBy,
980
+ );
981
+ if ((privateKey?.originalLength ?? -1) > 0 && user.hasPrivateKey) {
982
+ user.loadWallet(
983
+ mnemonic ?? this.recoverMnemonic(user, userDoc.mnemonicRecovery),
984
+ );
985
+ }
986
+ return user;
987
+ }
988
+
989
+ /**
990
+ * Challenges a given userDoc with a given mnemonic, returns a system and user Member
991
+ * @param userDoc The userDoc in question
992
+ * @param mnemonic The mnemonic to challenge against
993
+ * @returns A promise containing the user and system Members
994
+ * @throws InvalidCredentialsError if the challenge fails
995
+ * @throws AccountLockedError if the account is locked
996
+ * @throws PendingEmailVerificationError if the email is not verified
997
+ * @throws AccountStatusError if the account status is invalid
998
+ */
999
+ public async challengeUserWithMnemonic(
1000
+ userDoc: IUserDocument,
1001
+ mnemonic: SecureString,
1002
+ session?: ClientSession,
1003
+ ): Promise<{
1004
+ userMember: BackendMember;
1005
+ adminMember: BackendMember;
1006
+ }> {
1007
+ try {
1008
+ // Verify provided mnemonic corresponds to the stored mnemonic HMAC (no password required)
1009
+ // This prevents any valid mnemonic from authenticating as another user.
1010
+ const MnemonicModel =
1011
+ ModelRegistry.instance.getTypedModel<IMnemonicDocument>(
1012
+ BaseModelName.Mnemonic,
1013
+ );
1014
+ if (!userDoc.mnemonicId) {
1015
+ throw new InvalidCredentialsError();
1016
+ }
1017
+ const mnemonicDoc = await MnemonicModel.findById(userDoc.mnemonicId)
1018
+ .select('hmac')
1019
+ .session(session ?? null)
1020
+ .lean()
1021
+ .exec();
1022
+ if (!mnemonicDoc) {
1023
+ throw new InvalidCredentialsError();
1024
+ }
1025
+ const computedHmac = this.mnemonicService.getMnemonicHmac(mnemonic);
1026
+ console.log('Debug mnemonic auth:', {
1027
+ userDocId: userDoc._id.toString(),
1028
+ userDocEmail: userDoc.email,
1029
+ userDocUsername: userDoc.username,
1030
+ mnemonicDocId: mnemonicDoc._id?.toString(),
1031
+ storedHmac: mnemonicDoc.hmac,
1032
+ computedHmac,
1033
+ mnemonicsMatch: computedHmac === mnemonicDoc.hmac,
1034
+ });
1035
+ if (computedHmac !== mnemonicDoc.hmac) {
1036
+ throw new InvalidCredentialsError();
1037
+ }
1038
+
1039
+ // Create a Member from the provided mnemonic to get the keys
1040
+ const { wallet } = this.eciesService.walletAndSeedFromMnemonic(mnemonic);
1041
+ const privateKey = wallet.getPrivateKey();
1042
+ const publicKey = wallet.getPublicKey();
1043
+ const publicKeyWithPrefix = Buffer.concat([
1044
+ Buffer.from([this.application.constants.ECIES.PUBLIC_KEY_MAGIC]),
1045
+ publicKey,
1046
+ ]);
1047
+ const userMember = await this.makeUserFromUserDoc(
1048
+ userDoc,
1049
+ new SecureBuffer(privateKey),
1050
+ publicKeyWithPrefix,
1051
+ mnemonic,
1052
+ wallet,
1053
+ session,
1054
+ );
1055
+
1056
+ // Verify the public key matches the stored userDoc public key
1057
+ if (userMember.publicKey.toString('hex') !== userDoc.publicKey) {
1058
+ throw new InvalidCredentialsError();
1059
+ }
1060
+
1061
+ // Generate a nonce challenge to verify they can decrypt with their key
1062
+ const adminMember = SystemUserService.getSystemUser(
1063
+ this.application.environment,
1064
+ );
1065
+ const nonce = randomBytes(32);
1066
+ const signature = adminMember.sign(nonce);
1067
+ const payload = Buffer.concat([nonce, signature]);
1068
+
1069
+ const encryptedPayload = userMember.encryptData(payload);
1070
+ const decryptedPayload = userMember.decryptData(encryptedPayload);
1071
+
1072
+ // Verify the server's signature on the nonce
1073
+ const decryptedNonce = decryptedPayload.subarray(0, 32);
1074
+ const decryptedSignature = decryptedPayload.subarray(32);
1075
+
1076
+ const isSignatureValid = adminMember.verify(
1077
+ decryptedSignature as SignatureBuffer,
1078
+ decryptedNonce,
1079
+ );
1080
+
1081
+ if (!isSignatureValid || !nonce.equals(decryptedNonce)) {
1082
+ throw new InvalidCredentialsError();
1083
+ }
1084
+
1085
+ return {
1086
+ userMember,
1087
+ adminMember: adminMember,
1088
+ };
1089
+ } catch (error) {
1090
+ if (
1091
+ error instanceof InvalidCredentialsError ||
1092
+ error instanceof AccountLockedError ||
1093
+ error instanceof PendingEmailVerificationError ||
1094
+ error instanceof AccountStatusError
1095
+ ) {
1096
+ throw error;
1097
+ }
1098
+ throw new InvalidCredentialsError();
1099
+ }
1100
+ }
1101
+
1102
+ /**
1103
+ * Validates a login challenge response
1104
+ * @param challengeResponse The challenge response bytes in hex
1105
+ * @param email The email address of the user
1106
+ * @param username The username of the user
1107
+ * @param session The mongo session for the query
1108
+ * @returns A promise that resolves to the user document, user member, and system member
1109
+ */
1110
+ public async loginWithChallengeResponse(
1111
+ challengeResponse: string,
1112
+ email?: string,
1113
+ username?: string,
1114
+ session?: ClientSession,
1115
+ ): Promise<{
1116
+ userDoc: IUserDocument;
1117
+ userMember: BackendMember;
1118
+ adminMember: BackendMember;
1119
+ }> {
1120
+ const challengeBuffer = Buffer.from(challengeResponse, 'hex');
1121
+ // validate the expected challenge response length (8 + 32 + 64 = 104 bytes)
1122
+ if (
1123
+ challengeBuffer.length !=
1124
+ this.application.constants.DirectLoginChallengeLength
1125
+ ) {
1126
+ throw new InvalidChallengeResponseError();
1127
+ }
1128
+ // disassemble the challengeResponse into time, nonce, signature
1129
+ const time = challengeBuffer.subarray(0, 8); // 16 hex characters
1130
+ const nonce = challengeBuffer.subarray(8, 40); // 64 hex characters
1131
+ const signature = challengeBuffer.subarray(40); // 65 * 2 hex characters
1132
+
1133
+ const timeMs = parseInt(time.toString('hex'), 16);
1134
+ if (
1135
+ new Date().getTime() - timeMs >
1136
+ this.application.constants.LoginChallengeExpiration
1137
+ ) {
1138
+ throw new LoginChallengeExpiredError();
1139
+ }
1140
+
1141
+ const userDoc = await this.findUser(email, username, session);
1142
+ if (!userDoc && email) {
1143
+ const engine = getEciesI18nEngine();
1144
+ throw new InvalidEmailError(InvalidEmailErrorType.Missing, engine);
1145
+ } else if (!userDoc) {
1146
+ throw new InvalidUsernameError();
1147
+ }
1148
+ // re-sign the time + nonce and check if the signature matches
1149
+ const adminMember = SystemUserService.getSystemUser(
1150
+ this.application.environment,
1151
+ );
1152
+ const timeAndNonce = Buffer.concat([time, nonce]);
1153
+ const expectedSignature = adminMember.sign(timeAndNonce);
1154
+ if (expectedSignature.toString('hex') !== signature.toString('hex')) {
1155
+ throw new InvalidChallengeResponseError();
1156
+ }
1157
+
1158
+ const userMember = await this.makeUserFromUserDoc(
1159
+ userDoc,
1160
+ undefined,
1161
+ undefined,
1162
+ undefined,
1163
+ undefined,
1164
+ session,
1165
+ );
1166
+
1167
+ return {
1168
+ userDoc,
1169
+ userMember,
1170
+ adminMember: adminMember,
1171
+ };
1172
+ }
1173
+
1174
+ /**
1175
+ * Authenticate a user with client-verified challenge (skips server-side challenge)
1176
+ * @returns The authenticated user document.
1177
+ */
1178
+ public async loginWithClientVerifiedChallenge(
1179
+ usernameOrEmail: string,
1180
+ mnemonic: SecureString,
1181
+ session?: ClientSession,
1182
+ ): Promise<{
1183
+ userDoc: IUserDocument;
1184
+ userMember: BackendMember;
1185
+ adminMember: BackendMember;
1186
+ }> {
1187
+ const UserModel = this.application.getModel<IUserDocument>(
1188
+ BaseModelName.User,
1189
+ );
1190
+ const userQuery = validator.isEmail(usernameOrEmail)
1191
+ ? UserModel.findOne({ email: usernameOrEmail.toLowerCase() }).select(
1192
+ '_id username email accountStatus deletedAt mnemonicId publicKey passwordWrappedPrivateKey',
1193
+ )
1194
+ : UserModel.findOne({ username: usernameOrEmail })
1195
+ .collation({ locale: 'en', strength: 2 })
1196
+ .select(
1197
+ '_id username email accountStatus deletedAt mnemonicId publicKey passwordWrappedPrivateKey',
1198
+ );
1199
+ const userDoc = await userQuery.session(session ?? null);
1200
+
1201
+ if (!userDoc || userDoc.deletedAt) {
1202
+ throw new InvalidCredentialsError();
1203
+ }
1204
+
1205
+ // Check account status
1206
+ switch (userDoc.accountStatus) {
1207
+ case AccountStatus.Active:
1208
+ break;
1209
+ case AccountStatus.AdminLock:
1210
+ throw new AccountLockedError();
1211
+ case AccountStatus.PendingEmailVerification:
1212
+ throw new PendingEmailVerificationError();
1213
+ default:
1214
+ throw new AccountStatusError(userDoc.accountStatus);
1215
+ }
1216
+
1217
+ // Verify mnemonic matches user (simplified verification)
1218
+ try {
1219
+ const MnemonicModel = this.application.getModel<IMnemonicDocument>(
1220
+ BaseModelName.Mnemonic,
1221
+ );
1222
+ if (!userDoc.mnemonicId) {
1223
+ throw new InvalidCredentialsError();
1224
+ }
1225
+ const mnemonicDoc = await MnemonicModel.findById(userDoc.mnemonicId)
1226
+ .select('hmac')
1227
+ .session(session ?? null)
1228
+ .lean()
1229
+ .exec();
1230
+ if (!mnemonicDoc) {
1231
+ throw new InvalidCredentialsError();
1232
+ }
1233
+ const computedHmac = this.mnemonicService.getMnemonicHmac(mnemonic);
1234
+ if (computedHmac !== mnemonicDoc.hmac) {
1235
+ throw new InvalidCredentialsError();
1236
+ }
1237
+
1238
+ // Create Member from mnemonic
1239
+ const { wallet } = this.eciesService.walletAndSeedFromMnemonic(mnemonic);
1240
+ const privateKey = wallet.getPrivateKey();
1241
+ const publicKey = wallet.getPublicKey();
1242
+ const publicKeyWithPrefix = Buffer.concat([
1243
+ Buffer.from([this.application.constants.ECIES.PUBLIC_KEY_MAGIC]),
1244
+ publicKey,
1245
+ ]);
1246
+ const userMember = await this.makeUserFromUserDoc(
1247
+ userDoc,
1248
+ new SecureBuffer(privateKey),
1249
+ publicKeyWithPrefix,
1250
+ mnemonic,
1251
+ wallet,
1252
+ session,
1253
+ );
1254
+
1255
+ // Verify public key matches
1256
+ if (userMember.publicKey.toString('hex') !== userDoc.publicKey) {
1257
+ throw new InvalidCredentialsError();
1258
+ }
1259
+
1260
+ const adminMember = SystemUserService.getSystemUser(
1261
+ this.application.environment,
1262
+ );
1263
+
1264
+ return {
1265
+ userMember,
1266
+ adminMember,
1267
+ userDoc,
1268
+ };
1269
+ } catch (error) {
1270
+ if (
1271
+ error instanceof InvalidCredentialsError ||
1272
+ error instanceof AccountLockedError ||
1273
+ error instanceof PendingEmailVerificationError ||
1274
+ error instanceof AccountStatusError
1275
+ ) {
1276
+ throw error;
1277
+ }
1278
+ throw new InvalidCredentialsError();
1279
+ }
1280
+ }
1281
+
1282
+ /**
1283
+ * Authenticate a user with their mnemonic.
1284
+ * @returns The authenticated user document.
1285
+ */
1286
+ public async loginWithMnemonic(
1287
+ usernameOrEmail: string,
1288
+ mnemonic: SecureString,
1289
+ session?: ClientSession,
1290
+ ): Promise<{
1291
+ userDoc: IUserDocument;
1292
+ userMember: BackendMember;
1293
+ adminMember: BackendMember;
1294
+ }> {
1295
+ const UserModel = ModelRegistry.instance.getTypedModel<IUserDocument>(
1296
+ BaseModelName.User,
1297
+ );
1298
+ const userQuery = validator.isEmail(usernameOrEmail)
1299
+ ? UserModel.findOne({ email: usernameOrEmail.toLowerCase() }).select(
1300
+ '_id username email accountStatus deletedAt mnemonicId publicKey passwordWrappedPrivateKey',
1301
+ )
1302
+ : UserModel.findOne({ username: usernameOrEmail })
1303
+ .collation({ locale: 'en', strength: 2 })
1304
+ .select(
1305
+ '_id username email accountStatus deletedAt mnemonicId publicKey passwordWrappedPrivateKey',
1306
+ );
1307
+ const userDoc = await userQuery.session(session ?? null);
1308
+
1309
+ if (!userDoc || userDoc.deletedAt) {
1310
+ throw new InvalidCredentialsError();
1311
+ }
1312
+
1313
+ // Check account status
1314
+ switch (userDoc.accountStatus) {
1315
+ case AccountStatus.Active:
1316
+ break;
1317
+ case AccountStatus.AdminLock:
1318
+ throw new AccountLockedError();
1319
+ case AccountStatus.PendingEmailVerification:
1320
+ throw new PendingEmailVerificationError();
1321
+ default:
1322
+ throw new AccountStatusError(userDoc.accountStatus);
1323
+ }
1324
+
1325
+ const challengeResponse = await this.challengeUserWithMnemonic(
1326
+ userDoc,
1327
+ mnemonic,
1328
+ session,
1329
+ );
1330
+ return { ...challengeResponse, userDoc };
1331
+ }
1332
+
1333
+ /**
1334
+ * Authenticate a user with their password (for key-wrapped accounts).
1335
+ * @returns The authenticated user document.
1336
+ */
1337
+ public async loginWithPassword(
1338
+ usernameOrEmail: string,
1339
+ password: string,
1340
+ session?: ClientSession,
1341
+ ): Promise<{
1342
+ userDoc: IUserDocument;
1343
+ userMember: BackendMember;
1344
+ adminMember: BackendMember;
1345
+ }> {
1346
+ const UserModel = this.application.getModel<IUserDocument>(
1347
+ BaseModelName.User,
1348
+ );
1349
+ const query = validator.isEmail(usernameOrEmail)
1350
+ ? UserModel.findOne({ email: usernameOrEmail.toLowerCase() })
1351
+ : UserModel.findOne({ username: usernameOrEmail }).collation({
1352
+ locale: 'en',
1353
+ strength: 2,
1354
+ });
1355
+
1356
+ const userDoc: IUserDocument | null = await query
1357
+ .session(session ?? null)
1358
+ .exec();
1359
+
1360
+ if (!userDoc || userDoc.deletedAt) {
1361
+ throw new InvalidCredentialsError();
1362
+ }
1363
+
1364
+ // Check account status
1365
+ switch (userDoc.accountStatus) {
1366
+ case AccountStatus.Active:
1367
+ break;
1368
+ case AccountStatus.AdminLock:
1369
+ throw new AccountLockedError();
1370
+ case AccountStatus.PendingEmailVerification:
1371
+ throw new PendingEmailVerificationError();
1372
+ default:
1373
+ throw new AccountStatusError(userDoc.accountStatus);
1374
+ }
1375
+
1376
+ // Check if user has password-based authentication set up (Option B requires passwordWrappedPrivateKey)
1377
+ if (!userDoc.passwordWrappedPrivateKey || !userDoc.mnemonicId) {
1378
+ throw new InvalidCredentialsError();
1379
+ }
1380
+ // Unwrap password-wrapped private key and complete challenge with possession of private key
1381
+ const unwrapped = await this.keyWrappingService.unwrapSecretAsync(
1382
+ userDoc.passwordWrappedPrivateKey!,
1383
+ password,
1384
+ );
1385
+
1386
+ // Build user member with unwrapped private key to decrypt challenge
1387
+ // Note: userMember now owns the unwrapped SecureBuffer, so we don't dispose it here
1388
+ const userMember = await this.makeUserFromUserDoc(
1389
+ userDoc,
1390
+ unwrapped,
1391
+ undefined,
1392
+ undefined,
1393
+ undefined,
1394
+ session,
1395
+ );
1396
+
1397
+ // Generate a nonce challenge signed by system
1398
+ const adminMember = SystemUserService.getSystemUser(
1399
+ this.application.environment,
1400
+ );
1401
+ const nonce = randomBytes(32);
1402
+ const signature = adminMember.sign(nonce);
1403
+ const payload = Buffer.concat([nonce, signature]);
1404
+
1405
+ const encryptedPayload = userMember.encryptData(payload);
1406
+ const decryptedPayload = userMember.decryptData(encryptedPayload);
1407
+
1408
+ const decryptedNonce = decryptedPayload.subarray(0, 32);
1409
+ const decryptedSignature = decryptedPayload.subarray(32);
1410
+
1411
+ const isSignatureValid = adminMember.verify(
1412
+ decryptedSignature as SignatureBuffer,
1413
+ decryptedNonce,
1414
+ );
1415
+ if (!isSignatureValid || !nonce.equals(decryptedNonce)) {
1416
+ throw new InvalidCredentialsError();
1417
+ }
1418
+ return { userDoc, userMember, adminMember: adminMember };
1419
+ }
1420
+
1421
+ /**
1422
+ * Re-send a previously sent email token
1423
+ * @param userId The user id
1424
+ * @param session The session to use for the query
1425
+ * @returns void
1426
+ * @throws EmailTokenUsedOrInvalidError
1427
+ */
1428
+ public async resendEmailToken(
1429
+ userId: string,
1430
+ type: EmailTokenType,
1431
+ session?: ClientSession,
1432
+ debug = false,
1433
+ ): Promise<void> {
1434
+ const EmailTokenModel =
1435
+ ModelRegistry.instance.getTypedModel<IEmailTokenDocument>(
1436
+ BaseModelName.EmailToken,
1437
+ );
1438
+ return await this.withTransaction<void>(
1439
+ async (sess: ClientSession | undefined) => {
1440
+ // look up the most recent email token for a given user, then send it
1441
+ const emailToken: IEmailTokenDocument | null =
1442
+ await EmailTokenModel.findOne({
1443
+ userId,
1444
+ type,
1445
+ expiresAt: { $gt: new Date() },
1446
+ })
1447
+ .session(sess ?? null)
1448
+ .sort({ createdAt: -1 })
1449
+ .limit(1);
1450
+
1451
+ if (!emailToken) {
1452
+ throw new EmailTokenUsedOrInvalidError();
1453
+ }
1454
+
1455
+ await this.sendEmailToken(emailToken, sess, debug);
1456
+ },
1457
+ session,
1458
+ {
1459
+ timeoutMs: this.application.environment.mongo.transactionTimeout * 5,
1460
+ },
1461
+ );
1462
+ }
1463
+
1464
+ /**
1465
+ * Verify the email token and update the user's account status
1466
+ * @param emailToken The email token to verify
1467
+ * @param session The session to use for the query
1468
+ * @returns void
1469
+ * @throws EmailTokenUsedOrInvalidError
1470
+ * @throws EmailTokenExpiredError
1471
+ * @throws EmailVerifiedError
1472
+ * @throws UserNotFoundError
1473
+ */
1474
+ public async verifyAccountTokenAndComplete(
1475
+ emailToken: string,
1476
+ session?: ClientSession,
1477
+ ): Promise<void> {
1478
+ let alreadyVerified = false;
1479
+
1480
+ await this.withTransaction<void>(
1481
+ async (sess: ClientSession | undefined) => {
1482
+ const EmailTokenModel =
1483
+ ModelRegistry.instance.getTypedModel<IEmailTokenDocument>(
1484
+ BaseModelName.EmailToken,
1485
+ );
1486
+ const UserModel = ModelRegistry.instance.getTypedModel<IUserDocument>(
1487
+ BaseModelName.User,
1488
+ );
1489
+ const token: IEmailTokenDocument | null = await this.findEmailToken(
1490
+ emailToken,
1491
+ EmailTokenType.AccountVerification,
1492
+ sess,
1493
+ );
1494
+
1495
+ if (!token) {
1496
+ throw new EmailTokenUsedOrInvalidError();
1497
+ }
1498
+
1499
+ if (token.expiresAt < new Date()) {
1500
+ await EmailTokenModel.deleteOne({ _id: token._id }).session(
1501
+ sess ?? null,
1502
+ );
1503
+ throw new EmailTokenExpiredError();
1504
+ }
1505
+
1506
+ const user: IUserDocument | null = await UserModel.findById(
1507
+ token.userId,
1508
+ ).session(sess ?? null);
1509
+
1510
+ if (!user || user.deletedAt) {
1511
+ throw new UserNotFoundError();
1512
+ }
1513
+
1514
+ if (user.emailVerified) {
1515
+ // Delete the token and mark to throw error after transaction commits
1516
+ await EmailTokenModel.deleteOne({ _id: token._id }).session(
1517
+ sess ?? null,
1518
+ );
1519
+ alreadyVerified = true;
1520
+ return;
1521
+ }
1522
+
1523
+ // set user email to token email and mark as verified
1524
+ user.email = token.email;
1525
+ user.emailVerified = true;
1526
+ user.accountStatus = AccountStatus.Active;
1527
+ user.updatedBy = user._id;
1528
+ await user.save({ session: sess });
1529
+
1530
+ // Delete the token after successful verification
1531
+ await EmailTokenModel.deleteOne({ _id: token._id }).session(
1532
+ sess ?? null,
1533
+ );
1534
+
1535
+ // add the user to the member role
1536
+ const memberRoleId = await this.roleService.getRoleIdByName(
1537
+ this.application.constants.MemberRole as Role,
1538
+ sess,
1539
+ );
1540
+ if (memberRoleId) {
1541
+ await this.roleService.addUserToRole(
1542
+ memberRoleId,
1543
+ user._id,
1544
+ user._id,
1545
+ sess,
1546
+ );
1547
+ } else {
1548
+ throw new Error('Member role not found');
1549
+ }
1550
+ },
1551
+ session,
1552
+ {
1553
+ timeoutMs: this.application.environment.mongo.transactionTimeout * 5,
1554
+ },
1555
+ );
1556
+
1557
+ if (alreadyVerified) {
1558
+ throw new EmailVerifiedError(409);
1559
+ }
1560
+ }
1561
+
1562
+ /**
1563
+ * Validate the email token
1564
+ * @param token The token to validate
1565
+ * @param restrictType The type of email token to validate (or throw)
1566
+ * @param session The session to use for the query
1567
+ * @returns void
1568
+ * @throws EmailTokenUsedOrInvalidError
1569
+ */
1570
+ public async validateEmailToken(
1571
+ token: string,
1572
+ restrictType?: EmailTokenType,
1573
+ session?: ClientSession,
1574
+ ): Promise<void> {
1575
+ return await this.withTransaction<void>(
1576
+ async (ses: ClientSession | undefined) => {
1577
+ const EmailTokenModel = this.application.getModel<IEmailTokenDocument>(
1578
+ BaseModelName.EmailToken,
1579
+ );
1580
+ const emailToken = await EmailTokenModel.findOne({
1581
+ token,
1582
+ ...(restrictType ? { type: EmailTokenType.PasswordReset } : {}),
1583
+ }).session(ses ?? null);
1584
+
1585
+ if (!emailToken) {
1586
+ throw new EmailTokenUsedOrInvalidError();
1587
+ } else if (emailToken.expiresAt < new Date()) {
1588
+ await EmailTokenModel.deleteOne({ _id: emailToken._id }).session(
1589
+ ses ?? null,
1590
+ );
1591
+ throw new EmailTokenExpiredError();
1592
+ }
1593
+
1594
+ // nothing else to do here, token is valid
1595
+ },
1596
+ session,
1597
+ {
1598
+ timeoutMs: this.application.environment.mongo.transactionTimeout * 5,
1599
+ },
1600
+ );
1601
+ }
1602
+
1603
+ /**
1604
+ * Updates the user's language
1605
+ * @param userId - The ID of the user
1606
+ * @param newLanguage - The new language
1607
+ * @param session - The session to use for the query
1608
+ * @returns The updated user
1609
+ */
1610
+ public async updateSiteLanguage(
1611
+ userId: string,
1612
+ newLanguage: string,
1613
+ session?: ClientSession,
1614
+ ): Promise<IRequestUserDTO> {
1615
+ return await this.withTransaction<IRequestUserDTO>(
1616
+ async (sess: ClientSession | undefined) => {
1617
+ const UserModel = ModelRegistry.instance.getTypedModel<IUserDocument>(
1618
+ BaseModelName.User,
1619
+ );
1620
+ const userDoc = await UserModel.findByIdAndUpdate(
1621
+ new Types.ObjectId(userId),
1622
+ {
1623
+ siteLanguage: newLanguage,
1624
+ },
1625
+ { new: true },
1626
+ ).session(sess ?? null);
1627
+ if (!userDoc) {
1628
+ throw new UserNotFoundError();
1629
+ }
1630
+ const roles = await this.roleService.getUserRoles(userDoc._id);
1631
+ const tokenRoles = this.roleService.rolesToTokenRoles(roles);
1632
+ return RequestUserService.makeRequestUserDTO(userDoc, tokenRoles);
1633
+ },
1634
+ session,
1635
+ {
1636
+ timeoutMs: this.application.environment.mongo.transactionTimeout * 5,
1637
+ },
1638
+ );
1639
+ }
1640
+
1641
+ /**
1642
+ * Changes the user's password by re-wrapping their master key
1643
+ * @param userId - The ID of the user
1644
+ * @param currentPassword - The current password
1645
+ * @param newPassword - The new password
1646
+ * @param session - The session to use for the query
1647
+ * @returns void
1648
+ */
1649
+ public async changePassword(
1650
+ userId: string,
1651
+ currentPassword: string,
1652
+ newPassword: string,
1653
+ session?: ClientSession,
1654
+ ): Promise<void> {
1655
+ return await this.withTransaction<void>(
1656
+ async (sess: ClientSession | undefined) => {
1657
+ const UserModel = ModelRegistry.instance.getTypedModel<IUserDocument>(
1658
+ BaseModelName.User,
1659
+ );
1660
+ const userDoc = await UserModel.findById(userId).session(sess ?? null);
1661
+ if (!userDoc || !userDoc.passwordWrappedPrivateKey) {
1662
+ throw new UserNotFoundError();
1663
+ }
1664
+
1665
+ if (!EciesConstants.PasswordRegex.test(newPassword)) {
1666
+ throw new InvalidNewPasswordError();
1667
+ }
1668
+
1669
+ const currentPasswordSecure = new SecureString(currentPassword);
1670
+ const newPasswordSecure = new SecureString(newPassword);
1671
+
1672
+ try {
1673
+ // Unwrap existing private key and rewrap under new password
1674
+ const priv = this.keyWrappingService.unwrapSecret(
1675
+ userDoc.passwordWrappedPrivateKey,
1676
+ currentPasswordSecure,
1677
+ );
1678
+ try {
1679
+ const wrapped = this.keyWrappingService.wrapSecret(
1680
+ priv,
1681
+ newPasswordSecure,
1682
+ );
1683
+ userDoc.passwordWrappedPrivateKey = wrapped;
1684
+ await userDoc.save({ session: sess });
1685
+ } finally {
1686
+ priv.dispose();
1687
+ }
1688
+ } catch (error: unknown) {
1689
+ // Re-throw original error so controller can map it properly
1690
+ // Re-throw original error so controller can map it properly
1691
+ throw error as Error;
1692
+ } finally {
1693
+ currentPasswordSecure.dispose();
1694
+ newPasswordSecure.dispose();
1695
+ }
1696
+ },
1697
+ session,
1698
+ {
1699
+ timeoutMs: this.application.environment.mongo.transactionTimeout * 5,
1700
+ },
1701
+ );
1702
+ }
1703
+
1704
+ /**
1705
+ * Retrieve an email token by its token string and type
1706
+ * @param token - The token string
1707
+ * @param type - The type of the email token
1708
+ * @param session - The session to use for the query
1709
+ * @returns The email token document or null if not found
1710
+ */
1711
+ public async findEmailToken(
1712
+ token: string,
1713
+ type?: EmailTokenType,
1714
+ session?: ClientSession,
1715
+ ): Promise<IEmailTokenDocument | null> {
1716
+ const EmailTokenModel =
1717
+ ModelRegistry.instance.getTypedModel<IEmailTokenDocument>(
1718
+ BaseModelName.EmailToken,
1719
+ );
1720
+ return await EmailTokenModel.findOne({
1721
+ token: token.toLowerCase().trim(),
1722
+ ...(type ? { type } : {}),
1723
+ expiresAt: { $gt: new Date() },
1724
+ }).session(session ?? null);
1725
+ }
1726
+
1727
+ /**
1728
+ * Verify email token is valid
1729
+ * @param token - The email token
1730
+ * @param session - The session to use for the query
1731
+ * @returns void
1732
+ */
1733
+ public async verifyEmailToken(
1734
+ token: string,
1735
+ type: EmailTokenType,
1736
+ session?: ClientSession,
1737
+ ): Promise<void> {
1738
+ return await this.withTransaction<void>(
1739
+ async (sess: ClientSession | undefined) => {
1740
+ // Find and validate the token
1741
+ const emailToken = await this.findEmailToken(token, type, sess);
1742
+
1743
+ if (!emailToken) {
1744
+ throw new EmailTokenUsedOrInvalidError();
1745
+ }
1746
+ },
1747
+ session,
1748
+ {
1749
+ timeoutMs: this.application.environment.mongo.transactionTimeout * 5,
1750
+ },
1751
+ );
1752
+ }
1753
+
1754
+ /**
1755
+ * Reset password using email token
1756
+ * @param token - The email token
1757
+ * @param newPassword - The new password
1758
+ * @param session - The session to use for the query
1759
+ * @returns void
1760
+ */
1761
+ public async resetPasswordWithToken(
1762
+ token: string,
1763
+ newPassword: string,
1764
+ credential?: string, // either mnemonic or current password; required
1765
+ session?: ClientSession,
1766
+ ): Promise<void> {
1767
+ if (!EciesConstants.PasswordRegex.test(newPassword)) {
1768
+ throw new InvalidNewPasswordError();
1769
+ }
1770
+ if (!credential) {
1771
+ throw new EmailTokenUsedOrInvalidError();
1772
+ }
1773
+
1774
+ return await this.withTransaction<void>(
1775
+ async (sess: ClientSession | undefined) => {
1776
+ const EmailTokenModel =
1777
+ ModelRegistry.instance.getTypedModel<IEmailTokenDocument>(
1778
+ BaseModelName.EmailToken,
1779
+ );
1780
+ const UserModel = ModelRegistry.instance.getTypedModel<IUserDocument>(
1781
+ BaseModelName.User,
1782
+ );
1783
+
1784
+ // Find and validate the token
1785
+ const emailToken = await this.findEmailToken(
1786
+ token,
1787
+ EmailTokenType.PasswordReset,
1788
+ sess,
1789
+ );
1790
+
1791
+ if (!emailToken) {
1792
+ throw new EmailTokenUsedOrInvalidError();
1793
+ }
1794
+
1795
+ // Find the user
1796
+ const userDoc = await UserModel.findById(emailToken.userId).session(
1797
+ sess ?? null,
1798
+ );
1799
+ if (!userDoc) {
1800
+ throw new UserNotFoundError();
1801
+ }
1802
+ // Update password-wrapped secrets based on credential type (Option B)
1803
+ const newPasswordSecure = new SecureString(newPassword);
1804
+ try {
1805
+ if (EciesConstants.MnemonicRegex.test(credential)) {
1806
+ // Credential is mnemonic: verify it belongs to this user via public key
1807
+ const providedMnemonic = new SecureString(credential);
1808
+ try {
1809
+ const { wallet } =
1810
+ this.eciesService.walletAndSeedFromMnemonic(providedMnemonic);
1811
+ const pub = Buffer.concat([
1812
+ Buffer.from([
1813
+ this.application.constants.ECIES.PUBLIC_KEY_MAGIC,
1814
+ ]),
1815
+ wallet.getPublicKey(),
1816
+ ]);
1817
+ if (pub.toString('hex') !== userDoc.publicKey) {
1818
+ throw new InvalidCredentialsError();
1819
+ }
1820
+
1821
+ // Derive private key from mnemonic and wrap it with new password
1822
+ const privateKey = wallet.getPrivateKey();
1823
+ const priv = new SecureBuffer(privateKey);
1824
+ try {
1825
+ const wrappedPriv = this.keyWrappingService.wrapSecret(
1826
+ priv,
1827
+ newPasswordSecure,
1828
+ );
1829
+ userDoc.passwordWrappedPrivateKey = wrappedPriv;
1830
+ await userDoc.save({ session: sess });
1831
+ } finally {
1832
+ priv.dispose();
1833
+ }
1834
+ } finally {
1835
+ providedMnemonic.dispose();
1836
+ }
1837
+ } else {
1838
+ // Credential is current password: unwrap existing master key
1839
+ if (!userDoc.passwordWrappedPrivateKey) {
1840
+ throw new InvalidCredentialsError();
1841
+ }
1842
+ const privateKeyBuf =
1843
+ await this.keyWrappingService.unwrapSecretAsync(
1844
+ userDoc.passwordWrappedPrivateKey!,
1845
+ credential,
1846
+ );
1847
+ try {
1848
+ // Re-wrap the existing private key under the new password
1849
+ const wrappedPriv = this.keyWrappingService.wrapSecret(
1850
+ privateKeyBuf,
1851
+ newPasswordSecure,
1852
+ );
1853
+ userDoc.passwordWrappedPrivateKey = wrappedPriv;
1854
+ await userDoc.save({ session: sess });
1855
+ } finally {
1856
+ privateKeyBuf.dispose();
1857
+ }
1858
+ }
1859
+
1860
+ // Delete the used token
1861
+ await EmailTokenModel.deleteOne({ _id: emailToken._id }).session(
1862
+ sess ?? null,
1863
+ );
1864
+
1865
+ // Dispose temporary master key
1866
+ } finally {
1867
+ newPasswordSecure.dispose();
1868
+ }
1869
+ },
1870
+ session,
1871
+ {
1872
+ timeoutMs: this.application.environment.mongo.transactionTimeout * 5,
1873
+ },
1874
+ );
1875
+ }
1876
+
1877
+ /**
1878
+ * Generate a login challenge for the client to sign
1879
+ * @returns The login challenge in hex
1880
+ */
1881
+ public generateDirectLoginChallenge(): string {
1882
+ const adminMember = SystemUserService.getSystemUser(
1883
+ this.application.environment,
1884
+ );
1885
+ const time = Buffer.alloc(8);
1886
+ time.writeBigUInt64BE(BigInt(new Date().getTime()));
1887
+ const nonce = randomBytes(32);
1888
+ const signature = adminMember.sign(Buffer.concat([time, nonce]));
1889
+ return Buffer.concat([time, nonce, signature]).toString('hex');
1890
+ }
1891
+
1892
+ /**
1893
+ * Verifies a direct login challenge response
1894
+ * @param serverSignedRequest The login challenge response in hex
1895
+ * @param session The mongoose session, if provided
1896
+ * @returns A promise with the user document and user member object
1897
+ */
1898
+ public async verifyDirectLoginChallenge(
1899
+ serverSignedRequest: string,
1900
+ signature: SignatureString,
1901
+ username?: string,
1902
+ email?: string,
1903
+ session?: ClientSession,
1904
+ ): Promise<{ userDoc: IUserDocument; userMember: BackendMember }> {
1905
+ return await this.withTransaction<{
1906
+ userDoc: IUserDocument;
1907
+ userMember: BackendMember;
1908
+ }>(
1909
+ async (sess: ClientSession | undefined) => {
1910
+ // serverSignedRequest is:
1911
+ // time (8) +
1912
+ // nonce (32) +
1913
+ // server signature (64) +
1914
+ // signature (64)
1915
+ if (
1916
+ serverSignedRequest.length <
1917
+ (8 + 32 + this.application.constants.ECIES.SIGNATURE_SIZE) * 2
1918
+ ) {
1919
+ throw new InvalidChallengeResponseError();
1920
+ }
1921
+ // get signed request into a buffer
1922
+ const requestBuffer = Buffer.from(serverSignedRequest, 'hex');
1923
+ // start tracking offset
1924
+ let offset = 0;
1925
+ // get the time
1926
+ const time = requestBuffer.subarray(offset, 8);
1927
+ offset += 8;
1928
+ // get the nonce
1929
+ const nonce = requestBuffer.subarray(offset, 40);
1930
+ offset += 32;
1931
+ // get the server signature
1932
+ const serverSignature = requestBuffer.subarray(
1933
+ offset,
1934
+ this.application.constants.ECIES.SIGNATURE_SIZE + 40,
1935
+ );
1936
+ offset += this.application.constants.ECIES.SIGNATURE_SIZE;
1937
+ const signedDataLength = offset;
1938
+ if (offset !== requestBuffer.length) {
1939
+ throw new InvalidChallengeResponseError();
1940
+ }
1941
+ // validate time is within acceptable range
1942
+ const timeMs = time.readBigUInt64BE();
1943
+ if (
1944
+ new Date().getTime() - Number(timeMs) >
1945
+ this.application.constants.LoginChallengeExpiration
1946
+ ) {
1947
+ throw new LoginChallengeExpiredError();
1948
+ }
1949
+ // validate the server's signature on the time + nonce portion
1950
+ const adminMember = SystemUserService.getSystemUser(
1951
+ this.application.environment,
1952
+ );
1953
+ if (
1954
+ !adminMember.verify(
1955
+ serverSignature as SignatureBuffer,
1956
+ Buffer.concat([time, nonce]),
1957
+ )
1958
+ ) {
1959
+ throw new InvalidChallengeResponseError();
1960
+ }
1961
+ // locate the user by email or username
1962
+ const userDoc = await this.findUser(email, username, sess);
1963
+ if (!userDoc) {
1964
+ throw new InvalidChallengeResponseError();
1965
+ }
1966
+ // get the user's member object
1967
+ const user = await this.makeUserFromUserDoc(
1968
+ userDoc,
1969
+ undefined,
1970
+ undefined,
1971
+ undefined,
1972
+ undefined,
1973
+ sess,
1974
+ );
1975
+ // get the signed portion of the response
1976
+ const signedData = requestBuffer.subarray(0, signedDataLength);
1977
+ // verify the user's signature on the signed portion
1978
+ if (
1979
+ !user.verify(
1980
+ Buffer.from(signature, 'hex') as SignatureBuffer,
1981
+ signedData,
1982
+ )
1983
+ ) {
1984
+ throw new InvalidChallengeResponseError();
1985
+ }
1986
+
1987
+ if (userDoc.directChallenge !== true) {
1988
+ throw new InvalidChallengeResponseError();
1989
+ }
1990
+
1991
+ // if the user is valid, try to use the token (prevents replay attacks)
1992
+ await DirectLoginTokenService.useToken(
1993
+ this.application,
1994
+ userDoc._id,
1995
+ nonce.toString('hex'),
1996
+ );
1997
+
1998
+ // if successful, update lastLogin
1999
+ await this.updateLastLogin(userDoc._id);
2000
+
2001
+ // return the user document and member object
2002
+ return { userDoc, userMember: user };
2003
+ },
2004
+ session,
2005
+ { timeoutMs: this.application.environment.mongo.transactionTimeout },
2006
+ );
2007
+ }
2008
+
2009
+ /**
2010
+ * Request a login link via email
2011
+ * @param email Email address
2012
+ * @param username Username
2013
+ * @param session Existing session, if any
2014
+ * @returns void
2015
+ */
2016
+ public async requestEmailLogin(
2017
+ email?: string,
2018
+ username?: string,
2019
+ session?: ClientSession,
2020
+ ): Promise<void> {
2021
+ return this.withTransaction<void>(
2022
+ async (sess: ClientSession | undefined) => {
2023
+ const userDoc = await this.findUser(email, username, sess);
2024
+ if (!userDoc) {
2025
+ return;
2026
+ }
2027
+ await this.createAndSendEmailToken(
2028
+ userDoc,
2029
+ EmailTokenType.LoginRequest,
2030
+ sess,
2031
+ this.application.environment.debug,
2032
+ );
2033
+ },
2034
+ session,
2035
+ {
2036
+ timeoutMs: this.application.environment.mongo.transactionTimeout,
2037
+ },
2038
+ );
2039
+ }
2040
+
2041
+ /**
2042
+ * Validate an email login token challenge
2043
+ * @param token The token to challenge
2044
+ * @param signature The signature of the token by the user's private key
2045
+ * @param session The session to use for the query
2046
+ * @returns The user document if the challenge is valid
2047
+ */
2048
+ public async validateEmailLoginTokenChallenge(
2049
+ token: string,
2050
+ signature: SignatureString,
2051
+ session?: ClientSession,
2052
+ ): Promise<IUserDocument> {
2053
+ return this.withTransaction<IUserDocument>(
2054
+ async (sess: ClientSession | undefined) => {
2055
+ const emailToken = await this.findEmailToken(
2056
+ token,
2057
+ EmailTokenType.LoginRequest,
2058
+ sess,
2059
+ );
2060
+ if (!emailToken) {
2061
+ throw new EmailTokenUsedOrInvalidError();
2062
+ }
2063
+ const userDoc = await this.findUser(emailToken.email, undefined, sess);
2064
+ if (!userDoc) {
2065
+ throw new UserNotFoundError();
2066
+ }
2067
+ const user = await this.makeUserFromUserDoc(
2068
+ userDoc,
2069
+ undefined,
2070
+ undefined,
2071
+ undefined,
2072
+ undefined,
2073
+ sess,
2074
+ );
2075
+ const result = user.verify(
2076
+ Buffer.from(signature, 'hex') as SignatureBuffer,
2077
+ Buffer.from(token, 'hex'),
2078
+ );
2079
+ if (!result) {
2080
+ throw new InvalidChallengeResponseError();
2081
+ }
2082
+ await emailToken.deleteOne({ session: sess ?? null });
2083
+ await this.updateLastLogin(userDoc._id);
2084
+ return userDoc;
2085
+ },
2086
+ session,
2087
+ {
2088
+ timeoutMs: this.application.environment.mongo.transactionTimeout,
2089
+ },
2090
+ );
2091
+ }
2092
+
2093
+ /**
2094
+ * Updates the user's last login time atomically
2095
+ * @param userId - The ID of the user
2096
+ * @returns void
2097
+ */
2098
+ public async updateLastLogin(userId: Types.ObjectId): Promise<void> {
2099
+ const UserModel = ModelRegistry.instance.get('User')?.model;
2100
+ try {
2101
+ // Check if the database connection is still open
2102
+ const connection = this.application.db.connection;
2103
+ if (connection.readyState !== 1) {
2104
+ // Connection is not open (0 = disconnected, 1 = connected, 2 = connecting, 3 = disconnecting)
2105
+ return; // Silently return if connection is not available
2106
+ }
2107
+
2108
+ // Use atomic update to avoid conflicts and ensure we only update lastLogin
2109
+ // Use a separate session to avoid interfering with any ongoing transactions
2110
+ await UserModel.updateOne(
2111
+ { _id: userId },
2112
+ {
2113
+ $set: { lastLogin: new Date() },
2114
+ $setOnInsert: {}, // Prevent any unintended document creation
2115
+ },
2116
+ {
2117
+ upsert: false, // Never create a new document
2118
+ runValidators: false, // Skip validation for performance since we're only updating lastLogin
2119
+ // Don't use any session to avoid transaction conflicts
2120
+ },
2121
+ );
2122
+ } catch (error) {
2123
+ // Check if the error is due to client being closed
2124
+ if (
2125
+ error instanceof Error &&
2126
+ (error.message.includes('client was closed') ||
2127
+ error.message.includes('MongoClientClosedError') ||
2128
+ error.name === 'MongoClientClosedError')
2129
+ ) {
2130
+ // This is expected during shutdown, don't log it as an error
2131
+ return;
2132
+ }
2133
+
2134
+ // If this fails, it's not critical for login functionality. Ignore and move on.
2135
+ }
2136
+ }
2137
+ }