@volcanicminds/backend 2.3.1 → 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 +240 -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 +25 -2
  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 +37 -14
  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 @@
1
+ {"version":3,"file":"global.js","sourceRoot":"","sources":["../../../../types/database/typeorm/global.ts"],"names":[],"mappings":""}
@@ -0,0 +1,318 @@
1
+ /* eslint-disable @typescript-eslint/no-explicit-any */
2
+ /* eslint-disable @typescript-eslint/no-empty-object-type */
3
+ import { FastifyRequest, FastifyReply } from 'fastify'
4
+ export { FastifyInstance } from 'fastify'
5
+ import { EntityManager } from 'typeorm'
6
+ import { MfaPolicy } from '../lib/config/constants.js'
7
+ import { VQuery, VFindResult, VHeaders } from './orm.js'
8
+
9
+ export { MfaPolicy, VQuery, VFindResult, VHeaders }
10
+
11
+ export interface AuthenticatedUser {
12
+ getId(): any
13
+ username: string
14
+ email: string
15
+ roles: Role[]
16
+ externalId: string
17
+ mfaEnabled?: boolean
18
+ }
19
+
20
+ export interface AuthenticatedToken {
21
+ getId(): any
22
+ name: string
23
+ roles: Role[]
24
+ }
25
+
26
+ export interface Role {
27
+ code: string
28
+ name: string
29
+ description: string
30
+ }
31
+
32
+ export interface Roles {
33
+ [option: string]: Role
34
+ }
35
+
36
+ export interface Data {
37
+ [option: string]: any
38
+ }
39
+
40
+ export interface RouteConfig {
41
+ title: string
42
+ description: string
43
+ enable: boolean
44
+ deprecated: boolean
45
+ tenantContext?: boolean
46
+ tags?: string[]
47
+ version: string
48
+ security?: any
49
+ params?: any
50
+ query?: any
51
+ body?: any
52
+ response?: any
53
+ consumes?: any
54
+ rawBody?: boolean
55
+ }
56
+
57
+ export interface Route {
58
+ method: string
59
+ path: string
60
+ handler: string
61
+ roles: Role[]
62
+ middlewares: string[]
63
+ config?: RouteConfig
64
+ rateLimit?: any
65
+ }
66
+
67
+ export interface GeneralConfig {
68
+ name: string
69
+ options: {
70
+ allow_multiple_admin: boolean
71
+ admin_can_change_passwords: boolean
72
+ allow_admin_change_password_users: boolean
73
+ reset_external_id_on_login: boolean
74
+ scheduler: boolean
75
+ embedded_auth: boolean
76
+ // MFA Configs
77
+ mfa_policy?: MfaPolicy | string
78
+ mfa_admin_forced_reset_email?: string
79
+ mfa_admin_forced_reset_until?: string
80
+ // Multi-Tenant Configs
81
+ multi_tenant?: {
82
+ enabled: boolean
83
+ resolver?: 'subdomain' | 'header' | 'query'
84
+ header_key?: string
85
+ query_key?: string
86
+ }
87
+ }
88
+ }
89
+
90
+ export interface JobSchedule {
91
+ active: boolean // boolean (required)
92
+ type?: string // cron|interval, default: interval
93
+ async?: boolean // boolean, default: true
94
+ preventOverrun?: boolean // boolean, default: true
95
+
96
+ cron?: {
97
+ expression?: string // required if type = 'cron', use cron syntax (if not specified cron will be disabled)
98
+ timezone?: string // optional, like "Europe/Rome" (to test)
99
+ }
100
+
101
+ interval?: {
102
+ days?: number // number, default 0
103
+ hours?: number // number, default 0
104
+ minutes?: number // number, default 0
105
+ seconds?: number // number, default 0
106
+ milliseconds?: number // number, default 0
107
+ runImmediately?: boolean // boolean, default: false
108
+ }
109
+ }
110
+
111
+ export interface ConfiguredRoute {
112
+ enable: boolean
113
+ tenantContext: boolean
114
+ method: any
115
+ path: string
116
+ handler: any
117
+ rawBody: boolean
118
+ rateLimit: any
119
+ file: string
120
+ func: any
121
+ base: string
122
+ middlewares: string[]
123
+ roles: Role[]
124
+ doc: {
125
+ summary?: string
126
+ description?: string
127
+ deprecated?: boolean
128
+ tags?: string[]
129
+ version?: string
130
+ security?: any
131
+ params?: any
132
+ querystring?: any
133
+ body?: any
134
+ response?: any
135
+ consumes?: any
136
+ }
137
+ }
138
+
139
+ export interface TrackChanges {
140
+ enable: boolean
141
+ method: string
142
+ path: string
143
+ entity: string
144
+ changeEntity: string
145
+ fields?: {
146
+ includes?: string[] | null
147
+ excludes?: string[] | null
148
+ } | null
149
+ primaryKey?: string | null
150
+ }
151
+
152
+ export interface TrackChangesList {
153
+ [option: string]: TrackChanges
154
+ }
155
+
156
+ export interface UserManagement {
157
+ isImplemented(): boolean
158
+ isValidUser(data: any): boolean
159
+ createUser(data: any): any | null
160
+ deleteUser(data: any): any | null
161
+ resetExternalId(data: any): any | null
162
+ updateUserById(id: string, user: any): any | null
163
+ retrieveUserById(id: string): any | null
164
+ retrieveUserByEmail(email: string): any | null
165
+ retrieveUserByResetPasswordToken(code: string): any | null
166
+ retrieveUserByConfirmationToken(code: string): any | null
167
+ retrieveUserByUsername(username: string): any | null
168
+ retrieveUserByExternalId(externalId: string): any | null
169
+ retrieveUserByPassword(email: string, password: string): any | null
170
+ changePassword(email: string, password: string, oldPassword: string): any | null
171
+ forgotPassword(email: string): any | null
172
+ resetPassword(user: any, password: string): any | null
173
+ userConfirmation(user: any)
174
+ blockUserById(id: string, reason: string): any | null
175
+ unblockUserById(id: string): any | null
176
+ countQuery(data: VQuery): any | null
177
+ findQuery(data: VQuery): VFindResult<any> | null
178
+ disableUserById(id: string): any | null
179
+
180
+ // MFA Persistence Methods
181
+ saveMfaSecret(userId: string, secret: string): Promise<boolean>
182
+ retrieveMfaSecret(userId: string): Promise<string | null>
183
+ enableMfa(userId: string): Promise<boolean>
184
+ disableMfa(userId: string): Promise<boolean>
185
+
186
+ // Emergency Reset
187
+ forceDisableMfaForAdmin(email: string): Promise<boolean>
188
+ }
189
+
190
+ export interface TokenManagement {
191
+ isImplemented(): boolean
192
+ isValidToken(data: any): boolean
193
+ createToken(data: any): any | null
194
+ resetExternalId(id: string): any | null
195
+ updateTokenById(id: string, token: any): any | null
196
+ retrieveTokenById(id: string): any | null
197
+ retrieveTokenByExternalId(id: string): any | null
198
+ blockTokenById(id: string, reason: string): any | null
199
+ unblockTokenById(id: string): any | null
200
+ countQuery(data: VQuery): any | null
201
+ findQuery(data: VQuery): VFindResult<any> | null
202
+ removeTokenById(id: string): any | null
203
+ }
204
+
205
+ export interface DataBaseManagement {
206
+ isImplemented(): boolean
207
+ synchronizeSchemas(): any | null
208
+ retrieveBy(entityName, entityId): any | null
209
+ addChange(entityName, entityId, status, userId, contents, changeEntity): any | null
210
+ }
211
+
212
+ export interface MfaManagement {
213
+ generateSetup(appName: string, email: string): Promise<{ secret: string; uri: string; qrCode: string }>
214
+ verify(token: string, secret: string): boolean
215
+ }
216
+
217
+ // Callback type signature: (uploadOrId, req, res) => void
218
+ export type TransferCallback = (data: any, req: any, res: any) => void
219
+
220
+ export interface TransferManagement {
221
+ isImplemented(): boolean
222
+ getPath(): string
223
+ getServer(): any
224
+ onUploadCreate(callback: TransferCallback): void
225
+ onUploadFinish(callback: TransferCallback): void
226
+ onUploadTerminate(callback: TransferCallback): void
227
+ handle(req: any, res: any): Promise<void>
228
+ isValid(req: FastifyRequest): Promise<boolean>
229
+ }
230
+
231
+ export interface TenantManagement {
232
+ isImplemented(): boolean
233
+ resolveTenant(req: FastifyRequest): Promise<any | null>
234
+ switchContext(tenant: any, db?: EntityManager): Promise<void>
235
+ createTenant?(data: any): Promise<void>
236
+ deleteTenant?(id: string): Promise<void>
237
+ listTenants?(): Promise<any[]>
238
+ getTenant?(id: string): Promise<any | null>
239
+ updateTenant?(id: string, data: any): Promise<any | null>
240
+ restoreTenant?(id: string): Promise<any | null>
241
+ }
242
+
243
+ declare module 'fastify' {
244
+ export interface FastifyRequest {
245
+ user?: AuthenticatedUser
246
+ token?: AuthenticatedToken
247
+ startedAt?: Date
248
+ data(): Data & VQuery
249
+ parameters(): Data
250
+ roles(): string[]
251
+ hasRole(role: Role): boolean
252
+ payloadSize?: number
253
+ trackingData?: any
254
+ runner?: any
255
+ tenant?: any
256
+ /**
257
+ * The Tenant-Aware EntityManager for this request.
258
+ * MUST be used for all DB operations within this request scope.
259
+ */
260
+ db?: EntityManager
261
+ /** Raw request body, populated by `fastify-raw-body` when enabled on the route. */
262
+ rawBody?: string | Buffer
263
+ /** Multipart helpers, populated by `@fastify/multipart`. */
264
+ isMultipart(): boolean
265
+ file(options?: any): Promise<any>
266
+ files(options?: any): AsyncIterableIterator<any>
267
+ }
268
+ export interface FastifyReply {
269
+ payloadSize?: number
270
+ }
271
+ }
272
+
273
+ export interface FastifyRequest extends FastifyRequest {
274
+ user?: AuthenticatedUser
275
+ token?: AuthenticatedToken
276
+ startedAt?: Date
277
+ data(): Data & VQuery
278
+ parameters(): Data
279
+ roles(): string[]
280
+ hasRole(role: Role): boolean
281
+ payloadSize?: number
282
+ trackingData?: any
283
+ runner?: any
284
+ tenant?: any
285
+ /**
286
+ * The Tenant-Aware EntityManager for this request.
287
+ * MUST be used for all DB operations within this request scope.
288
+ */
289
+ db?: EntityManager
290
+ /** Raw request body, populated by `fastify-raw-body` when enabled on the route. */
291
+ rawBody?: string | Buffer
292
+ /** Multipart helpers, populated by `@fastify/multipart`. */
293
+ isMultipart(): boolean
294
+ file(options?: any): Promise<any>
295
+ files(options?: any): AsyncIterableIterator<any>
296
+ }
297
+
298
+ export interface FastifyReply extends FastifyReply {
299
+ payloadSize?: number
300
+ }
301
+
302
+ export interface global {}
303
+
304
+ declare global {
305
+ var log: any
306
+ var server: any
307
+ var config: GeneralConfig
308
+ var transferConfig: TransferConfig
309
+ var transferPath: string | null
310
+ var roles: Roles
311
+ var tracking: TrackChangesList
312
+ var trackingConfig: Data
313
+ var connection: any
314
+ var entity: any
315
+ var repository: any
316
+ }
317
+
318
+ export { global }
@@ -0,0 +1,22 @@
1
+ export interface VQuery {
2
+ page?: number
3
+ pageSize?: number
4
+ take?: number
5
+ skip?: number
6
+ sort?: string[] | string
7
+ _logic?: string
8
+ [key: string]: unknown
9
+ }
10
+
11
+ export interface VHeaders {
12
+ 'v-count': number
13
+ 'v-total': number
14
+ 'v-page': number
15
+ 'v-pageSize': number
16
+ 'v-pageCount': number
17
+ }
18
+
19
+ export interface VFindResult<T> {
20
+ records: T[]
21
+ headers: VHeaders
22
+ }
@@ -3,6 +3,10 @@ import { FastifyReply, FastifyRequest } from 'fastify'
3
3
  import * as regExp from '../../../util/regexp.js'
