@digitaldefiance/node-express-suite-mongo 4.23.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (390) hide show
  1. package/README.md +152 -0
  2. package/package.json +51 -0
  3. package/src/__tests__/fixtures/index.d.ts +2 -0
  4. package/src/__tests__/fixtures/index.d.ts.map +1 -0
  5. package/src/__tests__/fixtures/index.js +5 -0
  6. package/src/__tests__/fixtures/index.js.map +1 -0
  7. package/src/__tests__/fixtures/model-mocks.mock.d.ts +12 -0
  8. package/src/__tests__/fixtures/model-mocks.mock.d.ts.map +1 -0
  9. package/src/__tests__/fixtures/model-mocks.mock.js +102 -0
  10. package/src/__tests__/fixtures/model-mocks.mock.js.map +1 -0
  11. package/src/__tests__/helpers/application.mock.d.ts +4 -0
  12. package/src/__tests__/helpers/application.mock.d.ts.map +1 -0
  13. package/src/__tests__/helpers/application.mock.js +35 -0
  14. package/src/__tests__/helpers/application.mock.js.map +1 -0
  15. package/src/__tests__/helpers/index.d.ts +5 -0
  16. package/src/__tests__/helpers/index.d.ts.map +1 -0
  17. package/src/__tests__/helpers/index.js +8 -0
  18. package/src/__tests__/helpers/index.js.map +1 -0
  19. package/src/__tests__/helpers/mongoose-memory.d.ts +14 -0
  20. package/src/__tests__/helpers/mongoose-memory.d.ts.map +1 -0
  21. package/src/__tests__/helpers/mongoose-memory.js +49 -0
  22. package/src/__tests__/helpers/mongoose-memory.js.map +1 -0
  23. package/src/__tests__/helpers/setup-test-env.d.ts +13 -0
  24. package/src/__tests__/helpers/setup-test-env.d.ts.map +1 -0
  25. package/src/__tests__/helpers/setup-test-env.js +131 -0
  26. package/src/__tests__/helpers/setup-test-env.js.map +1 -0
  27. package/src/__tests__/index.d.ts +3 -0
  28. package/src/__tests__/index.d.ts.map +1 -0
  29. package/src/__tests__/index.js +6 -0
  30. package/src/__tests__/index.js.map +1 -0
  31. package/src/builders/application-builder.d.ts +38 -0
  32. package/src/builders/application-builder.d.ts.map +1 -0
  33. package/src/builders/application-builder.js +82 -0
  34. package/src/builders/application-builder.js.map +1 -0
  35. package/src/builders/index.d.ts +2 -0
  36. package/src/builders/index.d.ts.map +1 -0
  37. package/src/builders/index.js +5 -0
  38. package/src/builders/index.js.map +1 -0
  39. package/src/controllers/index.d.ts +3 -0
  40. package/src/controllers/index.d.ts.map +1 -0
  41. package/src/controllers/index.js +6 -0
  42. package/src/controllers/index.js.map +1 -0
  43. package/src/controllers/mongo-base.d.ts +55 -0
  44. package/src/controllers/mongo-base.d.ts.map +1 -0
  45. package/src/controllers/mongo-base.js +108 -0
  46. package/src/controllers/mongo-base.js.map +1 -0
  47. package/src/controllers/user.d.ts +61 -0
  48. package/src/controllers/user.d.ts.map +1 -0
  49. package/src/controllers/user.js +944 -0
  50. package/src/controllers/user.js.map +1 -0
  51. package/src/documents/base.d.ts +15 -0
  52. package/src/documents/base.d.ts.map +1 -0
  53. package/src/documents/base.js +8 -0
  54. package/src/documents/base.js.map +1 -0
  55. package/src/documents/email-token.d.ts +15 -0
  56. package/src/documents/email-token.d.ts.map +1 -0
  57. package/src/documents/email-token.js +8 -0
  58. package/src/documents/email-token.js.map +1 -0
  59. package/src/documents/index.d.ts +8 -0
  60. package/src/documents/index.d.ts.map +1 -0
  61. package/src/documents/index.js +3 -0
  62. package/src/documents/index.js.map +1 -0
  63. package/src/documents/mnemonic.d.ts +16 -0
  64. package/src/documents/mnemonic.d.ts.map +1 -0
  65. package/src/documents/mnemonic.js +8 -0
  66. package/src/documents/mnemonic.js.map +1 -0
  67. package/src/documents/role.d.ts +15 -0
  68. package/src/documents/role.d.ts.map +1 -0
  69. package/src/documents/role.js +8 -0
  70. package/src/documents/role.js.map +1 -0
  71. package/src/documents/used-direct-login-token.d.ts +16 -0
  72. package/src/documents/used-direct-login-token.d.ts.map +1 -0
  73. package/src/documents/used-direct-login-token.js +8 -0
  74. package/src/documents/used-direct-login-token.js.map +1 -0
  75. package/src/documents/user-role.d.ts +16 -0
  76. package/src/documents/user-role.d.ts.map +1 -0
  77. package/src/documents/user-role.js +8 -0
  78. package/src/documents/user-role.js.map +1 -0
  79. package/src/documents/user.d.ts +16 -0
  80. package/src/documents/user.d.ts.map +1 -0
  81. package/src/documents/user.js +8 -0
  82. package/src/documents/user.js.map +1 -0
  83. package/src/enumerations/base-model-name.d.ts +43 -0
  84. package/src/enumerations/base-model-name.d.ts.map +1 -0
  85. package/src/enumerations/base-model-name.js +39 -0
  86. package/src/enumerations/base-model-name.js.map +1 -0
  87. package/src/enumerations/index.d.ts +3 -0
  88. package/src/enumerations/index.d.ts.map +1 -0
  89. package/src/enumerations/index.js +6 -0
  90. package/src/enumerations/index.js.map +1 -0
  91. package/src/enumerations/schema-collection.d.ts +39 -0
  92. package/src/enumerations/schema-collection.d.ts.map +1 -0
  93. package/src/enumerations/schema-collection.js +43 -0
  94. package/src/enumerations/schema-collection.js.map +1 -0
  95. package/src/errors/index.d.ts +5 -0
  96. package/src/errors/index.d.ts.map +1 -0
  97. package/src/errors/index.js +8 -0
  98. package/src/errors/index.js.map +1 -0
  99. package/src/errors/invalid-backup-code-version.d.ts +5 -0
  100. package/src/errors/invalid-backup-code-version.d.ts.map +1 -0
  101. package/src/errors/invalid-backup-code-version.js +14 -0
  102. package/src/errors/invalid-backup-code-version.js.map +1 -0
  103. package/src/errors/invalid-model.d.ts +18 -0
  104. package/src/errors/invalid-model.d.ts.map +1 -0
  105. package/src/errors/invalid-model.js +26 -0
  106. package/src/errors/invalid-model.js.map +1 -0
  107. package/src/errors/model-not-registered.d.ts +18 -0
  108. package/src/errors/model-not-registered.d.ts.map +1 -0
  109. package/src/errors/model-not-registered.js +26 -0
  110. package/src/errors/model-not-registered.js.map +1 -0
  111. package/src/errors/mongoose-validation.d.ts +28 -0
  112. package/src/errors/mongoose-validation.d.ts.map +1 -0
  113. package/src/errors/mongoose-validation.js +33 -0
  114. package/src/errors/mongoose-validation.js.map +1 -0
  115. package/src/index.d.ts +19 -0
  116. package/src/index.d.ts.map +1 -0
  117. package/src/index.js +31 -0
  118. package/src/index.js.map +1 -0
  119. package/src/interfaces/api-mongo-validation-error-response.d.ts +16 -0
  120. package/src/interfaces/api-mongo-validation-error-response.d.ts.map +1 -0
  121. package/src/interfaces/api-mongo-validation-error-response.js +8 -0
  122. package/src/interfaces/api-mongo-validation-error-response.js.map +1 -0
  123. package/src/interfaces/database-init-result-tx.d.ts +27 -0
  124. package/src/interfaces/database-init-result-tx.d.ts.map +1 -0
  125. package/src/interfaces/database-init-result-tx.js +3 -0
  126. package/src/interfaces/database-init-result-tx.js.map +1 -0
  127. package/src/interfaces/db-init-result.d.ts +16 -0
  128. package/src/interfaces/db-init-result.d.ts.map +1 -0
  129. package/src/interfaces/db-init-result.js +8 -0
  130. package/src/interfaces/db-init-result.js.map +1 -0
  131. package/src/interfaces/discriminator-collections.d.ts +17 -0
  132. package/src/interfaces/discriminator-collections.d.ts.map +1 -0
  133. package/src/interfaces/discriminator-collections.js +8 -0
  134. package/src/interfaces/discriminator-collections.js.map +1 -0
  135. package/src/interfaces/environment-mongo.d.ts +88 -0
  136. package/src/interfaces/environment-mongo.d.ts.map +1 -0
  137. package/src/interfaces/environment-mongo.js +8 -0
  138. package/src/interfaces/environment-mongo.js.map +1 -0
  139. package/src/interfaces/index.d.ts +13 -0
  140. package/src/interfaces/index.d.ts.map +1 -0
  141. package/src/interfaces/index.js +16 -0
  142. package/src/interfaces/index.js.map +1 -0
  143. package/src/interfaces/models/email-token.d.ts +12 -0
  144. package/src/interfaces/models/email-token.d.ts.map +1 -0
  145. package/src/interfaces/models/email-token.js +8 -0
  146. package/src/interfaces/models/email-token.js.map +1 -0
  147. package/src/interfaces/models/index.d.ts +8 -0
  148. package/src/interfaces/models/index.d.ts.map +1 -0
  149. package/src/interfaces/models/index.js +11 -0
  150. package/src/interfaces/models/index.js.map +1 -0
  151. package/src/interfaces/models/mnemonic.d.ts +13 -0
  152. package/src/interfaces/models/mnemonic.d.ts.map +1 -0
  153. package/src/interfaces/models/mnemonic.js +8 -0
  154. package/src/interfaces/models/mnemonic.js.map +1 -0
  155. package/src/interfaces/models/role.d.ts +12 -0
  156. package/src/interfaces/models/role.d.ts.map +1 -0
  157. package/src/interfaces/models/role.js +8 -0
  158. package/src/interfaces/models/role.js.map +1 -0
  159. package/src/interfaces/models/token-role.d.ts +19 -0
  160. package/src/interfaces/models/token-role.d.ts.map +1 -0
  161. package/src/interfaces/models/token-role.js +8 -0
  162. package/src/interfaces/models/token-role.js.map +1 -0
  163. package/src/interfaces/models/used-direct-login-token.d.ts +19 -0
  164. package/src/interfaces/models/used-direct-login-token.d.ts.map +1 -0
  165. package/src/interfaces/models/used-direct-login-token.js +8 -0
  166. package/src/interfaces/models/used-direct-login-token.js.map +1 -0
  167. package/src/interfaces/models/user-role.d.ts +19 -0
  168. package/src/interfaces/models/user-role.d.ts.map +1 -0
  169. package/src/interfaces/models/user-role.js +8 -0
  170. package/src/interfaces/models/user-role.js.map +1 -0
  171. package/src/interfaces/models/user.d.ts +21 -0
  172. package/src/interfaces/models/user.d.ts.map +1 -0
  173. package/src/interfaces/models/user.js +8 -0
  174. package/src/interfaces/models/user.js.map +1 -0
  175. package/src/interfaces/mongo-application.d.ts +47 -0
  176. package/src/interfaces/mongo-application.d.ts.map +1 -0
  177. package/src/interfaces/mongo-application.js +10 -0
  178. package/src/interfaces/mongo-application.js.map +1 -0
  179. package/src/interfaces/mongo-errors.d.ts +13 -0
  180. package/src/interfaces/mongo-errors.d.ts.map +1 -0
  181. package/src/interfaces/mongo-errors.js +8 -0
  182. package/src/interfaces/mongo-errors.js.map +1 -0
  183. package/src/interfaces/mongoose-document-store.d.ts +42 -0
  184. package/src/interfaces/mongoose-document-store.d.ts.map +1 -0
  185. package/src/interfaces/mongoose-document-store.js +10 -0
  186. package/src/interfaces/mongoose-document-store.js.map +1 -0
  187. package/src/interfaces/schema.d.ts +37 -0
  188. package/src/interfaces/schema.d.ts.map +1 -0
  189. package/src/interfaces/schema.js +8 -0
  190. package/src/interfaces/schema.js.map +1 -0
  191. package/src/interfaces/server-init-result.d.ts +45 -0
  192. package/src/interfaces/server-init-result.d.ts.map +1 -0
  193. package/src/interfaces/server-init-result.js +8 -0
  194. package/src/interfaces/server-init-result.js.map +1 -0
  195. package/src/interfaces/test-environment.d.ts +22 -0
  196. package/src/interfaces/test-environment.d.ts.map +1 -0
  197. package/src/interfaces/test-environment.js +8 -0
  198. package/src/interfaces/test-environment.js.map +1 -0
  199. package/src/model-registry.d.ts +79 -0
  200. package/src/model-registry.d.ts.map +1 -0
  201. package/src/model-registry.js +97 -0
  202. package/src/model-registry.js.map +1 -0
  203. package/src/models/email-token.d.ts +24 -0
  204. package/src/models/email-token.d.ts.map +1 -0
  205. package/src/models/email-token.js +16 -0
  206. package/src/models/email-token.js.map +1 -0
  207. package/src/models/index.d.ts +7 -0
  208. package/src/models/index.d.ts.map +1 -0
  209. package/src/models/index.js +10 -0
  210. package/src/models/index.js.map +1 -0
  211. package/src/models/mnemonic.d.ts +24 -0
  212. package/src/models/mnemonic.d.ts.map +1 -0
  213. package/src/models/mnemonic.js +27 -0
  214. package/src/models/mnemonic.js.map +1 -0
  215. package/src/models/role.d.ts +24 -0
  216. package/src/models/role.d.ts.map +1 -0
  217. package/src/models/role.js +27 -0
  218. package/src/models/role.js.map +1 -0
  219. package/src/models/used-direct-login-token.d.ts +24 -0
  220. package/src/models/used-direct-login-token.d.ts.map +1 -0
  221. package/src/models/used-direct-login-token.js +16 -0
  222. package/src/models/used-direct-login-token.js.map +1 -0
  223. package/src/models/user-role.d.ts +23 -0
  224. package/src/models/user-role.d.ts.map +1 -0
  225. package/src/models/user-role.js +26 -0
  226. package/src/models/user-role.js.map +1 -0
  227. package/src/models/user.d.ts +24 -0
  228. package/src/models/user.d.ts.map +1 -0
  229. package/src/models/user.js +27 -0
  230. package/src/models/user.js.map +1 -0
  231. package/src/mongo-application-concrete.d.ts +30 -0
  232. package/src/mongo-application-concrete.d.ts.map +1 -0
  233. package/src/mongo-application-concrete.js +46 -0
  234. package/src/mongo-application-concrete.js.map +1 -0
  235. package/src/plugins/index.d.ts +2 -0
  236. package/src/plugins/index.d.ts.map +1 -0
  237. package/src/plugins/index.js +5 -0
  238. package/src/plugins/index.js.map +1 -0
  239. package/src/plugins/mongo-database-plugin.d.ts +116 -0
  240. package/src/plugins/mongo-database-plugin.d.ts.map +1 -0
  241. package/src/plugins/mongo-database-plugin.js +230 -0
  242. package/src/plugins/mongo-database-plugin.js.map +1 -0
  243. package/src/routers/api.d.ts +29 -0
  244. package/src/routers/api.d.ts.map +1 -0
  245. package/src/routers/api.js +84 -0
  246. package/src/routers/api.js.map +1 -0
  247. package/src/routers/index.d.ts +2 -0
  248. package/src/routers/index.d.ts.map +1 -0
  249. package/src/routers/index.js +5 -0
  250. package/src/routers/index.js.map +1 -0
  251. package/src/schemas/email-token.d.ts +65 -0
  252. package/src/schemas/email-token.d.ts.map +1 -0
  253. package/src/schemas/email-token.js +68 -0
  254. package/src/schemas/email-token.js.map +1 -0
  255. package/src/schemas/index.d.ts +8 -0
  256. package/src/schemas/index.d.ts.map +1 -0
  257. package/src/schemas/index.js +11 -0
  258. package/src/schemas/index.js.map +1 -0
  259. package/src/schemas/mnemonic.d.ts +37 -0
  260. package/src/schemas/mnemonic.d.ts.map +1 -0
  261. package/src/schemas/mnemonic.js +41 -0
  262. package/src/schemas/mnemonic.js.map +1 -0
  263. package/src/schemas/role.d.ts +57 -0
  264. package/src/schemas/role.d.ts.map +1 -0
  265. package/src/schemas/role.js +102 -0
  266. package/src/schemas/role.js.map +1 -0
  267. package/src/schemas/schema.d.ts +62 -0
  268. package/src/schemas/schema.d.ts.map +1 -0
  269. package/src/schemas/schema.js +81 -0
  270. package/src/schemas/schema.js.map +1 -0
  271. package/src/schemas/used-direct-login-token.d.ts +49 -0
  272. package/src/schemas/used-direct-login-token.d.ts.map +1 -0
  273. package/src/schemas/used-direct-login-token.js +35 -0
  274. package/src/schemas/used-direct-login-token.js.map +1 -0
  275. package/src/schemas/user-role.d.ts +52 -0
  276. package/src/schemas/user-role.d.ts.map +1 -0
  277. package/src/schemas/user-role.js +67 -0
  278. package/src/schemas/user-role.js.map +1 -0
  279. package/src/schemas/user.d.ts +43 -0
  280. package/src/schemas/user.d.ts.map +1 -0
  281. package/src/schemas/user.js +214 -0
  282. package/src/schemas/user.js.map +1 -0
  283. package/src/services/backup-code.d.ts +118 -0
  284. package/src/services/backup-code.d.ts.map +1 -0
  285. package/src/services/backup-code.js +320 -0
  286. package/src/services/backup-code.js.map +1 -0
  287. package/src/services/database-initialization.d.ts +137 -0
  288. package/src/services/database-initialization.d.ts.map +1 -0
  289. package/src/services/database-initialization.js +911 -0
  290. package/src/services/database-initialization.js.map +1 -0
  291. package/src/services/db-init-cache.d.ts +18 -0
  292. package/src/services/db-init-cache.d.ts.map +1 -0
  293. package/src/services/db-init-cache.js +7 -0
  294. package/src/services/db-init-cache.js.map +1 -0
  295. package/src/services/direct-login-token.d.ts +28 -0
  296. package/src/services/direct-login-token.d.ts.map +1 -0
  297. package/src/services/direct-login-token.js +62 -0
  298. package/src/services/direct-login-token.js.map +1 -0
  299. package/src/services/index.d.ts +17 -0
  300. package/src/services/index.d.ts.map +1 -0
  301. package/src/services/index.js +20 -0
  302. package/src/services/index.js.map +1 -0
  303. package/src/services/jwt.d.ts +20 -0
  304. package/src/services/jwt.d.ts.map +1 -0
  305. package/src/services/jwt.js +79 -0
  306. package/src/services/jwt.js.map +1 -0
  307. package/src/services/mnemonic.d.ts +30 -0
  308. package/src/services/mnemonic.d.ts.map +1 -0
  309. package/src/services/mnemonic.js +80 -0
  310. package/src/services/mnemonic.js.map +1 -0
  311. package/src/services/mongo-authentication-provider.d.ts +27 -0
  312. package/src/services/mongo-authentication-provider.d.ts.map +1 -0
  313. package/src/services/mongo-authentication-provider.js +97 -0
  314. package/src/services/mongo-authentication-provider.js.map +1 -0
  315. package/src/services/mongo-backup-code-store.d.ts +40 -0
  316. package/src/services/mongo-backup-code-store.d.ts.map +1 -0
  317. package/src/services/mongo-backup-code-store.js +104 -0
  318. package/src/services/mongo-backup-code-store.js.map +1 -0
  319. package/src/services/mongo-base.d.ts +24 -0
  320. package/src/services/mongo-base.d.ts.map +1 -0
  321. package/src/services/mongo-base.js +28 -0
  322. package/src/services/mongo-base.js.map +1 -0
  323. package/src/services/mongoose-collection.d.ts +52 -0
  324. package/src/services/mongoose-collection.d.ts.map +1 -0
  325. package/src/services/mongoose-collection.js +326 -0
  326. package/src/services/mongoose-collection.js.map +1 -0
  327. package/src/services/mongoose-database.d.ts +64 -0
  328. package/src/services/mongoose-database.d.ts.map +1 -0
  329. package/src/services/mongoose-database.js +121 -0
  330. package/src/services/mongoose-database.js.map +1 -0
  331. package/src/services/mongoose-document-store.d.ts +108 -0
  332. package/src/services/mongoose-document-store.d.ts.map +1 -0
  333. package/src/services/mongoose-document-store.js +265 -0
  334. package/src/services/mongoose-document-store.js.map +1 -0
  335. package/src/services/mongoose-session-adapter.d.ts +39 -0
  336. package/src/services/mongoose-session-adapter.d.ts.map +1 -0
  337. package/src/services/mongoose-session-adapter.js +63 -0
  338. package/src/services/mongoose-session-adapter.js.map +1 -0
  339. package/src/services/request-user.d.ts +22 -0
  340. package/src/services/request-user.d.ts.map +1 -0
  341. package/src/services/request-user.js +66 -0
  342. package/src/services/request-user.js.map +1 -0
  343. package/src/services/role.d.ts +97 -0
  344. package/src/services/role.d.ts.map +1 -0
  345. package/src/services/role.js +288 -0
  346. package/src/services/role.js.map +1 -0
  347. package/src/services/user.d.ts +362 -0
  348. package/src/services/user.d.ts.map +1 -0
  349. package/src/services/user.js +1504 -0
  350. package/src/services/user.js.map +1 -0
  351. package/src/testing.d.ts +9 -0
  352. package/src/testing.d.ts.map +1 -0
  353. package/src/testing.js +12 -0
  354. package/src/testing.js.map +1 -0
  355. package/src/transactions/index.d.ts +2 -0
  356. package/src/transactions/index.d.ts.map +1 -0
  357. package/src/transactions/index.js +5 -0
  358. package/src/transactions/index.js.map +1 -0
  359. package/src/transactions/transaction-manager.d.ts +37 -0
  360. package/src/transactions/transaction-manager.d.ts.map +1 -0
  361. package/src/transactions/transaction-manager.js +50 -0
  362. package/src/transactions/transaction-manager.js.map +1 -0
  363. package/src/types/index.d.ts +26 -0
  364. package/src/types/index.d.ts.map +1 -0
  365. package/src/types/index.js +9 -0
  366. package/src/types/index.js.map +1 -0
  367. package/src/types/mongoose-helpers.d.ts +16 -0
  368. package/src/types/mongoose-helpers.d.ts.map +1 -0
  369. package/src/types/mongoose-helpers.js +8 -0
  370. package/src/types/mongoose-helpers.js.map +1 -0
  371. package/src/utils/default-mongo-uri-validator.d.ts +15 -0
  372. package/src/utils/default-mongo-uri-validator.d.ts.map +1 -0
  373. package/src/utils/default-mongo-uri-validator.js +46 -0
  374. package/src/utils/default-mongo-uri-validator.js.map +1 -0
  375. package/src/utils/index.d.ts +5 -0
  376. package/src/utils/index.d.ts.map +1 -0
  377. package/src/utils/index.js +8 -0
  378. package/src/utils/index.js.map +1 -0
  379. package/src/utils/mongo-error-response.d.ts +17 -0
  380. package/src/utils/mongo-error-response.d.ts.map +1 -0
  381. package/src/utils/mongo-error-response.js +21 -0
  382. package/src/utils/mongo-error-response.js.map +1 -0
  383. package/src/utils/mongo-transaction.d.ts +39 -0
  384. package/src/utils/mongo-transaction.d.ts.map +1 -0
  385. package/src/utils/mongo-transaction.js +131 -0
  386. package/src/utils/mongo-transaction.js.map +1 -0
  387. package/src/utils/object-id.d.ts +11 -0
  388. package/src/utils/object-id.d.ts.map +1 -0
  389. package/src/utils/object-id.js +17 -0
  390. package/src/utils/object-id.js.map +1 -0
