@volcanicminds/backend 2.2.20 → 2.3.1

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 (61) hide show
  1. package/README.md +30 -0
  2. package/dist/index.d.ts.map +1 -1
  3. package/dist/index.js +12 -169
  4. package/dist/index.js.map +1 -1
  5. package/dist/lib/api/auth/controller/auth.d.ts +3 -9
  6. package/dist/lib/api/auth/controller/auth.d.ts.map +1 -1
  7. package/dist/lib/api/auth/controller/auth.js +58 -75
  8. package/dist/lib/api/auth/controller/auth.js.map +1 -1
  9. package/dist/lib/api/auth/routes.d.ts +0 -45
  10. package/dist/lib/api/auth/routes.d.ts.map +1 -1
  11. package/dist/lib/api/auth/routes.js +15 -32
  12. package/dist/lib/api/auth/routes.js.map +1 -1
  13. package/dist/lib/api/tool/controller/tool.d.ts.map +1 -1
  14. package/dist/lib/api/tool/controller/tool.js +4 -0
  15. package/dist/lib/api/tool/controller/tool.js.map +1 -1
  16. package/dist/lib/api/users/controller/user.d.ts +10 -1
  17. package/dist/lib/api/users/controller/user.d.ts.map +1 -1
  18. package/dist/lib/api/users/controller/user.js +56 -2
  19. package/dist/lib/api/users/controller/user.js.map +1 -1
  20. package/dist/lib/api/users/routes.d.ts +67 -0
  21. package/dist/lib/api/users/routes.d.ts.map +1 -1
  22. package/dist/lib/api/users/routes.js +55 -2
  23. package/dist/lib/api/users/routes.js.map +1 -1
  24. package/dist/lib/config/general.d.ts +7 -0
  25. package/dist/lib/config/general.d.ts.map +1 -1
  26. package/dist/lib/config/general.js +8 -1
  27. package/dist/lib/config/general.js.map +1 -1
  28. package/dist/lib/config/plugins.d.ts +17 -0
  29. package/dist/lib/config/plugins.d.ts.map +1 -1
  30. package/dist/lib/config/plugins.js +8 -0
  31. package/dist/lib/config/plugins.js.map +1 -1
  32. package/dist/lib/defaults/managers.d.ts +8 -0
  33. package/dist/lib/defaults/managers.d.ts.map +1 -0
  34. package/dist/lib/defaults/managers.js +71 -0
  35. package/dist/lib/defaults/managers.js.map +1 -0
  36. package/dist/lib/hooks/onRequest.d.ts.map +1 -1
  37. package/dist/lib/hooks/onRequest.js +73 -3
  38. package/dist/lib/hooks/onRequest.js.map +1 -1
  39. package/dist/lib/hooks/onResponse.d.ts.map +1 -1
  40. package/dist/lib/hooks/onResponse.js +5 -0
  41. package/dist/lib/hooks/onResponse.js.map +1 -1
  42. package/dist/lib/loader/general.d.ts.map +1 -1
  43. package/dist/lib/loader/general.js +6 -1
  44. package/dist/lib/loader/general.js.map +1 -1
  45. package/dist/lib/loader/tenant.d.ts +3 -0
  46. package/dist/lib/loader/tenant.d.ts.map +1 -0
  47. package/dist/lib/loader/tenant.js +61 -0
  48. package/dist/lib/loader/tenant.js.map +1 -0
  49. package/lib/api/auth/controller/auth.ts +66 -82
  50. package/lib/api/auth/routes.ts +18 -33
  51. package/lib/api/tool/controller/tool.ts +5 -0
  52. package/lib/api/users/controller/user.ts +69 -2
  53. package/lib/api/users/routes.ts +58 -2
  54. package/lib/config/general.ts +8 -1
  55. package/lib/config/plugins.ts +8 -0
  56. package/lib/defaults/managers.ts +88 -0
  57. package/lib/hooks/onRequest.ts +92 -4
  58. package/lib/hooks/onResponse.ts +6 -0
  59. package/lib/loader/general.ts +6 -1
  60. package/lib/loader/tenant.ts +79 -0
  61. package/package.json +2 -1
