@lenne.tech/nest-server 11.27.7 → 11.28.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.
Files changed (42) hide show
  1. package/.claude/rules/role-system.md +90 -0
  2. package/.claude/rules/versioning.md +17 -2
  3. package/FRAMEWORK-API.md +27 -1
  4. package/dist/core/common/decorators/restricted.decorator.js +11 -7
  5. package/dist/core/common/decorators/restricted.decorator.js.map +1 -1
  6. package/dist/core/common/exceptions/access-denied.exception.d.ts +4 -0
  7. package/dist/core/common/exceptions/access-denied.exception.js +12 -0
  8. package/dist/core/common/exceptions/access-denied.exception.js.map +1 -0
  9. package/dist/core/common/helpers/input.helper.js +4 -2
  10. package/dist/core/common/helpers/input.helper.js.map +1 -1
  11. package/dist/core/common/helpers/service.helper.js +4 -3
  12. package/dist/core/common/helpers/service.helper.js.map +1 -1
  13. package/dist/core/modules/auth/guards/roles.guard.js +2 -2
  14. package/dist/core/modules/auth/guards/roles.guard.js.map +1 -1
  15. package/dist/core/modules/better-auth/better-auth-roles.guard.js +1 -1
  16. package/dist/core/modules/better-auth/better-auth-roles.guard.js.map +1 -1
  17. package/dist/core/modules/better-auth/core-better-auth.controller.js +1 -3
  18. package/dist/core/modules/better-auth/core-better-auth.controller.js.map +1 -1
  19. package/dist/core/modules/tenant/core-tenant-member.model.js +3 -3
  20. package/dist/core/modules/tenant/core-tenant-member.model.js.map +1 -1
  21. package/dist/core/modules/tenant/core-tenant.guard.js +7 -6
  22. package/dist/core/modules/tenant/core-tenant.guard.js.map +1 -1
  23. package/dist/index.d.ts +1 -0
  24. package/dist/index.js +1 -0
  25. package/dist/index.js.map +1 -1
  26. package/dist/server/modules/user/user.service.js +1 -1
  27. package/dist/server/modules/user/user.service.js.map +1 -1
  28. package/dist/tsconfig.build.tsbuildinfo +1 -1
  29. package/docs/REQUEST-LIFECYCLE.md +39 -0
  30. package/migration-guides/11.27.7-to-11.28.0.md +350 -0
  31. package/package.json +1 -1
  32. package/src/core/common/decorators/restricted.decorator.ts +34 -12
  33. package/src/core/common/exceptions/access-denied.exception.ts +49 -0
  34. package/src/core/common/helpers/input.helper.ts +9 -4
  35. package/src/core/common/helpers/service.helper.ts +6 -4
  36. package/src/core/modules/auth/guards/roles.guard.ts +5 -4
  37. package/src/core/modules/better-auth/better-auth-roles.guard.ts +4 -2
  38. package/src/core/modules/better-auth/core-better-auth.controller.ts +5 -6
  39. package/src/core/modules/tenant/core-tenant-member.model.ts +8 -3
  40. package/src/core/modules/tenant/core-tenant.guard.ts +26 -11
  41. package/src/index.ts +1 -0
  42. package/src/server/modules/user/user.service.ts +3 -2
