@lenne.tech/nest-server 11.19.0 → 11.20.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/dist/core/common/helpers/db.helper.d.ts +1 -1
  2. package/dist/core/common/helpers/db.helper.js +10 -4
  3. package/dist/core/common/helpers/db.helper.js.map +1 -1
  4. package/dist/core/common/helpers/input.helper.d.ts +1 -1
  5. package/dist/core/common/helpers/input.helper.js +6 -2
  6. package/dist/core/common/helpers/input.helper.js.map +1 -1
  7. package/dist/core/common/interceptors/check-security.interceptor.js +8 -0
  8. package/dist/core/common/interceptors/check-security.interceptor.js.map +1 -1
  9. package/dist/core/common/interfaces/server-options.interface.d.ts +6 -0
  10. package/dist/core/common/middleware/request-context.middleware.js +8 -0
  11. package/dist/core/common/middleware/request-context.middleware.js.map +1 -1
  12. package/dist/core/common/plugins/mongoose-audit-fields.plugin.js +72 -0
  13. package/dist/core/common/plugins/mongoose-audit-fields.plugin.js.map +1 -1
  14. package/dist/core/common/plugins/mongoose-password.plugin.js +35 -0
  15. package/dist/core/common/plugins/mongoose-password.plugin.js.map +1 -1
  16. package/dist/core/common/plugins/mongoose-role-guard.plugin.js +61 -0
  17. package/dist/core/common/plugins/mongoose-role-guard.plugin.js.map +1 -1
  18. package/dist/core/common/plugins/mongoose-tenant.plugin.d.ts +1 -0
  19. package/dist/core/common/plugins/mongoose-tenant.plugin.js +108 -0
  20. package/dist/core/common/plugins/mongoose-tenant.plugin.js.map +1 -0
  21. package/dist/core/common/services/request-context.service.d.ts +5 -0
  22. package/dist/core/common/services/request-context.service.js +14 -0
  23. package/dist/core/common/services/request-context.service.js.map +1 -1
  24. package/dist/core.module.js +4 -0
  25. package/dist/core.module.js.map +1 -1
  26. package/dist/index.d.ts +1 -0
  27. package/dist/index.js +1 -0
  28. package/dist/index.js.map +1 -1
  29. package/dist/tsconfig.build.tsbuildinfo +1 -1
  30. package/package.json +11 -12
  31. package/src/core/common/helpers/db.helper.ts +13 -6
  32. package/src/core/common/helpers/input.helper.ts +6 -2
  33. package/src/core/common/interceptors/check-security.interceptor.ts +8 -0
  34. package/src/core/common/interfaces/server-options.interface.ts +80 -0
  35. package/src/core/common/middleware/request-context.middleware.ts +8 -1
  36. package/src/core/common/plugins/mongoose-audit-fields.plugin.ts +84 -0
  37. package/src/core/common/plugins/mongoose-password.plugin.ts +41 -1
  38. package/src/core/common/plugins/mongoose-role-guard.plugin.ts +65 -1
  39. package/src/core/common/plugins/mongoose-tenant.plugin.ts +165 -0
  40. package/src/core/common/services/request-context.service.ts +37 -0
  41. package/src/core.module.ts +5 -0
  42. package/src/index.ts +1 -0
