@volcanicminds/backend 2.3.2 → 3.0.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 (173) hide show
  1. package/README.md +221 -28
  2. package/dist/index.d.ts +3 -2
  3. package/dist/index.d.ts.map +1 -1
  4. package/dist/index.js +24 -36
  5. package/dist/index.js.map +1 -1
  6. package/dist/lib/api/auth/controller/auth.d.ts.map +1 -1
  7. package/dist/lib/api/auth/controller/auth.js +14 -7
  8. package/dist/lib/api/auth/controller/auth.js.map +1 -1
  9. package/dist/lib/api/health/controller/health.d.ts +1 -1
  10. package/dist/lib/api/health/controller/health.d.ts.map +1 -1
  11. package/dist/lib/api/health/controller/health.js +1 -1
  12. package/dist/lib/api/health/controller/health.js.map +1 -1
  13. package/dist/lib/api/health/routes.d.ts +1 -0
  14. package/dist/lib/api/health/routes.d.ts.map +1 -1
  15. package/dist/lib/api/health/routes.js +1 -0
  16. package/dist/lib/api/health/routes.js.map +1 -1
  17. package/dist/lib/api/tenants/controller/tenants.d.ts +17 -0
  18. package/dist/lib/api/tenants/controller/tenants.d.ts.map +1 -0
  19. package/dist/lib/api/tenants/controller/tenants.js +150 -0
  20. package/dist/lib/api/tenants/controller/tenants.js.map +1 -0
  21. package/dist/lib/api/tenants/routes.d.ts +167 -0
  22. package/dist/lib/api/tenants/routes.d.ts.map +1 -0
  23. package/dist/lib/api/tenants/routes.js +143 -0
  24. package/dist/lib/api/tenants/routes.js.map +1 -0
  25. package/dist/lib/config/plugins.js +1 -1
  26. package/dist/lib/config/plugins.js.map +1 -1
  27. package/dist/lib/database/typeorm/entities/change.d.ts +11 -0
  28. package/dist/lib/database/typeorm/entities/change.d.ts.map +1 -0
  29. package/dist/lib/database/typeorm/entities/change.js +4 -0
  30. package/dist/lib/database/typeorm/entities/change.js.map +1 -0
  31. package/dist/lib/database/typeorm/entities/tenant.d.ts +13 -0
  32. package/dist/lib/database/typeorm/entities/tenant.d.ts.map +1 -0
  33. package/dist/lib/database/typeorm/entities/tenant.js +4 -0
  34. package/dist/lib/database/typeorm/entities/tenant.js.map +1 -0
  35. package/dist/lib/database/typeorm/entities/token.d.ts +16 -0
  36. package/dist/lib/database/typeorm/entities/token.d.ts.map +1 -0
  37. package/dist/lib/database/typeorm/entities/token.js +4 -0
  38. package/dist/lib/database/typeorm/entities/token.js.map +1 -0
  39. package/dist/lib/database/typeorm/entities/user.d.ts +28 -0
  40. package/dist/lib/database/typeorm/entities/user.d.ts.map +1 -0
  41. package/dist/lib/database/typeorm/entities/user.js +4 -0
  42. package/dist/lib/database/typeorm/entities/user.js.map +1 -0
  43. package/dist/lib/database/typeorm/loader/dataBaseManager.d.ts +6 -0
  44. package/dist/lib/database/typeorm/loader/dataBaseManager.d.ts.map +1 -0
  45. package/dist/lib/database/typeorm/loader/dataBaseManager.js +73 -0
  46. package/dist/lib/database/typeorm/loader/dataBaseManager.js.map +1 -0
  47. package/dist/lib/database/typeorm/loader/entities.d.ts +6 -0
  48. package/dist/lib/database/typeorm/loader/entities.d.ts.map +1 -0
  49. package/dist/lib/database/typeorm/loader/entities.js +37 -0
  50. package/dist/lib/database/typeorm/loader/entities.js.map +1 -0
  51. package/dist/lib/database/typeorm/loader/tenantManager.d.ts +17 -0
  52. package/dist/lib/database/typeorm/loader/tenantManager.d.ts.map +1 -0
  53. package/dist/lib/database/typeorm/loader/tenantManager.js +193 -0
  54. package/dist/lib/database/typeorm/loader/tenantManager.js.map +1 -0
  55. package/dist/lib/database/typeorm/loader/tokenManager.d.ts +23 -0
  56. package/dist/lib/database/typeorm/loader/tokenManager.d.ts.map +1 -0
  57. package/dist/lib/database/typeorm/loader/tokenManager.js +131 -0
  58. package/dist/lib/database/typeorm/loader/tokenManager.js.map +1 -0
  59. package/dist/lib/database/typeorm/loader/userManager.d.ts +39 -0
  60. package/dist/lib/database/typeorm/loader/userManager.d.ts.map +1 -0
  61. package/dist/lib/database/typeorm/loader/userManager.js +358 -0
  62. package/dist/lib/database/typeorm/loader/userManager.js.map +1 -0
  63. package/dist/lib/database/typeorm/query/builder.d.ts +2 -0
  64. package/dist/lib/database/typeorm/query/builder.d.ts.map +1 -0
  65. package/dist/lib/database/typeorm/query/builder.js +33 -0
  66. package/dist/lib/database/typeorm/query/builder.js.map +1 -0
  67. package/dist/lib/database/typeorm/query/parser.d.ts +2 -0
  68. package/dist/lib/database/typeorm/query/parser.d.ts.map +1 -0
  69. package/dist/lib/database/typeorm/query/parser.js +27 -0
  70. package/dist/lib/database/typeorm/query/parser.js.map +1 -0
  71. package/dist/lib/database/typeorm/query.d.ts +36 -0
  72. package/dist/lib/database/typeorm/query.d.ts.map +1 -0
  73. package/dist/lib/database/typeorm/query.js +259 -0
  74. package/dist/lib/database/typeorm/query.js.map +1 -0
  75. package/dist/lib/database/typeorm/util/crypto.d.ts +3 -0
  76. package/dist/lib/database/typeorm/util/crypto.d.ts.map +1 -0
  77. package/dist/lib/database/typeorm/util/crypto.js +46 -0
  78. package/dist/lib/database/typeorm/util/crypto.js.map +1 -0
  79. package/dist/lib/database/typeorm/util/error.d.ts +5 -0
  80. package/dist/lib/database/typeorm/util/error.d.ts.map +1 -0
  81. package/dist/lib/database/typeorm/util/error.js +8 -0
  82. package/dist/lib/database/typeorm/util/error.js.map +1 -0
  83. package/dist/lib/database/typeorm/util/logger.d.ts +7 -0
  84. package/dist/lib/database/typeorm/util/logger.d.ts.map +1 -0
  85. package/dist/lib/database/typeorm/util/logger.js +31 -0
  86. package/dist/lib/database/typeorm/util/logger.js.map +1 -0
  87. package/dist/lib/database/typeorm/util/yn.d.ts +2 -0
  88. package/dist/lib/database/typeorm/util/yn.d.ts.map +1 -0
  89. package/dist/lib/database/typeorm/util/yn.js +15 -0
  90. package/dist/lib/database/typeorm/util/yn.js.map +1 -0
  91. package/dist/lib/defaults/managers.d.ts.map +1 -1
  92. package/dist/lib/defaults/managers.js +186 -58
  93. package/dist/lib/defaults/managers.js.map +1 -1
  94. package/dist/lib/hooks/onRequest.d.ts.map +1 -1
  95. package/dist/lib/hooks/onRequest.js +11 -48
  96. package/dist/lib/hooks/onRequest.js.map +1 -1
  97. package/dist/lib/loader/router.d.ts.map +1 -1
  98. package/dist/lib/loader/router.js +92 -83
  99. package/dist/lib/loader/router.js.map +1 -1
  100. package/dist/lib/loader/tenant.d.ts.map +1 -1
  101. package/dist/lib/loader/tenant.js +18 -1
  102. package/dist/lib/loader/tenant.js.map +1 -1
  103. package/dist/lib/locales/en.json +12 -0
  104. package/dist/lib/locales/it.json +12 -0
  105. package/dist/lib/schemas/tenant.d.ts +88 -0
  106. package/dist/lib/schemas/tenant.d.ts.map +1 -0
  107. package/dist/lib/schemas/tenant.js +45 -0
  108. package/dist/lib/schemas/tenant.js.map +1 -0
  109. package/dist/lib/util/regexp.d.ts +2 -0
  110. package/dist/lib/util/regexp.d.ts.map +1 -1
  111. package/dist/lib/util/regexp.js +5 -3
  112. package/dist/lib/util/regexp.js.map +1 -1
  113. package/dist/lib/util/secret.d.ts +11 -0
  114. package/dist/lib/util/secret.d.ts.map +1 -0
  115. package/dist/lib/util/secret.js +70 -0
  116. package/dist/lib/util/secret.js.map +1 -0
  117. package/dist/server.js +4 -1
  118. package/dist/server.js.map +1 -1
  119. package/dist/typeorm.d.ts +15 -0
  120. package/dist/typeorm.d.ts.map +1 -0
  121. package/dist/typeorm.js +86 -0
  122. package/dist/typeorm.js.map +1 -0
  123. package/dist/types/database/typeorm/global.d.ts +56 -0
  124. package/dist/types/database/typeorm/global.d.ts.map +1 -0
  125. package/dist/types/database/typeorm/global.js +2 -0
  126. package/dist/types/database/typeorm/global.js.map +1 -0
  127. package/dist/types/global.d.ts +318 -0
  128. package/dist/types/orm.d.ts +22 -0
  129. package/lib/api/auth/controller/auth.ts +28 -12
  130. package/lib/api/health/controller/health.ts +1 -1
  131. package/lib/api/health/routes.ts +1 -0
  132. package/lib/api/tenants/controller/tenants.ts +211 -0
  133. package/lib/api/tenants/routes.ts +145 -0
  134. package/lib/config/plugins.ts +1 -1
  135. package/lib/database/typeorm/entities/change.ts +11 -0
  136. package/lib/database/typeorm/entities/tenant.ts +20 -0
  137. package/lib/database/typeorm/entities/token.ts +18 -0
  138. package/lib/database/typeorm/entities/user.ts +33 -0
  139. package/lib/database/typeorm/loader/dataBaseManager.ts +89 -0
  140. package/lib/database/typeorm/loader/entities.ts +47 -0
  141. package/lib/database/typeorm/loader/tenantManager.ts +260 -0
  142. package/lib/database/typeorm/loader/tokenManager.ts +157 -0
  143. package/lib/database/typeorm/loader/userManager.ts +432 -0
  144. package/lib/database/typeorm/query/builder.ts +39 -0
  145. package/lib/database/typeorm/query/parser.ts +29 -0
  146. package/lib/database/typeorm/query.ts +326 -0
  147. package/lib/database/typeorm/util/crypto.ts +52 -0
  148. package/lib/database/typeorm/util/error.ts +7 -0
  149. package/lib/database/typeorm/util/logger.ts +35 -0
  150. package/lib/database/typeorm/util/yn.ts +20 -0
  151. package/lib/defaults/managers.ts +191 -64
  152. package/lib/hooks/onRequest.ts +12 -64
  153. package/lib/loader/router.ts +133 -110
  154. package/lib/loader/tenant.ts +29 -13
  155. package/lib/schemas/tenant.ts +49 -0
  156. package/lib/util/regexp.ts +31 -8
  157. package/lib/util/secret.ts +113 -0
  158. package/package.json +54 -13
  159. package/dist/lib/apollo/context.d.ts +0 -6
  160. package/dist/lib/apollo/context.d.ts.map +0 -1
  161. package/dist/lib/apollo/context.js +0 -5
  162. package/dist/lib/apollo/context.js.map +0 -1
  163. package/dist/lib/apollo/resolvers.d.ts +0 -8
  164. package/dist/lib/apollo/resolvers.d.ts.map +0 -1
  165. package/dist/lib/apollo/resolvers.js +0 -8
  166. package/dist/lib/apollo/resolvers.js.map +0 -1
  167. package/dist/lib/apollo/type-defs.d.ts +0 -3
  168. package/dist/lib/apollo/type-defs.d.ts.map +0 -1
  169. package/dist/lib/apollo/type-defs.js +0 -8
  170. package/dist/lib/apollo/type-defs.js.map +0 -1
  171. package/lib/apollo/context.ts +0 -11
  172. package/lib/apollo/resolvers.ts +0 -12
  173. package/lib/apollo/type-defs.ts +0 -9
