@volcanicminds/backend 2.2.21 → 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 (55) hide show
  1. package/dist/index.d.ts.map +1 -1
  2. package/dist/index.js +9 -169
  3. package/dist/index.js.map +1 -1
  4. package/dist/lib/api/auth/controller/auth.d.ts +0 -9
  5. package/dist/lib/api/auth/controller/auth.d.ts.map +1 -1
  6. package/dist/lib/api/auth/controller/auth.js +32 -75
  7. package/dist/lib/api/auth/controller/auth.js.map +1 -1
  8. package/dist/lib/api/auth/routes.d.ts +0 -45
  9. package/dist/lib/api/auth/routes.d.ts.map +1 -1
  10. package/dist/lib/api/auth/routes.js +1 -32
  11. package/dist/lib/api/auth/routes.js.map +1 -1
  12. package/dist/lib/api/tool/controller/tool.d.ts.map +1 -1
  13. package/dist/lib/api/tool/controller/tool.js +4 -0
  14. package/dist/lib/api/tool/controller/tool.js.map +1 -1
  15. package/dist/lib/api/users/controller/user.d.ts +10 -1
  16. package/dist/lib/api/users/controller/user.d.ts.map +1 -1
  17. package/dist/lib/api/users/controller/user.js +56 -2
  18. package/dist/lib/api/users/controller/user.js.map +1 -1
  19. package/dist/lib/api/users/routes.d.ts +67 -0
  20. package/dist/lib/api/users/routes.d.ts.map +1 -1
  21. package/dist/lib/api/users/routes.js +55 -2
  22. package/dist/lib/api/users/routes.js.map +1 -1
  23. package/dist/lib/config/general.d.ts +7 -0
  24. package/dist/lib/config/general.d.ts.map +1 -1
  25. package/dist/lib/config/general.js +8 -1
  26. package/dist/lib/config/general.js.map +1 -1
  27. package/dist/lib/defaults/managers.d.ts +8 -0
  28. package/dist/lib/defaults/managers.d.ts.map +1 -0
  29. package/dist/lib/defaults/managers.js +71 -0
  30. package/dist/lib/defaults/managers.js.map +1 -0
  31. package/dist/lib/hooks/onRequest.d.ts.map +1 -1
  32. package/dist/lib/hooks/onRequest.js +54 -2
  33. package/dist/lib/hooks/onRequest.js.map +1 -1
  34. package/dist/lib/hooks/onResponse.d.ts.map +1 -1
  35. package/dist/lib/hooks/onResponse.js +5 -0
  36. package/dist/lib/hooks/onResponse.js.map +1 -1
  37. package/dist/lib/loader/general.d.ts.map +1 -1
  38. package/dist/lib/loader/general.js +6 -1
  39. package/dist/lib/loader/general.js.map +1 -1
  40. package/dist/lib/loader/tenant.d.ts +3 -0
  41. package/dist/lib/loader/tenant.d.ts.map +1 -0
  42. package/dist/lib/loader/tenant.js +61 -0
  43. package/dist/lib/loader/tenant.js.map +1 -0
  44. package/lib/api/auth/controller/auth.ts +35 -81
  45. package/lib/api/auth/routes.ts +4 -33
  46. package/lib/api/tool/controller/tool.ts +5 -0
  47. package/lib/api/users/controller/user.ts +69 -2
  48. package/lib/api/users/routes.ts +58 -2
  49. package/lib/config/general.ts +8 -1
  50. package/lib/defaults/managers.ts +88 -0
  51. package/lib/hooks/onRequest.ts +72 -7
  52. package/lib/hooks/onResponse.ts +6 -0
  53. package/lib/loader/general.ts +6 -1
  54. package/lib/loader/tenant.ts +79 -0
  55. package/package.json +1 -1
@@ -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
 
@@ -55,9 +112,7 @@ export default async (req, reply) => {
55
112
  let bearerToken: string | undefined
56
113
 
57
114
  if (AUTH_MODE === 'COOKIE') {
58
- // COOKIE MODE: ONLY Check Cookie
59
115
  const cookieToken = req.cookies['auth_token']
60
- // If cookie is signed:
61
116
  if (cookieToken) {
62
117
  const unsigned = req.unsignCookie(cookieToken)
63
118
  if (unsigned.valid && unsigned.value) {
@@ -65,7 +120,6 @@ export default async (req, reply) => {
65
120
  }
66
121
  }
67
122
  } else {
68
- // BEARER MODE: ONLY Check Header
69
123
  const auth = req.headers?.authorization || ''
70
124
  const [prefix, token] = auth.split(' ')
71
125
  if (prefix === 'Bearer' && token != null) {
@@ -77,7 +131,19 @@ export default async (req, reply) => {
77
131
  try {
78
132
  const tokenData = reply.server.jwt.verify(bearerToken)
79
133
 
80
- // --- 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
81
147
  if (tokenData.role === 'pre-auth-mfa') {
82
148
  const currentUrl = req.routeOptions.url || req.raw.url
83
149
  const isAllowed = MFA_SETUP_WHITELIST.some((url) => currentUrl.endsWith(url))
@@ -143,9 +209,6 @@ export default async (req, reply) => {
143
209
  })
144
210
  }
145
211
  }
146
- } else {
147
- // No token found, but maybe route is public?
148
- // If route requires roles and no token, it will be caught by permissions check below (bearerToken is null)
149
212
  }
150
213
 
151
214
  if (cfg.requiredRoles?.length > 0) {
@@ -160,3 +223,5 @@ export default async (req, reply) => {
160
223
  }
161
224
  }
162
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.21",
3
+ "version": "2.3.1",
4
4
  "type": "module",
5
5
  "codename": "rome",
6
6
  "license": "MIT",