@@ -0,0 +1,165 @@
1
+ import { ConfigService } from '../services/config.service';
2
+ import { RequestContext } from '../services/request-context.service';
3
+
4
+ /**
5
+ * Mongoose plugin that provides automatic tenant-based data isolation.
6
+ * Only activates on schemas that have a `tenantId` path defined.
7
+ *
8
+ * Follows the same pattern as mongooseRoleGuardPlugin and mongooseAuditFieldsPlugin:
9
+ * - Plain function, registered globally in connectionFactory
10
+ * - Reads RequestContext (AsyncLocalStorage) and ConfigService.configFastButReadOnly
11
+ * - Activates conditionally based on schema structure
12
+ *
13
+ * **Behavior:**
14
+ * - Queries are automatically filtered by the current user's tenantId
15
+ * - New documents get tenantId set automatically from context
16
+ * - Aggregates get a $match stage prepended
17
+ *
18
+ * **No filter applied when:**
19
+ * - No RequestContext (system operations, cron jobs, migrations)
20
+ * - `bypassTenantGuard` is active (via `RequestContext.runWithBypassTenantGuard()`)
21
+ * - Schema's model name is in `excludeSchemas` config
22
+ * - No user on request (public endpoints)
23
+ *
24
+ * **User without tenantId:**
25
+ * - Filters by `{ tenantId: null }` — sees only data without tenant assignment
26
+ * - Falsy values (undefined, null, empty string '') are all treated as "no tenant"
27
+ */
28
+ export function mongooseTenantPlugin(schema) {
29
+ // Only activate on schemas with a tenantId path
30
+ if (!schema.path('tenantId')) {
31
+ return;
32
+ }
33
+
34
+ // Performance index
35
+ schema.index({ tenantId: 1 });
36
+
37
+ // === Query filter hooks (explicit names, no regex → no double-filtering) ===
38
+ const queryHooks = [
39
+ 'find',
40
+ 'findOne',
41
+ 'findOneAndUpdate',
42
+ 'findOneAndDelete',
43
+ 'findOneAndReplace',
44
+ 'countDocuments',
45
+ 'distinct',
46
+ 'updateOne',
47
+ 'updateMany',
48
+ 'deleteOne',
49
+ 'deleteMany',
50
+ 'replaceOne',
51
+ ];
52
+
53
+ for (const hookName of queryHooks) {
54
+ schema.pre(hookName, function () {
55
+ // Query hooks: `this` is a Mongoose Query — modelName is on `this.model`
56
+ const modelName = this.model?.modelName;
57
+ const tenantId = resolveTenantId(modelName);
58
+ if (tenantId !== undefined) {
59
+ this.where({ tenantId });
60
+ }
61
+ });
62
+ }
63
+
64
+ // === Save: set tenantId automatically on new documents ===
65
+ // Intentional asymmetry: writes only set tenantId when truthy (not null).
66
+ // A user without tenantId creates "unassigned" documents, which the null-filter
67
+ // in query hooks will still make visible to them on reads.
68
+ schema.pre('save', function () {
69
+ if (this.isNew && !this['tenantId']) {
70
+ // Document hooks: `this` is the document instance — modelName is on the constructor (the Model class)
71
+ const modelName = (this.constructor as any).modelName;
72
+ const tenantId = resolveTenantId(modelName);
73
+ if (tenantId) {
74
+ this['tenantId'] = tenantId;
75
+ }
76
+ }
77
+ });
78
+
79
+ // === insertMany (Mongoose 9: first arg is docs array, no next callback) ===
80
+ schema.pre('insertMany', function (docs: any[]) {
81
+ // Model-level hooks: `this` is the Model class itself — modelName is a direct property
82
+ const modelName = this.modelName;
83
+ const tenantId = resolveTenantId(modelName);
84
+ if (tenantId && Array.isArray(docs)) {
85
+ for (const doc of docs) {
86
+ if (!doc.tenantId) {
87
+ doc.tenantId = tenantId;
88
+ }
89
+ }
90
+ }
91
+ });
92
+
93
+ // === bulkWrite: filter queries and auto-set tenantId on inserts ===
94
+ schema.pre('bulkWrite', function (ops: any[]) {
95
+ // Model-level hooks: `this` is the Model class itself — modelName is a direct property
96
+ const modelName = this.modelName;
97
+ const tenantId = resolveTenantId(modelName);
98
+ if (tenantId === undefined) return;
99
+
100
+ for (const op of ops) {
101
+ if ('insertOne' in op) {
102
+ // Auto-set tenantId on insert (only if truthy, consistent with save hook)
103
+ if (tenantId && !op.insertOne.document.tenantId) {
104
+ op.insertOne.document.tenantId = tenantId;
105
+ }
106
+ } else if ('updateOne' in op) {
107
+ op.updateOne.filter = { ...op.updateOne.filter, tenantId };
108
+ } else if ('updateMany' in op) {
109
+ op.updateMany.filter = { ...op.updateMany.filter, tenantId };
110
+ } else if ('replaceOne' in op) {
111
+ op.replaceOne.filter = { ...op.replaceOne.filter, tenantId };
112
+ } else if ('deleteOne' in op) {
113
+ op.deleteOne.filter = { ...op.deleteOne.filter, tenantId };
114
+ } else if ('deleteMany' in op) {
115
+ op.deleteMany.filter = { ...op.deleteMany.filter, tenantId };
116
+ }
117
+ }
118
+ });
119
+
120
+ // === Aggregate: prepend $match stage ===
121
+ schema.pre('aggregate', function () {
122
+ // Aggregate hooks: `this` is the Aggregation pipeline — the model is on the internal `_model` property
123
+ const modelName = (this as any)._model?.modelName;
124
+ const tenantId = resolveTenantId(modelName);
125
+ if (tenantId !== undefined) {
126
+ this.pipeline().unshift({ $match: { tenantId } });
127
+ }
128
+ });
129
+ }
130
+
131
+ /**
132
+ * Resolve tenant ID from RequestContext.
133
+ *
134
+ * @returns
135
+ * - `undefined` → no filter should be applied
136
+ * - `string` → filter by this tenant ID
137
+ * - `null` → filter by `{ tenantId: null }` (user without tenant sees only unassigned data)
138
+ */
139
+ function resolveTenantId(modelName?: string): string | null | undefined {
140
+ // Defense-in-depth: check config even if plugin is registered
141
+ const mtConfig = ConfigService.configFastButReadOnly?.multiTenancy;
142
+ if (!mtConfig || mtConfig.enabled === false) return undefined;
143
+
144
+ const context = RequestContext.get();
145
+
146
+ // No RequestContext (system operation, cron, migration) → no filter
147
+ if (!context) return undefined;
148
+
149
+ // Explicit bypass
150
+ if (context.bypassTenantGuard) return undefined;
151
+
152
+ // Check excluded schemas (model names, e.g. ['User', 'Session'])
153
+ if (modelName && mtConfig.excludeSchemas?.includes(modelName)) return undefined;
154
+
155
+ const tenantId = context.tenantId;
156
+
157
+ // User has tenantId → filter by it (empty string is treated as falsy = no tenant)
158
+ if (tenantId) return tenantId;
159
+
160
+ // User is logged in but has no tenantId (undefined, null, or '') → null filter (sees only data without tenant)
161
+ if (context.currentUser) return null;
162
+
163
+ // No user (public endpoint) → no filter
164
+ return undefined;
165
+ }
@@ -9,6 +9,10 @@ export interface IRequestContext {
9
9
  language?: string;
10
10
  /** When true, mongooseRoleGuardPlugin allows role changes regardless of user permissions */
11
11
  bypassRoleGuard?: boolean;
12
+ /** When true, mongooseTenantPlugin skips tenant filtering */
13
+ bypassTenantGuard?: boolean;
14
+ /** Tenant ID resolved from the current user */
15
+ tenantId?: string;
12
16
  }
