@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,145 @@
|
|
|
1
|
+
const { multi_tenant } = global.config?.options || {}
|
|
2
|
+
const isEnabled = multi_tenant?.enabled || false
|
|
3
|
+
|
|
4
|
+
export default {
|
|
5
|
+
config: {
|
|
6
|
+
title: 'Tenant Management',
|
|
7
|
+
description: 'Administration of tenants (global scope)',
|
|
8
|
+
controller: 'controller',
|
|
9
|
+
enable: isEnabled,
|
|
10
|
+
tenantContext: false, // Critical: Operate on global scope (public schema), bypass tenant context
|
|
11
|
+
tags: ['tenants']
|
|
12
|
+
},
|
|
13
|
+
routes: [
|
|
14
|
+
{
|
|
15
|
+
method: 'GET',
|
|
16
|
+
path: '/',
|
|
17
|
+
roles: [roles.admin],
|
|
18
|
+
handler: 'tenants.list',
|
|
19
|
+
middlewares: ['global.isAuthenticated'],
|
|
20
|
+
config: {
|
|
21
|
+
title: 'List Tenants',
|
|
22
|
+
description: 'Retrieve all tenants.',
|
|
23
|
+
response: {
|
|
24
|
+
200: { $ref: 'tenantListResponseSchema#' }
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
method: 'POST',
|
|
30
|
+
path: '/',
|
|
31
|
+
roles: [roles.admin],
|
|
32
|
+
handler: 'tenants.create',
|
|
33
|
+
middlewares: ['global.isAuthenticated'],
|
|
34
|
+
config: {
|
|
35
|
+
title: 'Create Tenant',
|
|
36
|
+
description: 'Create a new tenant with its own schema.',
|
|
37
|
+
body: { $ref: 'tenantBodySchema#' },
|
|
38
|
+
response: {
|
|
39
|
+
201: { $ref: 'tenantResponseSchema#' }
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
method: 'GET',
|
|
45
|
+
path: '/:id',
|
|
46
|
+
roles: [roles.admin],
|
|
47
|
+
handler: 'tenants.findOne',
|
|
48
|
+
middlewares: ['global.isAuthenticated'],
|
|
49
|
+
config: {
|
|
50
|
+
title: 'Get Tenant',
|
|
51
|
+
description: 'Retrieve a single tenant by ID.',
|
|
52
|
+
params: { $ref: 'globalParamsSchema#' },
|
|
53
|
+
response: {
|
|
54
|
+
200: { $ref: 'tenantResponseSchema#' }
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
method: 'PUT',
|
|
60
|
+
path: '/:id',
|
|
61
|
+
roles: [roles.admin],
|
|
62
|
+
handler: 'tenants.update',
|
|
63
|
+
middlewares: ['global.isAuthenticated'],
|
|
64
|
+
config: {
|
|
65
|
+
title: 'Update Tenant',
|
|
66
|
+
description: 'Update an existing tenant.',
|
|
67
|
+
params: { $ref: 'globalParamsSchema#' },
|
|
68
|
+
body: { $ref: 'tenantUpdateBodySchema#' },
|
|
69
|
+
response: {
|
|
70
|
+
200: { $ref: 'tenantResponseSchema#' }
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
method: 'DELETE',
|
|
76
|
+
path: '/:id',
|
|
77
|
+
roles: [roles.admin],
|
|
78
|
+
handler: 'tenants.remove',
|
|
79
|
+
middlewares: ['global.isAuthenticated'],
|
|
80
|
+
config: {
|
|
81
|
+
title: 'Archive Tenant',
|
|
82
|
+
description: 'Soft delete (archive) a tenant.',
|
|
83
|
+
params: { $ref: 'globalParamsSchema#' },
|
|
84
|
+
response: {
|
|
85
|
+
200: { $ref: 'defaultResponse#' }
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
},
|
|
89
|
+
{
|
|
90
|
+
method: 'POST',
|
|
91
|
+
path: '/:id/restore',
|
|
92
|
+
roles: [roles.admin],
|
|
93
|
+
handler: 'tenants.restore',
|
|
94
|
+
middlewares: ['global.isAuthenticated'],
|
|
95
|
+
config: {
|
|
96
|
+
title: 'Restore Tenant',
|
|
97
|
+
description: 'Restore a soft-deleted tenant.',
|
|
98
|
+
params: { $ref: 'globalParamsSchema#' },
|
|
99
|
+
response: {
|
|
100
|
+
200: { $ref: 'defaultResponse#' }
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
},
|
|
104
|
+
{
|
|
105
|
+
method: 'POST',
|
|
106
|
+
path: '/impersonate',
|
|
107
|
+
roles: [roles.admin],
|
|
108
|
+
handler: 'tenants.impersonate',
|
|
109
|
+
middlewares: ['global.isAuthenticated'],
|
|
110
|
+
config: {
|
|
111
|
+
title: 'Impersonate User',
|
|
112
|
+
description:
|
|
113
|
+
'Generate an impersonation token for a specific user in a target tenant (System Admin or Tenant Admin).',
|
|
114
|
+
tenantContext: true, // Enable tenant context to allow Tenant Admins to be authenticated
|
|
115
|
+
// Optional: Define body schema for documentation
|
|
116
|
+
body: {
|
|
117
|
+
type: 'object',
|
|
118
|
+
properties: {
|
|
119
|
+
targetTenantSlug: { type: 'string' },
|
|
120
|
+
targetTenantId: { type: 'string' },
|
|
121
|
+
targetRole: { type: 'string' },
|
|
122
|
+
targetUserEmail: { type: 'string' },
|
|
123
|
+
targetUserId: { type: 'string' }
|
|
124
|
+
}
|
|
125
|
+
},
|
|
126
|
+
response: {
|
|
127
|
+
200: {
|
|
128
|
+
type: 'object',
|
|
129
|
+
properties: {
|
|
130
|
+
token: { type: 'string' },
|
|
131
|
+
expiresAt: { type: 'string', format: 'date-time' },
|
|
132
|
+
impersonatedUser: {
|
|
133
|
+
type: 'object',
|
|
134
|
+
properties: {
|
|
135
|
+
email: { type: 'string' },
|
|
136
|
+
id: { type: 'string' }
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
]
|
|
145
|
+
}
|
package/lib/config/plugins.ts
CHANGED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { BaseEntity } from 'typeorm'
|
|
2
|
+
|
|
3
|
+
export abstract class Change extends BaseEntity {
|
|
4
|
+
abstract createdAt: Date
|
|
5
|
+
abstract updatedAt: Date
|
|
6
|
+
abstract userId: string
|
|
7
|
+
abstract status: string
|
|
8
|
+
abstract entityName: string
|
|
9
|
+
abstract entityId: string
|
|
10
|
+
abstract contents: object
|
|
11
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
2
|
+
import { BaseEntity } from 'typeorm'
|
|
3
|
+
|
|
4
|
+
export abstract class Tenant extends BaseEntity {
|
|
5
|
+
abstract id: any
|
|
6
|
+
abstract name: string
|
|
7
|
+
abstract slug: string
|
|
8
|
+
|
|
9
|
+
// Campi specifici per strategia
|
|
10
|
+
abstract dbSchema?: string // Per Postgres (schema splitting)
|
|
11
|
+
abstract dbName?: string // Per Mongo (database splitting, opzionale)
|
|
12
|
+
|
|
13
|
+
abstract status: 'active' | 'suspended' | 'archived'
|
|
14
|
+
|
|
15
|
+
abstract createdAt: Date
|
|
16
|
+
abstract updatedAt: Date
|
|
17
|
+
|
|
18
|
+
// Configurazione opzionale
|
|
19
|
+
abstract getConfig(): Record<string, any>
|
|
20
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
2
|
+
import { BaseEntity } from 'typeorm'
|
|
3
|
+
|
|
4
|
+
export abstract class Token extends BaseEntity {
|
|
5
|
+
// abstract id: any
|
|
6
|
+
abstract externalId: string
|
|
7
|
+
abstract name: string
|
|
8
|
+
abstract description: string
|
|
9
|
+
abstract blocked: boolean
|
|
10
|
+
abstract blockedReason: string
|
|
11
|
+
abstract blockedAt: Date
|
|
12
|
+
abstract roles: string[]
|
|
13
|
+
abstract createdAt: Date
|
|
14
|
+
abstract updatedAt: Date
|
|
15
|
+
abstract deletedAt: Date
|
|
16
|
+
abstract setId(id: any)
|
|
17
|
+
abstract getId(): any
|
|
18
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
2
|
+
import { BaseEntity } from 'typeorm'
|
|
3
|
+
|
|
4
|
+
export abstract class User extends BaseEntity {
|
|
5
|
+
// abstract id: any
|
|
6
|
+
abstract externalId: string
|
|
7
|
+
abstract username: string
|
|
8
|
+
abstract email: string
|
|
9
|
+
abstract password: string
|
|
10
|
+
abstract confirmed: boolean
|
|
11
|
+
abstract confirmedAt: Date
|
|
12
|
+
abstract passwordChangedAt: Date
|
|
13
|
+
abstract blocked: boolean
|
|
14
|
+
abstract blockedReason: string
|
|
15
|
+
abstract blockedAt: Date
|
|
16
|
+
abstract resetPasswordToken: string
|
|
17
|
+
abstract resetPasswordTokenAt: Date
|
|
18
|
+
abstract confirmationToken: string
|
|
19
|
+
abstract roles: string[]
|
|
20
|
+
abstract version: number
|
|
21
|
+
abstract createdAt: Date
|
|
22
|
+
abstract updatedAt: Date
|
|
23
|
+
abstract deletedAt: Date
|
|
24
|
+
|
|
25
|
+
// MFA Fields
|
|
26
|
+
abstract mfaEnabled: boolean
|
|
27
|
+
abstract mfaSecret: string
|
|
28
|
+
abstract mfaType: string
|
|
29
|
+
abstract mfaRecoveryCodes: string[]
|
|
30
|
+
|
|
31
|
+
abstract setId(id: any): void
|
|
32
|
+
abstract getId(): any
|
|
33
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { QueryRunner, EntityManager } from 'typeorm'
|
|
2
|
+
import * as log from '../util/logger.js'
|
|
3
|
+
|
|
4
|
+
export function isImplemented() {
|
|
5
|
+
return true
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export async function synchronizeSchemas(runner?: QueryRunner) {
|
|
9
|
+
try {
|
|
10
|
+
if (runner) {
|
|
11
|
+
await runner.connection.synchronize()
|
|
12
|
+
} else {
|
|
13
|
+
await global.connection.synchronize()
|
|
14
|
+
}
|
|
15
|
+
return true
|
|
16
|
+
} catch (error) {
|
|
17
|
+
log.error(`Volcanic-TypeORM: ${error}`)
|
|
18
|
+
throw error
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export async function retrieveBy(entityName: string, entityId: string, context?: QueryRunner | EntityManager) {
|
|
23
|
+
try {
|
|
24
|
+
const { multi_tenant } = (global as any).config?.options || {}
|
|
25
|
+
|
|
26
|
+
if (multi_tenant?.enabled && !context) {
|
|
27
|
+
const errorMsg =
|
|
28
|
+
'[DataBaseManager] ⛔️ CRITICAL: Missing DB Context (Runner/Manager) in Multi-Tenant Environment! Implicit fallback to public schema forbidden.'
|
|
29
|
+
log.error(errorMsg)
|
|
30
|
+
throw new Error(errorMsg)
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
if (context) {
|
|
34
|
+
// Handle QueryRunner logic
|
|
35
|
+
if ((context as QueryRunner).manager) {
|
|
36
|
+
return await (context as QueryRunner).manager.findOneBy(entityName, { id: entityId })
|
|
37
|
+
}
|
|
38
|
+
// Handle EntityManager logic
|
|
39
|
+
return await (context as EntityManager).findOneBy(entityName, { id: entityId })
|
|
40
|
+
}
|
|
41
|
+
return await global.connection.getRepository(global.entity[entityName]).findOneBy({ id: entityId })
|
|
42
|
+
} catch (error) {
|
|
43
|
+
if (!(entityName in global.entity)) {
|
|
44
|
+
log.error(`Volcanic-TypeORM: ${entityName} not found in global.entity`)
|
|
45
|
+
}
|
|
46
|
+
throw error
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export async function addChange(
|
|
51
|
+
entityName: string,
|
|
52
|
+
entityId: string,
|
|
53
|
+
status: string,
|
|
54
|
+
userId: string,
|
|
55
|
+
contents: unknown,
|
|
56
|
+
changeEntity = 'Change',
|
|
57
|
+
context?: QueryRunner | EntityManager
|
|
58
|
+
) {
|
|
59
|
+
try {
|
|
60
|
+
const { multi_tenant } = (global as any).config?.options || {}
|
|
61
|
+
|
|
62
|
+
if (multi_tenant?.enabled && !context) {
|
|
63
|
+
const errorMsg =
|
|
64
|
+
'[DataBaseManager] ⛔️ CRITICAL: Missing DB Context (Runner/Manager) in Multi-Tenant Environment! Implicit fallback to public schema forbidden.'
|
|
65
|
+
log.error(errorMsg)
|
|
66
|
+
throw new Error(errorMsg)
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (context) {
|
|
70
|
+
let repo
|
|
71
|
+
if ((context as QueryRunner).manager) {
|
|
72
|
+
repo = (context as QueryRunner).manager.getRepository(changeEntity)
|
|
73
|
+
} else {
|
|
74
|
+
repo = (context as EntityManager).getRepository(changeEntity)
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const newChange = repo.create({ entityName, entityId, status, userId, contents })
|
|
78
|
+
return repo.save(newChange)
|
|
79
|
+
}
|
|
80
|
+
const repo = global.connection.getRepository(global.entity[changeEntity])
|
|
81
|
+
const newChange = repo.create({ entityName, entityId, status, userId, contents })
|
|
82
|
+
return repo.save(newChange)
|
|
83
|
+
} catch (error) {
|
|
84
|
+
if (!(changeEntity in global.entity)) {
|
|
85
|
+
log.error(`Volcanic-TypeORM: ${changeEntity} not found in global.entity`)
|
|
86
|
+
}
|
|
87
|
+
throw error
|
|
88
|
+
}
|
|
89
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
2
|
+
import * as path from 'path'
|
|
3
|
+
import { globSync } from 'glob'
|
|
4
|
+
import pluralize from 'pluralize'
|
|
5
|
+
import { fileURLToPath, pathToFileURL } from 'url'
|
|
6
|
+
|
|
7
|
+
const __filename = fileURLToPath(import.meta.url)
|
|
8
|
+
const __dirname = path.dirname(__filename)
|
|
9
|
+
|
|
10
|
+
export async function load() {
|
|
11
|
+
const classes: any = {}
|
|
12
|
+
const repositories: any = {}
|
|
13
|
+
const entities: any[] = []
|
|
14
|
+
|
|
15
|
+
const patterns = [
|
|
16
|
+
path.join(__dirname, '..', 'entities', '*.e.{ts,js}'),
|
|
17
|
+
path.join(process.cwd(), 'src', 'entities', '*.e.{ts,js}'),
|
|
18
|
+
path.join(process.cwd(), 'dist', 'src', 'entities', '*.e.{ts,js}')
|
|
19
|
+
]
|
|
20
|
+
|
|
21
|
+
for (const pattern of patterns) {
|
|
22
|
+
const files = globSync(pattern, { nodir: true, windowsPathsNoEscape: true })
|
|
23
|
+
|
|
24
|
+
const modules = await Promise.all(
|
|
25
|
+
files.map((f) => {
|
|
26
|
+
const fileUrl = pathToFileURL(f).href
|
|
27
|
+
return import(fileUrl)
|
|
28
|
+
})
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
for (const entityModule of modules) {
|
|
32
|
+
const entityClass = entityModule.default || entityModule
|
|
33
|
+
const entityNames = Object.keys(entityClass)
|
|
34
|
+
|
|
35
|
+
entityNames.forEach((name) => {
|
|
36
|
+
const entity = entityClass[name]
|
|
37
|
+
if (typeof entity === 'function') {
|
|
38
|
+
classes[name] = entity
|
|
39
|
+
repositories[pluralize(name.toLowerCase())] = entity
|
|
40
|
+
entities.push(entity)
|
|
41
|
+
}
|
|
42
|
+
})
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return { classes, repositories, entities }
|
|
47
|
+
}
|
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
2
|
+
import { DataSource, EntityManager } from 'typeorm'
|
|
3
|
+
import { Tenant } from '../entities/tenant.js'
|
|
4
|
+
|
|
5
|
+
export class TenantManager {
|
|
6
|
+
constructor(private dataSource: DataSource) {}
|
|
7
|
+
|
|
8
|
+
isImplemented() {
|
|
9
|
+
return true
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
async resolveTenant(req: any): Promise<Tenant | null> {
|
|
13
|
+
const { multi_tenant } = (global as any).config?.options || {}
|
|
14
|
+
const headerKey = multi_tenant?.header_key || 'x-tenant-id'
|
|
15
|
+
|
|
16
|
+
// 1. Single Tenant Mode (Default)
|
|
17
|
+
if (!multi_tenant?.enabled) {
|
|
18
|
+
return null
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// 2. Multi-Tenant Mode: Extract Identifiers
|
|
22
|
+
// Parse JWT from headers if not already attached (Fastify might attach it to req.user)
|
|
23
|
+
const jwtTid = req.user?.tid
|
|
24
|
+
const headerTid = req.headers[headerKey]
|
|
25
|
+
|
|
26
|
+
// 3. Validation Logic (Strict)
|
|
27
|
+
// Rule A: Header is MANDATORY in Multi-Tenant
|
|
28
|
+
if (!headerTid) {
|
|
29
|
+
// Log debug and return null to let the caller handle 404/403
|
|
30
|
+
if ((global as any).log?.d) (global as any).log.debug('[TenantManager] No tenant header found')
|
|
31
|
+
return null
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Rule B: JWT is OPTIONAL (For Public Routes/Login).
|
|
35
|
+
// Rule C: If the JWT carries a tenant binding, the header MUST match it.
|
|
36
|
+
// A token bound to tenant X requesting tenant Y via header is a tenant-isolation
|
|
37
|
+
// bypass (IDOR) attempt — REJECT it, do not merely warn. If a privileged
|
|
38
|
+
// System-Admin impersonation flow is ever required, gate it HERE behind an
|
|
39
|
+
// explicit, verified role check on `req.user` (e.g. a dedicated `system_admin`
|
|
40
|
+
// role) — never relax this default.
|
|
41
|
+
if (jwtTid && jwtTid !== headerTid) {
|
|
42
|
+
if ((global as any).log?.w)
|
|
43
|
+
(global as any).log.warn(`[TenantManager] Tenant spoofing blocked: Token(${jwtTid}) vs Header(${headerTid})`)
|
|
44
|
+
return null
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Prefer the JWT-bound tenant when present; fall back to the header (public/login routes).
|
|
48
|
+
const tenantId = jwtTid || headerTid
|
|
49
|
+
|
|
50
|
+
// 5. Database Lookup
|
|
51
|
+
const tenantRepo = this.dataSource.getRepository('Tenant')
|
|
52
|
+
|
|
53
|
+
// Support lookup by ID or SLUG
|
|
54
|
+
const tenant = (await tenantRepo.findOne({
|
|
55
|
+
where: [{ slug: tenantId }]
|
|
56
|
+
})) as unknown as Tenant
|
|
57
|
+
|
|
58
|
+
if (!tenant) {
|
|
59
|
+
if ((global as any).log?.d) (global as any).log.debug(`[TenantManager] Tenant ${tenantId} not found in DB`)
|
|
60
|
+
return null
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (tenant.status !== 'active') {
|
|
64
|
+
if ((global as any).log?.d) (global as any).log.debug(`[TenantManager] Tenant ${tenantId} is not active`)
|
|
65
|
+
return null
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return tenant
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Switches the database context to the specific tenant schema.
|
|
73
|
+
* CRITICAL SECURITY: This method MUST receive an EntityManager (db) when using Postgres Multi-Tenancy.
|
|
74
|
+
* Modifying the global search_path is strictly forbidden to prevent data leaks.
|
|
75
|
+
*/
|
|
76
|
+
async switchContext(tenant: Tenant, db?: EntityManager) {
|
|
77
|
+
const driver = this.dataSource.driver.options.type
|
|
78
|
+
|
|
79
|
+
if (driver === 'postgres') {
|
|
80
|
+
const schema = tenant.dbSchema || 'public'
|
|
81
|
+
const safeSchema = schema.replace(/[^a-z0-9_]/gi, '')
|
|
82
|
+
|
|
83
|
+
if (db) {
|
|
84
|
+
if ((global as any).log?.t)
|
|
85
|
+
(global as any).log.trace(`[TenantManager] Context-Aware Switch Schema to: ${safeSchema}`)
|
|
86
|
+
// Execute SET search_path only on the specific transactional QueryRunner
|
|
87
|
+
await db.query(`SET search_path TO "${safeSchema}", public`)
|
|
88
|
+
} else {
|
|
89
|
+
// STRICT SECURITY CHECK
|
|
90
|
+
// If no db (EntityManager) is provided, we used to fallback to global dataSource.
|
|
91
|
+
// This is now FORBIDDEN as it poisons the connection pool.
|
|
92
|
+
const errorMsg =
|
|
93
|
+
'[TenantManager] ⛔️ CRITICAL: Attempted UNSAFE global context switch without QueryRunner. This operation has been blocked to prevent data leaks.'
|
|
94
|
+
if ((global as any).log?.f) (global as any).log.fatal(errorMsg)
|
|
95
|
+
throw new Error(errorMsg)
|
|
96
|
+
}
|
|
97
|
+
} else if (driver === 'mongodb') {
|
|
98
|
+
// Mongo implementation stub
|
|
99
|
+
if ((global as any).log?.w)
|
|
100
|
+
(global as any).log.warn('[TenantManager] Mongo Multi-Tenancy context switch is not enforcing isolation yet.')
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
async createTenant(data: any): Promise<Tenant> {
|
|
105
|
+
const driver = this.dataSource.driver.options.type
|
|
106
|
+
const repo = this.dataSource.getRepository('Tenant')
|
|
107
|
+
|
|
108
|
+
// Data validation should be handled by Schema validation in API
|
|
109
|
+
// Enforce lowercase slug
|
|
110
|
+
if (data.slug) data.slug = data.slug.toLowerCase()
|
|
111
|
+
|
|
112
|
+
// Check existence
|
|
113
|
+
const existing = await repo.findOne({
|
|
114
|
+
where: [{ slug: data.slug }, { dbSchema: data.dbSchema }]
|
|
115
|
+
})
|
|
116
|
+
if (existing) {
|
|
117
|
+
throw new Error('Tenant with this slug or dbSchema already exists')
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const tenant = repo.create({
|
|
121
|
+
...data,
|
|
122
|
+
status: 'active'
|
|
123
|
+
})
|
|
124
|
+
const savedTenant = (await repo.save(tenant)) as any as Tenant
|
|
125
|
+
|
|
126
|
+
if (driver === 'postgres') {
|
|
127
|
+
const schema = data.dbSchema
|
|
128
|
+
if (!schema) throw new Error('dbSchema is required for Postgres Multi-Tenancy')
|
|
129
|
+
|
|
130
|
+
const safeSchema = schema.replace(/[^a-z0-9_]/gi, '')
|
|
131
|
+
const qr = this.dataSource.createQueryRunner()
|
|
132
|
+
await qr.connect()
|
|
133
|
+
try {
|
|
134
|
+
await qr.query(`CREATE SCHEMA IF NOT EXISTS "${safeSchema}"`)
|
|
135
|
+
|
|
136
|
+
// --- Schema Synchronization ---
|
|
137
|
+
// We create an ephemeral DataSource to force TypeORM to synchronize only the specific schema.
|
|
138
|
+
// This is necessary because the global DataSource is bound to the public/default schema.
|
|
139
|
+
if ((global as any).log?.i)
|
|
140
|
+
(global as any).log.info(`[TenantManager] Synchronizing tables for schema: ${safeSchema}`)
|
|
141
|
+
|
|
142
|
+
const tenantDs = new DataSource({
|
|
143
|
+
...(this.dataSource.options as any),
|
|
144
|
+
schema: safeSchema,
|
|
145
|
+
synchronize: true,
|
|
146
|
+
name: `sync_${safeSchema}_${Date.now()}`
|
|
147
|
+
})
|
|
148
|
+
await tenantDs.initialize()
|
|
149
|
+
await tenantDs.destroy()
|
|
150
|
+
|
|
151
|
+
if ((global as any).log?.i)
|
|
152
|
+
(global as any).log.info(`[TenantManager] Schema ${safeSchema} synchronized successfully`)
|
|
153
|
+
|
|
154
|
+
await qr.startTransaction()
|
|
155
|
+
|
|
156
|
+
// Context Switch within the same transaction to seed the user
|
|
157
|
+
// Use SET LOCAL so it automatically reverts at the end of the transaction
|
|
158
|
+
await qr.query(`SET LOCAL search_path TO "${safeSchema}", public`)
|
|
159
|
+
|
|
160
|
+
if (data.adminEmail && data.adminPassword) {
|
|
161
|
+
if ((global as any).log?.i) (global as any).log.info(`[TenantManager] Seeding Admin User: ${data.adminEmail}`)
|
|
162
|
+
const { createUser } = await import('./userManager.js')
|
|
163
|
+
|
|
164
|
+
await createUser(
|
|
165
|
+
{
|
|
166
|
+
email: data.adminEmail,
|
|
167
|
+
password: data.adminPassword,
|
|
168
|
+
roles: ['admin'],
|
|
169
|
+
firstName: data.adminFirstName || 'Tenant',
|
|
170
|
+
lastName: data.adminLastName || 'Admin'
|
|
171
|
+
} as any,
|
|
172
|
+
qr
|
|
173
|
+
)
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
await qr.commitTransaction()
|
|
177
|
+
if ((global as any).log?.i) (global as any).log.info(`[TenantManager] Created Schema & Admin: ${safeSchema}`)
|
|
178
|
+
} catch (err) {
|
|
179
|
+
if (qr.isTransactionActive) {
|
|
180
|
+
await qr.rollbackTransaction()
|
|
181
|
+
}
|
|
182
|
+
throw err
|
|
183
|
+
} finally {
|
|
184
|
+
try {
|
|
185
|
+
// Reset to public for safety before releasing
|
|
186
|
+
await qr.query(`SET search_path TO public`)
|
|
187
|
+
} catch (cleanupErr) {
|
|
188
|
+
if ((global as any).log?.e)
|
|
189
|
+
(global as any).log.error(`[TenantManager] Failed to reset search_path: ${cleanupErr}`)
|
|
190
|
+
}
|
|
191
|
+
await qr.release()
|
|
192
|
+
}
|
|
193
|
+
} else {
|
|
194
|
+
// Mongo or others
|
|
195
|
+
if ((global as any).log?.i) (global as any).log.info(`[TenantManager] Created Tenant Record for ${driver}`)
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
return savedTenant
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
async deleteTenant(id: string): Promise<void> {
|
|
202
|
+
const repo = this.dataSource.getRepository('Tenant')
|
|
203
|
+
await repo.softDelete(id)
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
async restoreTenant(id: string) {
|
|
207
|
+
const repo = this.dataSource.getRepository('Tenant')
|
|
208
|
+
await repo.restore(id)
|
|
209
|
+
return repo.findOneBy({ id })
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
async getTenant(id: string) {
|
|
213
|
+
const repo = this.dataSource.getRepository('Tenant')
|
|
214
|
+
return (await repo.findOneBy({ id })) as unknown as Tenant
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
async updateTenant(id: string, data: Partial<Tenant>) {
|
|
218
|
+
const repo = this.dataSource.getRepository('Tenant')
|
|
219
|
+
// Exclude dbSchema from updates as it requires migration of tables
|
|
220
|
+
|
|
221
|
+
const { dbSchema: _ignore, ...updateData } = data
|
|
222
|
+
await repo.update(id, updateData as any)
|
|
223
|
+
return repo.findOneBy({ id })
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
async listTenants() {
|
|
227
|
+
const repo = this.dataSource.getRepository('Tenant')
|
|
228
|
+
return repo.find({ order: { createdAt: 'DESC' } as any })
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* Safe utility to execute a function within a specific Tenant Context (e.g. for Background Jobs or System Admin tasks).
|
|
233
|
+
* This handles the creation, connection, context switch, and release of a QueryRunner automatically.
|
|
234
|
+
*/
|
|
235
|
+
async runInTenantContext<T>(tenantId: string, callback: (em: EntityManager) => Promise<T>): Promise<T> {
|
|
236
|
+
const tenant = await this.getTenant(tenantId)
|
|
237
|
+
if (!tenant) throw new Error(`Tenant ${tenantId} not found`)
|
|
238
|
+
if (tenant.status !== 'active') throw new Error(`Tenant ${tenantId} is not active`)
|
|
239
|
+
|
|
240
|
+
const qr = this.dataSource.createQueryRunner()
|
|
241
|
+
await qr.connect()
|
|
242
|
+
|
|
243
|
+
try {
|
|
244
|
+
// Enforce context switch on this specific runner
|
|
245
|
+
await this.switchContext(tenant, qr.manager)
|
|
246
|
+
|
|
247
|
+
// Execute callback with the isolated EntityManager
|
|
248
|
+
const result = await callback(qr.manager)
|
|
249
|
+
return result
|
|
250
|
+
} finally {
|
|
251
|
+
try {
|
|
252
|
+
await qr.query('SET search_path TO public')
|
|
253
|
+
} catch (e) {
|
|
254
|
+
if ((global as any).log?.w)
|
|
255
|
+
(global as any).log.warn(`[TenantManager] Failed to reset search_path context: ${e}`)
|
|
256
|
+
}
|
|
257
|
+
await qr.release()
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
}
|