@@ -0,0 +1,1504 @@
1
+ "use strict";
2
+ /**
3
+ * @fileoverview Comprehensive user management service.
4
+ * Handles user authentication, registration, password management, email verification,
5
+ * mnemonic recovery, backup codes, and all user-related operations.
6
+ * @module services/user
7
+ */
8
+ Object.defineProperty(exports, "__esModule", { value: true });
9
+ exports.UserService = void 0;
10
+ const tslib_1 = require("tslib");
11
+ const ecies_lib_1 = require("@digitaldefiance/ecies-lib");
12
+ const mongoose_types_1 = require("@digitaldefiance/mongoose-types");
13
+ const node_ecies_lib_1 = require("@digitaldefiance/node-ecies-lib");
14
+ const suite_core_lib_1 = require("@digitaldefiance/suite-core-lib");
15
+ const suite_core_lib_2 = require("@digitaldefiance/suite-core-lib");
16
+ const crypto_1 = require("crypto");
17
+ const validator_1 = tslib_1.__importDefault(require("validator"));
18
+ const node_express_suite_1 = require("@digitaldefiance/node-express-suite");
19
+ const model_registry_1 = require("../model-registry");
20
+ const mnemonic_1 = require("./mnemonic");
21
+ const request_user_1 = require("./request-user");
22
+ const base_model_name_1 = require("../enumerations/base-model-name");
23
+ const mongoose_validation_1 = require("../errors/mongoose-validation");
24
+ const direct_login_token_1 = require("./direct-login-token");
25
+ /**
26
+ * Comprehensive service for user management and authentication.
27
+ * Provides methods for user creation, authentication (mnemonic/password/challenge),
28
+ * email verification, password reset, backup code recovery, and settings management.
29
+ * @template T - User document type
30
+ * @template TID - Platform ID type
31
+ * @template TDate - Date type
32
+ * @template TLanguage - String type for site language
33
+ * @template TAccountStatus - String type for account status
34
+ * @template _TEnvironment - Environment type
35
+ * @template _TConstants - Constants type
36
+ * @template _TBaseDocument - Base document type
37
+ * @template TUser - User base interface type
38
+ * @template TTokenRole - Token role interface type
39
+ * @template TApplication - Application interface type
40
+ * @extends {BaseService<TID, TApplication>}
41
+ */
42
+ class UserService extends node_express_suite_1.BaseService {
43
+ roleService;
44
+ eciesService;
45
+ keyWrappingService;
46
+ mnemonicService;
47
+ emailService;
48
+ backupCodeService;
49
+ serverUrl;
50
+ disableEmailSend;
51
+ constructor(application, roleService, emailService, keyWrappingService, backupCodeService) {
52
+ super(application);
53
+ this.roleService = roleService;
54
+ this.emailService = emailService;
55
+ this.keyWrappingService = keyWrappingService;
56
+ this.backupCodeService = backupCodeService;
57
+ this.serverUrl = application.environment.serverUrl;
58
+ this.disableEmailSend = application.environment.disableEmailSend;
59
+ const config = {
60
+ curveName: this.application.constants.ECIES.CURVE_NAME,
61
+ primaryKeyDerivationPath: this.application.constants.ECIES.PRIMARY_KEY_DERIVATION_PATH,
62
+ mnemonicStrength: this.application.constants.ECIES.MNEMONIC_STRENGTH,
63
+ symmetricAlgorithm: this.application.constants.ECIES.SYMMETRIC_ALGORITHM_CONFIGURATION,
64
+ symmetricKeyBits: this.application.constants.ECIES.SYMMETRIC.KEY_BITS,
65
+ symmetricKeyMode: this.application.constants.ECIES.SYMMETRIC.MODE,
66
+ };
67
+ this.eciesService = new node_ecies_lib_1.ECIESService(config);
68
+ const mnemonicModel = model_registry_1.ModelRegistry.instance.getTypedModel(base_model_name_1.BaseModelName.Mnemonic);
69
+ this.mnemonicService = new mnemonic_1.MnemonicService(mnemonicModel, application.environment.mnemonicHmacSecret, this.application.constants);
70
+ }
71
+ /**
72
+ * Given a User Document, make a User DTO
73
+ * @param user a User Document
74
+ * @returns An IUserDTO
75
+ */
76
+ static userToUserDTO(user) {
77
+ const provider = (0, node_ecies_lib_1.getEnhancedNodeIdProvider)();
78
+ const userId = user._id;
79
+ return {
80
+ ...(user instanceof mongoose_types_1.Document ? user.toObject() : user),
81
+ _id: provider.validate(provider.toBytes(userId)) &&
82
+ provider.idToString(userId),
83
+ createdAt: (user.createdAt instanceof Date
84
+ ? user.createdAt.toString()
85
+ : user.createdAt),
86
+ createdBy: provider.validate(provider.toBytes(user.createdBy)) &&
87
+ provider.idToString(user.createdBy),
88
+ updatedAt: (user.updatedAt instanceof Date
89
+ ? user.updatedAt.toString()
90
+ : user.updatedAt),
91
+ updatedBy: provider.validate(provider.toBytes(user.updatedBy)) &&
92
+ provider.idToString(user.updatedBy),
93
+ ...(user.lastLogin
94
+ ? {
95
+ lastLogin: (user.lastLogin instanceof Date
96
+ ? user.lastLogin.toString()
97
+ : user.lastLogin),
98
+ }
99
+ : {}),
100
+ ...(user.deletedAt
101
+ ? {
102
+ deletedAt: (user.deletedAt instanceof Date
103
+ ? user.deletedAt.toString()
104
+ : user.deletedAt),
105
+ }
106
+ : {}),
107
+ ...(user.deletedBy
108
+ ? {
109
+ deletedBy: provider.validate(provider.toBytes(user.deletedBy)) &&
110
+ provider.idToString(user.deletedBy),
111
+ }
112
+ : {}),
113
+ };
114
+ }
115
+ /**
116
+ * Given a User DTO, reconstitute ids and dates
117
+ * @param user a User DTO
118
+ * @returns An IUserBackendObject
119
+ */
120
+ hydrateUserDTOToBackend(user) {
121
+ const provider = (0, node_ecies_lib_1.getEnhancedNodeIdProvider)();
122
+ return {
123
+ ...user,
124
+ _id: provider.idFromString(user._id),
125
+ ...(user.lastLogin ? { lastLogin: new Date(user.lastLogin) } : {}),
126
+ createdAt: new Date(user.createdAt),
127
+ createdBy: provider.idFromString(user.createdBy),
128
+ updatedAt: new Date(user.updatedAt),
129
+ updatedBy: provider.idFromString(user.updatedBy),
130
+ ...(user.deletedAt ? { deletedAt: new Date(user.deletedAt) } : {}),
131
+ ...(user.deletedBy
132
+ ? {
133
+ deletedBy: provider.idFromString(user.deletedBy),
134
+ }
135
+ : {}),
136
+ ...(user.mnemonicId
137
+ ? { mnemonicId: provider.idFromString(user.mnemonicId) }
138
+ : {}),
139
+ };
140
+ }
141
+ /**
142
+ * Create a new email token to send to the user for email verification
143
+ * @param userDoc The user to create the email token for
144
+ * @param type The type of email token to create
145
+ * @param session The session to use for the query
146
+ * @returns The email token document
147
+ */
148
+ async createEmailToken(userDoc, type, session) {
149
+ const EmailTokenModel = model_registry_1.ModelRegistry.instance.getTypedModel(base_model_name_1.BaseModelName.EmailToken);
150
+ // If we already have a session, use it directly to avoid nested transactions
151
+ if (session) {
152
+ const now = new Date();
153
+ const tokenData = {
154
+ userId: userDoc._id,
155
+ type: type,
156
+ email: userDoc.email,
157
+ token: (0, crypto_1.randomBytes)(this.application.constants.EmailTokenLength).toString('hex'),
158
+ createdAt: now,
159
+ updatedAt: now,
160
+ expiresAt: new Date(now.getTime() + this.application.constants.EmailTokenExpiration),
161
+ };
162
+ // Use findOneAndUpdate with upsert to avoid duplicate key errors
163
+ const emailToken = await EmailTokenModel.findOneAndUpdate({
164
+ userId: userDoc._id,
165
+ email: userDoc.email,
166
+ type: type,
167
+ }, tokenData, {
168
+ upsert: true,
169
+ new: true,
170
+ session,
171
+ });
172
+ if (!emailToken) {
173
+ throw new suite_core_lib_1.TranslatableSuiteError(suite_core_lib_1.SuiteCoreStringKey.Error_FailedToCreateEmailToken);
174
+ }
175
+ return emailToken;
176
+ }
177
+ // Only create a new transaction if no session is provided
178
+ return await this.withTransaction(async (_sess) => {
179
+ const sess = _sess;
180
+ const now = new Date();
181
+ const tokenData = {
182
+ userId: userDoc._id,
183
+ type: type,
184
+ email: userDoc.email,
185
+ token: (0, crypto_1.randomBytes)(this.application.constants.EmailTokenLength).toString('hex'),
186
+ createdAt: now,
187
+ updatedAt: now,
188
+ expiresAt: new Date(now.getTime() + this.application.constants.EmailTokenExpiration),
189
+ };
190
+ // Use findOneAndUpdate with upsert to avoid duplicate key errors
191
+ const emailToken = await EmailTokenModel.findOneAndUpdate({
192
+ userId: userDoc._id,
193
+ email: userDoc.email,
194
+ type: type,
195
+ }, tokenData, {
196
+ upsert: true,
197
+ new: true,
198
+ session: sess,
199
+ });
200
+ if (!emailToken) {
201
+ throw new suite_core_lib_1.TranslatableSuiteError(suite_core_lib_1.SuiteCoreStringKey.Error_FailedToCreateEmailToken);
202
+ }
203
+ return emailToken;
204
+ }, undefined, {
205
+ timeoutMs: this.application.environment.mongo.transactionTimeout,
206
+ });
207
+ }
208
+ /**
209
+ * Create and send an email token to the user for email verification
210
+ * @param user The user to send the email token to
211
+ * @param type The type of email token to send
212
+ * @param session The session to use for the query
213
+ * @returns The email token document
214
+ */
215
+ async createAndSendEmailToken(user, type = suite_core_lib_1.EmailTokenType.AccountVerification, session, debug = false) {
216
+ const emailToken = await this.createEmailToken(user, type, session);
217
+ try {
218
+ await this.sendEmailToken(emailToken, session, debug);
219
+ }
220
+ catch {
221
+ // keep parity with previous behavior: continue returning token even if email send fails
222
+ }
223
+ return emailToken;
224
+ }
225
+ /**
226
+ * Create and send an email token directly within an existing transaction
227
+ * @param user The user to send the email token to
228
+ * @param type The type of email token to send
229
+ * @param session The session to use for the query (required)
230
+ * @param debug Whether to enable debug logging
231
+ * @returns The email token document
232
+ */
233
+ async createAndSendEmailTokenDirect(user, type = suite_core_lib_1.EmailTokenType.AccountVerification, session, debug = false) {
234
+ const EmailTokenModel = model_registry_1.ModelRegistry.instance.getTypedModel(base_model_name_1.BaseModelName.EmailToken);
235
+ // Create token directly within the existing session using upsert
236
+ const now = new Date();
237
+ const tokenData = {
238
+ userId: user._id,
239
+ type: type,
240
+ email: user.email,
241
+ token: (0, crypto_1.randomBytes)(this.application.constants.EmailTokenLength).toString('hex'),
242
+ createdAt: now,
243
+ updatedAt: now,
244
+ expiresAt: new Date(now.getTime() + this.application.constants.EmailTokenExpiration),
245
+ };
246
+ // Use findOneAndUpdate with upsert to avoid duplicate key errors
247
+ const emailToken = await EmailTokenModel.findOneAndUpdate({
248
+ userId: user._id,
249
+ email: user.email,
250
+ type: type,
251
+ }, tokenData, {
252
+ upsert: true,
253
+ new: true,
254
+ session,
255
+ });
256
+ if (!emailToken) {
257
+ throw new suite_core_lib_1.TranslatableSuiteError(suite_core_lib_1.SuiteCoreStringKey.Error_FailedToCreateEmailToken);
258
+ }
259
+ try {
260
+ await this.sendEmailToken(emailToken, session, debug);
261
+ }
262
+ catch {
263
+ // Ignore email send errors in direct token creation
264
+ }
265
+ return emailToken;
266
+ }
267
+ /**
268
+ * Send an email token to the user for email verification
269
+ * @param emailToken The email token to send
270
+ * @param session The session to use for the query
271
+ * @returns void
272
+ */
273
+ async sendEmailToken(emailToken, session, debug = false) {
274
+ if (this.disableEmailSend) {
275
+ (0, node_express_suite_1.debugLog)(debug, 'log', 'Email sending disabled for testing');
276
+ // Still update lastSent and expiration to keep token valid during tests
277
+ emailToken.lastSent = new Date();
278
+ emailToken.expiresAt = new Date(Date.now() + this.application.constants.EmailTokenExpiration);
279
+ await emailToken.save({ session });
280
+ return;
281
+ }
282
+ if (emailToken.lastSent &&
283
+ emailToken.lastSent.getTime() +
284
+ this.application.constants.EmailTokenResendInterval >
285
+ Date.now()) {
286
+ throw new suite_core_lib_1.EmailTokenSentTooRecentlyError(emailToken.lastSent);
287
+ }
288
+ let subjectString;
289
+ let bodyString;
290
+ let url;
291
+ switch (emailToken.type) {
292
+ case suite_core_lib_1.EmailTokenType.AccountVerification:
293
+ subjectString = suite_core_lib_1.SuiteCoreStringKey.Email_ConfirmationSubjectTemplate;
294
+ bodyString = suite_core_lib_1.SuiteCoreStringKey.Email_ConfirmationBody;
295
+ url = `${this.serverUrl}/verify-email?token=${emailToken.token}`;
296
+ break;
297
+ case suite_core_lib_1.EmailTokenType.PasswordReset:
298
+ subjectString = suite_core_lib_1.SuiteCoreStringKey.Email_ResetPasswordSubjectTemplate;
299
+ bodyString = suite_core_lib_1.SuiteCoreStringKey.Email_ResetPasswordBody;
300
+ url = `${this.serverUrl}/forgot-password?token=${emailToken.token}`;
301
+ break;
302
+ case suite_core_lib_1.EmailTokenType.LoginRequest:
303
+ subjectString = suite_core_lib_1.SuiteCoreStringKey.Email_LoginRequestSubjectTemplate;
304
+ bodyString = suite_core_lib_1.SuiteCoreStringKey.Email_LoginRequestBody;
305
+ url = `${this.serverUrl}/challenge?token=${emailToken.token}`;
306
+ break;
307
+ case suite_core_lib_1.EmailTokenType.MnemonicRecoveryRequest:
308
+ case suite_core_lib_1.EmailTokenType.PrivateKeyRequest:
309
+ default:
310
+ throw new suite_core_lib_1.TranslatableSuiteError(suite_core_lib_1.SuiteCoreStringKey.Error_InvalidEmailTokenType);
311
+ }
312
+ const emailSubject = (0, suite_core_lib_1.getSuiteCoreTranslation)(subjectString);
313
+ const emailText = `${(0, suite_core_lib_1.getSuiteCoreTranslation)(bodyString)}\r\n\r\n${url}`;
314
+ const emailHtml = `<p>${(0, suite_core_lib_1.getSuiteCoreTranslation)(bodyString)}</p><br/><p><a href="${url}">${url}</a></p><p>${(0, suite_core_lib_1.getSuiteCoreTranslation)(suite_core_lib_1.SuiteCoreStringKey.Email_LinkExpiresInTemplate)}</p>`;
315
+ try {
316
+ // Use the EmailService to send the email
317
+ await this.emailService.sendEmail(emailToken.email, emailSubject, emailText, emailHtml);
318
+ // update last sent/expiration
319
+ emailToken.lastSent = new Date();
320
+ emailToken.expiresAt = new Date(Date.now() + this.application.constants.EmailTokenExpiration);
321
+ await emailToken.save({ session });
322
+ }
323
+ catch {
324
+ throw new suite_core_lib_1.EmailTokenFailedToSendError(emailToken.type);
325
+ }
326
+ }
327
+ /**
328
+ * Find a user by email or username and enforce account status checks
329
+ * @param email Optional email
330
+ * @param username Optional username
331
+ * @param session Optional mongoose session
332
+ * @throws UsernameOrEmailRequiredError if neither provided
333
+ * @throws InvalidCredentialsError if not found or deleted
334
+ * @throws AccountLockedError | PendingEmailVerificationError | AccountStatusError per status
335
+ */
336
+ async findUser(email, username, session) {
337
+ if (!email && !username) {
338
+ throw new suite_core_lib_2.UsernameOrEmailRequiredError();
339
+ }
340
+ const UserModel = model_registry_1.ModelRegistry.instance.getTypedModel(base_model_name_1.BaseModelName.User);
341
+ let userDoc = null;
342
+ try {
343
+ if (email) {
344
+ userDoc = await UserModel.findOne({
345
+ email: email.toLowerCase(),
346
+ })
347
+ .session(session ?? null)
348
+ .exec();
349
+ }
350
+ else if (username) {
351
+ userDoc = await UserModel.findOne({ username })
352
+ .collation({ locale: 'en', strength: 2 })
353
+ .session(session ?? null)
354
+ .exec();
355
+ }
356
+ }
357
+ catch {
358
+ // Database error in findUser - convert to InvalidCredentialsError for security
359
+ throw new suite_core_lib_1.InvalidCredentialsError();
360
+ }
361
+ if (!userDoc || userDoc.deletedAt) {
362
+ if (email) {
363
+ throw new suite_core_lib_1.InvalidEmailError(ecies_lib_1.InvalidEmailErrorType.Missing);
364
+ }
365
+ throw new suite_core_lib_2.UserNotFoundError();
366
+ }
367
+ switch (userDoc.accountStatus) {
368
+ case suite_core_lib_1.AccountStatus.Active:
369
+ break;
370
+ case suite_core_lib_1.AccountStatus.AdminLock:
371
+ throw new suite_core_lib_1.AccountLockedError();
372
+ case suite_core_lib_1.AccountStatus.PendingEmailVerification:
373
+ throw new suite_core_lib_1.PendingEmailVerificationError();
374
+ default:
375
+ throw new suite_core_lib_1.AccountStatusError(userDoc.accountStatus);
376
+ }
377
+ return userDoc;
378
+ }
379
+ /**
380
+ * Finds a user record by ID
381
+ * @param userId The user ID
382
+ * @param throwIfNotActive Whether to throw if the user is inactive
383
+ * @param session The active session, if present
384
+ * @returns The user document
385
+ */
386
+ async findUserById(userId, throwIfNotActive, session, select) {
387
+ const UserModel = model_registry_1.ModelRegistry.instance.getTypedModel(base_model_name_1.BaseModelName.User);
388
+ const baseQuery = UserModel.findById(userId).session(session ?? null);
389
+ if (select) {
390
+ // Always include fields needed for status checks
391
+ const merged = this.ensureRequiredFieldsInProjection(select, [
392
+ 'deletedAt',
393
+ 'accountStatus',
394
+ ]);
395
+ baseQuery.select(merged);
396
+ }
397
+ const userDoc = (await baseQuery.exec());
398
+ if (!userDoc || userDoc.deletedAt) {
399
+ throw new suite_core_lib_2.UserNotFoundError();
400
+ }
401
+ if (throwIfNotActive) {
402
+ switch (userDoc.accountStatus) {
403
+ case suite_core_lib_1.AccountStatus.Active:
404
+ break;
405
+ case suite_core_lib_1.AccountStatus.AdminLock:
406
+ throw new suite_core_lib_1.AccountLockedError();
407
+ case suite_core_lib_1.AccountStatus.PendingEmailVerification:
408
+ throw new suite_core_lib_1.PendingEmailVerificationError();
409
+ default:
410
+ throw new suite_core_lib_1.AccountStatusError(userDoc.accountStatus);
411
+ }
412
+ }
413
+ return userDoc;
414
+ }
415
+ /**
416
+ * Ensure required fields are present in a projection for queries that rely on them.
417
+ * Supports string and object-style projections. For inclusion projections, adds fields.
418
+ * For exclusion projections, ensures required fields are not excluded.
419
+ */
420
+ ensureRequiredFieldsInProjection(select, required) {
421
+ if (typeof select === 'string') {
422
+ const parts = select
423
+ .split(/\s+/)
424
+ .map((s) => s.trim())
425
+ .filter(Boolean);
426
+ const exclusions = new Set(parts.filter((p) => p.startsWith('-')).map((p) => p.slice(1)));
427
+ // Remove exclusions on required fields
428
+ for (const r of required) {
429
+ exclusions.delete(r);
430
+ }
431
+ const cleaned = parts.filter((p) => !p.startsWith('-'));
432
+ for (const r of required) {
433
+ if (!cleaned.includes(r))
434
+ cleaned.push(r);
435
+ }
436
+ const result = [...cleaned, ...[...exclusions].map((r) => `-${r}`)];
437
+ return result.join(' ');
438
+ }
439
+ if (select && typeof select === 'object') {
440
+ const proj = { ...select };
441
+ const values = Object.values(proj);
442
+ const hasInclusions = values.some((v) => v === 1 || v === true);
443
+ if (hasInclusions) {
444
+ for (const r of required) {
445
+ proj[r] = 1;
446
+ }
447
+ }
448
+ else {
449
+ const keysToRemove = required.filter((r) => proj[r] === 0 || proj[r] === false || proj[r] === -1);
450
+ keysToRemove.forEach((key) => delete proj[key]);
451
+ }
452
+ return proj;
453
+ }
454
+ return select;
455
+ }
456
+ /**
457
+ * Fill in the default values to a user object
458
+ * @param newUser The user object to fill in
459
+ * @param createdBy The user ID of the user creating the new user
460
+ * @returns The filled in user
461
+ */
462
+ fillUserDefaults(newUser, createdBy, backupCodes, encryptedMnemonic, userId) {
463
+ return {
464
+ ...(userId ? { _id: userId } : {}),
465
+ timezone: 'UTC',
466
+ ...newUser,
467
+ email: newUser.email.toLowerCase(),
468
+ emailVerified: false,
469
+ darkMode: false,
470
+ accountStatus: suite_core_lib_1.AccountStatus.PendingEmailVerification,
471
+ siteLanguage: 'en-US',
472
+ duressPasswords: [],
473
+ publicKey: '',
474
+ backupCodes,
475
+ mnemonicRecovery: encryptedMnemonic,
476
+ currency: 'USD',
477
+ directChallenge: true,
478
+ createdAt: new Date(),
479
+ createdBy: createdBy,
480
+ updatedAt: new Date(),
481
+ updatedBy: createdBy,
482
+ };
483
+ }
484
+ /**
485
+ * Create a new user document from an IUser and unhashed password
486
+ * @param newUser The user object
487
+ * @returns The new user document
488
+ */
489
+ async makeUserDoc(newUser) {
490
+ const UserModel = model_registry_1.ModelRegistry.instance.getTypedModel(base_model_name_1.BaseModelName.User);
491
+ const newUserDoc = new UserModel(newUser);
492
+ const validationError = newUserDoc.validateSync();
493
+ if (validationError) {
494
+ throw new mongoose_validation_1.MongooseValidationError(validationError.errors);
495
+ }
496
+ return newUserDoc;
497
+ }
498
+ /**
499
+ * Create a new user.
500
+ * Do not set createdBy to a new (non-existing) ObjectId unless you also set newUserId to it.
501
+ * If newUserId is not set, one will be generated.
502
+ * @param systemUser The system user performing the operation
503
+ * @param userData Username, email, password in a ICreateUserBasics object
504
+ * @param createdBy The user id of the user creating the user
505
+ * @param newUserId the user id of the new user object- usually the createdBy user id.
506
+ * @param session The session to use for the query
507
+ * @param debug Whether to log debug information
508
+ * @param password The password to use for the new user (optional, if not provided, mnemonic will be used)
509
+ * @returns The new user document
510
+ */
511
+ async newUser(systemUser, userData, createdBy, newUserId, session, debug = false, password, userProvidedMnemonic) {
512
+ const provider = (0, node_ecies_lib_1.getEnhancedNodeIdProvider)();
513
+ const _newUserId = newUserId ?? provider.generateTyped();
514
+ if (!this.application.constants.UsernameRegex.test(userData.username)) {
515
+ throw new suite_core_lib_1.InvalidUsernameError();
516
+ }
517
+ if (password && !this.application.constants.PasswordRegex.test(password)) {
518
+ throw new node_express_suite_1.InvalidNewPasswordError();
519
+ }
520
+ const UserModel = model_registry_1.ModelRegistry.instance.getTypedModel(base_model_name_1.BaseModelName.User);
521
+ return await this.withTransaction(async (_sess) => {
522
+ const sess = _sess;
523
+ const existingEmail = await UserModel.findOne({
524
+ email: userData.email.toLowerCase(),
525
+ }).session(sess ?? null);
526
+ if (existingEmail) {
527
+ throw new suite_core_lib_1.EmailInUseError();
528
+ }
529
+ const existingUsername = await UserModel.findOne({
530
+ username: { $regex: new RegExp(`^${userData.username}$`, 'i') },
531
+ }).session(sess ?? null);
532
+ if (existingUsername) {
533
+ throw new suite_core_lib_2.UsernameInUseError();
534
+ }
535
+ let mnemonic;
536
+ let member;
537
+ if (userProvidedMnemonic) {
538
+ // User-provided mnemonic path: validate, check uniqueness, use directly
539
+ const trimmedMnemonic = userProvidedMnemonic.trim();
540
+ if (!this.application.constants.MnemonicRegex.test(trimmedMnemonic)) {
541
+ throw new suite_core_lib_1.TranslatableSuiteError(suite_core_lib_1.SuiteCoreStringKey.Validation_MnemonicRegex);
542
+ }
543
+ const trimmedMnemonicSecure = new ecies_lib_1.SecureString(trimmedMnemonic);
544
+ try {
545
+ const exists = await this.mnemonicService.mnemonicExists(trimmedMnemonicSecure, sess);
546
+ if (exists) {
547
+ throw new suite_core_lib_1.TranslatableSuiteError(suite_core_lib_1.SuiteCoreStringKey.Validation_MnemonicInUse);
548
+ }
549
+ const { member: newMember, mnemonic: newMnemonic } = node_ecies_lib_1.Member.newMember(this.eciesService, ecies_lib_1.MemberType.User, userData.username, new ecies_lib_1.EmailString(userData.email), trimmedMnemonicSecure, createdBy);
550
+ member = newMember;
551
+ mnemonic = newMnemonic;
552
+ }
553
+ catch (e) {
554
+ trimmedMnemonicSecure.dispose();
555
+ throw e;
556
+ }
557
+ }
558
+ else {
559
+ // Server-generated mnemonic path: retry loop until unique mnemonic found
560
+ while (!mnemonic || !member) {
561
+ try {
562
+ const { member: newMember, mnemonic: newMnemonic } = node_ecies_lib_1.Member.newMember(this.eciesService, ecies_lib_1.MemberType.User, userData.username, new ecies_lib_1.EmailString(userData.email), undefined, createdBy);
563
+ // make sure the new mnemonic is not already in the database
564
+ const mnemonicExists = await this.mnemonicService.mnemonicExists(newMnemonic, sess);
565
+ if (!mnemonicExists) {
566
+ member = newMember;
567
+ mnemonic = newMnemonic;
568
+ }
569
+ }
570
+ catch {
571
+ // If we fail to create a new member, we will retry until we succeed.
572
+ // This is to ensure that we do not end up with duplicate mnemonics.
573
+ (0, node_express_suite_1.debugLog)(debug, 'warn', 'Failed to create a new member, retrying...');
574
+ }
575
+ }
576
+ }
577
+ const backupCodes = node_express_suite_1.BackupCode.generateBackupCodes();
578
+ const encryptedBackupCodes = await node_express_suite_1.BackupCode.encryptBackupCodes(member, systemUser, backupCodes);
579
+ const encryptedMnemonic = member
580
+ .encryptData(Buffer.from(mnemonic.value ?? '', 'utf-8'))
581
+ .toString('hex');
582
+ const newUserDoc = new UserModel({
583
+ ...this.fillUserDefaults(userData, createdBy ?? _newUserId, encryptedBackupCodes, encryptedMnemonic, _newUserId),
584
+ publicKey: member.publicKey.toString('hex'),
585
+ });
586
+ const validationError = newUserDoc.validateSync();
587
+ if (validationError) {
588
+ throw new mongoose_validation_1.MongooseValidationError(validationError.errors);
589
+ }
590
+ // Always add HMAC-only mnemonic doc
591
+ const newMnemonicDoc = await this.mnemonicService.addMnemonic(mnemonic, sess);
592
+ if (newMnemonicDoc) {
593
+ newUserDoc.mnemonicId = newMnemonicDoc._id;
594
+ }
595
+ // If password provided, wrap the ECIES private key with the password (Option B)
596
+ if (password) {
597
+ const passwordSecure = new ecies_lib_1.SecureString(password);
598
+ try {
599
+ const priv = new ecies_lib_1.SecureBuffer(member.privateKey.value);
600
+ try {
601
+ const wrapped = this.keyWrappingService.wrapSecret(priv, passwordSecure, this.application.constants);
602
+ newUserDoc.passwordWrappedPrivateKey = wrapped;
603
+ }
604
+ finally {
605
+ priv.dispose();
606
+ }
607
+ }
608
+ finally {
609
+ passwordSecure.dispose();
610
+ }
611
+ }
612
+ const savedUserDoc = await newUserDoc.save({ session: sess });
613
+ const memberRoleId = await this.roleService.getRoleIdByName(this.application.constants.MemberRole, sess);
614
+ if (!memberRoleId) {
615
+ throw new suite_core_lib_1.TranslatableSuiteError(suite_core_lib_1.SuiteCoreStringKey.Error_FailedToLookupRoleTemplate, {
616
+ ROLE: (0, suite_core_lib_1.getSuiteCoreTranslation)(suite_core_lib_1.SuiteCoreStringKey.Common_Member),
617
+ });
618
+ }
619
+ await this.roleService.addUserToRole(memberRoleId, savedUserDoc._id, _newUserId, sess);
620
+ return {
621
+ user: savedUserDoc,
622
+ mnemonic: mnemonic.value ?? '',
623
+ backupCodes: backupCodes.map((code) => code.value ?? ''),
624
+ ...(password ? { password } : {}),
625
+ };
626
+ }, session, {
627
+ timeoutMs: this.application.environment.mongo.transactionTimeout * 10,
628
+ });
629
+ }
630
+ /**
631
+ * Get the backup codes for a user.
632
+ * Requires the user not be deleted or inactive
633
+ */
634
+ async getEncryptedUserBackupCodes(userId, session) {
635
+ const userWithCodes = await this.findUserById(userId, true, session);
636
+ return userWithCodes.backupCodes;
637
+ }
638
+ /**
639
+ * Resets the given user's backup codes
640
+ * @param backupUser The user to generate codes for
641
+ * @param session The current session, if any
642
+ * @returns A promise of an array of backup codes
643
+ */
644
+ async resetUserBackupCodes(backupUser, systemUser, session) {
645
+ if (!backupUser.hasPrivateKey) {
646
+ throw new suite_core_lib_1.PrivateKeyRequiredError();
647
+ }
648
+ const backupCodes = node_express_suite_1.BackupCode.generateBackupCodes();
649
+ const encryptedBackupCodes = await node_express_suite_1.BackupCode.encryptBackupCodes(backupUser, systemUser, backupCodes);
650
+ const UserModel = model_registry_1.ModelRegistry.instance.get('User')?.model;
651
+ return await this.withTransaction(async (_sess) => {
652
+ const sess = _sess;
653
+ await UserModel.updateOne({ _id: backupUser.id }, { $set: { backupCodes: encryptedBackupCodes } }, { session: sess });
654
+ return backupCodes;
655
+ }, session, {
656
+ timeoutMs: this.application.environment.mongo.transactionTimeout,
657
+ });
658
+ }
659
+ /**
660
+ * Recover a user's mnemonic from an encrypted mnemonic
661
+ * @param user The user whose mnemonic to recover
662
+ * @param encryptedMnemonic The encrypted mnemonic
663
+ * @returns The recovered mnemonic
664
+ */
665
+ recoverMnemonic(user, encryptedMnemonic) {
666
+ if (!encryptedMnemonic) {
667
+ throw new suite_core_lib_1.TranslatableSuiteHandleableError(suite_core_lib_1.SuiteCoreStringKey.MnemonicRecovery_Missing, undefined, undefined, {
668
+ statusCode: 400,
669
+ });
670
+ }
671
+ return new ecies_lib_1.SecureString(user.decryptData(Buffer.from(encryptedMnemonic, 'hex')).toString('utf-8'));
672
+ }
673
+ /**
674
+ * Make a Member from a user document and optional private key
675
+ * @param userDoc The user document
676
+ * @param privateKey Optional private key to load the wallet
677
+ * @param publicKey Optional public key to override the userDoc public key
678
+ * @param session The current session, if any
679
+ * @returns A promise containing the created Member
680
+ */
681
+ async makeUserFromUserDoc(userDoc, privateKey, publicKey, mnemonic, wallet, session) {
682
+ const memberType = await this.roleService.getMemberType(userDoc, session);
683
+ const user = new node_ecies_lib_1.Member(this.eciesService, memberType, userDoc.username, new ecies_lib_1.EmailString(userDoc.email), publicKey ?? Buffer.from(userDoc.publicKey, 'hex'), privateKey, wallet, userDoc._id, new Date(userDoc.createdAt), new Date(userDoc.updatedAt), userDoc.createdBy);
684
+ if ((privateKey?.originalLength ?? -1) > 0 &&
685
+ user.hasPrivateKey &&
686
+ !wallet) {
687
+ user.loadWallet(mnemonic ?? this.recoverMnemonic(user, userDoc.mnemonicRecovery));
688
+ }
689
+ return user;
690
+ }
691
+ /**
692
+ * Challenges a given userDoc with a given mnemonic, returns a system and user Member
693
+ * @param userDoc The userDoc in question
694
+ * @param mnemonic The mnemonic to challenge against
695
+ * @returns A promise containing the user and system Members
696
+ * @throws InvalidCredentialsError if the challenge fails
697
+ * @throws AccountLockedError if the account is locked
698
+ * @throws PendingEmailVerificationError if the email is not verified
699
+ * @throws AccountStatusError if the account status is invalid
700
+ */
701
+ async challengeUserWithMnemonic(userDoc, mnemonic, session) {
702
+ try {
703
+ // Verify provided mnemonic corresponds to the stored mnemonic HMAC (no password required)
704
+ // This prevents any valid mnemonic from authenticating as another user.
705
+ const MnemonicModel = model_registry_1.ModelRegistry.instance.getTypedModel(base_model_name_1.BaseModelName.Mnemonic);
706
+ if (!userDoc.mnemonicId) {
707
+ throw new suite_core_lib_1.InvalidCredentialsError();
708
+ }
709
+ const mnemonicDoc = await MnemonicModel.findById(userDoc.mnemonicId)
710
+ .select('hmac')
711
+ .session(session ?? null)
712
+ .lean()
713
+ .exec();
714
+ if (!mnemonicDoc) {
715
+ throw new suite_core_lib_1.InvalidCredentialsError();
716
+ }
717
+ const computedHmac = this.mnemonicService.getMnemonicHmac(mnemonic);
718
+ if (computedHmac !== mnemonicDoc.hmac) {
719
+ throw new suite_core_lib_1.InvalidCredentialsError();
720
+ }
721
+ // Create a Member from the provided mnemonic to get the keys
722
+ const { wallet } = this.eciesService.walletAndSeedFromMnemonic(mnemonic);
723
+ const privateKey = wallet.getPrivateKey();
724
+ // Get compressed public key (already includes prefix)
725
+ const publicKeyWithPrefix = this.eciesService.getPublicKey(Buffer.from(privateKey));
726
+ const userMember = await this.makeUserFromUserDoc(userDoc, new ecies_lib_1.SecureBuffer(privateKey), publicKeyWithPrefix, mnemonic, wallet, session);
727
+ // Verify the public key matches the stored userDoc public key
728
+ if (userMember.publicKey.toString('hex') !== userDoc.publicKey) {
729
+ throw new suite_core_lib_1.InvalidCredentialsError();
730
+ }
731
+ // Generate a nonce challenge to verify they can decrypt with their key
732
+ const adminMember = node_express_suite_1.SystemUserService.getSystemUser(this.application.environment, this.application.constants);
733
+ const nonce = (0, crypto_1.randomBytes)(32);
734
+ const signature = adminMember.sign(nonce);
735
+ const payload = Buffer.concat([nonce, signature]);
736
+ const encryptedPayload = userMember.encryptData(payload);
737
+ const decryptedPayload = userMember.decryptData(encryptedPayload);
738
+ // Verify the server's signature on the nonce
739
+ const decryptedNonce = decryptedPayload.subarray(0, 32);
740
+ const decryptedSignature = decryptedPayload.subarray(32);
741
+ const isSignatureValid = adminMember.verify(decryptedSignature, decryptedNonce);
742
+ if (!isSignatureValid || !nonce.equals(decryptedNonce)) {
743
+ throw new suite_core_lib_1.InvalidCredentialsError();
744
+ }
745
+ return {
746
+ userMember,
747
+ adminMember: adminMember,
748
+ };
749
+ }
750
+ catch (error) {
751
+ if (error instanceof suite_core_lib_1.InvalidCredentialsError ||
752
+ error instanceof suite_core_lib_1.AccountLockedError ||
753
+ error instanceof suite_core_lib_1.PendingEmailVerificationError ||
754
+ error instanceof suite_core_lib_1.AccountStatusError) {
755
+ throw error;
756
+ }
757
+ throw new suite_core_lib_1.InvalidCredentialsError();
758
+ }
759
+ }
760
+ /**
761
+ * Validates a login challenge response
762
+ * @param challengeResponse The challenge response bytes in hex
763
+ * @param email The email address of the user
764
+ * @param username The username of the user
765
+ * @param session The mongo session for the query
766
+ * @returns A promise that resolves to the user document, user member, and system member
767
+ */
768
+ async loginWithChallengeResponse(challengeResponse, email, username, session) {
769
+ const challengeBuffer = Buffer.from(challengeResponse, 'hex');
770
+ // validate the expected challenge response length (8 + 32 + 64 = 104 bytes)
771
+ if (challengeBuffer.length !=
772
+ this.application.constants.DirectLoginChallengeLength) {
773
+ throw new suite_core_lib_1.InvalidChallengeResponseError();
774
+ }
775
+ // disassemble the challengeResponse into time, nonce, signature
776
+ const time = challengeBuffer.subarray(0, 8); // 16 hex characters
777
+ const nonce = challengeBuffer.subarray(8, 40); // 64 hex characters
778
+ const signature = challengeBuffer.subarray(40); // 65 * 2 hex characters
779
+ const timeMs = parseInt(time.toString('hex'), 16);
780
+ if (new Date().getTime() - timeMs >
781
+ this.application.constants.LoginChallengeExpiration) {
782
+ throw new suite_core_lib_1.LoginChallengeExpiredError();
783
+ }
784
+ const userDoc = await this.findUser(email, username, session);
785
+ if (!userDoc && email) {
786
+ throw new suite_core_lib_1.InvalidEmailError(ecies_lib_1.InvalidEmailErrorType.Missing);
787
+ }
788
+ else if (!userDoc) {
789
+ throw new suite_core_lib_2.UserNotFoundError();
790
+ }
791
+ // re-sign the time + nonce and check if the signature matches
792
+ const adminMember = node_express_suite_1.SystemUserService.getSystemUser(this.application.environment, this.application.constants);
793
+ const timeAndNonce = Buffer.concat([time, nonce]);
794
+ const expectedSignature = adminMember.sign(timeAndNonce);
795
+ if (expectedSignature.toString('hex') !== signature.toString('hex')) {
796
+ throw new suite_core_lib_1.InvalidChallengeResponseError();
797
+ }
798
+ const userMember = await this.makeUserFromUserDoc(userDoc, undefined, undefined, undefined, undefined, session);
799
+ return {
800
+ userDoc,
801
+ userMember,
802
+ adminMember: adminMember,
803
+ };
804
+ }
805
+ /**
806
+ * Authenticate a user with client-verified challenge (skips server-side challenge)
807
+ * @returns The authenticated user document.
808
+ */
809
+ async loginWithClientVerifiedChallenge(usernameOrEmail, mnemonic, session) {
810
+ const UserModel = this.application.getModel(base_model_name_1.BaseModelName.User);
811
+ const userQuery = validator_1.default.isEmail(usernameOrEmail)
812
+ ? UserModel.findOne({ email: usernameOrEmail.toLowerCase() }).select('_id username email accountStatus deletedAt mnemonicId publicKey passwordWrappedPrivateKey')
813
+ : UserModel.findOne({ username: usernameOrEmail })
814
+ .collation({ locale: 'en', strength: 2 })
815
+ .select('_id username email accountStatus deletedAt mnemonicId publicKey passwordWrappedPrivateKey');
816
+ const userDoc = await userQuery.session(session ?? null);
817
+ if (!userDoc || userDoc.deletedAt) {
818
+ throw new suite_core_lib_1.InvalidCredentialsError();
819
+ }
820
+ // Check account status
821
+ switch (userDoc.accountStatus) {
822
+ case suite_core_lib_1.AccountStatus.Active:
823
+ break;
824
+ case suite_core_lib_1.AccountStatus.AdminLock:
825
+ throw new suite_core_lib_1.AccountLockedError();
826
+ case suite_core_lib_1.AccountStatus.PendingEmailVerification:
827
+ throw new suite_core_lib_1.PendingEmailVerificationError();
828
+ default:
829
+ throw new suite_core_lib_1.AccountStatusError(userDoc.accountStatus);
830
+ }
831
+ // Verify mnemonic matches user (simplified verification)
832
+ try {
833
+ const MnemonicModel = this.application.getModel(base_model_name_1.BaseModelName.Mnemonic);
834
+ if (!userDoc.mnemonicId) {
835
+ throw new suite_core_lib_1.InvalidCredentialsError();
836
+ }
837
+ const mnemonicDoc = await MnemonicModel.findById(userDoc.mnemonicId)
838
+ .select('hmac')
839
+ .session(session ?? null)
840
+ .lean()
841
+ .exec();
842
+ if (!mnemonicDoc) {
843
+ throw new suite_core_lib_1.InvalidCredentialsError();
844
+ }
845
+ const computedHmac = this.mnemonicService.getMnemonicHmac(mnemonic);
846
+ if (computedHmac !== mnemonicDoc.hmac) {
847
+ throw new suite_core_lib_1.InvalidCredentialsError();
848
+ }
849
+ // Create Member from mnemonic
850
+ const { wallet } = this.eciesService.walletAndSeedFromMnemonic(mnemonic);
851
+ const privateKey = wallet.getPrivateKey();
852
+ // Get compressed public key (already includes prefix)
853
+ const publicKeyWithPrefix = this.eciesService.getPublicKey(Buffer.from(privateKey));
854
+ const userMember = await this.makeUserFromUserDoc(userDoc, new ecies_lib_1.SecureBuffer(privateKey), publicKeyWithPrefix, mnemonic, wallet, session);
855
+ // Verify public key matches
856
+ if (userMember.publicKey.toString('hex') !== userDoc.publicKey) {
857
+ throw new suite_core_lib_1.InvalidCredentialsError();
858
+ }
859
+ const adminMember = node_express_suite_1.SystemUserService.getSystemUser(this.application.environment, this.application.constants);
860
+ return {
861
+ userMember,
862
+ adminMember,
863
+ userDoc,
864
+ };
865
+ }
866
+ catch (error) {
867
+ if (error instanceof suite_core_lib_1.InvalidCredentialsError ||
868
+ error instanceof suite_core_lib_1.AccountLockedError ||
869
+ error instanceof suite_core_lib_1.PendingEmailVerificationError ||
870
+ error instanceof suite_core_lib_1.AccountStatusError) {
871
+ throw error;
872
+ }
873
+ throw new suite_core_lib_1.InvalidCredentialsError();
874
+ }
875
+ }
876
+ /**
877
+ * Authenticate a user with their mnemonic.
878
+ * @returns The authenticated user document.
879
+ */
880
+ async loginWithMnemonic(usernameOrEmail, mnemonic, session) {
881
+ const UserModel = model_registry_1.ModelRegistry.instance.getTypedModel(base_model_name_1.BaseModelName.User);
882
+ const userQuery = validator_1.default.isEmail(usernameOrEmail)
883
+ ? UserModel.findOne({ email: usernameOrEmail.toLowerCase() }).select('_id username email accountStatus deletedAt mnemonicId publicKey passwordWrappedPrivateKey')
884
+ : UserModel.findOne({ username: usernameOrEmail })
885
+ .collation({ locale: 'en', strength: 2 })
886
+ .select('_id username email accountStatus deletedAt mnemonicId publicKey passwordWrappedPrivateKey');
887
+ const userDoc = await userQuery.session(session ?? null);
888
+ if (!userDoc || userDoc.deletedAt) {
889
+ throw new suite_core_lib_1.InvalidCredentialsError();
890
+ }
891
+ // Check account status
892
+ switch (userDoc.accountStatus) {
893
+ case suite_core_lib_1.AccountStatus.Active:
894
+ break;
895
+ case suite_core_lib_1.AccountStatus.AdminLock:
896
+ throw new suite_core_lib_1.AccountLockedError();
897
+ case suite_core_lib_1.AccountStatus.PendingEmailVerification:
898
+ throw new suite_core_lib_1.PendingEmailVerificationError();
899
+ default:
900
+ throw new suite_core_lib_1.AccountStatusError(userDoc.accountStatus);
901
+ }
902
+ const challengeResponse = await this.challengeUserWithMnemonic(userDoc, mnemonic, session);
903
+ return { ...challengeResponse, userDoc };
904
+ }
905
+ /**
906
+ * Authenticate a user with their password (for key-wrapped accounts).
907
+ * @returns The authenticated user document.
908
+ */
909
+ async loginWithPassword(usernameOrEmail, password, session) {
910
+ const UserModel = this.application.getModel(base_model_name_1.BaseModelName.User);
911
+ const query = validator_1.default.isEmail(usernameOrEmail)
912
+ ? UserModel.findOne({ email: usernameOrEmail.toLowerCase() })
913
+ : UserModel.findOne({ username: usernameOrEmail }).collation({
914
+ locale: 'en',
915
+ strength: 2,
916
+ });
917
+ const userDoc = await query
918
+ .session(session ?? null)
919
+ .exec();
920
+ if (!userDoc || userDoc.deletedAt) {
921
+ throw new suite_core_lib_1.InvalidCredentialsError();
922
+ }
923
+ // Check account status
924
+ switch (userDoc.accountStatus) {
925
+ case suite_core_lib_1.AccountStatus.Active:
926
+ break;
927
+ case suite_core_lib_1.AccountStatus.AdminLock:
928
+ throw new suite_core_lib_1.AccountLockedError();
929
+ case suite_core_lib_1.AccountStatus.PendingEmailVerification:
930
+ throw new suite_core_lib_1.PendingEmailVerificationError();
931
+ default:
932
+ throw new suite_core_lib_1.AccountStatusError(userDoc.accountStatus);
933
+ }
934
+ // Check if user has password-based authentication set up (Option B requires passwordWrappedPrivateKey)
935
+ if (!userDoc.passwordWrappedPrivateKey || !userDoc.mnemonicId) {
936
+ throw new suite_core_lib_1.PasswordLoginNotEnabledError();
937
+ }
938
+ // Unwrap password-wrapped private key and complete challenge with possession of private key
939
+ const unwrapped = await this.keyWrappingService.unwrapSecretAsync(userDoc.passwordWrappedPrivateKey, password, this.application.constants);
940
+ // Build user member with unwrapped private key to decrypt challenge
941
+ // Note: userMember now owns the unwrapped SecureBuffer, so we don't dispose it here
942
+ const userMember = await this.makeUserFromUserDoc(userDoc, unwrapped, undefined, undefined, undefined, session);
943
+ // Generate a nonce challenge signed by system
944
+ const adminMember = node_express_suite_1.SystemUserService.getSystemUser(this.application.environment, this.application.constants);
945
+ const nonce = (0, crypto_1.randomBytes)(32);
946
+ const signature = adminMember.sign(nonce);
947
+ const payload = Buffer.concat([nonce, signature]);
948
+ const encryptedPayload = userMember.encryptData(payload);
949
+ const decryptedPayload = userMember.decryptData(encryptedPayload);
950
+ const decryptedNonce = decryptedPayload.subarray(0, 32);
951
+ const decryptedSignature = decryptedPayload.subarray(32);
952
+ const isSignatureValid = adminMember.verify(decryptedSignature, decryptedNonce);
953
+ if (!isSignatureValid || !nonce.equals(decryptedNonce)) {
954
+ throw new suite_core_lib_1.InvalidCredentialsError();
955
+ }
956
+ return { userDoc, userMember, adminMember: adminMember };
957
+ }
958
+ /**
959
+ * Re-send a previously sent email token
960
+ * @param userId The user id
961
+ * @param session The session to use for the query
962
+ * @returns void
963
+ * @throws EmailTokenUsedOrInvalidError
964
+ */
965
+ async resendEmailToken(userId, type, session, debug = false) {
966
+ const EmailTokenModel = model_registry_1.ModelRegistry.instance.getTypedModel(base_model_name_1.BaseModelName.EmailToken);
967
+ return await this.withTransaction(async (_sess) => {
968
+ const sess = _sess;
969
+ // look up the most recent email token for a given user, then send it
970
+ const emailToken = await EmailTokenModel.findOne({
971
+ userId,
972
+ type,
973
+ expiresAt: { $gt: new Date() },
974
+ })
975
+ .session(sess ?? null)
976
+ .sort({ createdAt: -1 })
977
+ .limit(1);
978
+ if (!emailToken) {
979
+ throw new suite_core_lib_1.EmailTokenUsedOrInvalidError();
980
+ }
981
+ await this.sendEmailToken(emailToken, sess, debug);
982
+ }, session, {
983
+ timeoutMs: this.application.environment.mongo.transactionTimeout * 5,
984
+ });
985
+ }
986
+ /**
987
+ * Verify the email token and update the user's account status
988
+ * @param emailToken The email token to verify
989
+ * @param session The session to use for the query
990
+ * @returns void
991
+ * @throws EmailTokenUsedOrInvalidError
992
+ * @throws EmailTokenExpiredError
993
+ * @throws EmailVerifiedError
994
+ * @throws UserNotFoundError
995
+ */
996
+ async verifyAccountTokenAndComplete(emailToken, session) {
997
+ let alreadyVerified = false;
998
+ await this.withTransaction(async (_sess) => {
999
+ const sess = _sess;
1000
+ const EmailTokenModel = model_registry_1.ModelRegistry.instance.getTypedModel(base_model_name_1.BaseModelName.EmailToken);
1001
+ const UserModel = model_registry_1.ModelRegistry.instance.getTypedModel(base_model_name_1.BaseModelName.User);
1002
+ const token = await this.findEmailToken(emailToken, suite_core_lib_1.EmailTokenType.AccountVerification, sess);
1003
+ if (!token) {
1004
+ throw new suite_core_lib_1.EmailTokenUsedOrInvalidError();
1005
+ }
1006
+ if (token.expiresAt < new Date()) {
1007
+ await EmailTokenModel.deleteOne({ _id: token._id }).session(sess ?? null);
1008
+ throw new suite_core_lib_1.EmailTokenExpiredError();
1009
+ }
1010
+ const user = await UserModel.findById(token.userId).session(sess ?? null);
1011
+ if (!user || user.deletedAt) {
1012
+ throw new suite_core_lib_2.UserNotFoundError();
1013
+ }
1014
+ if (user.emailVerified) {
1015
+ // Delete the token and mark to throw error after transaction commits
1016
+ await EmailTokenModel.deleteOne({ _id: token._id }).session(sess ?? null);
1017
+ alreadyVerified = true;
1018
+ return;
1019
+ }
1020
+ // set user email to token email and mark as verified
1021
+ user.email = token.email;
1022
+ user.emailVerified = true;
1023
+ user.accountStatus = suite_core_lib_1.AccountStatus.Active;
1024
+ user.updatedBy = user._id;
1025
+ await user.save({ session: sess });
1026
+ // Delete the token after successful verification
1027
+ await EmailTokenModel.deleteOne({ _id: token._id }).session(sess ?? null);
1028
+ // add the user to the member role
1029
+ const memberRoleId = await this.roleService.getRoleIdByName(this.application.constants.MemberRole, sess);
1030
+ if (memberRoleId) {
1031
+ await this.roleService.addUserToRole(memberRoleId, user._id, user._id, sess);
1032
+ }
1033
+ else {
1034
+ throw new Error('Member role not found');
1035
+ }
1036
+ }, session, {
1037
+ timeoutMs: this.application.environment.mongo.transactionTimeout * 5,
1038
+ });
1039
+ if (alreadyVerified) {
1040
+ throw new suite_core_lib_1.EmailVerifiedError(409);
1041
+ }
1042
+ }
1043
+ /**
1044
+ * Validate the email token
1045
+ * @param token The token to validate
1046
+ * @param restrictType The type of email token to validate (or throw)
1047
+ * @param session The session to use for the query
1048
+ * @returns void
1049
+ * @throws EmailTokenUsedOrInvalidError
1050
+ */
1051
+ async validateEmailToken(token, restrictType, session) {
1052
+ return await this.withTransaction(async (_sess) => {
1053
+ const sess = _sess;
1054
+ const EmailTokenModel = this.application.getModel(base_model_name_1.BaseModelName.EmailToken);
1055
+ const emailToken = await EmailTokenModel.findOne({
1056
+ token,
1057
+ ...(restrictType ? { type: suite_core_lib_1.EmailTokenType.PasswordReset } : {}),
1058
+ }).session(sess ?? null);
1059
+ if (!emailToken) {
1060
+ throw new suite_core_lib_1.EmailTokenUsedOrInvalidError();
1061
+ }
1062
+ else if (emailToken.expiresAt < new Date()) {
1063
+ await EmailTokenModel.deleteOne({ _id: emailToken._id }).session(sess ?? null);
1064
+ throw new suite_core_lib_1.EmailTokenExpiredError();
1065
+ }
1066
+ }, session, {
1067
+ timeoutMs: this.application.environment.mongo.transactionTimeout * 5,
1068
+ });
1069
+ }
1070
+ /**
1071
+ * Updates the user's language
1072
+ * @param userId - The ID of the user
1073
+ * @param newLanguage - The new language
1074
+ * @param session - The session to use for the query
1075
+ * @returns The updated user
1076
+ */
1077
+ async updateSiteLanguage(userId, newLanguage, session) {
1078
+ const provider = (0, node_ecies_lib_1.getEnhancedNodeIdProvider)();
1079
+ return await this.withTransaction(async (_sess) => {
1080
+ const sess = _sess;
1081
+ const UserModel = model_registry_1.ModelRegistry.instance.getTypedModel(base_model_name_1.BaseModelName.User);
1082
+ const userDoc = await UserModel.findByIdAndUpdate(provider.idFromString(userId), {
1083
+ siteLanguage: newLanguage,
1084
+ }, { new: true }).session(sess ?? null);
1085
+ if (!userDoc) {
1086
+ throw new suite_core_lib_2.UserNotFoundError();
1087
+ }
1088
+ const roles = await this.roleService.getUserRoles(userDoc._id);
1089
+ const tokenRoles = this.roleService.rolesToTokenRoles(roles);
1090
+ return request_user_1.RequestUserService.makeRequestUserDTO(userDoc, tokenRoles);
1091
+ }, session, {
1092
+ timeoutMs: this.application.environment.mongo.transactionTimeout * 5,
1093
+ });
1094
+ }
1095
+ /**
1096
+ * Updates the user's Dark Mode preference
1097
+ * @param userId - The ID of the user
1098
+ * @param newDarkMode - The new Dark Mode preference
1099
+ * @param session - The session to use for the query
1100
+ * @returns The updated user
1101
+ */
1102
+ async updateDarkMode(userId, newDarkMode, session) {
1103
+ const provider = (0, node_ecies_lib_1.getEnhancedNodeIdProvider)();
1104
+ return await this.withTransaction(async (_sess) => {
1105
+ const sess = _sess;
1106
+ const UserModel = model_registry_1.ModelRegistry.instance.getTypedModel(base_model_name_1.BaseModelName.User);
1107
+ const userDoc = await UserModel.findByIdAndUpdate(provider.idFromString(userId), {
1108
+ darkMode: newDarkMode,
1109
+ }, { new: true }).session(sess ?? null);
1110
+ if (!userDoc) {
1111
+ throw new suite_core_lib_2.UserNotFoundError();
1112
+ }
1113
+ const roles = await this.roleService.getUserRoles(userDoc._id);
1114
+ const tokenRoles = this.roleService.rolesToTokenRoles(roles);
1115
+ return request_user_1.RequestUserService.makeRequestUserDTO(userDoc, tokenRoles);
1116
+ }, session, {
1117
+ timeoutMs: this.application.environment.mongo.transactionTimeout * 5,
1118
+ });
1119
+ }
1120
+ /**
1121
+ * Updates multiple user settings at once
1122
+ * @param userId - The ID of the user
1123
+ * @param settings - Object containing settings to update
1124
+ * @param session - The session to use for the query
1125
+ * @returns The updated user
1126
+ */
1127
+ async updateUserSettings(userId, settings, session) {
1128
+ const provider = (0, node_ecies_lib_1.getEnhancedNodeIdProvider)();
1129
+ return await this.withTransaction(async (_sess) => {
1130
+ const sess = _sess;
1131
+ const UserModel = model_registry_1.ModelRegistry.instance.getTypedModel(base_model_name_1.BaseModelName.User);
1132
+ const userDoc = await UserModel.findById(provider.idFromString(userId)).session(sess ?? null);
1133
+ if (!userDoc) {
1134
+ throw new suite_core_lib_2.UserNotFoundError();
1135
+ }
1136
+ // Check if email is changing and if it's already in use
1137
+ if (settings.email &&
1138
+ settings.email.toLowerCase() !== userDoc.email.toLowerCase()) {
1139
+ const existingUser = await UserModel.findOne({
1140
+ email: settings.email.toLowerCase(),
1141
+ _id: { $ne: userDoc._id },
1142
+ }).session(sess ?? null);
1143
+ if (existingUser) {
1144
+ throw new suite_core_lib_1.EmailInUseError();
1145
+ }
1146
+ // Send verification email for new address
1147
+ userDoc.email = settings.email.toLowerCase();
1148
+ await this.createAndSendEmailTokenDirect(userDoc, suite_core_lib_1.EmailTokenType.AccountVerification, sess, this.application.environment.debug);
1149
+ }
1150
+ // Update other settings
1151
+ if (settings.timezone !== undefined)
1152
+ userDoc.timezone = settings.timezone;
1153
+ if (settings.siteLanguage !== undefined)
1154
+ userDoc.siteLanguage = settings.siteLanguage;
1155
+ if (settings.darkMode !== undefined)
1156
+ userDoc.darkMode = settings.darkMode;
1157
+ if (settings.currency !== undefined)
1158
+ userDoc.currency = settings.currency;
1159
+ if (settings.directChallenge !== undefined)
1160
+ userDoc.directChallenge = settings.directChallenge;
1161
+ await userDoc.save({ session: sess });
1162
+ const roles = await this.roleService.getUserRoles(userDoc._id);
1163
+ const tokenRoles = this.roleService.rolesToTokenRoles(roles);
1164
+ return request_user_1.RequestUserService.makeRequestUserDTO(userDoc, tokenRoles);
1165
+ }, session, {
1166
+ timeoutMs: this.application.environment.mongo.transactionTimeout * 5,
1167
+ });
1168
+ }
1169
+ /**
1170
+ * Changes the user's password by re-wrapping their master key
1171
+ * @param userId - The ID of the user
1172
+ * @param currentPassword - The current password
1173
+ * @param newPassword - The new password
1174
+ * @param session - The session to use for the query
1175
+ * @returns void
1176
+ */
1177
+ async changePassword(userId, currentPassword, newPassword, session) {
1178
+ const provider = (0, node_ecies_lib_1.getEnhancedNodeIdProvider)();
1179
+ return await this.withTransaction(async (_sess) => {
1180
+ const sess = _sess;
1181
+ const UserModel = model_registry_1.ModelRegistry.instance.getTypedModel(base_model_name_1.BaseModelName.User);
1182
+ const userDoc = await UserModel.findById(provider.idFromString(userId)).session(sess ?? null);
1183
+ if (!userDoc || !userDoc.passwordWrappedPrivateKey) {
1184
+ throw new suite_core_lib_2.UserNotFoundError();
1185
+ }
1186
+ if (!this.application.constants.PasswordRegex.test(newPassword)) {
1187
+ throw new node_express_suite_1.InvalidNewPasswordError();
1188
+ }
1189
+ const currentPasswordSecure = new ecies_lib_1.SecureString(currentPassword);
1190
+ const newPasswordSecure = new ecies_lib_1.SecureString(newPassword);
1191
+ try {
1192
+ // Unwrap existing private key and rewrap under new password
1193
+ const priv = this.keyWrappingService.unwrapSecret(userDoc.passwordWrappedPrivateKey, currentPasswordSecure);
1194
+ try {
1195
+ const wrapped = this.keyWrappingService.wrapSecret(priv, newPasswordSecure, this.application.constants);
1196
+ userDoc.passwordWrappedPrivateKey = wrapped;
1197
+ await userDoc.save({ session: sess });
1198
+ }
1199
+ finally {
1200
+ priv.dispose();
1201
+ }
1202
+ }
1203
+ catch (error) {
1204
+ // Re-throw original error so controller can map it properly
1205
+ // Re-throw original error so controller can map it properly
1206
+ throw error;
1207
+ }
1208
+ finally {
1209
+ currentPasswordSecure.dispose();
1210
+ newPasswordSecure.dispose();
1211
+ }
1212
+ }, session, {
1213
+ timeoutMs: this.application.environment.mongo.transactionTimeout * 5,
1214
+ });
1215
+ }
1216
+ /**
1217
+ * Retrieve an email token by its token string and type
1218
+ * @param token - The token string
1219
+ * @param type - The type of the email token
1220
+ * @param session - The session to use for the query
1221
+ * @returns The email token document or null if not found
1222
+ */
1223
+ async findEmailToken(token, type, session) {
1224
+ const EmailTokenModel = model_registry_1.ModelRegistry.instance.getTypedModel(base_model_name_1.BaseModelName.EmailToken);
1225
+ return await EmailTokenModel.findOne({
1226
+ token: token.toLowerCase().trim(),
1227
+ ...(type ? { type } : {}),
1228
+ expiresAt: { $gt: new Date() },
1229
+ }).session(session ?? null);
1230
+ }
1231
+ /**
1232
+ * Verify email token is valid
1233
+ * @param token - The email token
1234
+ * @param session - The session to use for the query
1235
+ * @returns void
1236
+ */
1237
+ async verifyEmailToken(token, type, session) {
1238
+ return await this.withTransaction(async (_sess) => {
1239
+ const sess = _sess;
1240
+ // Find and validate the token
1241
+ const emailToken = await this.findEmailToken(token, type, sess);
1242
+ if (!emailToken) {
1243
+ throw new suite_core_lib_1.EmailTokenUsedOrInvalidError();
1244
+ }
1245
+ }, session, {
1246
+ timeoutMs: this.application.environment.mongo.transactionTimeout * 5,
1247
+ });
1248
+ }
1249
+ /**
1250
+ * Reset password using email token
1251
+ * @param token - The email token
1252
+ * @param newPassword - The new password
1253
+ * @param session - The session to use for the query
1254
+ * @returns void
1255
+ */
1256
+ async resetPasswordWithToken(token, newPassword, credential, // either mnemonic or current password; required
1257
+ session) {
1258
+ if (!this.application.constants.PasswordRegex.test(newPassword)) {
1259
+ throw new node_express_suite_1.InvalidNewPasswordError();
1260
+ }
1261
+ if (!credential) {
1262
+ throw new suite_core_lib_1.EmailTokenUsedOrInvalidError();
1263
+ }
1264
+ return await this.withTransaction(async (_sess) => {
1265
+ const sess = _sess;
1266
+ const EmailTokenModel = model_registry_1.ModelRegistry.instance.getTypedModel(base_model_name_1.BaseModelName.EmailToken);
1267
+ const UserModel = model_registry_1.ModelRegistry.instance.getTypedModel(base_model_name_1.BaseModelName.User);
1268
+ // Find and validate the token
1269
+ const emailToken = await this.findEmailToken(token, suite_core_lib_1.EmailTokenType.PasswordReset, sess);
1270
+ if (!emailToken) {
1271
+ throw new suite_core_lib_1.EmailTokenUsedOrInvalidError();
1272
+ }
1273
+ // Find the user
1274
+ const userDoc = await UserModel.findById(emailToken.userId).session(sess ?? null);
1275
+ if (!userDoc) {
1276
+ throw new suite_core_lib_2.UserNotFoundError();
1277
+ }
1278
+ // Update password-wrapped secrets based on credential type (Option B)
1279
+ const newPasswordSecure = new ecies_lib_1.SecureString(newPassword);
1280
+ try {
1281
+ if (this.application.constants.MnemonicRegex.test(credential)) {
1282
+ // Credential is mnemonic: verify it belongs to this user via public key
1283
+ const providedMnemonic = new ecies_lib_1.SecureString(credential);
1284
+ try {
1285
+ const { wallet } = this.eciesService.walletAndSeedFromMnemonic(providedMnemonic);
1286
+ const privateKey = wallet.getPrivateKey();
1287
+ // Get compressed public key (already includes prefix)
1288
+ const pub = this.eciesService.getPublicKey(Buffer.from(privateKey));
1289
+ if (pub.toString('hex') !== userDoc.publicKey) {
1290
+ throw new suite_core_lib_1.InvalidCredentialsError();
1291
+ }
1292
+ // Wrap private key with new password
1293
+ const priv = new ecies_lib_1.SecureBuffer(privateKey);
1294
+ try {
1295
+ const wrappedPriv = this.keyWrappingService.wrapSecret(priv, newPasswordSecure, this.application.constants);
1296
+ userDoc.passwordWrappedPrivateKey = wrappedPriv;
1297
+ await userDoc.save({ session: sess });
1298
+ }
1299
+ finally {
1300
+ priv.dispose();
1301
+ }
1302
+ }
1303
+ finally {
1304
+ providedMnemonic.dispose();
1305
+ }
1306
+ }
1307
+ else {
1308
+ // Credential is current password: unwrap existing master key
1309
+ if (!userDoc.passwordWrappedPrivateKey) {
1310
+ throw new suite_core_lib_1.InvalidCredentialsError();
1311
+ }
1312
+ const privateKeyBuf = await this.keyWrappingService.unwrapSecretAsync(userDoc.passwordWrappedPrivateKey, credential, this.application.constants);
1313
+ try {
1314
+ // Re-wrap the existing private key under the new password
1315
+ const wrappedPriv = this.keyWrappingService.wrapSecret(privateKeyBuf, newPasswordSecure, this.application.constants);
1316
+ userDoc.passwordWrappedPrivateKey = wrappedPriv;
1317
+ await userDoc.save({ session: sess });
1318
+ }
1319
+ finally {
1320
+ privateKeyBuf.dispose();
1321
+ }
1322
+ }
1323
+ // Delete the used token
1324
+ await EmailTokenModel.deleteOne({ _id: emailToken._id }).session(sess ?? null);
1325
+ // Dispose temporary master key
1326
+ }
1327
+ finally {
1328
+ newPasswordSecure.dispose();
1329
+ }
1330
+ }, session, {
1331
+ timeoutMs: this.application.environment.mongo.transactionTimeout * 5,
1332
+ });
1333
+ }
1334
+ /**
1335
+ * Generate a login challenge for the client to sign
1336
+ * @returns The login challenge in hex
1337
+ */
1338
+ generateDirectLoginChallenge() {
1339
+ const adminMember = node_express_suite_1.SystemUserService.getSystemUser(this.application.environment, this.application.constants);
1340
+ const time = Buffer.alloc(8);
1341
+ time.writeBigUInt64BE(BigInt(new Date().getTime()));
1342
+ const nonce = (0, crypto_1.randomBytes)(32);
1343
+ const signature = adminMember.sign(Buffer.concat([time, nonce]));
1344
+ return Buffer.concat([time, nonce, signature]).toString('hex');
1345
+ }
1346
+ /**
1347
+ * Verifies a direct login challenge response
1348
+ * @param serverSignedRequest The login challenge response in hex
1349
+ * @param session The mongoose session, if provided
1350
+ * @returns A promise with the user document and user member object
1351
+ */
1352
+ async verifyDirectLoginChallenge(serverSignedRequest, signature, username, email, session) {
1353
+ return await this.withTransaction(async (_sess) => {
1354
+ const sess = _sess;
1355
+ // serverSignedRequest is:
1356
+ // time (8) +
1357
+ // nonce (32) +
1358
+ // server signature (64) +
1359
+ // signature (64)
1360
+ if (serverSignedRequest.length <
1361
+ (8 + 32 + this.application.constants.ECIES.SIGNATURE_SIZE) * 2) {
1362
+ throw new suite_core_lib_1.InvalidChallengeResponseError();
1363
+ }
1364
+ // get signed request into a buffer
1365
+ const requestBuffer = Buffer.from(serverSignedRequest, 'hex');
1366
+ // start tracking offset
1367
+ let offset = 0;
1368
+ // get the time
1369
+ const time = requestBuffer.subarray(offset, 8);
1370
+ offset += 8;
1371
+ // get the nonce
1372
+ const nonce = requestBuffer.subarray(offset, 40);
1373
+ offset += 32;
1374
+ // get the server signature
1375
+ const serverSignature = requestBuffer.subarray(offset, this.application.constants.ECIES.SIGNATURE_SIZE + 40);
1376
+ offset += this.application.constants.ECIES.SIGNATURE_SIZE;
1377
+ const signedDataLength = offset;
1378
+ if (offset !== requestBuffer.length) {
1379
+ throw new suite_core_lib_1.InvalidChallengeResponseError();
1380
+ }
1381
+ // validate time is within acceptable range
1382
+ const timeMs = time.readBigUInt64BE();
1383
+ if (new Date().getTime() - Number(timeMs) >
1384
+ this.application.constants.LoginChallengeExpiration) {
1385
+ throw new suite_core_lib_1.LoginChallengeExpiredError();
1386
+ }
1387
+ // validate the server's signature on the time + nonce portion
1388
+ const adminMember = node_express_suite_1.SystemUserService.getSystemUser(this.application.environment, this.application.constants);
1389
+ if (!adminMember.verify(serverSignature, Buffer.concat([time, nonce]))) {
1390
+ throw new suite_core_lib_1.InvalidChallengeResponseError();
1391
+ }
1392
+ // locate the user by email or username
1393
+ const userDoc = await this.findUser(email, username, sess);
1394
+ if (!userDoc) {
1395
+ throw new suite_core_lib_1.InvalidChallengeResponseError();
1396
+ }
1397
+ // get the user's member object
1398
+ const user = await this.makeUserFromUserDoc(userDoc, undefined, undefined, undefined, undefined, sess);
1399
+ // get the signed portion of the response
1400
+ const signedData = requestBuffer.subarray(0, signedDataLength);
1401
+ // verify the user's signature on the signed portion
1402
+ if (!user.verify(Buffer.from(signature, 'hex'), signedData)) {
1403
+ throw new suite_core_lib_1.InvalidChallengeResponseError();
1404
+ }
1405
+ if (userDoc.directChallenge !== true) {
1406
+ throw new suite_core_lib_1.DirectChallengeNotEnabledError();
1407
+ }
1408
+ // if the user is valid, try to use the token (prevents replay attacks)
1409
+ await direct_login_token_1.DirectLoginTokenService.useToken(this.application, userDoc._id, nonce.toString('hex'));
1410
+ // if successful, update lastLogin
1411
+ await this.updateLastLogin(userDoc._id);
1412
+ // return the user document and member object
1413
+ return { userDoc, userMember: user };
1414
+ }, session, { timeoutMs: this.application.environment.mongo.transactionTimeout });
1415
+ }
1416
+ /**
1417
+ * Request a login link via email
1418
+ * @param email Email address
1419
+ * @param username Username
1420
+ * @param session Existing session, if any
1421
+ * @returns void
1422
+ */
1423
+ async requestEmailLogin(email, username, session) {
1424
+ return this.withTransaction(async (_sess) => {
1425
+ const sess = _sess;
1426
+ const userDoc = await this.findUser(email, username, sess);
1427
+ if (!userDoc) {
1428
+ return;
1429
+ }
1430
+ await this.createAndSendEmailToken(userDoc, suite_core_lib_1.EmailTokenType.LoginRequest, sess, this.application.environment.debug);
1431
+ }, session, {
1432
+ timeoutMs: this.application.environment.mongo.transactionTimeout,
1433
+ });
1434
+ }
1435
+ /**
1436
+ * Validate an email login token challenge
1437
+ * @param token The token to challenge
1438
+ * @param signature The signature of the token by the user's private key
1439
+ * @param session The session to use for the query
1440
+ * @returns The user document if the challenge is valid
1441
+ */
1442
+ async validateEmailLoginTokenChallenge(token, signature, session) {
1443
+ return this.withTransaction(async (_sess) => {
1444
+ const sess = _sess;
1445
+ const emailToken = await this.findEmailToken(token, suite_core_lib_1.EmailTokenType.LoginRequest, sess);
1446
+ if (!emailToken) {
1447
+ throw new suite_core_lib_1.EmailTokenUsedOrInvalidError();
1448
+ }
1449
+ const userDoc = await this.findUser(emailToken.email, undefined, sess);
1450
+ if (!userDoc) {
1451
+ throw new suite_core_lib_2.UserNotFoundError();
1452
+ }
1453
+ const user = await this.makeUserFromUserDoc(userDoc, undefined, undefined, undefined, undefined, sess);
1454
+ const result = user.verify(Buffer.from(signature, 'hex'), Buffer.from(token, 'hex'));
1455
+ if (!result) {
1456
+ throw new suite_core_lib_1.InvalidChallengeResponseError();
1457
+ }
1458
+ await emailToken.deleteOne({ session: sess ?? null });
1459
+ await this.updateLastLogin(userDoc._id);
1460
+ return userDoc;
1461
+ }, session, {
1462
+ timeoutMs: this.application.environment.mongo.transactionTimeout,
1463
+ });
1464
+ }
1465
+ /**
1466
+ * Updates the user's last login time atomically
1467
+ * @param userId - The ID of the user
1468
+ * @returns void
1469
+ */
1470
+ async updateLastLogin(userId) {
1471
+ const UserModel = model_registry_1.ModelRegistry.instance.get('User')?.model;
1472
+ try {
1473
+ // Check if the database connection is still open
1474
+ const connection = this.application.db.connection;
1475
+ if (connection.readyState !== 1) {
1476
+ // Connection is not open (0 = disconnected, 1 = connected, 2 = connecting, 3 = disconnecting)
1477
+ return; // Silently return if connection is not available
1478
+ }
1479
+ // Use atomic update to avoid conflicts and ensure we only update lastLogin
1480
+ // Use a separate session to avoid interfering with any ongoing transactions
1481
+ await UserModel.updateOne({ _id: userId }, {
1482
+ $set: { lastLogin: new Date() },
1483
+ $setOnInsert: {}, // Prevent any unintended document creation
1484
+ }, {
1485
+ upsert: false, // Never create a new document
1486
+ runValidators: false, // Skip validation for performance since we're only updating lastLogin
1487
+ // Don't use any session to avoid transaction conflicts
1488
+ });
1489
+ }
1490
+ catch (error) {
1491
+ // Check if the error is due to client being closed
1492
+ if (error instanceof Error &&
1493
+ (error.message.includes('client was closed') ||
1494
+ error.message.includes('MongoClientClosedError') ||
1495
+ error.name === 'MongoClientClosedError')) {
1496
+ // This is expected during shutdown, don't log it as an error
1497
+ return;
1498
+ }
1499
+ // If this fails, it's not critical for login functionality. Ignore and move on.
1500
+ }
1501
+ }
1502
+ }
1503
+ exports.UserService = UserService;
1504
+ //# sourceMappingURL=user.js.map