@lenne.tech/nest-server 11.20.1 → 11.21.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 +444 -100
- package/dist/core/common/decorators/restricted.decorator.d.ts +1 -0
- package/dist/core/common/decorators/restricted.decorator.js +4 -1
- package/dist/core/common/decorators/restricted.decorator.js.map +1 -1
- package/dist/core/common/helpers/input.helper.js +11 -8
- package/dist/core/common/helpers/input.helper.js.map +1 -1
- package/dist/core/common/interceptors/check-security.interceptor.js +10 -8
- package/dist/core/common/interceptors/check-security.interceptor.js.map +1 -1
- package/dist/core/common/interfaces/server-options.interface.d.ts +5 -1
- package/dist/core/common/middleware/request-context.middleware.js +10 -6
- package/dist/core/common/middleware/request-context.middleware.js.map +1 -1
- package/dist/core/common/plugins/mongoose-tenant.plugin.js +40 -24
- package/dist/core/common/plugins/mongoose-tenant.plugin.js.map +1 -1
- package/dist/core/common/services/email.service.d.ts +5 -1
- package/dist/core/common/services/email.service.js +16 -2
- package/dist/core/common/services/email.service.js.map +1 -1
- package/dist/core/common/services/request-context.service.d.ts +3 -0
- package/dist/core/common/services/request-context.service.js +6 -0
- package/dist/core/common/services/request-context.service.js.map +1 -1
- package/dist/core/modules/auth/guards/roles.guard.js +6 -10
- package/dist/core/modules/auth/guards/roles.guard.js.map +1 -1
- package/dist/core/modules/auth/tokens.decorator.d.ts +1 -1
- package/dist/core/modules/better-auth/better-auth-roles.guard.js +5 -6
- package/dist/core/modules/better-auth/better-auth-roles.guard.js.map +1 -1
- package/dist/core/modules/better-auth/core-better-auth-user.mapper.d.ts +6 -0
- package/dist/core/modules/better-auth/core-better-auth-user.mapper.js +52 -17
- package/dist/core/modules/better-auth/core-better-auth-user.mapper.js.map +1 -1
- package/dist/core/modules/better-auth/core-better-auth.service.d.ts +3 -1
- package/dist/core/modules/better-auth/core-better-auth.service.js +14 -0
- package/dist/core/modules/better-auth/core-better-auth.service.js.map +1 -1
- package/dist/core/modules/tenant/core-tenant-member.model.d.ts +11 -0
- package/dist/core/modules/tenant/core-tenant-member.model.js +106 -0
- package/dist/core/modules/tenant/core-tenant-member.model.js.map +1 -0
- package/dist/core/modules/tenant/core-tenant.decorators.d.ts +3 -0
- package/dist/core/modules/tenant/core-tenant.decorators.js +12 -0
- package/dist/core/modules/tenant/core-tenant.decorators.js.map +1 -0
- package/dist/core/modules/tenant/core-tenant.enums.d.ts +13 -0
- package/dist/core/modules/tenant/core-tenant.enums.js +25 -0
- package/dist/core/modules/tenant/core-tenant.enums.js.map +1 -0
- package/dist/core/modules/tenant/core-tenant.guard.d.ts +25 -0
- package/dist/core/modules/tenant/core-tenant.guard.js +271 -0
- package/dist/core/modules/tenant/core-tenant.guard.js.map +1 -0
- package/dist/core/modules/tenant/core-tenant.helpers.d.ts +7 -0
- package/dist/core/modules/tenant/core-tenant.helpers.js +60 -0
- package/dist/core/modules/tenant/core-tenant.helpers.js.map +1 -0
- package/dist/core/modules/tenant/core-tenant.module.d.ts +12 -0
- package/dist/core/modules/tenant/core-tenant.module.js +58 -0
- package/dist/core/modules/tenant/core-tenant.module.js.map +1 -0
- package/dist/core/modules/tenant/core-tenant.service.d.ts +19 -0
- package/dist/core/modules/tenant/core-tenant.service.js +170 -0
- package/dist/core/modules/tenant/core-tenant.service.js.map +1 -0
- package/dist/core/modules/user/core-user.service.js +12 -1
- package/dist/core/modules/user/core-user.service.js.map +1 -1
- package/dist/core.module.js +11 -0
- package/dist/core.module.js.map +1 -1
- package/dist/index.d.ts +7 -0
- package/dist/index.js +7 -0
- package/dist/index.js.map +1 -1
- package/dist/tsconfig.build.tsbuildinfo +1 -1
- package/package.json +35 -24
- package/src/core/common/decorators/restricted.decorator.ts +12 -2
- package/src/core/common/helpers/input.helper.ts +24 -9
- package/src/core/common/interceptors/check-security.interceptor.ts +19 -13
- package/src/core/common/interfaces/server-options.interface.ts +80 -28
- package/src/core/common/middleware/request-context.middleware.ts +12 -5
- package/src/core/common/plugins/mongoose-tenant.plugin.ts +78 -45
- package/src/core/common/services/email.service.ts +26 -5
- package/src/core/common/services/request-context.service.ts +15 -1
- package/src/core/modules/auth/guards/roles.guard.ts +10 -10
- package/src/core/modules/better-auth/better-auth-roles.guard.ts +9 -6
- package/src/core/modules/better-auth/core-better-auth-user.mapper.ts +86 -21
- package/src/core/modules/better-auth/core-better-auth.service.ts +27 -2
- package/src/core/modules/tenant/INTEGRATION-CHECKLIST.md +165 -0
- package/src/core/modules/tenant/README.md +268 -0
- package/src/core/modules/tenant/core-tenant-member.model.ts +121 -0
- package/src/core/modules/tenant/core-tenant.decorators.ts +46 -0
- package/src/core/modules/tenant/core-tenant.enums.ts +77 -0
- package/src/core/modules/tenant/core-tenant.guard.ts +441 -0
- package/src/core/modules/tenant/core-tenant.helpers.ts +103 -0
- package/src/core/modules/tenant/core-tenant.module.ts +102 -0
- package/src/core/modules/tenant/core-tenant.service.ts +244 -0
- package/src/core/modules/user/core-user.service.ts +17 -1
- package/src/core.module.ts +15 -0
- package/src/index.ts +12 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lenne.tech/nest-server",
|
|
3
|
-
"version": "11.
|
|
3
|
+
"version": "11.21.1",
|
|
4
4
|
"description": "Modern, fast, powerful Node.js web framework in TypeScript based on Nest with a GraphQL API and a connection to MongoDB (or other databases).",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"node",
|
|
@@ -73,26 +73,26 @@
|
|
|
73
73
|
"node": ">= 20"
|
|
74
74
|
},
|
|
75
75
|
"dependencies": {
|
|
76
|
-
"@apollo/server": "5.
|
|
76
|
+
"@apollo/server": "5.5.0",
|
|
77
77
|
"@as-integrations/express5": "1.1.2",
|
|
78
|
-
"@better-auth/passkey": "1.5.
|
|
78
|
+
"@better-auth/passkey": "1.5.5",
|
|
79
79
|
"@getbrevo/brevo": "3.0.1",
|
|
80
80
|
"@nestjs/apollo": "13.2.4",
|
|
81
|
-
"@nestjs/common": "11.1.
|
|
82
|
-
"@nestjs/core": "11.1.
|
|
81
|
+
"@nestjs/common": "11.1.17",
|
|
82
|
+
"@nestjs/core": "11.1.17",
|
|
83
83
|
"@nestjs/graphql": "13.2.4",
|
|
84
84
|
"@nestjs/jwt": "11.0.2",
|
|
85
85
|
"@nestjs/mongoose": "11.0.4",
|
|
86
86
|
"@nestjs/passport": "11.0.5",
|
|
87
|
-
"@nestjs/platform-express": "11.1.
|
|
87
|
+
"@nestjs/platform-express": "11.1.17",
|
|
88
88
|
"@nestjs/schedule": "6.1.1",
|
|
89
89
|
"@nestjs/swagger": "11.2.6",
|
|
90
90
|
"@nestjs/terminus": "11.1.1",
|
|
91
|
-
"@nestjs/websockets": "11.1.
|
|
91
|
+
"@nestjs/websockets": "11.1.17",
|
|
92
92
|
"@tus/file-store": "2.0.0",
|
|
93
93
|
"@tus/server": "2.3.0",
|
|
94
94
|
"bcrypt": "6.0.0",
|
|
95
|
-
"better-auth": "1.5.
|
|
95
|
+
"better-auth": "1.5.5",
|
|
96
96
|
"class-transformer": "0.5.1",
|
|
97
97
|
"class-validator": "0.15.1",
|
|
98
98
|
"compression": "1.8.1",
|
|
@@ -100,18 +100,18 @@
|
|
|
100
100
|
"dotenv": "17.3.1",
|
|
101
101
|
"ejs": "5.0.1",
|
|
102
102
|
"express": "5.2.1",
|
|
103
|
-
"graphql": "16.13.
|
|
103
|
+
"graphql": "16.13.2",
|
|
104
104
|
"graphql-query-complexity": "1.1.0",
|
|
105
105
|
"graphql-subscriptions": "3.0.0",
|
|
106
106
|
"graphql-upload": "15.0.2",
|
|
107
107
|
"js-sha256": "0.11.1",
|
|
108
108
|
"json-to-graphql-query": "2.3.0",
|
|
109
109
|
"lodash": "4.17.23",
|
|
110
|
-
"mongodb": "7.1.
|
|
111
|
-
"mongoose": "9.3.
|
|
110
|
+
"mongodb": "7.1.1",
|
|
111
|
+
"mongoose": "9.3.3",
|
|
112
112
|
"multer": "2.1.1",
|
|
113
113
|
"node-mailjet": "6.0.11",
|
|
114
|
-
"nodemailer": "8.0.
|
|
114
|
+
"nodemailer": "8.0.4",
|
|
115
115
|
"passport": "0.7.0",
|
|
116
116
|
"passport-jwt": "4.0.1",
|
|
117
117
|
"reflect-metadata": "0.2.2",
|
|
@@ -122,31 +122,31 @@
|
|
|
122
122
|
},
|
|
123
123
|
"devDependencies": {
|
|
124
124
|
"@compodoc/compodoc": "1.2.1",
|
|
125
|
-
"@nestjs/cli": "11.0.
|
|
126
|
-
"@nestjs/schematics": "11.0.
|
|
127
|
-
"@nestjs/testing": "11.1.
|
|
125
|
+
"@nestjs/cli": "11.0.17",
|
|
126
|
+
"@nestjs/schematics": "11.0.10",
|
|
127
|
+
"@nestjs/testing": "11.1.17",
|
|
128
128
|
"@swc/cli": "0.8.0",
|
|
129
|
-
"@swc/core": "1.15.
|
|
129
|
+
"@swc/core": "1.15.21",
|
|
130
130
|
"@types/compression": "1.8.1",
|
|
131
131
|
"@types/cookie-parser": "1.4.10",
|
|
132
132
|
"@types/ejs": "3.1.5",
|
|
133
133
|
"@types/express": "5.0.6",
|
|
134
134
|
"@types/lodash": "4.17.24",
|
|
135
135
|
"@types/multer": "2.1.0",
|
|
136
|
-
"@types/node": "25.
|
|
136
|
+
"@types/node": "25.5.0",
|
|
137
137
|
"@types/nodemailer": "7.0.11",
|
|
138
138
|
"@types/passport": "1.0.17",
|
|
139
139
|
"@types/supertest": "7.2.0",
|
|
140
|
-
"@vitest/coverage-v8": "4.
|
|
141
|
-
"@vitest/ui": "4.
|
|
140
|
+
"@vitest/coverage-v8": "4.1.2",
|
|
141
|
+
"@vitest/ui": "4.1.2",
|
|
142
142
|
"ansi-colors": "4.1.3",
|
|
143
143
|
"find-file-up": "2.0.1",
|
|
144
144
|
"husky": "9.1.7",
|
|
145
145
|
"nodemon": "3.1.14",
|
|
146
146
|
"npm-watch": "0.13.0",
|
|
147
147
|
"otpauth": "9.5.0",
|
|
148
|
-
"oxfmt": "0.
|
|
149
|
-
"oxlint": "1.
|
|
148
|
+
"oxfmt": "0.43.0",
|
|
149
|
+
"oxlint": "1.58.0",
|
|
150
150
|
"rimraf": "6.1.3",
|
|
151
151
|
"supertest": "7.2.2",
|
|
152
152
|
"ts-node": "10.9.2",
|
|
@@ -157,7 +157,7 @@
|
|
|
157
157
|
"vite": "7.3.1",
|
|
158
158
|
"vite-plugin-node": "7.0.0",
|
|
159
159
|
"vite-tsconfig-paths": "6.1.1",
|
|
160
|
-
"vitest": "4.
|
|
160
|
+
"vitest": "4.1.2"
|
|
161
161
|
},
|
|
162
162
|
"main": "dist/index.js",
|
|
163
163
|
"types": "dist/index.d.ts",
|
|
@@ -179,10 +179,21 @@
|
|
|
179
179
|
"minimatch@<3.1.4": "3.1.4",
|
|
180
180
|
"minimatch@>=9.0.0 <9.0.7": "9.0.7",
|
|
181
181
|
"minimatch@>=10.0.0 <10.2.3": "10.2.4",
|
|
182
|
-
"rollup@>=4.0.0 <4.
|
|
182
|
+
"rollup@>=4.0.0 <4.60.1": "4.60.1",
|
|
183
183
|
"ajv@<6.14.0": "6.14.0",
|
|
184
184
|
"ajv@>=7.0.0-alpha.0 <8.18.0": "8.18.0",
|
|
185
|
-
"file-type@>=13.0.0 <21.3.
|
|
185
|
+
"file-type@>=13.0.0 <21.3.2": "21.3.2",
|
|
186
|
+
"undici@>=7.0.0 <7.24.0": "7.24.3",
|
|
187
|
+
"yauzl@<3.2.1": "3.2.1",
|
|
188
|
+
"flatted@<=3.4.1": "3.4.2",
|
|
189
|
+
"srvx@<0.11.13": "0.11.13",
|
|
190
|
+
"handlebars@>=4.0.0 <4.7.9": "4.7.9",
|
|
191
|
+
"brace-expansion@<1.1.13": "1.1.13",
|
|
192
|
+
"brace-expansion@>=4.0.0 <5.0.5": "5.0.5",
|
|
193
|
+
"picomatch@<2.3.2": "2.3.2",
|
|
194
|
+
"picomatch@>=4.0.0 <4.0.4": "4.0.4",
|
|
195
|
+
"path-to-regexp@>=8.0.0 <8.4.0": "8.4.1",
|
|
196
|
+
"kysely@>=0.26.0 <0.28.15": "0.28.15"
|
|
186
197
|
},
|
|
187
198
|
"onlyBuiltDependencies": [
|
|
188
199
|
"bcrypt",
|
|
@@ -5,7 +5,9 @@ import _ = require('lodash');
|
|
|
5
5
|
import { ProcessType } from '../enums/process-type.enum';
|
|
6
6
|
import { RoleEnum } from '../enums/role.enum';
|
|
7
7
|
import { equalIds, getIncludedIds } from '../helpers/db.helper';
|
|
8
|
+
import { RequestContext } from '../services/request-context.service';
|
|
8
9
|
import { RequireAtLeastOne } from '../types/required-at-least-one.type';
|
|
10
|
+
import { checkRoleAccess } from '../../modules/tenant/core-tenant.helpers';
|
|
9
11
|
|
|
10
12
|
/**
|
|
11
13
|
* Restricted meta key
|
|
@@ -61,7 +63,14 @@ export const getRestricted = (object: unknown, propertyKey?: string): Restricted
|
|
|
61
63
|
*/
|
|
62
64
|
export const checkRestricted = (
|
|
63
65
|
data: any,
|
|
64
|
-
user: {
|
|
66
|
+
user: {
|
|
67
|
+
emailVerified?: any;
|
|
68
|
+
hasRole: (roles: string[]) => boolean;
|
|
69
|
+
id: any;
|
|
70
|
+
roles?: string[];
|
|
71
|
+
verified?: any;
|
|
72
|
+
verifiedAt?: any;
|
|
73
|
+
},
|
|
65
74
|
options: {
|
|
66
75
|
allowCreatorOfParent?: boolean;
|
|
67
76
|
checkObjectItself?: boolean;
|
|
@@ -163,7 +172,8 @@ export const checkRestricted = (
|
|
|
163
172
|
(roles.includes(RoleEnum.S_CREATOR) &&
|
|
164
173
|
(('createdBy' in data && equalIds(data.createdBy, user)) ||
|
|
165
174
|
(config.allowCreatorOfParent && !('createdBy' in data) && config.isCreatorOfParent))) ||
|
|
166
|
-
(roles.includes(RoleEnum.S_VERIFIED) && (user?.verified || user?.verifiedAt || user?.emailVerified))
|
|
175
|
+
(roles.includes(RoleEnum.S_VERIFIED) && (user?.verified || user?.verifiedAt || user?.emailVerified)) ||
|
|
176
|
+
(user?.id && checkRoleAccess(roles, user?.roles, RequestContext.get()?.tenantRole))
|
|
167
177
|
) {
|
|
168
178
|
valid = true;
|
|
169
179
|
}
|
|
@@ -783,15 +783,30 @@ export function processDeep(
|
|
|
783
783
|
specialProperties?: string[];
|
|
784
784
|
},
|
|
785
785
|
): any {
|
|
786
|
-
// Set options
|
|
787
|
-
const
|
|
788
|
-
processedObjects: new WeakMap(),
|
|
789
|
-
specialClasses: [],
|
|
790
|
-
specialFunctions: [],
|
|
791
|
-
specialProperties: [],
|
|
792
|
-
...options,
|
|
786
|
+
// Set options once and reuse for all recursive calls (avoids creating new objects per property)
|
|
787
|
+
const resolvedOptions = {
|
|
788
|
+
processedObjects: options?.processedObjects ?? new WeakMap(),
|
|
789
|
+
specialClasses: options?.specialClasses ?? [],
|
|
790
|
+
specialFunctions: options?.specialFunctions ?? [],
|
|
791
|
+
specialProperties: options?.specialProperties ?? [],
|
|
793
792
|
};
|
|
794
793
|
|
|
794
|
+
return processDeepInternal(data, func, resolvedOptions);
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
/** Internal recursive implementation that reuses the resolved options object */
|
|
798
|
+
function processDeepInternal(
|
|
799
|
+
data: any,
|
|
800
|
+
func: (data: any) => any,
|
|
801
|
+
options: {
|
|
802
|
+
processedObjects: WeakMap<any, boolean>;
|
|
803
|
+
specialClasses: ((new (args: any[]) => any) | string)[];
|
|
804
|
+
specialFunctions: string[];
|
|
805
|
+
specialProperties: string[];
|
|
806
|
+
},
|
|
807
|
+
): any {
|
|
808
|
+
const { processedObjects, specialClasses, specialFunctions, specialProperties } = options;
|
|
809
|
+
|
|
795
810
|
// Check for falsifiable values
|
|
796
811
|
if (!data) {
|
|
797
812
|
return func(data);
|
|
@@ -806,7 +821,7 @@ export function processDeep(
|
|
|
806
821
|
|
|
807
822
|
// Process array
|
|
808
823
|
if (Array.isArray(data)) {
|
|
809
|
-
return func(data.map((item) =>
|
|
824
|
+
return func(data.map((item) => processDeepInternal(item, func, options)));
|
|
810
825
|
}
|
|
811
826
|
|
|
812
827
|
// Process object
|
|
@@ -826,7 +841,7 @@ export function processDeep(
|
|
|
826
841
|
}
|
|
827
842
|
}
|
|
828
843
|
for (const [key, value] of Object.entries(data)) {
|
|
829
|
-
data[key] =
|
|
844
|
+
data[key] = processDeepInternal(value, func, options);
|
|
830
845
|
}
|
|
831
846
|
return func(data);
|
|
832
847
|
}
|
|
@@ -62,18 +62,17 @@ export class CheckSecurityInterceptor implements NestInterceptor {
|
|
|
62
62
|
|
|
63
63
|
// Check data
|
|
64
64
|
if (data && typeof data === 'object' && typeof data.securityCheck === 'function') {
|
|
65
|
-
|
|
65
|
+
// Only capture pre-check state when debug is active (JSON.stringify is expensive)
|
|
66
|
+
const dataJson = this.config.debug ? JSON.stringify(data) : undefined;
|
|
66
67
|
const response = data.securityCheck(user, force);
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
}
|
|
76
|
-
});
|
|
68
|
+
if (this.config.debug && dataJson !== JSON.stringify(response)) {
|
|
69
|
+
const id = getStringIds(data);
|
|
70
|
+
console.debug(
|
|
71
|
+
'CheckSecurityInterceptor: securityCheck changed data of type',
|
|
72
|
+
data.constructor.name,
|
|
73
|
+
id && !Array.isArray(id) ? `with ID: ${id}` : '',
|
|
74
|
+
);
|
|
75
|
+
}
|
|
77
76
|
if (response && !data._doNotCheckSecurityDeep) {
|
|
78
77
|
for (const key of Object.keys(response)) {
|
|
79
78
|
response[key] = check(response[key]);
|
|
@@ -109,12 +108,19 @@ export class CheckSecurityInterceptor implements NestInterceptor {
|
|
|
109
108
|
// Fallback: Remove known secret fields regardless of model type (recursive into plain objects)
|
|
110
109
|
const isPlainLike = (val: any): boolean => {
|
|
111
110
|
if (!val || typeof val !== 'object' || Array.isArray(val)) return false;
|
|
112
|
-
// Skip Streams, Buffers, Dates, RegExps
|
|
111
|
+
// Skip Streams, Buffers, Dates, RegExps, Maps, Sets
|
|
113
112
|
if (typeof val.pipe === 'function') return false;
|
|
114
113
|
if (Buffer.isBuffer(val)) return false;
|
|
115
114
|
if (val instanceof Date || val instanceof RegExp) return false;
|
|
115
|
+
if (val instanceof Map || val instanceof Set) return false;
|
|
116
|
+
// Skip Mongoose documents and BSON types (they have circular internal references)
|
|
117
|
+
if (val.$__ !== undefined || val._bsontype !== undefined) return false;
|
|
116
118
|
const proto = Object.getPrototypeOf(val);
|
|
117
|
-
|
|
119
|
+
// Only recurse into actual plain objects (created via {} or Object.create(null)).
|
|
120
|
+
// Previously used `typeof val.constructor === 'function'` which was too broad and
|
|
121
|
+
// caused infinite recursion on Mongoose Schema.Types.Mixed fields whose internal
|
|
122
|
+
// objects (Schema, SchemaType) have circular references.
|
|
123
|
+
return proto === null || proto === Object.prototype;
|
|
118
124
|
};
|
|
119
125
|
const visited = new WeakSet();
|
|
120
126
|
const removeSecrets = (data: any) => {
|
|
@@ -817,15 +817,12 @@ export interface IJwt {
|
|
|
817
817
|
}
|
|
818
818
|
|
|
819
819
|
/**
|
|
820
|
-
* Multi-tenancy configuration
|
|
820
|
+
* Multi-tenancy configuration for automatic tenant-based data isolation.
|
|
821
821
|
*
|
|
822
822
|
* Follows the "presence implies enabled" pattern:
|
|
823
823
|
* - `undefined`: Feature disabled (no overhead)
|
|
824
824
|
* - `{}`: Feature enabled with defaults
|
|
825
825
|
* - `{ enabled: false }`: Pre-configured but disabled
|
|
826
|
-
*/
|
|
827
|
-
/**
|
|
828
|
-
* Multi-tenancy configuration for automatic tenant-based data isolation.
|
|
829
826
|
*
|
|
830
827
|
* @since 11.20.0
|
|
831
828
|
*/
|
|
@@ -838,26 +835,85 @@ export interface IMultiTenancy {
|
|
|
838
835
|
enabled?: boolean;
|
|
839
836
|
|
|
840
837
|
/**
|
|
841
|
-
*
|
|
838
|
+
* Model names (NOT collection names) to exclude from tenant filtering.
|
|
839
|
+
* These schemas will not have tenant isolation applied.
|
|
840
|
+
* The TenantMember model is always excluded automatically.
|
|
841
|
+
* @example ['User', 'Session']
|
|
842
|
+
*/
|
|
843
|
+
excludeSchemas?: string[];
|
|
844
|
+
|
|
845
|
+
/**
|
|
846
|
+
* Header name for tenant selection.
|
|
847
|
+
* The header value contains the tenant ID for the current request.
|
|
848
|
+
* @default 'x-tenant-id'
|
|
849
|
+
* @since 11.21.0
|
|
850
|
+
*/
|
|
851
|
+
headerName?: string;
|
|
852
|
+
|
|
853
|
+
/**
|
|
854
|
+
* Mongoose model name for the membership collection.
|
|
855
|
+
* Must be registered via MongooseModule.forFeature().
|
|
856
|
+
* @default 'TenantMember'
|
|
857
|
+
* @since 11.21.0
|
|
858
|
+
*/
|
|
859
|
+
membershipModel?: string;
|
|
860
|
+
|
|
861
|
+
/**
|
|
862
|
+
* Whether system admins (RoleEnum.ADMIN) bypass the membership check.
|
|
863
|
+
* When true, admins can access any tenant without being a member.
|
|
864
|
+
* @default true
|
|
865
|
+
* @since 11.21.0
|
|
866
|
+
*/
|
|
867
|
+
adminBypass?: boolean;
|
|
868
|
+
|
|
869
|
+
/**
|
|
870
|
+
* Custom role hierarchy for tenant membership roles.
|
|
871
|
+
* Keys = role names (stored in membership documents), values = numeric levels.
|
|
872
|
+
* Higher value = more privileges. Multiple roles can share the same level.
|
|
873
|
+
*
|
|
874
|
+
* Hierarchy roles use level comparison: a higher level includes all lower levels.
|
|
875
|
+
* Roles NOT in this config are treated as "normal roles" with exact match semantics.
|
|
876
|
+
*
|
|
877
|
+
* Use `createHierarchyRoles(hierarchy)` to generate type-safe constants for decorators.
|
|
842
878
|
*
|
|
843
|
-
*
|
|
844
|
-
*
|
|
845
|
-
*
|
|
846
|
-
*
|
|
879
|
+
* @default { member: 1, manager: 2, owner: 3 }
|
|
880
|
+
* @since 11.21.0
|
|
881
|
+
*
|
|
882
|
+
* @example
|
|
883
|
+
* ```typescript
|
|
884
|
+
* // config.env.ts
|
|
885
|
+
* multiTenancy: {
|
|
886
|
+
* roleHierarchy: { viewer: 1, editor: 2, manager: 2, admin: 3, owner: 4 }
|
|
887
|
+
* }
|
|
847
888
|
*
|
|
848
|
-
*
|
|
849
|
-
*
|
|
889
|
+
* // roles.ts
|
|
890
|
+
* import { createHierarchyRoles } from '@lenne.tech/nest-server';
|
|
891
|
+
* export const HR = createHierarchyRoles({ viewer: 1, editor: 2, manager: 2, admin: 3, owner: 4 });
|
|
850
892
|
*
|
|
851
|
-
*
|
|
893
|
+
* // resolver.ts
|
|
894
|
+
* @Roles(HR.EDITOR) // requires level >= 2 (editor, manager, admin, owner)
|
|
895
|
+
* ```
|
|
852
896
|
*/
|
|
853
|
-
|
|
897
|
+
roleHierarchy?: Record<string, number>;
|
|
854
898
|
|
|
855
899
|
/**
|
|
856
|
-
*
|
|
857
|
-
*
|
|
858
|
-
*
|
|
900
|
+
* TTL in milliseconds for the tenant guard's in-memory membership cache.
|
|
901
|
+
* The cache avoids repeated DB lookups when the same user accesses the same tenant.
|
|
902
|
+
* Set to 0 to disable caching (useful for testing or security-critical deployments).
|
|
903
|
+
*
|
|
904
|
+
* **Important:** This cache is process-local. In horizontally scaled deployments
|
|
905
|
+
* (multiple server instances), membership changes on one instance are not reflected
|
|
906
|
+
* on other instances until the TTL expires. For security-sensitive deployments,
|
|
907
|
+
* reduce the TTL or set to 0 to disable.
|
|
908
|
+
*
|
|
909
|
+
* Note: `CoreBetterAuthUserMapper` has an independent 15-second user cache for
|
|
910
|
+
* roles and verified status. Both caches affect revocation latency. To control both,
|
|
911
|
+
* set this to 0 and override `USER_CACHE_TTL_MS` in a custom mapper.
|
|
912
|
+
*
|
|
913
|
+
* @default 30000 (30 seconds)
|
|
914
|
+
* @since 11.21.1
|
|
859
915
|
*/
|
|
860
|
-
|
|
916
|
+
cacheTtlMs?: number;
|
|
861
917
|
}
|
|
862
918
|
|
|
863
919
|
/**
|
|
@@ -1365,19 +1421,19 @@ export interface IServerOptions {
|
|
|
1365
1421
|
/**
|
|
1366
1422
|
* Multi-tenancy configuration for tenant-based data isolation.
|
|
1367
1423
|
*
|
|
1368
|
-
* When enabled,
|
|
1369
|
-
*
|
|
1424
|
+
* When enabled, provides header-based tenant selection with membership validation.
|
|
1425
|
+
* The active tenant is determined by the X-Tenant-Id header on each request.
|
|
1426
|
+
* A global Mongoose plugin automatically filters all queries by `tenantId`.
|
|
1370
1427
|
*
|
|
1371
1428
|
* Follows the "presence implies enabled" pattern:
|
|
1372
1429
|
* - `undefined`: Disabled (no overhead, backward compatible)
|
|
1373
|
-
* - `{}`: Enabled with defaults
|
|
1374
|
-
* - `{ userField: 'organizationId' }`: Enabled with custom user field
|
|
1430
|
+
* - `{}`: Enabled with defaults (X-Tenant-Id header, admin bypass)
|
|
1375
1431
|
* - `{ enabled: false }`: Pre-configured but disabled
|
|
1376
1432
|
*
|
|
1377
1433
|
* The plugin activates automatically on any schema that has a `tenantId` field.
|
|
1378
1434
|
* Schemas without `tenantId` are not affected.
|
|
1379
1435
|
*
|
|
1380
|
-
* @since 11.
|
|
1436
|
+
* @since 11.21.0
|
|
1381
1437
|
* @default undefined (disabled)
|
|
1382
1438
|
*
|
|
1383
1439
|
* @example
|
|
@@ -1385,14 +1441,10 @@ export interface IServerOptions {
|
|
|
1385
1441
|
* // Enable with defaults
|
|
1386
1442
|
* multiTenancy: {},
|
|
1387
1443
|
*
|
|
1388
|
-
* // Enable with excluded schemas
|
|
1444
|
+
* // Enable with excluded schemas and custom header
|
|
1389
1445
|
* multiTenancy: {
|
|
1390
1446
|
* excludeSchemas: ['User', 'Session'],
|
|
1391
|
-
*
|
|
1392
|
-
*
|
|
1393
|
-
* // Custom user field
|
|
1394
|
-
* multiTenancy: {
|
|
1395
|
-
* userField: 'organizationId',
|
|
1447
|
+
* headerName: 'x-tenant-id',
|
|
1396
1448
|
* },
|
|
1397
1449
|
* ```
|
|
1398
1450
|
*/
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { Injectable, NestMiddleware } from '@nestjs/common';
|
|
2
2
|
import { NextFunction, Request, Response } from 'express';
|
|
3
3
|
|
|
4
|
-
import { ConfigService } from '../services/config.service';
|
|
5
4
|
import { IRequestContext, RequestContext } from '../services/request-context.service';
|
|
6
5
|
|
|
7
6
|
/**
|
|
@@ -21,10 +20,18 @@ export class RequestContextMiddleware implements NestMiddleware {
|
|
|
21
20
|
return req.headers?.['accept-language'] || undefined;
|
|
22
21
|
},
|
|
23
22
|
get tenantId() {
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
23
|
+
// Only return tenant ID set by CoreTenantGuard (after membership validation).
|
|
24
|
+
// The raw header is NEVER used for plugin filtering.
|
|
25
|
+
return (req as any).tenantId ?? undefined;
|
|
26
|
+
},
|
|
27
|
+
get tenantIds() {
|
|
28
|
+
return (req as any).tenantIds ?? undefined;
|
|
29
|
+
},
|
|
30
|
+
get tenantRole() {
|
|
31
|
+
return (req as any).tenantRole ?? undefined;
|
|
32
|
+
},
|
|
33
|
+
get isAdminBypass() {
|
|
34
|
+
return (req as any).isAdminBypass ?? false;
|
|
28
35
|
},
|
|
29
36
|
};
|
|
30
37
|
RequestContext.run(context, () => next());
|