@@ -90,7 +90,40 @@ export async function isAdmin(req: FastifyRequest, reply: FastifyReply) {
90
90
  return reply.send({ isAdmin: user?.getId() && req.hasRole(roles.admin) })
91
91
  }
92
92
 
93
- export async function resetMfa(req: FastifyRequest, reply: FastifyReply) {
93
+ export async function block(req: FastifyRequest, reply: FastifyReply) {
94
+ if (!req.server['userManager'].isImplemented()) {
95
+ throw new Error('Not implemented')
96
+ }
97
+
98
+ if (!req.hasRole(roles.admin) && !req.hasRole(roles.backoffice)) {
99
+ return reply.status(403).send({ statusCode: 403, code: 'ROLE_NOT_ALLOWED', message: 'Not allowed to block a user' })
100
+ }
101
+
102
+ const { id: userId } = req.parameters()
103
+ const { reason } = req.data()
104
+
105
+ let user = await req.server['userManager'].blockUserById(userId, reason, req.runner)
106
+ user = await req.server['userManager'].resetExternalId(user.getId(), req.runner)
107
+ return { ok: !!user.getId() }
108
+ }
109
+
110
+ export async function unblock(req: FastifyRequest, reply: FastifyReply) {
111
+ if (!req.server['userManager'].isImplemented()) {
112
+ throw new Error('Not implemented')
113
+ }
114
+
115
+ if (!req.hasRole(roles.admin) && !req.hasRole(roles.backoffice)) {
116
+ return reply
117
+ .status(403)
118
+ .send({ statusCode: 403, code: 'ROLE_NOT_ALLOWED', message: 'Not allowed to unblock a user' })
119
+ }
120
+
121
+ const { id: userId } = req.parameters()
122
+ const user = await req.server['userManager'].unblockUserById(userId, req.runner)
123
+ return { ok: !!user.getId() }
124
+ }
125
+
126
+ export async function resetMfaByAdmin(req: FastifyRequest, reply: FastifyReply) {
94
127
  const { id } = req.parameters()
95
128
 
96
129
  if (!req.hasRole(roles.admin)) {
@@ -102,10 +135,44 @@ export async function resetMfa(req: FastifyRequest, reply: FastifyReply) {
102
135
  }
103
136
 
104
137
  try {
105
- await req.server['userManager'].disableMfa(id)
138
+ await req.server['userManager'].disableMfa(id, req.runner)
106
139
  return { ok: true }
107
140
  } catch (error) {
108
141
  req.log.error(error)
109
142
  return reply.status(500).send(new Error('Failed to reset MFA'))
110
143
  }
111
144
  }
145
+
146
+ export async function resetPasswordByAdmin(req: FastifyRequest, reply: FastifyReply) {
147
+ // Check if feature is enabled via config
148
+ if (config.options?.allow_admin_change_password_users !== true) {
149
+ return reply.status(404).send()
150
+ }
151
+
152
+ if (!req.hasRole(roles.admin)) {
153
+ return reply.status(403).send(new Error('Only admins can reset user passwords'))
154
+ }
155
+
156
+ const { id } = req.parameters()
157
+ if (!id) {
158
+ return reply.status(400).send(new Error('Missing user id'))
159
+ }
160
+
161
+ const { password } = req.data()
162
+ if (!password) {
163
+ return reply.status(400).send(new Error('Missing password in request body'))
164
+ }
165
+
166
+ try {
167
+ const user = await req.server['userManager'].retrieveUserById(id)
168
+ if (!user) {
169
+ return reply.status(404).send(new Error('User not found'))
170
+ }
171
+
172
+ await req.server['userManager'].resetPassword(user, password)
173
+ return { ok: true }
174
+ } catch (error) {
175
+ req.log.error(error)
176
+ return reply.status(500).send(new Error('Failed to reset password'))
177
+ }
178
+ }
@@ -180,20 +180,76 @@ export default {
180
180
  }
181
181
  }
182
182
  },
