@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.
- package/README.md +30 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +12 -169
- package/dist/index.js.map +1 -1
- package/dist/lib/api/auth/controller/auth.d.ts +3 -9
- package/dist/lib/api/auth/controller/auth.d.ts.map +1 -1
- package/dist/lib/api/auth/controller/auth.js +58 -75
- package/dist/lib/api/auth/controller/auth.js.map +1 -1
- package/dist/lib/api/auth/routes.d.ts +0 -45
- package/dist/lib/api/auth/routes.d.ts.map +1 -1
- package/dist/lib/api/auth/routes.js +15 -32
- package/dist/lib/api/auth/routes.js.map +1 -1
- package/dist/lib/api/tool/controller/tool.d.ts.map +1 -1
- package/dist/lib/api/tool/controller/tool.js +4 -0
- package/dist/lib/api/tool/controller/tool.js.map +1 -1
- package/dist/lib/api/users/controller/user.d.ts +10 -1
- package/dist/lib/api/users/controller/user.d.ts.map +1 -1
- package/dist/lib/api/users/controller/user.js +56 -2
- package/dist/lib/api/users/controller/user.js.map +1 -1
- package/dist/lib/api/users/routes.d.ts +67 -0
- package/dist/lib/api/users/routes.d.ts.map +1 -1
- package/dist/lib/api/users/routes.js +55 -2
- package/dist/lib/api/users/routes.js.map +1 -1
- package/dist/lib/config/general.d.ts +7 -0
- package/dist/lib/config/general.d.ts.map +1 -1
- package/dist/lib/config/general.js +8 -1
- package/dist/lib/config/general.js.map +1 -1
- package/dist/lib/config/plugins.d.ts +17 -0
- package/dist/lib/config/plugins.d.ts.map +1 -1
- package/dist/lib/config/plugins.js +8 -0
- package/dist/lib/config/plugins.js.map +1 -1
- package/dist/lib/defaults/managers.d.ts +8 -0
- package/dist/lib/defaults/managers.d.ts.map +1 -0
- package/dist/lib/defaults/managers.js +71 -0
- package/dist/lib/defaults/managers.js.map +1 -0
- package/dist/lib/hooks/onRequest.d.ts.map +1 -1
- package/dist/lib/hooks/onRequest.js +73 -3
- package/dist/lib/hooks/onRequest.js.map +1 -1
- package/dist/lib/hooks/onResponse.d.ts.map +1 -1
- package/dist/lib/hooks/onResponse.js +5 -0
- package/dist/lib/hooks/onResponse.js.map +1 -1
- package/dist/lib/loader/general.d.ts.map +1 -1
- package/dist/lib/loader/general.js +6 -1
- package/dist/lib/loader/general.js.map +1 -1
- package/dist/lib/loader/tenant.d.ts +3 -0
- package/dist/lib/loader/tenant.d.ts.map +1 -0
- package/dist/lib/loader/tenant.js +61 -0
- package/dist/lib/loader/tenant.js.map +1 -0
- package/lib/api/auth/controller/auth.ts +66 -82
- package/lib/api/auth/routes.ts +18 -33
- package/lib/api/tool/controller/tool.ts +5 -0
- package/lib/api/users/controller/user.ts +69 -2
- package/lib/api/users/routes.ts +58 -2
- package/lib/config/general.ts +8 -1
- package/lib/config/plugins.ts +8 -0
- package/lib/defaults/managers.ts +88 -0
- package/lib/hooks/onRequest.ts +92 -4
- package/lib/hooks/onResponse.ts +6 -0
- package/lib/loader/general.ts +6 -1
- package/lib/loader/tenant.ts +79 -0
- 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
|
|
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
|
+
}
|
package/lib/api/users/routes.ts
CHANGED
|
@@ -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.
|
|
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
|
}
|
package/lib/config/general.ts
CHANGED
|
@@ -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
|
}
|
package/lib/config/plugins.ts
CHANGED
|
@@ -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
|
+
}
|
package/lib/hooks/onRequest.ts
CHANGED
|
@@ -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
|
|
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 (
|
|
130
|
+
if (bearerToken) {
|
|
57
131
|
try {
|
|
58
132
|
const tokenData = reply.server.jwt.verify(bearerToken)
|
|
59
133
|
|
|
60
|
-
//
|
|
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
|
+
|
package/lib/hooks/onResponse.ts
CHANGED
package/lib/loader/general.ts
CHANGED
|
@@ -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.
|
|
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",
|