@lenne.tech/nest-server 11.20.0 → 11.21.0
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/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/db.helper.d.ts +1 -1
- package/dist/core/common/helpers/db.helper.js +10 -4
- package/dist/core/common/helpers/db.helper.js.map +1 -1
- package/dist/core/common/helpers/input.helper.d.ts +1 -1
- package/dist/core/common/helpers/input.helper.js +6 -2
- package/dist/core/common/helpers/input.helper.js.map +1 -1
- package/dist/core/common/interceptors/check-security.interceptor.js +13 -1
- package/dist/core/common/interceptors/check-security.interceptor.js.map +1 -1
- package/dist/core/common/interfaces/server-options.interface.d.ts +4 -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/request-context.service.d.ts +3 -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/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/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 +13 -0
- package/dist/core/modules/tenant/core-tenant.guard.js +162 -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 +17 -0
- package/dist/core/modules/tenant/core-tenant.service.js +160 -0
- package/dist/core/modules/tenant/core-tenant.service.js.map +1 -0
- 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 +12 -10
- package/src/core/common/decorators/restricted.decorator.ts +12 -2
- package/src/core/common/helpers/db.helper.ts +13 -6
- package/src/core/common/helpers/input.helper.ts +6 -2
- package/src/core/common/interceptors/check-security.interceptor.ts +17 -2
- package/src/core/common/interfaces/server-options.interface.ts +63 -30
- 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/request-context.service.ts +7 -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/tenant/INTEGRATION-CHECKLIST.md +165 -0
- package/src/core/modules/tenant/README.md +232 -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 +240 -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 +235 -0
- package/src/core.module.ts +15 -0
- package/src/index.ts +12 -0
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
import { BadRequestException, Injectable, Logger, NotFoundException } from '@nestjs/common';
|
|
2
|
+
import { InjectModel } from '@nestjs/mongoose';
|
|
3
|
+
import { Model } from 'mongoose';
|
|
4
|
+
|
|
5
|
+
import { ConfigService } from '../../common/services/config.service';
|
|
6
|
+
import { RequestContext } from '../../common/services/request-context.service';
|
|
7
|
+
import { CoreTenantMemberModel } from './core-tenant-member.model';
|
|
8
|
+
import { DEFAULT_ROLE_HIERARCHY, TENANT_MEMBER_MODEL_TOKEN, TenantMemberStatus } from './core-tenant.enums';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Core service for tenant membership operations.
|
|
12
|
+
*
|
|
13
|
+
* Projects should extend this service via the Module Inheritance Pattern
|
|
14
|
+
* to add custom logic (e.g., tenant creation, invitation flows).
|
|
15
|
+
*
|
|
16
|
+
* @example
|
|
17
|
+
* ```typescript
|
|
18
|
+
* @Injectable()
|
|
19
|
+
* export class TenantService extends CoreTenantService {
|
|
20
|
+
* override async addMember(tenantId: string, userId: string, role?: string) {
|
|
21
|
+
* const member = await super.addMember(tenantId, userId, role);
|
|
22
|
+
* // Custom: send notification email
|
|
23
|
+
* await this.notificationService.sendInvite(userId, tenantId);
|
|
24
|
+
* return member;
|
|
25
|
+
* }
|
|
26
|
+
* }
|
|
27
|
+
* ```
|
|
28
|
+
*/
|
|
29
|
+
@Injectable()
|
|
30
|
+
export class CoreTenantService {
|
|
31
|
+
protected readonly logger = new Logger(CoreTenantService.name);
|
|
32
|
+
|
|
33
|
+
constructor(@InjectModel(TENANT_MEMBER_MODEL_TOKEN) protected readonly memberModel: Model<CoreTenantMemberModel>) {}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Get the configured role hierarchy.
|
|
37
|
+
*/
|
|
38
|
+
protected getHierarchy(): Record<string, number> {
|
|
39
|
+
return ConfigService.configFastButReadOnly?.multiTenancy?.roleHierarchy ?? DEFAULT_ROLE_HIERARCHY;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Get the default (lowest) role name from the hierarchy.
|
|
44
|
+
*/
|
|
45
|
+
protected getDefaultRole(): string {
|
|
46
|
+
const hierarchy = this.getHierarchy();
|
|
47
|
+
const entries = Object.entries(hierarchy);
|
|
48
|
+
if (entries.length === 0) return 'member';
|
|
49
|
+
return entries.reduce((a, b) => (a[1] <= b[1] ? a : b))[0];
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Get the highest role name from the hierarchy.
|
|
54
|
+
*/
|
|
55
|
+
protected getHighestRole(): string {
|
|
56
|
+
const hierarchy = this.getHierarchy();
|
|
57
|
+
const entries = Object.entries(hierarchy);
|
|
58
|
+
if (entries.length === 0) return 'owner';
|
|
59
|
+
return entries.reduce((a, b) => (a[1] >= b[1] ? a : b))[0];
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Find all active tenant memberships for a user.
|
|
64
|
+
*/
|
|
65
|
+
async findMemberships(userId: string): Promise<CoreTenantMemberModel[]> {
|
|
66
|
+
return this.memberModel.find({ status: TenantMemberStatus.ACTIVE, user: userId }).lean().exec() as Promise<
|
|
67
|
+
CoreTenantMemberModel[]
|
|
68
|
+
>;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Get a single membership (any status).
|
|
73
|
+
*/
|
|
74
|
+
async getMembership(tenantId: string, userId: string): Promise<CoreTenantMemberModel | null> {
|
|
75
|
+
return this.memberModel
|
|
76
|
+
.findOne({ tenant: tenantId, user: userId })
|
|
77
|
+
.lean()
|
|
78
|
+
.exec() as Promise<CoreTenantMemberModel | null>;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Add a member to a tenant.
|
|
83
|
+
* Uses bypassTenantGuard to avoid tenant filtering on the membership collection itself.
|
|
84
|
+
*
|
|
85
|
+
* @param role - Role name from the configured hierarchy. Defaults to the lowest role.
|
|
86
|
+
*/
|
|
87
|
+
async addMember(
|
|
88
|
+
tenantId: string,
|
|
89
|
+
userId: string,
|
|
90
|
+
role?: string,
|
|
91
|
+
invitedById?: string,
|
|
92
|
+
): Promise<CoreTenantMemberModel> {
|
|
93
|
+
if (!tenantId?.trim()) {
|
|
94
|
+
throw new BadRequestException('tenantId must not be empty');
|
|
95
|
+
}
|
|
96
|
+
if (!userId?.trim()) {
|
|
97
|
+
throw new BadRequestException('userId must not be empty');
|
|
98
|
+
}
|
|
99
|
+
const effectiveRole = role ?? this.getDefaultRole();
|
|
100
|
+
|
|
101
|
+
// Check for existing membership
|
|
102
|
+
const existing = await this.getMembership(tenantId, userId);
|
|
103
|
+
if (existing) {
|
|
104
|
+
if (existing.status === TenantMemberStatus.ACTIVE) {
|
|
105
|
+
throw new BadRequestException('User is already an active member of this tenant');
|
|
106
|
+
}
|
|
107
|
+
// Reactivate suspended/invited membership
|
|
108
|
+
return RequestContext.runWithBypassTenantGuard(async () => {
|
|
109
|
+
return this.memberModel
|
|
110
|
+
.findOneAndUpdate(
|
|
111
|
+
{ tenant: tenantId, user: userId },
|
|
112
|
+
{
|
|
113
|
+
invitedBy: invitedById,
|
|
114
|
+
joinedAt: new Date(),
|
|
115
|
+
role: effectiveRole,
|
|
116
|
+
status: TenantMemberStatus.ACTIVE,
|
|
117
|
+
},
|
|
118
|
+
{ new: true },
|
|
119
|
+
)
|
|
120
|
+
.lean()
|
|
121
|
+
.exec() as Promise<CoreTenantMemberModel>;
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
return RequestContext.runWithBypassTenantGuard(async () => {
|
|
126
|
+
const doc = await this.memberModel.create({
|
|
127
|
+
invitedBy: invitedById,
|
|
128
|
+
joinedAt: new Date(),
|
|
129
|
+
role: effectiveRole,
|
|
130
|
+
status: TenantMemberStatus.ACTIVE,
|
|
131
|
+
tenant: tenantId,
|
|
132
|
+
user: userId,
|
|
133
|
+
});
|
|
134
|
+
return doc.toObject() as CoreTenantMemberModel;
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Remove a member from a tenant (sets status to SUSPENDED).
|
|
140
|
+
* Prevents removing the last owner (highest role).
|
|
141
|
+
*/
|
|
142
|
+
async removeMember(tenantId: string, userId: string): Promise<CoreTenantMemberModel> {
|
|
143
|
+
if (!tenantId?.trim()) {
|
|
144
|
+
throw new BadRequestException('tenantId must not be empty');
|
|
145
|
+
}
|
|
146
|
+
if (!userId?.trim()) {
|
|
147
|
+
throw new BadRequestException('userId must not be empty');
|
|
148
|
+
}
|
|
149
|
+
await this.assertNotLastOwner(tenantId, userId);
|
|
150
|
+
|
|
151
|
+
return RequestContext.runWithBypassTenantGuard(async () => {
|
|
152
|
+
const result = await this.memberModel
|
|
153
|
+
.findOneAndUpdate(
|
|
154
|
+
{ status: TenantMemberStatus.ACTIVE, tenant: tenantId, user: userId },
|
|
155
|
+
{ status: TenantMemberStatus.SUSPENDED },
|
|
156
|
+
{ new: true },
|
|
157
|
+
)
|
|
158
|
+
.lean()
|
|
159
|
+
.exec();
|
|
160
|
+
|
|
161
|
+
if (!result) {
|
|
162
|
+
throw new NotFoundException('Membership not found');
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
return result as CoreTenantMemberModel;
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Update a member's role within a tenant.
|
|
171
|
+
* Prevents demoting the last owner (highest role).
|
|
172
|
+
*/
|
|
173
|
+
async updateMemberRole(tenantId: string, userId: string, role: string): Promise<CoreTenantMemberModel> {
|
|
174
|
+
if (!tenantId?.trim()) {
|
|
175
|
+
throw new BadRequestException('tenantId must not be empty');
|
|
176
|
+
}
|
|
177
|
+
if (!userId?.trim()) {
|
|
178
|
+
throw new BadRequestException('userId must not be empty');
|
|
179
|
+
}
|
|
180
|
+
if (!role?.trim()) {
|
|
181
|
+
throw new BadRequestException('role must not be empty');
|
|
182
|
+
}
|
|
183
|
+
const highestRole = this.getHighestRole();
|
|
184
|
+
|
|
185
|
+
// If demoting from highest role, ensure it's not the last one
|
|
186
|
+
const existing = await this.getMembership(tenantId, userId);
|
|
187
|
+
if (existing?.role === highestRole && role !== highestRole) {
|
|
188
|
+
await this.assertNotLastOwner(tenantId, userId);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
return RequestContext.runWithBypassTenantGuard(async () => {
|
|
192
|
+
const result = await this.memberModel
|
|
193
|
+
.findOneAndUpdate(
|
|
194
|
+
{ status: TenantMemberStatus.ACTIVE, tenant: tenantId, user: userId },
|
|
195
|
+
{ role },
|
|
196
|
+
{ new: true },
|
|
197
|
+
)
|
|
198
|
+
.lean()
|
|
199
|
+
.exec();
|
|
200
|
+
|
|
201
|
+
if (!result) {
|
|
202
|
+
throw new NotFoundException('Active membership not found');
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
return result as CoreTenantMemberModel;
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Ensure the given user is not the last owner (highest role) of the tenant.
|
|
211
|
+
* Throws BadRequestException if removing/demoting them would leave the tenant without an owner.
|
|
212
|
+
*
|
|
213
|
+
* Note: This uses a read-check-act pattern which has a theoretical TOCTOU race under
|
|
214
|
+
* concurrent requests. For production environments with high concurrency, consider using
|
|
215
|
+
* MongoDB transactions (requires replica set) in your extended service.
|
|
216
|
+
*/
|
|
217
|
+
async assertNotLastOwner(tenantId: string, userId: string): Promise<void> {
|
|
218
|
+
const highestRole = this.getHighestRole();
|
|
219
|
+
|
|
220
|
+
return RequestContext.runWithBypassTenantGuard(async () => {
|
|
221
|
+
const ownerCount = await this.memberModel.countDocuments({
|
|
222
|
+
role: highestRole,
|
|
223
|
+
status: TenantMemberStatus.ACTIVE,
|
|
224
|
+
tenant: tenantId,
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
if (ownerCount <= 1) {
|
|
228
|
+
const membership = await this.getMembership(tenantId, userId);
|
|
229
|
+
if (membership?.role === highestRole) {
|
|
230
|
+
throw new BadRequestException('Cannot remove or demote the last owner of a tenant');
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
});
|
|
234
|
+
}
|
|
235
|
+
}
|
package/src/core.module.ts
CHANGED
|
@@ -33,6 +33,7 @@ import { ErrorCodeModule } from './core/modules/error-code/error-code.module';
|
|
|
33
33
|
import { CoreHealthCheckModule } from './core/modules/health-check/core-health-check.module';
|
|
34
34
|
import { CorePermissionsModule } from './core/modules/permissions/core-permissions.module';
|
|
35
35
|
import { CoreSystemSetupModule } from './core/modules/system-setup/core-system-setup.module';
|
|
36
|
+
import { CoreTenantModule } from './core/modules/tenant/core-tenant.module';
|
|
36
37
|
|
|
37
38
|
/**
|
|
38
39
|
* Core module (dynamic)
|
|
@@ -390,6 +391,20 @@ export class CoreModule implements NestModule {
|
|
|
390
391
|
imports.push(CoreSystemSetupModule);
|
|
391
392
|
}
|
|
392
393
|
|
|
394
|
+
// Add CoreTenantModule when multiTenancy is configured (presence implies enabled)
|
|
395
|
+
if (config.multiTenancy && config.multiTenancy.enabled !== false) {
|
|
396
|
+
// Auto-add TenantMember to excludeSchemas (membership is tenant-spanning)
|
|
397
|
+
const membershipModelName = config.multiTenancy.membershipModel ?? 'TenantMember';
|
|
398
|
+
if (!config.multiTenancy.excludeSchemas) {
|
|
399
|
+
config.multiTenancy.excludeSchemas = [];
|
|
400
|
+
}
|
|
401
|
+
if (!config.multiTenancy.excludeSchemas.includes(membershipModelName)) {
|
|
402
|
+
config.multiTenancy.excludeSchemas.push(membershipModelName);
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
imports.push(CoreTenantModule.forRoot({ modelName: membershipModelName }));
|
|
406
|
+
}
|
|
407
|
+
|
|
393
408
|
// Set exports
|
|
394
409
|
const exports: any[] = [ConfigService, EmailService, TemplateService, MailjetService];
|
|
395
410
|
if (!process.env.VITEST && isGraphQlEnabled) {
|
package/src/index.ts
CHANGED
|
@@ -202,6 +202,18 @@ export * from './core/modules/system-setup/core-system-setup.service';
|
|
|
202
202
|
|
|
203
203
|
export * from './core/modules/tus';
|
|
204
204
|
|
|
205
|
+
// =====================================================================================================================
|
|
206
|
+
// Core - Modules - Tenant
|
|
207
|
+
// =====================================================================================================================
|
|
208
|
+
|
|
209
|
+
export * from './core/modules/tenant/core-tenant-member.model';
|
|
210
|
+
export * from './core/modules/tenant/core-tenant.decorators';
|
|
211
|
+
export * from './core/modules/tenant/core-tenant.enums';
|
|
212
|
+
export * from './core/modules/tenant/core-tenant.guard';
|
|
213
|
+
export * from './core/modules/tenant/core-tenant.helpers';
|
|
214
|
+
export * from './core/modules/tenant/core-tenant.module';
|
|
215
|
+
export * from './core/modules/tenant/core-tenant.service';
|
|
216
|
+
|
|
205
217
|
// =====================================================================================================================
|
|
206
218
|
// Core - Modules - User
|
|
207
219
|
// =====================================================================================================================
|