@@ -88,9 +88,11 @@ export class BetterAuthRolesGuard implements CanActivate {
88
88
  // Combine handler and class roles (handler takes precedence, like Reflector.getAll)
89
89
  const roles = mergeRolesMetadata([handlerRoles, classRoles]);
90
90
 
91
- // Check if locked - always deny
91
+ // Check if locked - always deny. 403, never 401: the endpoint is locked permanently, so
92
+ // authenticating can never grant access and a 401 ("authenticate and retry") would be a lie.
93
+ // Kept in sync with RolesGuard (see .claude/rules/better-auth.md).
92
94
  if (roles && roles.includes(RoleEnum.S_NO_ONE)) {
93
- throw new UnauthorizedException(ErrorCode.UNAUTHORIZED);
95
+ throw new ForbiddenException(ErrorCode.ACCESS_DENIED);
94
96
  }
95
97
 
96
98
  // If no roles required, or S_EVERYONE is set, allow access without authentication
@@ -5,6 +5,7 @@ import {
5
5
  Controller,
6
6
  Get,
7
7
  HttpCode,
8
+ HttpException,
8
9
  HttpStatus,
9
10
  InternalServerErrorException,
10
11
  Logger,
@@ -932,12 +933,10 @@ export class CoreBetterAuthController {
932
933
  } catch (error) {
933
934
  this.logger.error(`Better Auth handler error: ${error instanceof Error ? error.message : 'Unknown error'}`);
934
935
 
935
- // Re-throw NestJS exceptions
936
- if (
937
- error instanceof BadRequestException ||
938
- error instanceof UnauthorizedException ||
939
- error instanceof InternalServerErrorException
940
- ) {
936
+ // Re-throw NestJS exceptions. Matching HttpException (not an enumeration of subclasses) keeps
937
+ // any deliberately thrown status intact — an enumeration silently masks everything it forgets
938
+ // (e.g. a 403 from the rights checks) as a 500.
939
+ if (error instanceof HttpException) {
941
940
  throw error;
942
941
  }
943
942
 
@@ -1,10 +1,10 @@
1
- import { UnauthorizedException } from '@nestjs/common';
2
1
  import { ObjectType } from '@nestjs/graphql';
3
2
  import { Schema } from '@nestjs/mongoose';
4
3
 
5
4
  import { Restricted } from '../../common/decorators/restricted.decorator';
6
5
  import { UnifiedField } from '../../common/decorators/unified-field.decorator';
7
6
  import { RoleEnum } from '../../common/enums/role.enum';
7
+ import { accessDeniedException } from '../../common/exceptions/access-denied.exception';
8
8
  import { CorePersistenceModel } from '../../common/models/core-persistence.model';
9
9
  import { RequestContext } from '../../common/services/request-context.service';
10
10
  import { DefaultHR, TenantMemberStatus } from './core-tenant.enums';
@@ -98,7 +98,11 @@ export class CoreTenantMemberModel extends CorePersistenceModel {
98
98
  */
99
99
  override securityCheck(user: any, force?: boolean): this {
100
100
  if (force) return this;
101
- if (!user) throw new UnauthorizedException('Access to tenant membership denied');
101
+ // accessDeniedException picks the status from the requester's auth state: 401 here (no user),
102
+ // 403 below (authenticated, but not owner / admin / tenant manager). Model-level denials must
103
+ // use this helper too — a permission error must never reach the client as a 401, or frontends
104
+ // treat it as an expired session and log the user out.
105
+ if (!user) throw accessDeniedException(user);
102
106
 
103
107
  // Own membership or system admin
104
108
  if (user.id === this.user || user.hasRole?.(RoleEnum.ADMIN)) return this;
@@ -116,6 +120,7 @@ export class CoreTenantMemberModel extends CorePersistenceModel {
116
120
  return this;
117
121
  }
118
122
 
119
- throw new UnauthorizedException('Access to tenant membership denied');
123
+ // Reached only when `user` is set (the `!user` case returned above), so this is a 403.
124
+ throw accessDeniedException(user);
120
125
  }
121
126
  }
@@ -1,4 +1,12 @@
1
- import { CanActivate, ExecutionContext, ForbiddenException, Injectable, Logger, OnModuleDestroy } from '@nestjs/common';
1
+ import {
2
+ CanActivate,
3
+ ExecutionContext,
4
+ ForbiddenException,
5
+ Injectable,
6
+ Logger,
7
+ OnModuleDestroy,
8
+ UnauthorizedException,
9
+ } from '@nestjs/common';
2
10
  import { Reflector } from '@nestjs/core';
3
11
  import { GqlContextType, GqlExecutionContext } from '@nestjs/graphql';
4
12
  import { InjectModel } from '@nestjs/mongoose';
@@ -6,6 +14,7 @@ import { Model } from 'mongoose';
6
14
 
7
15
  import { RoleEnum } from '../../common/enums/role.enum';
8
16
  import { ConfigService } from '../../common/services/config.service';
17
+ import { ErrorCode } from '../error-code/error-codes';
9
18
  import { CoreTenantMemberModel } from './core-tenant-member.model';
10
19
  import { SKIP_TENANT_CHECK_KEY } from './core-tenant.decorators';
11
20
  import { TENANT_MEMBER_MODEL_TOKEN, TenantMemberStatus } from './core-tenant.enums';
@@ -73,9 +82,14 @@ interface CachedTenantIds {
73
82
  * 5. @SkipTenantCheck → role check against user.roles, no tenant context
74
83
  * 6. BetterAuth auto-skip (betterAuth.skipTenantCheck config + no header) → skip, no tenant context
75
84
  *
85
+ * Status codes (RFC 9110, aligned with RolesGuard since v11.28.0):
86
+ * - Missing authentication → 401 (re-authenticating IS the remedy, so the client must be told to)
87
+ * - Authenticated but lacking a right / membership → 403
88
+ * - S_NO_ONE → always 403 (authenticating can never unlock it)
89
+ *
76
90
  * HEADER PRESENT:
77
91
  * - System ADMIN (adminBypass: true) → set req.tenantId + isAdminBypass
78
- * - No user → 403 "Authentication required for tenant access"
92
+ * - No user → 401 (tenant access requires authentication)
79
93
  * - Authenticated non-admin user:
80
94
  * - Active member → checkRoleAccess against membership.role → set req.tenantId + tenantRole
81
95
  * - Not active member → ALWAYS 403
@@ -86,7 +100,7 @@ interface CachedTenantIds {
86
100
  * → resolveUserTenantIds with minLevel filter
87
101
  * - Authenticated + no checkable roles → resolveUserTenantIds (all memberships)
88
102
  * - No user + no checkable roles → pass (plugin safety net catches tenantId-schema access)
89
- * - No user + checkable roles → 403 "Authentication required"
103
+ * - No user + checkable roles → 401 (authentication required)
90
104
  */
91
105
  @Injectable()
92
106
  export class CoreTenantGuard implements CanActivate, OnModuleDestroy {
@@ -216,8 +230,9 @@ export class CoreTenantGuard implements CanActivate, OnModuleDestroy {
216
230
 
217
231
  // Defense-in-depth: S_NO_ONE is normally caught by RolesGuard/BetterAuthRolesGuard upstream,
218
232
  // but guard it here too in case CoreTenantGuard runs standalone (e.g., custom guard chains).
233
+ // Always 403 — the same code the role guards return since v11.28.0.
219
234
  if (roles.includes(RoleEnum.S_NO_ONE)) {
220
- throw new ForbiddenException('Access denied');
235
+ throw new ForbiddenException(ErrorCode.ACCESS_DENIED);
221
236
  }
222
237
 
223
238
  const sEveryoneGrantsAccess: boolean = systemCheckRoles.includes(RoleEnum.S_EVERYONE);
@@ -258,7 +273,7 @@ export class CoreTenantGuard implements CanActivate, OnModuleDestroy {
258
273
  const sUserGrantsAccess: boolean = systemCheckRoles.includes(RoleEnum.S_USER);
259
274
  if (sUserGrantsAccess) {
260
275
  if (!user) {
261
- throw new ForbiddenException('Authentication required');
276
+ throw new UnauthorizedException(ErrorCode.UNAUTHORIZED);
262
277
  }
263
278
  if (headerTenantId && !hasSkipDecorator) {
264
279
  return this.handleSystemRoleWithTenantHeader(user, headerTenantId, request, isAdmin);
@@ -276,7 +291,7 @@ export class CoreTenantGuard implements CanActivate, OnModuleDestroy {
276
291
  const sVerifiedGrantsAccess: boolean = systemCheckRoles.includes(RoleEnum.S_VERIFIED);
277
292
  if (sVerifiedGrantsAccess) {
278
293
  if (!user) {
279
- throw new ForbiddenException('Authentication required');
294
+ throw new UnauthorizedException(ErrorCode.UNAUTHORIZED);
280
295
  }
281
296
  const isVerified = !!(user.verified || user.verifiedAt || user.emailVerified);
282
297
  if (!isVerified) {
@@ -337,9 +352,9 @@ export class CoreTenantGuard implements CanActivate, OnModuleDestroy {
337
352
  return true;
338
353
  }
339
354
 
340
- // No user + header → 403 (tenant access requires authentication)
355
+ // No user + header → 401 (tenant access requires authentication; re-auth is the remedy)
341
356
  if (!user) {
342
- throw new ForbiddenException('Authentication required for tenant access');
357
+ throw new UnauthorizedException(ErrorCode.UNAUTHORIZED);
343
358
  }
344
359
 
345
360
  // Authenticated non-admin user: MUST be active member
@@ -375,9 +390,9 @@ export class CoreTenantGuard implements CanActivate, OnModuleDestroy {
375
390
 
376
391
  // Checkable roles present
377
392
  if (checkableRoles.length > 0) {
378
- // No user + roles required → 403
393
+ // No user + roles required → 401 (re-auth is the remedy, so it must not be a 403)
379
394
  if (!user) {
380
- throw new ForbiddenException('Authentication required');
395
+ throw new UnauthorizedException(ErrorCode.UNAUTHORIZED);
381
396
  }
382
397
 
383
398
  // Check role access against user.roles (hierarchy: level comparison, normal: exact match)
@@ -604,7 +619,7 @@ export class CoreTenantGuard implements CanActivate, OnModuleDestroy {
604
619
  if (checkableRoles.length > 0) {
605
620
  // Defense-in-depth: reject unauthenticated access even if RolesGuard is absent
606
621
  if (!user) {
607
- throw new ForbiddenException('Authentication required');
622
+ throw new UnauthorizedException(ErrorCode.UNAUTHORIZED);
608
623
  }
609
624
  if (!isAdmin && !checkRoleAccess(checkableRoles, user.roles, undefined)) {
610
625
  throw new ForbiddenException('Insufficient role');
package/src/index.ts CHANGED
@@ -26,6 +26,7 @@ export * from './core/common/enums/logical-operator.enum';
26
26
  export * from './core/common/enums/process-type.enum';
27
27
  export * from './core/common/enums/role.enum';
28
28
  export * from './core/common/enums/sort-order.emum';
29
+ export * from './core/common/exceptions/access-denied.exception';
29
30
  export * from './core/common/filters/http-exception-log.filter';
30
31
  export * from './core/common/helpers/common.helper';
31
32
  export * from './core/common/helpers/config.helper';
@@ -98,9 +98,10 @@ export class UserService extends CoreUserService<User, UserInput, UserCreateInpu
98
98
  */
99
99
  async setAvatar(file: Express.Multer.File, user: User): Promise<string> {
100
100
  const dbUser = await this.mainDbModel.findOne({ id: user.id }).exec();
101
- // Check user
101
+ // Check user: the token is valid but the account no longer exists, so the session really is
102
+ // invalid — 401 is right here. (A permission error would have to be 403, see accessDeniedException.)
102
103
  if (!dbUser) {
103
- throw new UnauthorizedException('User is not allowed to set the avatar');
104
+ throw new UnauthorizedException('User of the current session no longer exists');
104
105
  }
105
106
 
106
107
  // Check file