@volcanicminds/backend 2.3.2 → 3.0.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 +221 -28
- package/dist/index.d.ts +3 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +24 -36
- package/dist/index.js.map +1 -1
- package/dist/lib/api/auth/controller/auth.d.ts.map +1 -1
- package/dist/lib/api/auth/controller/auth.js +14 -7
- package/dist/lib/api/auth/controller/auth.js.map +1 -1
- package/dist/lib/api/health/controller/health.d.ts +1 -1
- package/dist/lib/api/health/controller/health.d.ts.map +1 -1
- package/dist/lib/api/health/controller/health.js +1 -1
- package/dist/lib/api/health/controller/health.js.map +1 -1
- package/dist/lib/api/health/routes.d.ts +1 -0
- package/dist/lib/api/health/routes.d.ts.map +1 -1
- package/dist/lib/api/health/routes.js +1 -0
- package/dist/lib/api/health/routes.js.map +1 -1
- package/dist/lib/api/tenants/controller/tenants.d.ts +17 -0
- package/dist/lib/api/tenants/controller/tenants.d.ts.map +1 -0
- package/dist/lib/api/tenants/controller/tenants.js +150 -0
- package/dist/lib/api/tenants/controller/tenants.js.map +1 -0
- package/dist/lib/api/tenants/routes.d.ts +167 -0
- package/dist/lib/api/tenants/routes.d.ts.map +1 -0
- package/dist/lib/api/tenants/routes.js +143 -0
- package/dist/lib/api/tenants/routes.js.map +1 -0
- package/dist/lib/config/plugins.js +1 -1
- package/dist/lib/config/plugins.js.map +1 -1
- package/dist/lib/database/typeorm/entities/change.d.ts +11 -0
- package/dist/lib/database/typeorm/entities/change.d.ts.map +1 -0
- package/dist/lib/database/typeorm/entities/change.js +4 -0
- package/dist/lib/database/typeorm/entities/change.js.map +1 -0
- package/dist/lib/database/typeorm/entities/tenant.d.ts +13 -0
- package/dist/lib/database/typeorm/entities/tenant.d.ts.map +1 -0
- package/dist/lib/database/typeorm/entities/tenant.js +4 -0
- package/dist/lib/database/typeorm/entities/tenant.js.map +1 -0
- package/dist/lib/database/typeorm/entities/token.d.ts +16 -0
- package/dist/lib/database/typeorm/entities/token.d.ts.map +1 -0
- package/dist/lib/database/typeorm/entities/token.js +4 -0
- package/dist/lib/database/typeorm/entities/token.js.map +1 -0
- package/dist/lib/database/typeorm/entities/user.d.ts +28 -0
- package/dist/lib/database/typeorm/entities/user.d.ts.map +1 -0
- package/dist/lib/database/typeorm/entities/user.js +4 -0
- package/dist/lib/database/typeorm/entities/user.js.map +1 -0
- package/dist/lib/database/typeorm/loader/dataBaseManager.d.ts +6 -0
- package/dist/lib/database/typeorm/loader/dataBaseManager.d.ts.map +1 -0
- package/dist/lib/database/typeorm/loader/dataBaseManager.js +73 -0
- package/dist/lib/database/typeorm/loader/dataBaseManager.js.map +1 -0
- package/dist/lib/database/typeorm/loader/entities.d.ts +6 -0
- package/dist/lib/database/typeorm/loader/entities.d.ts.map +1 -0
- package/dist/lib/database/typeorm/loader/entities.js +37 -0
- package/dist/lib/database/typeorm/loader/entities.js.map +1 -0
- package/dist/lib/database/typeorm/loader/tenantManager.d.ts +17 -0
- package/dist/lib/database/typeorm/loader/tenantManager.d.ts.map +1 -0
- package/dist/lib/database/typeorm/loader/tenantManager.js +193 -0
- package/dist/lib/database/typeorm/loader/tenantManager.js.map +1 -0
- package/dist/lib/database/typeorm/loader/tokenManager.d.ts +23 -0
- package/dist/lib/database/typeorm/loader/tokenManager.d.ts.map +1 -0
- package/dist/lib/database/typeorm/loader/tokenManager.js +131 -0
- package/dist/lib/database/typeorm/loader/tokenManager.js.map +1 -0
- package/dist/lib/database/typeorm/loader/userManager.d.ts +39 -0
- package/dist/lib/database/typeorm/loader/userManager.d.ts.map +1 -0
- package/dist/lib/database/typeorm/loader/userManager.js +358 -0
- package/dist/lib/database/typeorm/loader/userManager.js.map +1 -0
- package/dist/lib/database/typeorm/query/builder.d.ts +2 -0
- package/dist/lib/database/typeorm/query/builder.d.ts.map +1 -0
- package/dist/lib/database/typeorm/query/builder.js +33 -0
- package/dist/lib/database/typeorm/query/builder.js.map +1 -0
- package/dist/lib/database/typeorm/query/parser.d.ts +2 -0
- package/dist/lib/database/typeorm/query/parser.d.ts.map +1 -0
- package/dist/lib/database/typeorm/query/parser.js +27 -0
- package/dist/lib/database/typeorm/query/parser.js.map +1 -0
- package/dist/lib/database/typeorm/query.d.ts +36 -0
- package/dist/lib/database/typeorm/query.d.ts.map +1 -0
- package/dist/lib/database/typeorm/query.js +259 -0
- package/dist/lib/database/typeorm/query.js.map +1 -0
- package/dist/lib/database/typeorm/util/crypto.d.ts +3 -0
- package/dist/lib/database/typeorm/util/crypto.d.ts.map +1 -0
- package/dist/lib/database/typeorm/util/crypto.js +46 -0
- package/dist/lib/database/typeorm/util/crypto.js.map +1 -0
- package/dist/lib/database/typeorm/util/error.d.ts +5 -0
- package/dist/lib/database/typeorm/util/error.d.ts.map +1 -0
- package/dist/lib/database/typeorm/util/error.js +8 -0
- package/dist/lib/database/typeorm/util/error.js.map +1 -0
- package/dist/lib/database/typeorm/util/logger.d.ts +7 -0
- package/dist/lib/database/typeorm/util/logger.d.ts.map +1 -0
- package/dist/lib/database/typeorm/util/logger.js +31 -0
- package/dist/lib/database/typeorm/util/logger.js.map +1 -0
- package/dist/lib/database/typeorm/util/yn.d.ts +2 -0
- package/dist/lib/database/typeorm/util/yn.d.ts.map +1 -0
- package/dist/lib/database/typeorm/util/yn.js +15 -0
- package/dist/lib/database/typeorm/util/yn.js.map +1 -0
- package/dist/lib/defaults/managers.d.ts.map +1 -1
- package/dist/lib/defaults/managers.js +186 -58
- package/dist/lib/defaults/managers.js.map +1 -1
- package/dist/lib/hooks/onRequest.d.ts.map +1 -1
- package/dist/lib/hooks/onRequest.js +11 -48
- package/dist/lib/hooks/onRequest.js.map +1 -1
- package/dist/lib/loader/router.d.ts.map +1 -1
- package/dist/lib/loader/router.js +92 -83
- package/dist/lib/loader/router.js.map +1 -1
- package/dist/lib/loader/tenant.d.ts.map +1 -1
- package/dist/lib/loader/tenant.js +18 -1
- package/dist/lib/loader/tenant.js.map +1 -1
- package/dist/lib/locales/en.json +12 -0
- package/dist/lib/locales/it.json +12 -0
- package/dist/lib/schemas/tenant.d.ts +88 -0
- package/dist/lib/schemas/tenant.d.ts.map +1 -0
- package/dist/lib/schemas/tenant.js +45 -0
- package/dist/lib/schemas/tenant.js.map +1 -0
- package/dist/lib/util/regexp.d.ts +2 -0
- package/dist/lib/util/regexp.d.ts.map +1 -1
- package/dist/lib/util/regexp.js +5 -3
- package/dist/lib/util/regexp.js.map +1 -1
- package/dist/lib/util/secret.d.ts +11 -0
- package/dist/lib/util/secret.d.ts.map +1 -0
- package/dist/lib/util/secret.js +70 -0
- package/dist/lib/util/secret.js.map +1 -0
- package/dist/server.js +4 -1
- package/dist/server.js.map +1 -1
- package/dist/typeorm.d.ts +15 -0
- package/dist/typeorm.d.ts.map +1 -0
- package/dist/typeorm.js +86 -0
- package/dist/typeorm.js.map +1 -0
- package/dist/types/database/typeorm/global.d.ts +56 -0
- package/dist/types/database/typeorm/global.d.ts.map +1 -0
- package/dist/types/database/typeorm/global.js +2 -0
- package/dist/types/database/typeorm/global.js.map +1 -0
- package/dist/types/global.d.ts +318 -0
- package/dist/types/orm.d.ts +22 -0
- package/lib/api/auth/controller/auth.ts +28 -12
- package/lib/api/health/controller/health.ts +1 -1
- package/lib/api/health/routes.ts +1 -0
- package/lib/api/tenants/controller/tenants.ts +211 -0
- package/lib/api/tenants/routes.ts +145 -0
- package/lib/config/plugins.ts +1 -1
- package/lib/database/typeorm/entities/change.ts +11 -0
- package/lib/database/typeorm/entities/tenant.ts +20 -0
- package/lib/database/typeorm/entities/token.ts +18 -0
- package/lib/database/typeorm/entities/user.ts +33 -0
- package/lib/database/typeorm/loader/dataBaseManager.ts +89 -0
- package/lib/database/typeorm/loader/entities.ts +47 -0
- package/lib/database/typeorm/loader/tenantManager.ts +260 -0
- package/lib/database/typeorm/loader/tokenManager.ts +157 -0
- package/lib/database/typeorm/loader/userManager.ts +432 -0
- package/lib/database/typeorm/query/builder.ts +39 -0
- package/lib/database/typeorm/query/parser.ts +29 -0
- package/lib/database/typeorm/query.ts +326 -0
- package/lib/database/typeorm/util/crypto.ts +52 -0
- package/lib/database/typeorm/util/error.ts +7 -0
- package/lib/database/typeorm/util/logger.ts +35 -0
- package/lib/database/typeorm/util/yn.ts +20 -0
- package/lib/defaults/managers.ts +191 -64
- package/lib/hooks/onRequest.ts +12 -64
- package/lib/loader/router.ts +138 -112
- package/lib/loader/tenant.ts +29 -13
- package/lib/schemas/tenant.ts +49 -0
- package/lib/util/regexp.ts +31 -8
- package/lib/util/secret.ts +113 -0
- package/package.json +56 -15
- package/dist/lib/apollo/context.d.ts +0 -6
- package/dist/lib/apollo/context.d.ts.map +0 -1
- package/dist/lib/apollo/context.js +0 -5
- package/dist/lib/apollo/context.js.map +0 -1
- package/dist/lib/apollo/resolvers.d.ts +0 -8
- package/dist/lib/apollo/resolvers.d.ts.map +0 -1
- package/dist/lib/apollo/resolvers.js +0 -8
- package/dist/lib/apollo/resolvers.js.map +0 -1
- package/dist/lib/apollo/type-defs.d.ts +0 -3
- package/dist/lib/apollo/type-defs.d.ts.map +0 -1
- package/dist/lib/apollo/type-defs.js +0 -8
- package/dist/lib/apollo/type-defs.js.map +0 -1
- package/lib/apollo/context.ts +0 -11
- package/lib/apollo/resolvers.ts +0 -12
- package/lib/apollo/type-defs.ts +0 -9
|
@@ -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 (!
|
|
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 &&
|
|
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 (!
|
|
233
|
+
if (!regExp.isEmail(email)) {
|
|
230
234
|
return reply.status(400).send(new Error('Email not valid'))
|
|
231
235
|
}
|
|
232
|
-
|
|
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('
|
|
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
|
-
|
|
337
|
-
|
|
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
|
-
|
|
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
|
-
|
package/lib/api/health/routes.ts
CHANGED
|
@@ -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
|
+
}
|