183
+ {
184
+ method: 'POST',
185
+ path: '/:id/block',
186
+ roles: [roles.admin, roles.backoffice],
187
+ handler: 'user.block',
188
+ middlewares: ['global.isAuthenticated'],
189
+ config: {
190
+ title: 'Block a user by id',
191
+ description: 'Block a user by id',
192
+ params: { $ref: 'onlyIdSchema#' },
193
+ body: { $ref: 'blockBodySchema#' },
194
+ response: {
195
+ 200: { $ref: 'defaultResponse#' }
196
+ }
197
+ }
198
+ },
199
+ {
200
+ method: 'POST',
201
+ path: '/:id/unblock',
202
+ roles: [roles.admin, roles.backoffice],
203
+ handler: 'user.unblock',
204
+ middlewares: ['global.isAuthenticated'],
205
+ config: {
206
+ title: 'Unblock a user by id',
207
+ description: 'Unblock a user by id',
208
+ params: { $ref: 'onlyIdSchema#' },
209
+ response: {
210
+ 200: { $ref: 'defaultResponse#' }
211
+ }
212
+ }
213
+ },
183
214
  {
184
215
  method: 'POST',
185
216
  path: '/:id/mfa/reset',
186
217
  roles: [roles.admin],
187
- handler: 'user.resetMfa',
218
+ handler: 'user.resetMfaByAdmin',
188
219
  middlewares: ['global.isAuthenticated'],
189
220
  config: {
190
- title: 'Reset MFA for user',
221
+ title: 'Reset MFA for specific user',
191
222
  description: 'Disable MFA for a specific user (Admin only)',
192
223
  params: { $ref: 'globalParamsSchema#' },
193
224
  response: {
194
225
  200: { $ref: 'defaultResponse#' }
195
226
  }
196
227
  }
228
+ },
229
+ {
230
+ method: 'POST',
231
+ path: '/:id/password/reset',
232
+ roles: [roles.admin],
233
+ handler: 'user.resetPasswordByAdmin',
234
+ middlewares: ['global.isAuthenticated'],
235
+ config: {
236
+ title: 'Reset password for specific user',
237
+ description:
238
+ 'Admin can reset password for a specific user. Requires config option allow_admin_change_password_users to be enabled.',
239
+ params: { $ref: 'globalParamsSchema#' },
240
+ body: {
241
+ type: 'object',
242
+ required: ['password'],
243
+ properties: {
244
+ password: { type: 'string', minLength: 6 }
245
+ }
246
+ },
247
+ response: {
248
+ 200: { $ref: 'defaultResponse#' }
249
+ }
250
+ }
197
251
  }
198
252
  ]
253
+
254
+
199
255
  }
@@ -3,11 +3,18 @@ export default {
3
3
  options: {
4
4
  allow_multiple_admin: false,
5
5
  admin_can_change_passwords: false,
6
+ allow_admin_change_password_users: false,
6
7
  reset_external_id_on_login: false,
7
8
  scheduler: false,
8
9
  embedded_auth: true,
9
10
  mfa_policy: process.env.MFA_POLICY || 'OPTIONAL', // OPTIONAL, MANDATORY, ONE_WAY
10
11
  mfa_admin_forced_reset_email: null,
11
- mfa_admin_forced_reset_until: null
12
+ mfa_admin_forced_reset_until: null,
13
+ multi_tenant: {
14
+ enabled: false,
15
+ resolver: 'subdomain', // subdomain, header, query
16
+ header_key: 'x-tenant-id',
17
+ query_key: 'tid'
18
+ }
12
19
  }
13
20
  }
@@ -61,5 +61,13 @@ export default [
61
61
  name: 'rawBody',
62
62
  enable: false,
63
63
  options: {}
64
+ },
65
+ {
66
+ name: 'cookie',
67
+ enable: process.env.AUTH_MODE === 'COOKIE',
68
+ options: {
69
+ secret: process.env.COOKIE_SECRET,
70
+ parseOptions: {}
71
+ }
64
72
  }
65
73
  ]