4
4
  import { MfaPolicy } from '../../../config/constants.js'
5
5
 
6
+ // Upper bound for the password accepted at login: a cheap guard against oversized
7
+ // payloads. Complexity is enforced only when a password is set, not at login.
8
+ const MAX_PASSWORD_LENGTH = 256
9
+
6
10
  export async function register(req: FastifyRequest, reply: FastifyReply) {
7
11
  const { password1: password, password2, ...data } = req.data()
8
12
 
@@ -13,7 +17,7 @@ export async function register(req: FastifyRequest, reply: FastifyReply) {
13
17
  if (!data.username) {
14
18
  return reply.status(400).send(new Error('Username not valid'))
15
19
  }
16
- if (!data.email || !regExp.email.test(data.email)) {
20
+ if (!regExp.isEmail(data.email)) {
17
21
  return reply.status(400).send(new Error('Email not valid'))
18
22
  }
19
23
  if (!password || !regExp.password.test(password)) {
@@ -136,7 +140,7 @@ export async function forgotPassword(req: FastifyRequest, reply: FastifyReply) {
136
140
  throw new Error('Not implemented')
137
141
  }
138
142
 
139
- if (!username && (!email || (email && !regExp.email.test(email)))) {
143
+ if (!username && !regExp.isEmail(email)) {
140
144
  return reply.status(400).send(new Error('Missing a valid user identifier'))
141
145
  }
142
146
 
@@ -226,10 +230,15 @@ export async function login(req: FastifyRequest, reply: FastifyReply) {
226
230
  throw new Error('Not implemented')
227
231
  }
228
232
 
229
- if (!email || !regExp.email.test(email)) {
233
+ if (!regExp.isEmail(email)) {
230
234
  return reply.status(400).send(new Error('Email not valid'))
231
235
  }
232
- if (!password || !regExp.password.test(password)) {
236
+ // At login we do NOT re-validate the password complexity policy: the password
237
+ // was already validated when it was set (register/change/reset), and bcrypt is
238
+ // the actual security gate. Re-checking the policy here adds no security and
239
+ // would lock out existing users whenever the policy changes. We only bound the
240
+ // input length as a cheap guard against oversized payloads.
241
+ if (!password || password.length > MAX_PASSWORD_LENGTH) {
233
242
  return reply.status(400).send(new Error('Password not valid'))
234
243
  }
235
244
 
@@ -241,7 +250,7 @@ export async function login(req: FastifyRequest, reply: FastifyReply) {
241
250
  const isValid = await req.server['userManager'].isValidUser(user)
242
251
 
243
252
  if (!isValid) {
244
- return reply.status(403).send(new Error('Wrong credentials'))
253
+ return reply.status(403).send(new Error('Invalid user'))
245
254
  }
246
255
 
247
256
  if (!(user.confirmed === true)) {
@@ -333,10 +342,21 @@ export async function refreshToken(req: FastifyRequest, reply: FastifyReply) {
333
342
  throw new Error('Not implemented')
334
343
  }
335
344
 
336
- const tokenData = (await reply.server.jwt.decode(token)) as { sub: number }
337
- const minAccettable = Math.floor(Date.now() / 1000) - 2592000 // 30 days
345
+ // Verify the signature of the (possibly expired) access token: `ignoreExpiration`
346
+ // lets a stale token through which is the whole point of refresh — but a forged
347
+ // or tampered token is now rejected (previously `decode` skipped signature checks).
348
+ let tokenData: { sub: number; iat?: number }
349
+ try {
350
+ tokenData = (await reply.server.jwt.verify(token, { ignoreExpiration: true })) as { sub: number; iat?: number }
351
+ } catch {
352
+ return reply.status(403).send(new Error('Invalid token'))
353
+ }
338
354
 
339
- if (tokenData?.sub > 0 && tokenData?.sub > minAccettable) {
355
+ // Reject refresh of access tokens issued too long ago. Use the real temporal
356
+ // claim (`iat`), not `sub` (the externalId): the old check compared a user id
357
+ // against a unix timestamp and was effectively dead code.
358
+ const minAccettable = Math.floor(Date.now() / 1000) - 2592000 // 30 days
359
+ if (!tokenData?.iat || tokenData.iat < minAccettable) {
340
360
  return reply.status(403).send(new Error('Token too old'))
341
361
  }
342
362
 
@@ -369,8 +389,6 @@ export async function invalidateTokens(req: FastifyRequest, reply: FastifyReply)
369
389
  return { ok: isValid }
370
390
  }
371
391
 
372
-
373
-
374
392
  export async function mfaSetup(req: FastifyRequest, reply: FastifyReply) {
375
393
  const user = req.user
376
394
  if (!user) return reply.status(401).send(new Error('Unauthorized'))
@@ -499,5 +517,3 @@ export async function mfaDisable(req: FastifyRequest, reply: FastifyReply) {
499
517
  return reply.status(500).send(new Error('Failed to disable MFA'))
500
518
  }
501
519
  }
502
-
503
-
@@ -1,5 +1,5 @@
1
1
  import { FastifyReply, FastifyRequest } from 'fastify'
2
2
 
3
- export function check(req: FastifyRequest, reply: FastifyReply) {
3
+ export function check(_req: FastifyRequest, reply: FastifyReply) {
4
4
  reply.send({ ok: true })
5
5
  }
@@ -15,6 +15,7 @@ export default {
15
15
  config: {
16
16
  title: 'Health check service',
17
17
  description: 'Health check service',
18
+ tenantContext: false,
18
19
  response: {
19
20
  200: { $ref: 'defaultResponse#' }
20
21
  }
@@ -0,0 +1,211 @@
1
+ import { FastifyReply, FastifyRequest } from 'fastify'
2
+
3
+ /*
4
+ * S12 — schema name hardening for `SET search_path`.
5
+ * Postgres identifiers cannot be parameterized, so the schema name is always
6
+ * string-interpolated. Mirror the canonical sanitization used by
7
+ * `@volcanicminds/typeorm`'s `tenantManager.switchContext`
8
+ * (`schema.replace(/[^a-z0-9_]/gi, '')`): strip anything outside the safe
9
+ * identifier alphabet. A legit schema name is unaffected; a tampered/malformed
10
+ * value collapses to empty and is rejected fail-fast instead of being injected.
11
+ */
12
+ export function sanitizeSchemaName(schema: string): string {
13
+ return (schema || '').replace(/[^a-z0-9_]/gi, '')
14
+ }
15
+
16
+ export async function list(req: FastifyRequest, reply: FastifyReply) {
17
+ const tm = req.server['tenantManager']
18
+ if (!tm.isImplemented()) return reply.status(501).send()
19
+
20
+ const tenants = await tm.listTenants()
21
+ return reply.send(tenants)
22
+ }
23
+
24
+ export async function create(req: FastifyRequest, reply: FastifyReply) {
25
+ const tm = req.server['tenantManager']
26
+ if (!tm.isImplemented()) return reply.status(501).send()
27
+
28
+ const data = req.data()
29
+ // Provisioning is handled by the manager implementation
30
+ const tenant = await tm.createTenant(data)
31
+
32
+ // Return created
33
+ return reply.code(201).send(tenant)
34
+ }
35
+
36
+ export async function findOne(req: FastifyRequest, reply: FastifyReply) {
37
+ const tm = req.server['tenantManager']
38
+ if (!tm.isImplemented()) return reply.status(501).send()
39
+
40
+ const { id } = req.parameters()
41
+ const tenant = await tm.getTenant(id)
42
+ if (!tenant) return reply.status(404).send()
43
+ return reply.send(tenant)
44
+ }
45
+
46
+ export async function update(req: FastifyRequest, reply: FastifyReply) {
47
+ const tm = req.server['tenantManager']
48
+ if (!tm.isImplemented()) return reply.status(501).send()
49
+
50
+ const { id } = req.parameters()
51
+ const data = req.data()
52
+ const tenant = await tm.updateTenant(id, data)
53
+ if (!tenant) return reply.status(404).send()
54
+ return reply.send(tenant)
55
+ }
56
+
57
+ export async function remove(req: FastifyRequest, reply: FastifyReply) {
58
+ const tm = req.server['tenantManager']
59
+ if (!tm.isImplemented()) return reply.status(501).send()
60
+
61
+ const { id } = req.parameters()
62
+ await tm.deleteTenant(id)
63
+ return reply.send({ message: 'Tenant archived/deleted', id })
64
+ }
65
+
66
+ export async function restore(req: FastifyRequest, reply: FastifyReply) {
67
+ const tm = req.server['tenantManager']
68
+ if (!tm.isImplemented()) return reply.status(501).send()
69
+
70
+ const { id } = req.parameters()
71
+ const tenant = await tm.restoreTenant(id)
72
+ return reply.send({ message: 'Tenant restored', tenant })
73
+ }
74
+
75
+ // Helper: Resolve Target Tenant
76
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
77
+ async function resolveTargetTenant(targetTenantSlug: string, targetTenantId: string, reqTenant: any) {
78
+ let resolvedSlug = targetTenantSlug
79
+ if (!resolvedSlug && !targetTenantId && reqTenant) {
80
+ resolvedSlug = reqTenant.slug
81
+ }
82
+
83
+ const Tenant = global.entity?.Tenant || global.connection.getRepository('Tenant').target
84
+ const tenantRepo = global.connection.getRepository(Tenant)
85
+
86
+ return tenantRepo.findOne({
87
+ where: [
88
+ { slug: resolvedSlug, status: 'active' },
89
+ { id: targetTenantId, status: 'active' }
90
+ ]
91
+ })
92
+ }
93
+
94
+ // Helper: Security Check
95
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
96
+ function checkImpersonationSecurity(req: FastifyRequest, targetTenant: any): boolean {
97
+ let isSystemAdmin = false
98
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
99
+ // @ts-ignore
100
+ if (req.user?.tenantId === 'system' || req.tenant?.slug === 'system') {
101
+ isSystemAdmin = true
102
+ }
103
+
104
+ let isTenantAdminForTarget = false
105
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
106
+ // @ts-ignore
107
+ if (req.tenant?.id === targetTenant.id || req.tenant?.slug === targetTenant.slug) {
108
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
109
+ // @ts-ignore
110
+ const userRoles = typeof req.roles === 'function' ? req.roles() : req.user?.roles || []
111
+ const roleCodes = userRoles.map((r: { code?: string } | string) => (typeof r === 'string' ? r : r?.code))
112
+
113
+ if (roleCodes.includes('admin')) {
114
+ isTenantAdminForTarget = true
115
+ }
116
+ }
117
+
118
+ return isSystemAdmin || isTenantAdminForTarget
119
+ }
120
+
121
+ // Helper: Resolve Target User
122
+ async function resolveTargetUser(dbSchema: string, targetUserId: string, targetUserEmail: string, targetRole: string) {
123
+ const safeSchema = sanitizeSchemaName(dbSchema)
124
+ if (!safeSchema) {
125
+ throw new Error('Invalid target tenant schema')
126
+ }
127
+
128
+ const qr = global.connection.createQueryRunner()
129
+ await qr.connect()
130
+
131
+ try {
132
+ await qr.query(`SET search_path TO "${safeSchema}", "public"`)
133
+
134
+ const UserEntity = global.entity?.User || 'User'
135
+ const targetUserRepo = qr.manager.getRepository(UserEntity)
136
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
137
+ let targetUser: any = null
138
+
139
+ const baseWhere = { blocked: false }
140
+
141
+ if (targetUserId) {
142
+ targetUser = await targetUserRepo.findOne({ where: { ...baseWhere, id: targetUserId } })
143
+ } else if (targetUserEmail) {
144
+ targetUser = await targetUserRepo.findOne({ where: { ...baseWhere, email: targetUserEmail } })
145
+ } else if (targetRole) {
146
+ targetUser = await targetUserRepo
147
+ .createQueryBuilder('user')
148
+ .where('user.blocked = :blocked', { blocked: false })
149
+ .andWhere(':role = ANY(user.roles)', { role: targetRole })
150
+ .getOne()
151
+ }
152
+
153
+ return targetUser
154
+ } finally {
155
+ await qr.release()
156
+ }
157
+ }
158
+
159
+ export async function impersonate(req: FastifyRequest, reply: FastifyReply) {
160
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any, prefer-const
161
+ let { targetTenantSlug, targetTenantId, targetRole, targetUserEmail, targetUserId } = req.body as any
162
+
163
+ // 1. Risoluzione Tenant
164
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
165
+ // @ts-ignore
166
+ const targetTenant = await resolveTargetTenant(targetTenantSlug, targetTenantId, req.tenant)
167
+ if (!targetTenant) {
168
+ return reply.code(404).send({ error: 'Target tenant not found or inactive' })
169
+ }
170
+
171
+ // 2. Security Check (Hardened)
172
+ const isAuthorized = checkImpersonationSecurity(req, targetTenant)
173
+ if (!isAuthorized) {
174
+ return reply
175
+ .code(403)
176
+ .send({ error: 'Unauthorized: Only System Admins or Tenant Admins can impersonate (Invalid Context)' })
177
+ }
178
+
179
+ // 3. Risoluzione Utente (su schema target)
180
+ const dbSchema = targetTenant.dbSchema
181
+ if (!dbSchema) {
182
+ throw new Error('Target tenant has no schema defined')
183
+ }
184
+
185
+ const targetUser = await resolveTargetUser(dbSchema, targetUserId, targetUserEmail, targetRole)
186
+
187
+ if (!targetUser) {
188
+ return reply.code(404).send({ error: 'Target user not found (or blocked) matching criteria' })
189
+ }
190
+
191
+ // 4. Generazione Token Impersonato
192
+ // We rely on fastify-jwt which is decorated on 'reply' (standard in volcanic-backend)
193
+ const token = await reply.jwtSign(
194
+ {
195
+ sub: targetUser.externalId, // Identity: The Local Admin
196
+ tid: targetTenant.id, // Context: The Target Tenant
197
+ roles: targetUser.roles, // Privileges: Inherited from Target
198
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
199
+ // @ts-ignore
200
+ impersonator: req.user.email, // Audit: Who is holding the puppet strings
201
+ iat: Math.floor(Date.now() / 1000)
202
+ },
203
+ { expiresIn: '24h' }
204
+ )
205
+
206
+ return {
207
+ token,
208
+ expiresAt: new Date(Date.now() + 24 * 60 * 60 * 1000),
209
+ impersonatedUser: { email: targetUser.email, id: targetUser.id }
210
+ }
211
+ }