13
17
 
14
18
  /**
@@ -66,4 +70,37 @@ export class RequestContext {
66
70
  };
67
71
  return this.storage.run(context, fn);
68
72
  }
73
+
74
+ static getTenantId(): string | undefined {
75
+ return this.storage.getStore()?.tenantId;
76
+ }
77
+
78
+ static isBypassTenantGuard(): boolean {
79
+ return this.storage.getStore()?.bypassTenantGuard === true;
80
+ }
81
+
82
+ /**
83
+ * Run a function with tenant guard bypass enabled.
84
+ * The current context is preserved; only bypassTenantGuard is added.
85
+ *
86
+ * Use this for cross-tenant operations, e.g.:
87
+ * - Admin dashboards viewing all tenants
88
+ * - Cron jobs processing data across tenants
89
+ * - Migration scripts
90
+ *
91
+ * @example
92
+ * ```typescript
93
+ * const allOrders = await RequestContext.runWithBypassTenantGuard(async () => {
94
+ * return this.orderService.find();
95
+ * });
96
+ * ```
97
+ */
98
+ static runWithBypassTenantGuard<T>(fn: () => T): T {
99
+ const currentStore = this.storage.getStore();
100
+ const context: IRequestContext = {
101
+ ...currentStore,
102
+ bypassTenantGuard: true,
103
+ };
104
+ return this.storage.run(context, fn);
105
+ }
69
106
  }
@@ -20,6 +20,7 @@ import { mongooseIdPlugin } from './core/common/plugins/mongoose-id.plugin';
20
20
  import { mongooseAuditFieldsPlugin } from './core/common/plugins/mongoose-audit-fields.plugin';
21
21
  import { mongoosePasswordPlugin } from './core/common/plugins/mongoose-password.plugin';
22
22
  import { mongooseRoleGuardPlugin } from './core/common/plugins/mongoose-role-guard.plugin';
23
+ import { mongooseTenantPlugin } from './core/common/plugins/mongoose-tenant.plugin';
23
24
  import { ConfigService } from './core/common/services/config.service';
24
25
  import { EmailService } from './core/common/services/email.service';
25
26
  import { MailjetService } from './core/common/services/mailjet.service';
@@ -214,6 +215,10 @@ export class CoreModule implements NestModule {
214
215
  if (config.security?.mongooseAuditFieldsPlugin !== false) {
215
216
  connection.plugin(mongooseAuditFieldsPlugin);
216
217
  }
218
+ // Add tenant isolation plugin (opt-in via multiTenancy config)
219
+ if (config.multiTenancy && config.multiTenancy.enabled !== false) {
220
+ connection.plugin(mongooseTenantPlugin);
221
+ }
217
222
  return connection;
218
223
  };
219
224
 
package/src/index.ts CHANGED
@@ -75,6 +75,7 @@ export * from './core/common/plugins/mongoose-id.plugin';
75
75
  export * from './core/common/plugins/mongoose-audit-fields.plugin';
76
76
  export * from './core/common/plugins/mongoose-password.plugin';
77
77
  export * from './core/common/plugins/mongoose-role-guard.plugin';
78
+ export * from './core/common/plugins/mongoose-tenant.plugin';
78
79
  export * from './core/common/scalars/any.scalar';
79
80
  export * from './core/common/scalars/date-timestamp.scalar';
80
81
  export * from './core/common/scalars/date.scalar';