@@ -0,0 +1,157 @@
1
+ /* eslint-disable no-useless-catch */
2
+ /* eslint-disable @typescript-eslint/no-explicit-any */
3
+ import * as Crypto from 'crypto'
4
+ import { QueryRunner, EntityManager } from 'typeorm'
5
+ import { ServiceError } from '../util/error.js'
6
+ import { executeCountQuery, executeFindQuery } from '../query.js'
7
+
8
+ /**
9
+ * Returns the appropriate token repository.
10
+ * Use this to support both explicit EntityManager (req.db) and QueryRunner.
11
+ */
12
+ function getTokenRepo(context?: QueryRunner | EntityManager) {
13
+ const { multi_tenant } = (global as any).config?.options || {}
14
+
15
+ if (multi_tenant?.enabled && !context) {
16
+ throw new Error('[TokenManager] ⛔️ CRITICAL: Missing DB Context (Runner/Manager) in Multi-Tenant Environment!')
17
+ }
18
+
19
+ // If no context provided (and not multi-tenant), fallback to global
20
+ if (!context) {
21
+ return global.connection.getRepository(global.entity.Token)
22
+ }
23
+
24
+ // Check if it's a QueryRunner (has .manager property)
25
+ if ((context as QueryRunner).manager) {
26
+ return (context as QueryRunner).manager.getRepository(global.entity.Token)
27
+ }
28
+
29
+ // Otherwise treat as EntityManager
30
+ return (context as EntityManager).getRepository(global.entity.Token)
31
+ }
32
+
33
+ export function isImplemented() {
34
+ return true
35
+ }
36
+
37
+ export async function isValidToken(data: typeof global.entity.Token) {
38
+ return !!data && (!!data._id || !!data.id) && !!data.externalId && !!data.name
39
+ }
40
+
41
+ export async function createToken(data: typeof global.entity.Token, runner?: QueryRunner) {
42
+ const { name, description } = data
43
+
44
+ if (!name) {
45
+ throw new ServiceError('Invalid parameters', 400)
46
+ }
47
+
48
+ try {
49
+ const repo = getTokenRepo(runner)
50
+ let externalId, token
51
+ do {
52
+ externalId = Crypto.randomUUID({ disableEntropyCache: true })
53
+ token = await repo.findOneBy({ externalId: externalId })
54
+ } while (token != null)
55
+
56
+ const newToken = repo.create({
57
+ ...data,
58
+ name: name,
59
+ description: description,
60
+ blocked: false,
61
+ blockedReason: null,
62
+ externalId: externalId
63
+ } as typeof global.entity.Token)
64
+
65
+ return repo.save(newToken)
66
+ } catch (error) {
67
+ throw error
68
+ }
69
+ }
70
+
71
+ export async function resetExternalId(id: string, runner?: QueryRunner) {
72
+ if (!id) {
73
+ throw new ServiceError('Invalid parameters', 400)
74
+ }
75
+
76
+ try {
77
+ const repo = getTokenRepo(runner)
78
+ let externalId, token
79
+ do {
80
+ externalId = Crypto.randomUUID({ disableEntropyCache: true })
81
+ token = await repo.findOneBy({ externalId: externalId })
82
+ } while (token != null)
83
+
84
+ return await updateTokenById(id, { externalId: externalId }, runner)
85
+ } catch (error) {
86
+ if (error?.code == 23505) {
87
+ throw new ServiceError('External ID not changed', 409)
88
+ }
89
+ throw error
90
+ }
91
+ }
92
+
93
+ export async function updateTokenById(id: string, token: typeof global.entity.Token, runner?: QueryRunner) {
94
+ if (!id || !token) {
95
+ throw new ServiceError('Invalid parameters', 400)
96
+ }
97
+ try {
98
+ const repo = getTokenRepo(runner)
99
+ const tokenEx = await repo.findOneBy({ id: id })
100
+ if (!tokenEx) {
101
+ throw new ServiceError('Token not found', 404)
102
+ }
103
+ const merged = repo.merge(tokenEx, token)
104
+ return repo.save(merged)
105
+ } catch (error) {
106
+ throw error
107
+ }
108
+ }
109
+
110
+ export async function retrieveTokenById(id: string, runner?: QueryRunner) {
111
+ if (!id) {
112
+ throw new ServiceError('Invalid parameters', 400)
113
+ }
114
+ try {
115
+ return await getTokenRepo(runner).findOneBy({ id: id })
116
+ } catch (error) {
117
+ throw error
118
+ }
119
+ }
120
+
121
+ export async function retrieveTokenByExternalId(externalId: string, runner?: QueryRunner) {
122
+ if (!externalId) {
123
+ throw new ServiceError('Invalid parameters', 400)
124
+ }
125
+ try {
126
+ return await getTokenRepo(runner).findOneBy({ externalId: externalId })
127
+ } catch (error) {
128
+ throw error
129
+ }
130
+ }
131
+
132
+ export async function blockTokenById(id: string, reason: string, runner?: QueryRunner) {
133
+ return updateTokenById(id, { blocked: true, blockedAt: new Date(), blockedReason: reason }, runner)
134
+ }
135
+
136
+ export async function unblockTokenById(id: string, runner?: QueryRunner) {
137
+ return updateTokenById(id, { blocked: false, blockedAt: new Date(), blockedReason: null }, runner)
138
+ }
139
+
140
+ export async function countQuery(data: any, runner?: QueryRunner) {
141
+ return await executeCountQuery(getTokenRepo(runner), data, {})
142
+ }
143
+
144
+ export async function findQuery(data: any, runner?: QueryRunner) {
145
+ return await executeFindQuery(getTokenRepo(runner), {}, data)
146
+ }
147
+
148
+ export async function removeTokenById(id: string, runner?: QueryRunner) {
149
+ if (!id) {
150
+ throw new ServiceError('Invalid parameters', 400)
151
+ }
152
+ try {
153
+ return await getTokenRepo(runner).delete(id)
154
+ } catch (error) {
155
+ throw error
156
+ }
157
+ }
@@ -0,0 +1,432 @@
1
+ /* eslint-disable no-useless-catch */
2
+ /* eslint-disable @typescript-eslint/no-explicit-any */
3
+ import * as bcrypt from 'bcrypt'
4
+ import * as Crypto from 'crypto'
5
+ import { QueryRunner, EntityManager } from 'typeorm'
6
+ import { ServiceError } from '../util/error.js'
7
+ import { executeCountQuery, executeFindQuery } from '../query.js'
8
+ import { encrypt, decrypt } from '../util/crypto.js'
9
+
10
+ /**
11
+ * Returns the appropriate user repository.
12
+ * Use this to support both explicit EntityManager (req.db) and QueryRunner.
13
+ */
14
+ function getUserRepo(context?: QueryRunner | EntityManager) {
15
+ const { multi_tenant } = (global as any).config?.options || {}
16
+
17
+ // Hardening: In Multi-Tenant, we MUST have a context (Runner or Manager).
18
+ // Implicit fallback to global.connection is dangerous.
19
+ if (multi_tenant?.enabled && !context) {
20
+ const errorMsg =
21
+ '[UserManager] ⛔️ CRITICAL: Missing DB Context (Runner/Manager) in Multi-Tenant Environment! Implicit fallback to public schema forbidden.'
22
+ console.error(errorMsg)
23
+ throw new Error(errorMsg)
24
+ }
25
+
26
+ // If no context provided (and not multi-tenant), fallback to global
27
+ if (!context) {
28
+ return global.connection.getRepository(global.entity.User)
29
+ }
30
+
31
+ // Check if it's a QueryRunner (has .manager property)
32
+ if ((context as QueryRunner).manager) {
33
+ return (context as QueryRunner).manager.getRepository(global.entity.User)
34
+ }
35
+
36
+ // Otherwise treat as EntityManager
37
+ return (context as EntityManager).getRepository(global.entity.User)
38
+ }
39
+
40
+ export function isImplemented() {
41
+ return true
42
+ }
43
+
44
+ export async function isValidUser(data: typeof global.entity.User) {
45
+ return !!data && (!!data._id || !!data.id) && !!data.externalId && !!data.email && !!data.password
46
+ }
47
+
48
+ export async function createUser(data: typeof global.entity.User, runner?: QueryRunner) {
49
+ const { username, email, password } = data
50
+
51
+ if (!email || !password) {
52
+ throw new ServiceError('Invalid parameters', 400)
53
+ }
54
+
55
+ const salt = await bcrypt.genSalt(12)
56
+ const hashedPassword = await bcrypt.hash(password, salt)
57
+
58
+ try {
59
+ const repo = getUserRepo(runner)
60
+ let externalId, user
61
+ do {
62
+ externalId = Crypto.randomUUID({ disableEntropyCache: true })
63
+
64
+ user = await repo.findOneBy({ externalId: externalId })
65
+ } while (user != null)
66
+
67
+ const newUser = repo.create({
68
+ ...data,
69
+ passwordChangedAt: new Date(),
70
+ confirmed: false,
71
+ confirmationToken: Crypto.randomBytes(64).toString('hex'),
72
+ blocked: false,
73
+ blockedReason: null,
74
+ externalId: externalId,
75
+ email: email,
76
+ username: username || email,
77
+ password: hashedPassword,
78
+ mfaEnabled: false,
79
+ mfaSecret: null,
80
+ mfaType: 'TOTP',
81
+ mfaRecoveryCodes: []
82
+ } as typeof global.entity.User)
83
+
84
+ return getUserRepo(runner).save(newUser)
85
+ } catch (error) {
86
+ if (error?.code == 23505) {
87
+ throw new ServiceError('Email or username already registered', 409)
88
+ }
89
+ throw error
90
+ }
91
+ }
92
+
93
+ export async function deleteUser(id: string, runner?: QueryRunner) {
94
+ if (!id) {
95
+ throw new ServiceError('Invalid parameters', 400)
96
+ }
97
+
98
+ try {
99
+ const userEx = await retrieveUserById(id, runner)
100
+ if (!userEx) {
101
+ throw new ServiceError('User not found', 404)
102
+ }
103
+
104
+ return getUserRepo(runner).delete(id)
105
+ } catch (error) {
106
+ throw error
107
+ }
108
+ }
109
+
110
+ export async function resetExternalId(id: string, runner?: QueryRunner) {
111
+ if (!id) {
112
+ throw new ServiceError('Invalid parameters', 400)
113
+ }
114
+
115
+ try {
116
+ let externalId, user
117
+ do {
118
+ externalId = Crypto.randomUUID({ disableEntropyCache: true })
119
+ user = await getUserRepo(runner).findOneBy({ externalId: externalId })
120
+ } while (user != null)
121
+
122
+ return await updateUserById(id, { externalId: externalId }, runner)
123
+ } catch (error) {
124
+ if (error?.code == 23505) {
125
+ throw new ServiceError('External ID not changed', 409)
126
+ }
127
+ throw error
128
+ }
129
+ }
130
+
131
+ export async function updateUserById(id: string, user: typeof global.entity.User, runner?: QueryRunner) {
132
+ if (!id || !user) {
133
+ throw new ServiceError('Invalid parameters', 400)
134
+ }
135
+ try {
136
+ const repo = getUserRepo(runner)
137
+ const userEx = await retrieveUserById(id, runner)
138
+ if (!userEx) {
139
+ throw new ServiceError('User not found', 404)
140
+ }
141
+ const merged = repo.merge(userEx, user)
142
+ return repo.save(merged)
143
+ } catch (error) {
144
+ throw error
145
+ }
146
+ }
147
+
148
+ export async function retrieveUserById(id: string, runner?: QueryRunner) {
149
+ if (!id) {
150
+ throw new ServiceError('Invalid parameters', 400)
151
+ }
152
+ try {
153
+ return await getUserRepo(runner).findOneBy({ id: id })
154
+ } catch (error) {
155
+ throw error
156
+ }
157
+ }
158
+
159
+ export async function retrieveUserByEmail(email: string, runner?: QueryRunner) {
160
+ if (!email) {
161
+ throw new ServiceError('Invalid parameters', 400)
162
+ }
163
+ try {
164
+ return await getUserRepo(runner).findOneBy({ email: email })
165
+ } catch (error) {
166
+ throw error
167
+ }
168
+ }
169
+
170
+ export async function retrieveUserByUsername(username: string, runner?: QueryRunner) {
171
+ if (!username) {
172
+ throw new ServiceError('Invalid parameters', 400)
173
+ }
174
+ try {
175
+ return await getUserRepo(runner).findOneBy({ username })
176
+ } catch (error) {
177
+ throw error
178
+ }
179
+ }
180
+
181
+ export async function retrieveUserByConfirmationToken(code: string, runner?: QueryRunner) {
182
+ if (!code) {
183
+ throw new ServiceError('Invalid parameters', 400)
184
+ }
185
+ try {
186
+ return await getUserRepo(runner).findOneBy({ confirmationToken: code })
187
+ } catch (error) {
188
+ throw error
189
+ }
190
+ }
191
+
192
+ export async function retrieveUserByResetPasswordToken(code: string, runner?: QueryRunner) {
193
+ if (!code) {
194
+ throw new ServiceError('Invalid parameters', 400)
195
+ }
196
+ try {
197
+ return await getUserRepo(runner).findOneBy({ resetPasswordToken: code })
198
+ } catch (error) {
199
+ throw error
200
+ }
201
+ }
202
+
203
+ export async function retrieveUserByExternalId(externalId: string, runner?: QueryRunner) {
204
+ if (!externalId) {
205
+ throw new ServiceError('Invalid parameters', 400)
206
+ }
207
+ try {
208
+ return await getUserRepo(runner).findOne({
209
+ where: { externalId: externalId },
210
+ cache: global.cacheTimeout
211
+ })
212
+ } catch (error) {
213
+ throw error
214
+ }
215
+ }
216
+
217
+ // Constant-cost bcrypt hash (cost 12, matching createUser) compared against when
218
+ // the user does not exist, so response time is the same whether or not the email
219
+ // is registered. Prevents user enumeration via timing. The plaintext is irrelevant.
220
+ const DUMMY_PASSWORD_HASH = '$2b$12$4sLKI6Ag4n6KjUBPqA4oJuAthEdYgbwUj7oIR8yj7IekjUCzUFRD2'
221
+
222
+ export async function retrieveUserByPassword(email: string, password: string, runner?: QueryRunner) {
223
+ if (!email || !password) {
224
+ throw new ServiceError('Invalid parameters', 400)
225
+ }
226
+ const user = await getUserRepo(runner).findOneBy({ email: email })
227
+ // Always run a bcrypt comparison (against a dummy hash when the user is missing)
228
+ // to keep the timing constant and avoid leaking whether the email exists.
229
+ const match = await bcrypt.compare(password, user?.password || DUMMY_PASSWORD_HASH)
230
+ return user && match ? user : null
231
+ }
232
+
233
+ export async function changePassword(email: string, password: string, oldPassword: string, runner?: QueryRunner) {
234
+ if (!email || !password || !oldPassword) {
235
+ throw new ServiceError('Invalid parameters', 400)
236
+ }
237
+ try {
238
+ const repo = getUserRepo(runner)
239
+ const user = await repo.findOneBy({ email: email })
240
+ // Guard the null deref (user not found) and keep the bcrypt cost constant
241
+ // regardless of existence, mirroring retrieveUserByPassword.
242
+ const match = await bcrypt.compare(oldPassword, user?.password || DUMMY_PASSWORD_HASH)
243
+ if (user && match) {
244
+ const salt = await bcrypt.genSalt(12)
245
+ const hashedPassword = await bcrypt.hash(password, salt)
246
+ return repo.save({ ...user, passwordChangedAt: new Date(), password: hashedPassword })
247
+ }
248
+ throw new ServiceError('Password not changed', 400)
249
+ } catch (error) {
250
+ throw error
251
+ }
252
+ }
253
+
254
+ export async function forgotPassword(email: string, runner?: QueryRunner) {
255
+ if (!email) {
256
+ throw new ServiceError('Invalid parameters', 400)
257
+ }
258
+ try {
259
+ const repo = getUserRepo(runner)
260
+ const user = await repo.findOneBy({ email: email })
261
+
262
+ if (user) {
263
+ return repo.save({
264
+ ...user,
265
+ resetPasswordTokenAt: new Date(),
266
+ resetPasswordToken: Crypto.randomBytes(64).toString('hex')
267
+ })
268
+ }
269
+ throw new ServiceError('Password not changed', 400)
270
+ } catch (error) {
271
+ throw error
272
+ }
273
+ }
274
+
275
+ export async function resetPassword(user: typeof global.entity.User, password: string, runner?: QueryRunner) {
276
+ if (!user || !password || !user?.email) {
277
+ throw new ServiceError('Invalid parameters', 400)
278
+ }
279
+ try {
280
+ const repo = getUserRepo(runner)
281
+ const userEx = await repo.findOneBy({ email: user.email })
282
+ if (!userEx) {
283
+ throw new Error('Wrong credentials')
284
+ }
285
+
286
+ const salt = await bcrypt.genSalt(12)
287
+ const hashedPassword = await bcrypt.hash(password, salt)
288
+ return repo.save({
289
+ ...userEx,
290
+ passwordChangedAt: new Date(),
291
+ confirmed: true,
292
+ confirmedAt: new Date(),
293
+ resetPasswordToken: null,
294
+ password: hashedPassword
295
+ })
296
+ } catch (error) {
297
+ throw error
298
+ }
299
+ }
300
+
301
+ export async function userConfirmation(user: typeof global.entity.User, runner?: QueryRunner) {
302
+ if (!user) {
303
+ throw new ServiceError('Invalid parameters', 400)
304
+ }
305
+ try {
306
+ return getUserRepo(runner).save({ ...user, confirmed: true, confirmedAt: new Date(), confirmationToken: null })
307
+ } catch (error) {
308
+ throw error
309
+ }
310
+ }
311
+
312
+ export async function blockUserById(id: string, reason: string, runner?: QueryRunner) {
313
+ return updateUserById(id, { blocked: true, blockedAt: new Date(), blockedReason: reason }, runner)
314
+ }
315
+
316
+ export async function unblockUserById(id: string, runner?: QueryRunner) {
317
+ return updateUserById(id, { blocked: false, blockedAt: new Date(), blockedReason: null }, runner)
318
+ }
319
+
320
+ export function isPasswordToBeChanged(user: typeof global.entity.User) {
321
+ if (process.env.PASSWORD_EXPIRATION_DAYS != null) {
322
+ let passwordExpirationDays = -1
323
+ try {
324
+ passwordExpirationDays = Number(process.env.PASSWORD_EXPIRATION_DAYS)
325
+ if (passwordExpirationDays <= 0) {
326
+ throw new Error('PASSWORD_EXPIRATION_DAYS_ENV_INVALID')
327
+ }
328
+ } catch (e) {
329
+ // `e` is already an Error; rethrow it as-is. `new Error(e)` stringified the
330
+ // Error into a useless "[object …]" / doubly-wrapped message.
331
+ throw e
332
+ }
333
+ const { passwordChangedAt } = user
334
+ const date1 = new Date(passwordChangedAt)
335
+ const date2 = new Date()
336
+ const differenceInTime = date2.getTime() - date1.getTime()
337
+ const differenceInDays = differenceInTime / (1000 * 3600 * 24)
338
+
339
+ return differenceInDays >= passwordExpirationDays
340
+ }
341
+
342
+ return false
343
+ }
344
+
345
+ export async function countQuery(data: any, runner?: QueryRunner) {
346
+ return await executeCountQuery(getUserRepo(runner), data)
347
+ }
348
+
349
+ export async function findQuery(data: any, runner?: QueryRunner) {
350
+ return await executeFindQuery(getUserRepo(runner), {}, data)
351
+ }
352
+
353
+ export async function disableUserById(id: string, runner?: QueryRunner) {
354
+ await updateUserById(
355
+ id,
356
+ { blocked: true, blockedAt: new Date(), blockedReason: 'User disabled to unregister' },
357
+ runner
358
+ )
359
+ return resetExternalId(id, runner)
360
+ }
361
+
362
+ // MFA Persistence Methods
363
+
364
+ export async function saveMfaSecret(userId: string, secret: string, runner?: QueryRunner) {
365
+ if (!userId || !secret) {
366
+ throw new ServiceError('Invalid parameters', 400)
367
+ }
368
+ const encryptedSecret = encrypt(secret)
369
+
370
+ await updateUserById(
371
+ userId,
372
+ {
373
+ mfaSecret: encryptedSecret,
374
+ mfaType: 'TOTP'
375
+ },
376
+ runner
377
+ )
378
+ return true
379
+ }
380
+
381
+ export async function retrieveMfaSecret(userId: string, runner?: QueryRunner) {
382
+ if (!userId) throw new ServiceError('Invalid parameters', 400)
383
+
384
+ const user = await getUserRepo(runner)
385
+ .createQueryBuilder('user')
386
+ .addSelect('user.mfaSecret')
387
+ .where('user.id = :id', { id: userId })
388
+ .getOne()
389
+
390
+ if (!user || !user.mfaSecret) return null
391
+
392
+ return decrypt(user.mfaSecret)
393
+ }
394
+
395
+ export async function enableMfa(userId: string, runner?: QueryRunner) {
396
+ if (!userId) throw new ServiceError('Invalid parameters', 400)
397
+ return await updateUserById(userId, { mfaEnabled: true }, runner)
398
+ }
399
+
400
+ export async function disableMfa(userId: string, runner?: QueryRunner) {
401
+ if (!userId) throw new ServiceError('Invalid parameters', 400)
402
+ return await updateUserById(userId, { mfaEnabled: false, mfaSecret: null, mfaRecoveryCodes: [] }, runner)
403
+ }
404
+
405
+ export async function forceDisableMfaForAdmin(email: string, runner?: QueryRunner) {
406
+ if (!email) return false
407
+
408
+ const user = await getUserRepo(runner).findOneBy({ email: email })
409
+ if (!user) {
410
+ return false
411
+ }
412
+
413
+ // Verify admin role (supports array of strings)
414
+ const hasAdmin =
415
+ user.roles && (user.roles.includes('admin') || user.roles.some((r) => r === 'admin' || r.code === 'admin'))
416
+
417
+ if (!hasAdmin) {
418
+ return false
419
+ }
420
+
421
+ await updateUserById(
422
+ user.id,
423
+ {
424
+ mfaEnabled: false,
425
+ mfaSecret: null,
426
+ mfaRecoveryCodes: []
427
+ },
428
+ runner
429
+ )
430
+
431
+ return true
432
+ }
@@ -0,0 +1,39 @@
1
+ /* eslint-disable @typescript-eslint/no-explicit-any */
2
+ const combineSqlAnd = (left, right) => {
3
+ if (Array.isArray(left) || Array.isArray(right)) {
4
+ throw new Error('Combining OR conditions with AND is not supported in this simplified SQL builder.')
5
+ }
6
+ return { ...left, ...right }
7
+ }
8
+
9
+ export const buildWhereFromAst = (ast: any, aliasMap: Map<string, any>, isMongo: boolean) => {
10
+ if (!ast) {
11
+ return {}
12
+ }
13
+
14
+ if (ast.type === 'operand') {
15
+ if (!aliasMap.has(ast.value)) {
16
+ throw new Error(`Alias "${ast.value}" used in _logic not found in query parameters.`)
17
+ }
18
+ return aliasMap.get(ast.value)
19
+ }
20
+
21
+ const left = buildWhereFromAst(ast.left, aliasMap, isMongo)
22
+ const right = buildWhereFromAst(ast.right, aliasMap, isMongo)
23
+
24
+ if (ast.type === 'AND') {
25
+ if (isMongo) {
26
+ return { $and: [left, right].flat() }
27
+ }
28
+ return combineSqlAnd(left, right)
29
+ }
30
+
31
+ if (ast.type === 'OR') {
32
+ if (isMongo) {
33
+ return { $or: [left, right].flat() }
34
+ }
35
+ return [left, right].flat()
36
+ }
37
+
38
+ return {}
39
+ }
@@ -0,0 +1,29 @@
1
+ export const parseLogicExpression = (logic: string) => {
2
+ const tokens = logic.match(/\(|\)|[A-Za-z0-9_:]+|\s*(AND|OR)\s*/g)?.filter((t) => t.trim()) || []
3
+ let pos = 0
4
+
5
+ const parseExpression = () => {
6
+ let left = parseTerm()
7
+ while (pos < tokens.length && (tokens[pos].trim() === 'OR' || tokens[pos].trim() === 'AND')) {
8
+ const operator = tokens[pos++].trim()
9
+ const right = parseTerm()
10
+ left = { type: operator, left, right }
11
+ }
12
+ return left
13
+ }
14
+
15
+ const parseTerm = () => {
16
+ if (tokens[pos] === '(') {
17
+ pos++
18
+ const node = parseExpression()
19
+ if (tokens[pos] !== ')') {
20
+ throw new Error('Mismatched parentheses in _logic expression')
21
+ }
22
+ pos++
23
+ return node
24
+ }
25
+ return { type: 'operand', value: tokens[pos++] }
26
+ }
27
+
28
+ return parseExpression()
29
+ }