@@ -0,0 +1,88 @@
1
+
2
+ import {
3
+ UserManagement,
4
+ TokenManagement,
5
+ DataBaseManagement,
6
+ MfaManagement,
7
+ TransferManagement,
8
+ TenantManagement,
9
+ TransferCallback
10
+ } from '../../types/global.js'
11
+ import { FastifyReply, FastifyRequest } from 'fastify'
12
+
13
+ // Default implementations that throw errors or return not implemented
14
+ export const defaultTenantManager: TenantManagement = {
15
+ isImplemented() { return false },
16
+ resolveTenant(_req) { throw new Error('Not implemented') },
17
+ switchContext(_tenant, _db?) { throw new Error('Not implemented') },
18
+ createTenant(_data) { throw new Error('Not implemented') },
19
+ deleteTenant(_id) { throw new Error('Not implemented') }
20
+ }
21
+
22
+ export const defaultUserManager: UserManagement = {
23
+ isImplemented() { return false },
24
+ isValidUser(_data: unknown) { throw new Error('Not implemented.') },
25
+ createUser(_data: unknown) { throw new Error('Not implemented.') },
26
+ deleteUser(_data: unknown) { throw new Error('Not implemented.') },
27
+ resetExternalId(_data: unknown) { throw new Error('Not implemented.') },
28
+ updateUserById(_id: string, _user: unknown) { throw new Error('Not implemented.') },
29
+ retrieveUserById(_id: string) { throw new Error('Not implemented.') },
30
+ retrieveUserByEmail(_email: string) { throw new Error('Not implemented.') },
31
+ retrieveUserByConfirmationToken(_code: string) { throw new Error('Not implemented.') },
32
+ retrieveUserByResetPasswordToken(_code: string) { throw new Error('Not implemented.') },
33
+ retrieveUserByUsername(_username: string) { throw new Error('Not implemented.') },
34
+ retrieveUserByExternalId(_externalId: string) { throw new Error('Not implemented.') },
35
+ retrieveUserByPassword(_email: string, _password: string) { throw new Error('Not implemented.') },
36
+ changePassword(_email: string, _password: string, _oldPassword: string) { throw new Error('Not implemented.') },
37
+ forgotPassword(_email: string) { throw new Error('Not implemented.') },
38
+ userConfirmation(_user: unknown) { throw new Error('Not implemented.') },
39
+ resetPassword(_user: unknown, _password: string) { throw new Error('Not implemented.') },
40
+ blockUserById(_id: string, _reason: string) { throw new Error('Not implemented.') },
41
+ unblockUserById(_data: unknown) { throw new Error('Not implemented.') },
42
+ countQuery(_data: unknown) { throw new Error('Not implemented.') },
43
+ findQuery(_data: unknown) { throw new Error('Not implemented.') },
44
+ disableUserById(_id: string) { throw new Error('Not implemented.') },
45
+ saveMfaSecret(_userId: string, _secret: string) { throw new Error('Not implemented.') },
46
+ retrieveMfaSecret(_userId: string) { throw new Error('Not implemented.') },
47
+ enableMfa(_userId: string) { throw new Error('Not implemented.') },
48
+ disableMfa(_userId: string) { throw new Error('Not implemented.') },
49
+ forceDisableMfaForAdmin(_email: string) { throw new Error('Not implemented.') }
50
+ }
51
+
52
+ export const defaultTokenManager: TokenManagement = {
53
+ isImplemented() { return false },
54
+ isValidToken(_data: unknown) { throw new Error('Not implemented.') },
55
+ createToken(_data: unknown) { throw new Error('Not implemented.') },
56
+ resetExternalId(_id: string) { throw new Error('Not implemented.') },
57
+ updateTokenById(_id: string, _token: unknown) { throw new Error('Not implemented.') },
58
+ retrieveTokenById(_id: string) { throw new Error('Not implemented.') },
59
+ retrieveTokenByExternalId(_id: string) { throw new Error('Not implemented.') },
60
+ blockTokenById(_id: string, _reason: string) { throw new Error('Not implemented.') },
61
+ unblockTokenById(_id: string) { throw new Error('Not implemented.') },
62
+ countQuery(_data: unknown) { throw new Error('Not implemented.') },
63
+ findQuery(_data: unknown) { throw new Error('Not implemented.') },
64
+ removeTokenById(_id: string) { throw new Error('Not implemented.') }
65
+ }
66
+
67
+ export const defaultDataBaseManager: DataBaseManagement = {
68
+ isImplemented() { return false },
69
+ synchronizeSchemas() { throw new Error('Not implemented.') },
70
+ retrieveBy(_entityName, _entityId) { throw new Error('Not implemented.') },
71
+ addChange(_entityName, _entityId, _status, _userId, _contents, _changeEntity) { throw new Error('Not implemented.') }
72
+ }
73
+
74
+ export const defaultMfaManager: MfaManagement = {
75
+ generateSetup(_appName: string, _email: string) { throw new Error('Not implemented.') },
76
+ verify(_token: string, _secret: string) { throw new Error('Not implemented.') }
77
+ }
78
+
79
+ export const defaultTransferManager: TransferManagement = {
80
+ isImplemented() { return false },
81
+ getPath() { throw new Error('Not implemented.') },
82
+ getServer() { throw new Error('Not implemented.') },
83
+ onUploadCreate(_callback: TransferCallback) { throw new Error('Not implemented.') },
84
+ onUploadFinish(_callback: TransferCallback) { throw new Error('Not implemented.') },
85
+ onUploadTerminate(_callback: TransferCallback) { throw new Error('Not implemented.') },
86
+ handle(_req: FastifyRequest, _res: FastifyReply) { throw new Error('Not implemented.') },
87
+ isValid(_req: FastifyRequest) { throw new Error('Not implemented.') }
88
+ }
@@ -23,6 +23,63 @@ const normalizeRoles = (rolesArray: any[] | undefined): string[] => {
23
23
  export default async (req, reply) => {
24
24
  if (log.i) req.startedAt = new Date()
25
25
 
26
+ const { multi_tenant } = global.config?.options || {}
27
+
28
+ if (multi_tenant?.enabled) {
29
+ let tenantSlug: string | undefined
30
+
31
+ if (multi_tenant.resolver === 'subdomain') {
32
+ const host = req.headers.host || ''
33
+ const parts = host.split('.')
34
+ if (parts.length >= 2) {
35
+ // FIXME: Improve subdomain extraction strategy. Currently assumes [slug].[domain].[tld] or [slug].localhost
36
+ if (parts[0] !== 'www') {
37
+ tenantSlug = parts[0]
38
+ }
39
+ }
40
+ } else if (multi_tenant.resolver === 'header') {
41
+ tenantSlug = req.headers[multi_tenant?.header_key || 'x-tenant-id'] as string
42
+ } else if (multi_tenant.resolver === 'query') {
43
+ tenantSlug = (req.query as any)[multi_tenant?.query_key || 'tid']
44
+ }
45
+
46
+ if (!tenantSlug) {
47
+ return reply.code(400).send({ statusCode: 400, error: 'Tenant ID missing', message: 'Tenant ID is required' })
48
+ }
49
+
50
+ if (!global.repository?.tenants) {
51
+ log.error('Multi-tenant enabled but global.repository.tenants not found')
52
+ return reply.code(500).send({ statusCode: 500, error: 'Internal Server Error' })
53
+ }
54
+
55
+ const tenant = await global.repository.tenants.findOneBy({ slug: tenantSlug })
56
+
57
+ if (!tenant) {
58
+ return reply
59
+ .code(404)
60
+ .send({ statusCode: 404, error: 'Tenant Not Found', message: `Tenant '${tenantSlug}' not found` })
61
+ }
62
+
63
+ if (tenant.status !== 'active') {
64
+ return reply.code(403).send({ statusCode: 403, error: 'Tenant Inactive', message: 'Tenant is not active' })
65
+ }
66
+
67
+ // Tenant Context Setup
68
+ const runner = global.connection.createQueryRunner()
69
+ await runner.connect()
70
+
71
+ // Validate schema name safety
72
+ if (!/^[a-z0-9_]+$/i.test(tenant.schema)) {
73
+ await runner.release()
74
+ return reply.code(400).send({ statusCode: 400, error: 'Invalid Schema Name' })
75
+ }
76
+
77
+ await runner.query(`SET search_path TO "${tenant.schema}", "public"`)
78
+
79
+ req.runner = runner
80
+ req.tenant = tenant
81
+ }
82
+
26
83
  req.data = () => getData(req)
27
84
  req.parameters = () => getParams(req)
28
85
 
@@ -49,15 +106,44 @@ export default async (req, reply) => {
49
106
  req.roles = () => [roles.public.code]
50
107
  req.hasRole = (r: Role) => req.roles().includes(r?.code)
51
108
 
52
- const auth = req.headers?.authorization || ''
53
109
  const cfg = req.routeOptions?.config || req.routeConfig || {}
54
- const [prefix, bearerToken] = auth.split(' ')
110
+ const AUTH_MODE = process.env.AUTH_MODE || 'BEARER'
111
+
112
+ let bearerToken: string | undefined
113
+
114
+ if (AUTH_MODE === 'COOKIE') {
115
+ const cookieToken = req.cookies['auth_token']
116
+ if (cookieToken) {
117
+ const unsigned = req.unsignCookie(cookieToken)
118
+ if (unsigned.valid && unsigned.value) {
119
+ bearerToken = unsigned.value
120
+ }
121
+ }
122
+ } else {
123
+ const auth = req.headers?.authorization || ''
124
+ const [prefix, token] = auth.split(' ')
125
+ if (prefix === 'Bearer' && token != null) {
126
+ bearerToken = token
127
+ }
128
+ }
55
129
 
56
- if (prefix === 'Bearer' && bearerToken != null) {
130
+ if (bearerToken) {
57
131
  try {
58
132
  const tokenData = reply.server.jwt.verify(bearerToken)
59
133
 
60
- // --- MFA GATEKEEPER ---
134
+ // Validate Tenant Access
135
+ const { multi_tenant } = global.config?.options || {}
136
+ if (multi_tenant?.enabled && req.tenant && tokenData.tid) {
137
+ if (tokenData.tid !== req.tenant.id) {
138
+ return reply.status(403).send({
139
+ statusCode: 403,
140
+ code: 'TENANT_MISMATCH',
141
+ message: 'Token does not belong to this tenant'
142
+ })
143
+ }
144
+ }
145
+
146
+ // MFA Gatekeeper Check
61
147
  if (tokenData.role === 'pre-auth-mfa') {
62
148
  const currentUrl = req.routeOptions.url || req.raw.url
63
149
  const isAllowed = MFA_SETUP_WHITELIST.some((url) => currentUrl.endsWith(url))
@@ -137,3 +223,5 @@ export default async (req, reply) => {
137
223
  }
138
224
  }
139
225
  }
226
+
227
+
@@ -1,4 +1,10 @@
1
1
  export default async (req, reply) => {
2
+ if (req.runner) {
3
+ if (!req.runner.isReleased) {
4
+ await req.runner.release()
5
+ }
6
+ }
7
+
2
8
  let extraMessage = ''
3
9
  if (log.i && req.startedAt) {
4
10
  const elapsed: number = new Date().getTime() - req.startedAt.getTime()
@@ -8,11 +8,16 @@ export async function load() {
8
8
  options: {
9
9
  allow_multiple_admin: false,
10
10
  admin_can_change_passwords: false,
11
+ allow_admin_change_password_users: false,
11
12
  reset_external_id_on_login: false,
12
13
  scheduler: false,
13
14
  embedded_auth: true,
14
15
  mfa_admin_forced_reset_email: undefined,
15
- mfa_admin_forced_reset_until: undefined
16
+ mfa_admin_forced_reset_until: undefined,
17
+ multi_tenant: {
18
+ enabled: false,
19
+ query_key: 'tid'
20
+ }
16
21
  }
17
22
  }
18
23
 
@@ -0,0 +1,79 @@
1
+ import { FastifyInstance } from 'fastify'
2
+ import { TenantManagement } from '../../types/global.js'
3
+
4
+ export async function apply(server: FastifyInstance) {
5
+ const { multi_tenant } = global.config.options || {}
6
+
7
+ // Se multi-tenant non è abilitato, usciamo subito
8
+ if (!multi_tenant?.enabled) {
9
+ if (log.d) log.debug('Multi-Tenant: Disabled by configuration')
10
+ return
11
+ }
12
+
13
+ if (log.i) log.info('Multi-Tenant: 🟢 Enabled')
14
+
15
+ // Hook globale per la risoluzione del tenant
16
+ // Deve essere eseguito all'inizio della richiesta
17
+ server.addHook('onRequest', async (req, reply) => {
18
+ // Recuperiamo il gestore tenant iniettato o di default
19
+ const tm = server['tenantManager'] as TenantManagement
20
+
21
+ // Controllo critico: se è abilitato il MT, DEVE esserci un manager implementato
22
+ if (!tm || !tm.isImplemented()) {
23
+ const errorMsg = 'Multi-Tenant enabled but no TenantManager provided/implemented!'
24
+ if (log.f) log.fatal(errorMsg)
25
+ throw new Error(errorMsg)
26
+ }
27
+
28
+ try {
29
+ // 1. Risoluzione Tenant
30
+ const tenant = await tm.resolveTenant(req)
31
+
32
+ if (!tenant) {
33
+ if (log.w) log.warn(`Multi-Tenant: Tenant resolution failed for request ${req.id}`)
34
+ return reply.code(404).send({
35
+ statusCode: 404,
36
+ error: 'Not Found',
37
+ message: 'Tenant not found or resolution failed'
38
+ })
39
+ }
40
+
41
+ // 2. Setup Contesto
42
+ req.tenant = tenant
43
+ if (log.t) log.trace(`Multi-Tenant: Context switched to ${tenant.slug || tenant.id}`)
44
+
45
+ // 3. Creazione QueryRunner (Context-Chain Root)
46
+ // Questo è il cuore della "Golden Solution": ogni richiesta ha il suo QueryRunner isolato
47
+ const dataSource = global.connection
48
+ if (dataSource) {
49
+ const qr = dataSource.createQueryRunner()
50
+ await qr.connect()
51
+
52
+ // Assegnamo il manager del QueryRunner alla richiesta
53
+ req.db = qr.manager
54
+
55
+ // 4. Switch Schema su QUESTO QueryRunner
56
+ // Passiamo il manager affinché il TenantManager possa eseguire "SET search_path" su questa connessione specifica
57
+ await tm.switchContext(tenant, qr.manager)
58
+
59
+ // 5. Cleanup: Rilascio del QueryRunner alla fine della richiesta
60
+ reply.raw.on('finish', async () => {
61
+ if (!qr.isReleased) {
62
+ await qr.release()
63
+ }
64
+ })
65
+ } else {
66
+ if (log.w) log.warn('Multi-Tenant: Global connection not found! Skipping DB context creation.')
67
+ }
68
+
69
+ } catch (err: unknown) {
70
+ const message = err instanceof Error ? err.message : String(err)
71
+ if (log.e) log.error(`Multi-Tenant Error: ${message}`)
72
+ return reply.code(500).send({
73
+ statusCode: 500,
74
+ error: 'Internal Server Error',
75
+ message: 'Tenant Context Switch Failed'
76
+ })
77
+ }
78
+ })
79
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@volcanicminds/backend",
3
- "version": "2.2.20",
3
+ "version": "2.3.1",
4
4
  "type": "module",
5
5
  "codename": "rome",
6
6
  "license": "MIT",
@@ -65,6 +65,7 @@
65
65
  "@apollo/server": "^5.2.0",
66
66
  "@as-integrations/fastify": "^3.1.0",
67
67
  "@fastify/compress": "^8.3.0",
68
+ "@fastify/cookie": "^11.0.2",
68
69
  "@fastify/cors": "^11.2.0",
69
70
  "@fastify/helmet": "^13.0.2",
70
71
  "@fastify/jwt": "^10.0.0",