@lenne.tech/nest-server 11.21.0 → 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/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 +5 -7
- package/dist/core/common/interceptors/check-security.interceptor.js.map +1 -1
- package/dist/core/common/interfaces/server-options.interface.d.ts +1 -0
- 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.js +6 -0
- package/dist/core/common/services/request-context.service.js.map +1 -1
- package/dist/core/modules/auth/tokens.decorator.d.ts +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.guard.d.ts +14 -2
- package/dist/core/modules/tenant/core-tenant.guard.js +123 -14
- package/dist/core/modules/tenant/core-tenant.guard.js.map +1 -1
- package/dist/core/modules/tenant/core-tenant.service.d.ts +3 -1
- package/dist/core/modules/tenant/core-tenant.service.js +14 -4
- package/dist/core/modules/tenant/core-tenant.service.js.map +1 -1
- 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/tsconfig.build.tsbuildinfo +1 -1
- package/package.json +30 -21
- package/src/core/common/helpers/input.helper.ts +24 -9
- package/src/core/common/interceptors/check-security.interceptor.ts +10 -11
- package/src/core/common/interfaces/server-options.interface.ts +19 -0
- package/src/core/common/services/email.service.ts +26 -5
- package/src/core/common/services/request-context.service.ts +8 -0
- 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/README.md +38 -2
- package/src/core/modules/tenant/core-tenant.guard.ts +219 -18
- package/src/core/modules/tenant/core-tenant.service.ts +13 -4
- package/src/core/modules/user/core-user.service.ts +17 -1
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { CanActivate, ExecutionContext, ForbiddenException, Injectable, Logger } from '@nestjs/common';
|
|
1
|
+
import { CanActivate, ExecutionContext, ForbiddenException, Injectable, Logger, OnModuleDestroy } from '@nestjs/common';
|
|
2
2
|
import { Reflector } from '@nestjs/core';
|
|
3
3
|
import { GqlContextType, GqlExecutionContext } from '@nestjs/graphql';
|
|
4
4
|
import { InjectModel } from '@nestjs/mongoose';
|
|
@@ -17,6 +17,22 @@ import {
|
|
|
17
17
|
mergeRolesMetadata,
|
|
18
18
|
} from './core-tenant.helpers';
|
|
19
19
|
|
|
20
|
+
/**
|
|
21
|
+
* Cached membership entry with TTL
|
|
22
|
+
*/
|
|
23
|
+
interface CachedMembership {
|
|
24
|
+
expiresAt: number;
|
|
25
|
+
result: CoreTenantMemberModel | null;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Cached tenant IDs entry with TTL
|
|
30
|
+
*/
|
|
31
|
+
interface CachedTenantIds {
|
|
32
|
+
expiresAt: number;
|
|
33
|
+
ids: string[];
|
|
34
|
+
}
|
|
35
|
+
|
|
20
36
|
/**
|
|
21
37
|
* Global guard for multi-tenancy with defense-in-depth security.
|
|
22
38
|
*
|
|
@@ -58,13 +74,81 @@ import {
|
|
|
58
74
|
* - No user + checkable roles → 403 "Authentication required"
|
|
59
75
|
*/
|
|
60
76
|
@Injectable()
|
|
61
|
-
export class CoreTenantGuard implements CanActivate {
|
|
77
|
+
export class CoreTenantGuard implements CanActivate, OnModuleDestroy {
|
|
62
78
|
private readonly logger = new Logger(CoreTenantGuard.name);
|
|
63
79
|
|
|
80
|
+
/**
|
|
81
|
+
* In-memory TTL cache for membership lookups.
|
|
82
|
+
* Key: `${userId}:${tenantId}`, Value: cached membership result with expiry.
|
|
83
|
+
* Eliminates repeated DB queries for the same user+tenant combination.
|
|
84
|
+
*/
|
|
85
|
+
private readonly membershipCache = new Map<string, CachedMembership>();
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* In-memory TTL cache for tenant ID resolution (no-header path).
|
|
89
|
+
* Key: `${userId}` or `${userId}:${minLevel}`, Value: cached tenant IDs with expiry.
|
|
90
|
+
*/
|
|
91
|
+
private readonly tenantIdsCache = new Map<string, CachedTenantIds>();
|
|
92
|
+
|
|
93
|
+
/** Cache TTL in milliseconds. Configurable via multiTenancy.cacheTtlMs (default: 30s, 0 = disabled) */
|
|
94
|
+
private cacheTtlMs: number = 30_000;
|
|
95
|
+
/** Maximum cache entries before eviction */
|
|
96
|
+
private static readonly MAX_CACHE_SIZE = 500;
|
|
97
|
+
/** Cleanup interval handle */
|
|
98
|
+
private cleanupInterval: NodeJS.Timeout | null = null;
|
|
99
|
+
/** Tracks the last seen config reference to detect config changes (e.g., roleHierarchy) */
|
|
100
|
+
private lastSeenConfig: object | null = null;
|
|
101
|
+
|
|
64
102
|
constructor(
|
|
65
103
|
private readonly reflector: Reflector,
|
|
66
104
|
@InjectModel(TENANT_MEMBER_MODEL_TOKEN) private readonly memberModel: Model<CoreTenantMemberModel>,
|
|
67
|
-
) {
|
|
105
|
+
) {
|
|
106
|
+
// Clean up expired cache entries every 60 seconds
|
|
107
|
+
this.cleanupInterval = setInterval(() => this.evictExpired(), 60_000);
|
|
108
|
+
if (this.cleanupInterval.unref) {
|
|
109
|
+
this.cleanupInterval.unref();
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
onModuleDestroy(): void {
|
|
114
|
+
if (this.cleanupInterval) {
|
|
115
|
+
clearInterval(this.cleanupInterval);
|
|
116
|
+
this.cleanupInterval = null;
|
|
117
|
+
}
|
|
118
|
+
this.membershipCache.clear();
|
|
119
|
+
this.tenantIdsCache.clear();
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Invalidate all cache entries for a specific user.
|
|
124
|
+
* Call this when memberships change (add/remove/update).
|
|
125
|
+
*
|
|
126
|
+
* Note: userId must not contain ':' characters (used as cache key delimiter).
|
|
127
|
+
* MongoDB ObjectIds and standard UUID formats are safe.
|
|
128
|
+
*
|
|
129
|
+
* @param userId - The user ID whose cache entries should be invalidated
|
|
130
|
+
*/
|
|
131
|
+
invalidateUser(userId: string): void {
|
|
132
|
+
for (const key of this.membershipCache.keys()) {
|
|
133
|
+
if (key.startsWith(`${userId}:`)) {
|
|
134
|
+
this.membershipCache.delete(key);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
for (const key of this.tenantIdsCache.keys()) {
|
|
138
|
+
if (key === userId || key.startsWith(`${userId}:`)) {
|
|
139
|
+
this.tenantIdsCache.delete(key);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Clear all cache entries.
|
|
146
|
+
* Useful when configuration changes (e.g., roleHierarchy) or for testing.
|
|
147
|
+
*/
|
|
148
|
+
invalidateAll(): void {
|
|
149
|
+
this.membershipCache.clear();
|
|
150
|
+
this.tenantIdsCache.clear();
|
|
151
|
+
}
|
|
68
152
|
|
|
69
153
|
async canActivate(context: ExecutionContext): Promise<boolean> {
|
|
70
154
|
const config = ConfigService.configFastButReadOnly?.multiTenancy;
|
|
@@ -72,6 +156,16 @@ export class CoreTenantGuard implements CanActivate {
|
|
|
72
156
|
return true;
|
|
73
157
|
}
|
|
74
158
|
|
|
159
|
+
// Detect config changes (e.g., roleHierarchy modified in tests) and flush caches
|
|
160
|
+
if (this.lastSeenConfig !== config) {
|
|
161
|
+
this.lastSeenConfig = config;
|
|
162
|
+
// Default 30s in production, 0 (disabled) in test environments to avoid stale data between test cases
|
|
163
|
+
const isTestEnv =
|
|
164
|
+
process.env.VITEST === 'true' || process.env.NODE_ENV === 'test' || process.env.NODE_ENV === 'e2e';
|
|
165
|
+
this.cacheTtlMs = config.cacheTtlMs ?? (isTestEnv ? 0 : 30_000);
|
|
166
|
+
this.invalidateAll();
|
|
167
|
+
}
|
|
168
|
+
|
|
75
169
|
const request = this.getRequest(context);
|
|
76
170
|
if (!request) {
|
|
77
171
|
return true;
|
|
@@ -86,13 +180,16 @@ export class CoreTenantGuard implements CanActivate {
|
|
|
86
180
|
// Read @Roles() metadata and filter to non-system roles
|
|
87
181
|
const rolesMetadata = this.reflector.getAll<string[][]>('roles', [context.getHandler(), context.getClass()]);
|
|
88
182
|
const roles = mergeRolesMetadata(rolesMetadata);
|
|
89
|
-
const checkableRoles = roles.filter((r) => !isSystemRole(r));
|
|
90
|
-
const minRequiredLevel = getMinRequiredLevel(checkableRoles);
|
|
91
183
|
|
|
92
184
|
const user = request.user;
|
|
93
185
|
const adminBypass = config.adminBypass !== false;
|
|
94
186
|
const isAdmin = adminBypass && user?.roles?.includes(RoleEnum.ADMIN);
|
|
95
187
|
|
|
188
|
+
// Filter to checkable (non-system) roles only when needed (avoids array allocation on fast paths)
|
|
189
|
+
const hasNonSystemRoles = roles.some((r) => !isSystemRole(r));
|
|
190
|
+
const checkableRoles = hasNonSystemRoles ? roles.filter((r) => !isSystemRole(r)) : [];
|
|
191
|
+
const minRequiredLevel = checkableRoles.length > 0 ? getMinRequiredLevel(checkableRoles) : undefined;
|
|
192
|
+
|
|
96
193
|
// @SkipTenantCheck → no tenant context, but role check against user.roles
|
|
97
194
|
const skipTenantCheck = this.reflector.getAllAndOverride<boolean>(SKIP_TENANT_CHECK_KEY, [
|
|
98
195
|
context.getHandler(),
|
|
@@ -125,14 +222,7 @@ export class CoreTenantGuard implements CanActivate {
|
|
|
125
222
|
}
|
|
126
223
|
|
|
127
224
|
// Authenticated non-admin user: MUST be active member
|
|
128
|
-
const membership = await this.
|
|
129
|
-
.findOne({
|
|
130
|
-
status: TenantMemberStatus.ACTIVE,
|
|
131
|
-
tenant: headerTenantId,
|
|
132
|
-
user: user.id,
|
|
133
|
-
})
|
|
134
|
-
.lean()
|
|
135
|
-
.exec();
|
|
225
|
+
const membership = await this.findMembershipCached(user.id, headerTenantId);
|
|
136
226
|
|
|
137
227
|
if (!membership) {
|
|
138
228
|
throw new ForbiddenException('Not a member of this tenant');
|
|
@@ -193,33 +283,59 @@ export class CoreTenantGuard implements CanActivate {
|
|
|
193
283
|
* This allows the tenant plugin to filter by { tenantId: { $in: tenantIds } }
|
|
194
284
|
* when no specific tenant header is set.
|
|
195
285
|
*
|
|
286
|
+
* Uses a process-level TTL cache to avoid repeated DB queries for the same user.
|
|
287
|
+
*
|
|
196
288
|
* @param minLevel - When set, only include memberships where role level >= minLevel
|
|
197
289
|
*/
|
|
198
290
|
private async resolveUserTenantIds(request: any, minLevel?: number): Promise<void> {
|
|
199
|
-
// Skip if already resolved
|
|
291
|
+
// Skip if already resolved on this request
|
|
200
292
|
if (request.tenantIds) {
|
|
201
293
|
return;
|
|
202
294
|
}
|
|
203
295
|
|
|
296
|
+
const userId = request.user.id;
|
|
297
|
+
const ttl = this.cacheTtlMs;
|
|
298
|
+
|
|
299
|
+
// When cache is enabled, check process-level cache
|
|
300
|
+
if (ttl > 0) {
|
|
301
|
+
const cacheKey = minLevel !== undefined ? `${userId}:${minLevel}` : userId;
|
|
302
|
+
const now = Date.now();
|
|
303
|
+
const cached = this.tenantIdsCache.get(cacheKey);
|
|
304
|
+
if (cached && now < cached.expiresAt) {
|
|
305
|
+
request.tenantIds = cached.ids;
|
|
306
|
+
return;
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
204
310
|
const memberships = await this.memberModel
|
|
205
311
|
.find({
|
|
206
312
|
status: TenantMemberStatus.ACTIVE,
|
|
207
|
-
user:
|
|
313
|
+
user: userId,
|
|
208
314
|
})
|
|
209
315
|
.select('tenant role')
|
|
210
316
|
.lean()
|
|
211
317
|
.exec();
|
|
212
318
|
|
|
319
|
+
let ids: string[];
|
|
213
320
|
if (minLevel !== undefined) {
|
|
214
321
|
const hierarchy = getRoleHierarchy();
|
|
215
|
-
|
|
322
|
+
ids = memberships
|
|
216
323
|
.filter((m) => {
|
|
217
324
|
const level = hierarchy[m.role as string] ?? 0;
|
|
218
325
|
return level >= minLevel;
|
|
219
326
|
})
|
|
220
|
-
.map((m) => m.tenant);
|
|
327
|
+
.map((m) => m.tenant as string);
|
|
221
328
|
} else {
|
|
222
|
-
|
|
329
|
+
ids = memberships.map((m) => m.tenant as string);
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
request.tenantIds = ids;
|
|
333
|
+
|
|
334
|
+
// Store in process-level cache when enabled
|
|
335
|
+
if (ttl > 0) {
|
|
336
|
+
const cacheKey = minLevel !== undefined ? `${userId}:${minLevel}` : userId;
|
|
337
|
+
this.evictIfOverCapacity(this.tenantIdsCache);
|
|
338
|
+
this.tenantIdsCache.set(cacheKey, { expiresAt: Date.now() + ttl, ids });
|
|
223
339
|
}
|
|
224
340
|
}
|
|
225
341
|
|
|
@@ -237,4 +353,89 @@ export class CoreTenantGuard implements CanActivate {
|
|
|
237
353
|
return null;
|
|
238
354
|
}
|
|
239
355
|
}
|
|
356
|
+
|
|
357
|
+
// ===================================================================================================================
|
|
358
|
+
// Cache helpers
|
|
359
|
+
// ===================================================================================================================
|
|
360
|
+
|
|
361
|
+
/**
|
|
362
|
+
* Look up a membership with process-level TTL cache.
|
|
363
|
+
* Avoids repeated DB queries when the same user accesses the same tenant repeatedly.
|
|
364
|
+
*/
|
|
365
|
+
private async findMembershipCached(userId: string, tenantId: string): Promise<CoreTenantMemberModel | null> {
|
|
366
|
+
const ttl = this.cacheTtlMs;
|
|
367
|
+
|
|
368
|
+
// Cache disabled (ttl = 0): always query DB
|
|
369
|
+
if (ttl <= 0) {
|
|
370
|
+
return this.memberModel
|
|
371
|
+
.findOne({ status: TenantMemberStatus.ACTIVE, tenant: tenantId, user: userId })
|
|
372
|
+
.lean()
|
|
373
|
+
.exec() as Promise<CoreTenantMemberModel | null>;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
const key = `${userId}:${tenantId}`;
|
|
377
|
+
const now = Date.now();
|
|
378
|
+
|
|
379
|
+
const cached = this.membershipCache.get(key);
|
|
380
|
+
if (cached && now < cached.expiresAt) {
|
|
381
|
+
return cached.result;
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
const result = (await this.memberModel
|
|
385
|
+
.findOne({
|
|
386
|
+
status: TenantMemberStatus.ACTIVE,
|
|
387
|
+
tenant: tenantId,
|
|
388
|
+
user: userId,
|
|
389
|
+
})
|
|
390
|
+
.lean()
|
|
391
|
+
.exec()) as CoreTenantMemberModel | null;
|
|
392
|
+
|
|
393
|
+
// Only cache positive results (active membership found).
|
|
394
|
+
// Negative results (null) are NOT cached to ensure that:
|
|
395
|
+
// - Newly added members are recognized immediately
|
|
396
|
+
// - Removed memberships lead to immediate denial
|
|
397
|
+
if (result) {
|
|
398
|
+
this.evictIfOverCapacity(this.membershipCache);
|
|
399
|
+
this.membershipCache.set(key, { expiresAt: now + ttl, result });
|
|
400
|
+
} else {
|
|
401
|
+
// Ensure stale positive cache entries are removed
|
|
402
|
+
this.membershipCache.delete(key);
|
|
403
|
+
}
|
|
404
|
+
return result;
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
/**
|
|
408
|
+
* Evict the oldest entry if the cache exceeds MAX_CACHE_SIZE.
|
|
409
|
+
* Uses a simple FIFO strategy (Map insertion order).
|
|
410
|
+
*/
|
|
411
|
+
private evictIfOverCapacity<T>(cache: Map<string, T>): void {
|
|
412
|
+
if (cache.size >= CoreTenantGuard.MAX_CACHE_SIZE) {
|
|
413
|
+
// Delete first 10% to avoid evicting on every insert
|
|
414
|
+
const deleteCount = Math.max(1, Math.floor(CoreTenantGuard.MAX_CACHE_SIZE * 0.1));
|
|
415
|
+
let deleted = 0;
|
|
416
|
+
for (const key of cache.keys()) {
|
|
417
|
+
if (deleted >= deleteCount) break;
|
|
418
|
+
cache.delete(key);
|
|
419
|
+
deleted++;
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
/**
|
|
425
|
+
* Remove all expired entries from both caches.
|
|
426
|
+
* Called periodically by the cleanup interval.
|
|
427
|
+
*/
|
|
428
|
+
private evictExpired(): void {
|
|
429
|
+
const now = Date.now();
|
|
430
|
+
for (const [key, entry] of this.membershipCache.entries()) {
|
|
431
|
+
if (now >= entry.expiresAt) {
|
|
432
|
+
this.membershipCache.delete(key);
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
for (const [key, entry] of this.tenantIdsCache.entries()) {
|
|
436
|
+
if (now >= entry.expiresAt) {
|
|
437
|
+
this.tenantIdsCache.delete(key);
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
}
|
|
240
441
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { BadRequestException, Injectable, Logger, NotFoundException } from '@nestjs/common';
|
|
1
|
+
import { BadRequestException, Injectable, Logger, NotFoundException, Optional } from '@nestjs/common';
|
|
2
2
|
import { InjectModel } from '@nestjs/mongoose';
|
|
3
3
|
import { Model } from 'mongoose';
|
|
4
4
|
|
|
@@ -6,6 +6,7 @@ import { ConfigService } from '../../common/services/config.service';
|
|
|
6
6
|
import { RequestContext } from '../../common/services/request-context.service';
|
|
7
7
|
import { CoreTenantMemberModel } from './core-tenant-member.model';
|
|
8
8
|
import { DEFAULT_ROLE_HIERARCHY, TENANT_MEMBER_MODEL_TOKEN, TenantMemberStatus } from './core-tenant.enums';
|
|
9
|
+
import { CoreTenantGuard } from './core-tenant.guard';
|
|
9
10
|
|
|
10
11
|
/**
|
|
11
12
|
* Core service for tenant membership operations.
|
|
@@ -30,7 +31,10 @@ import { DEFAULT_ROLE_HIERARCHY, TENANT_MEMBER_MODEL_TOKEN, TenantMemberStatus }
|
|
|
30
31
|
export class CoreTenantService {
|
|
31
32
|
protected readonly logger = new Logger(CoreTenantService.name);
|
|
32
33
|
|
|
33
|
-
constructor(
|
|
34
|
+
constructor(
|
|
35
|
+
@InjectModel(TENANT_MEMBER_MODEL_TOKEN) protected readonly memberModel: Model<CoreTenantMemberModel>,
|
|
36
|
+
@Optional() protected readonly tenantGuard?: CoreTenantGuard,
|
|
37
|
+
) {}
|
|
34
38
|
|
|
35
39
|
/**
|
|
36
40
|
* Get the configured role hierarchy.
|
|
@@ -106,7 +110,7 @@ export class CoreTenantService {
|
|
|
106
110
|
}
|
|
107
111
|
// Reactivate suspended/invited membership
|
|
108
112
|
return RequestContext.runWithBypassTenantGuard(async () => {
|
|
109
|
-
|
|
113
|
+
const result = (await this.memberModel
|
|
110
114
|
.findOneAndUpdate(
|
|
111
115
|
{ tenant: tenantId, user: userId },
|
|
112
116
|
{
|
|
@@ -118,7 +122,9 @@ export class CoreTenantService {
|
|
|
118
122
|
{ new: true },
|
|
119
123
|
)
|
|
120
124
|
.lean()
|
|
121
|
-
.exec() as
|
|
125
|
+
.exec()) as CoreTenantMemberModel;
|
|
126
|
+
this.tenantGuard?.invalidateUser(userId);
|
|
127
|
+
return result;
|
|
122
128
|
});
|
|
123
129
|
}
|
|
124
130
|
|
|
@@ -131,6 +137,7 @@ export class CoreTenantService {
|
|
|
131
137
|
tenant: tenantId,
|
|
132
138
|
user: userId,
|
|
133
139
|
});
|
|
140
|
+
this.tenantGuard?.invalidateUser(userId);
|
|
134
141
|
return doc.toObject() as CoreTenantMemberModel;
|
|
135
142
|
});
|
|
136
143
|
}
|
|
@@ -162,6 +169,7 @@ export class CoreTenantService {
|
|
|
162
169
|
throw new NotFoundException('Membership not found');
|
|
163
170
|
}
|
|
164
171
|
|
|
172
|
+
this.tenantGuard?.invalidateUser(userId);
|
|
165
173
|
return result as CoreTenantMemberModel;
|
|
166
174
|
});
|
|
167
175
|
}
|
|
@@ -202,6 +210,7 @@ export class CoreTenantService {
|
|
|
202
210
|
throw new NotFoundException('Active membership not found');
|
|
203
211
|
}
|
|
204
212
|
|
|
213
|
+
this.tenantGuard?.invalidateUser(userId);
|
|
205
214
|
return result as CoreTenantMemberModel;
|
|
206
215
|
});
|
|
207
216
|
}
|
|
@@ -253,7 +253,14 @@ export abstract class CoreUserService<
|
|
|
253
253
|
// Update and return user
|
|
254
254
|
return this.process(
|
|
255
255
|
async () => {
|
|
256
|
-
|
|
256
|
+
const user = await this.mainDbModel.findByIdAndUpdate(userId, { roles }).exec();
|
|
257
|
+
|
|
258
|
+
// Invalidate BetterAuth user cache so changed roles take effect immediately
|
|
259
|
+
if (this.options?.betterAuthUserMapper && (user as any)?.iamId) {
|
|
260
|
+
this.options.betterAuthUserMapper.invalidateUserCache((user as any).iamId);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
return user;
|
|
257
264
|
},
|
|
258
265
|
{ serviceOptions },
|
|
259
266
|
);
|
|
@@ -313,6 +320,15 @@ export abstract class CoreUserService<
|
|
|
313
320
|
}
|
|
314
321
|
}
|
|
315
322
|
|
|
323
|
+
// Invalidate BetterAuth user cache when roles or verified status may have changed
|
|
324
|
+
if (this.options?.betterAuthUserMapper && (oldUser as any)?.iamId) {
|
|
325
|
+
const rolesChanged = 'roles' in (input as any);
|
|
326
|
+
const verifiedChanged = 'verified' in (input as any) || 'emailVerified' in (input as any);
|
|
327
|
+
if (rolesChanged || verifiedChanged) {
|
|
328
|
+
this.options.betterAuthUserMapper.invalidateUserCache((oldUser as any).iamId);
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
|
|
316
332
|
return updatedUser;
|
|
317
333
|
}
|
|
318
334
|
|