@flusys/nestjs-shared 1.0.0-beta → 1.0.0-rc

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 (90) hide show
  1. package/README.md +56 -110
  2. package/cjs/classes/api-controller.class.js +9 -24
  3. package/cjs/classes/index.js +1 -0
  4. package/cjs/constants/index.js +14 -0
  5. package/cjs/constants/permissions.js +174 -0
  6. package/cjs/decorators/api-response.decorator.js +1 -1
  7. package/cjs/decorators/index.js +1 -0
  8. package/cjs/decorators/sanitize-html.decorator.js +36 -0
  9. package/cjs/dtos/filter-and-pagination.dto.js +24 -34
  10. package/cjs/dtos/pagination.dto.js +4 -8
  11. package/cjs/dtos/response-payload.dto.js +0 -41
  12. package/cjs/entities/identity.js +4 -4
  13. package/cjs/entities/user-root.js +13 -14
  14. package/cjs/guards/permission.guard.js +39 -94
  15. package/cjs/interceptors/index.js +1 -0
  16. package/cjs/interceptors/set-create-by-on-body.interceptor.js +2 -30
  17. package/cjs/interceptors/set-delete-by-on-body.interceptor.js +2 -30
  18. package/cjs/interceptors/set-update-by-on-body.interceptor.js +2 -30
  19. package/cjs/interceptors/set-user-field-on-body.interceptor.js +43 -0
  20. package/cjs/interceptors/slug.interceptor.js +30 -9
  21. package/cjs/interfaces/datasource.interface.js +4 -0
  22. package/cjs/interfaces/index.js +2 -1
  23. package/cjs/interfaces/module-config.interface.js +4 -0
  24. package/cjs/modules/cache/cache.module.js +3 -3
  25. package/cjs/modules/datasource/multi-tenant-datasource.service.js +30 -110
  26. package/cjs/modules/utils/utils.service.js +63 -145
  27. package/cjs/utils/error-handler.util.js +91 -13
  28. package/cjs/utils/html-sanitizer.util.js +74 -0
  29. package/cjs/utils/index.js +2 -0
  30. package/cjs/utils/query-helpers.util.js +53 -0
  31. package/classes/api-controller.class.d.ts +5 -5
  32. package/classes/api-service.class.d.ts +5 -5
  33. package/classes/index.d.ts +1 -0
  34. package/classes/request-scoped-api.service.d.ts +3 -2
  35. package/constants/index.d.ts +1 -0
  36. package/constants/permissions.d.ts +167 -0
  37. package/decorators/index.d.ts +1 -0
  38. package/decorators/sanitize-html.decorator.d.ts +2 -0
  39. package/dtos/filter-and-pagination.dto.d.ts +0 -2
  40. package/dtos/response-payload.dto.d.ts +0 -7
  41. package/fesm/classes/api-controller.class.js +9 -24
  42. package/fesm/classes/index.js +2 -0
  43. package/fesm/constants/index.js +2 -0
  44. package/fesm/constants/permissions.js +121 -0
  45. package/fesm/decorators/api-response.decorator.js +1 -1
  46. package/fesm/decorators/index.js +1 -0
  47. package/fesm/decorators/sanitize-html.decorator.js +45 -0
  48. package/fesm/dtos/filter-and-pagination.dto.js +26 -47
  49. package/fesm/dtos/pagination.dto.js +4 -8
  50. package/fesm/dtos/response-payload.dto.js +0 -38
  51. package/fesm/entities/identity.js +4 -4
  52. package/fesm/entities/user-root.js +13 -14
  53. package/fesm/guards/permission.guard.js +39 -94
  54. package/fesm/interceptors/index.js +1 -0
  55. package/fesm/interceptors/set-create-by-on-body.interceptor.js +4 -30
  56. package/fesm/interceptors/set-delete-by-on-body.interceptor.js +4 -30
  57. package/fesm/interceptors/set-update-by-on-body.interceptor.js +4 -30
  58. package/fesm/interceptors/set-user-field-on-body.interceptor.js +36 -0
  59. package/fesm/interceptors/slug.interceptor.js +31 -10
  60. package/fesm/interfaces/datasource.interface.js +20 -0
  61. package/fesm/interfaces/index.js +2 -1
  62. package/fesm/interfaces/module-config.interface.js +5 -0
  63. package/fesm/modules/cache/cache.module.js +2 -2
  64. package/fesm/modules/datasource/multi-tenant-datasource.service.js +30 -110
  65. package/fesm/modules/utils/utils.service.js +50 -143
  66. package/fesm/utils/error-handler.util.js +93 -14
  67. package/fesm/utils/html-sanitizer.util.js +82 -0
  68. package/fesm/utils/index.js +2 -0
  69. package/fesm/utils/query-helpers.util.js +78 -0
  70. package/interceptors/index.d.ts +1 -0
  71. package/interceptors/set-create-by-on-body.interceptor.d.ts +1 -5
  72. package/interceptors/set-delete-by-on-body.interceptor.d.ts +1 -5
  73. package/interceptors/set-update-by-on-body.interceptor.d.ts +1 -5
  74. package/interceptors/set-user-field-on-body.interceptor.d.ts +2 -0
  75. package/interceptors/slug.interceptor.d.ts +2 -1
  76. package/interfaces/api.interface.d.ts +2 -2
  77. package/interfaces/datasource.interface.d.ts +5 -0
  78. package/interfaces/identity.interface.d.ts +4 -4
  79. package/interfaces/index.d.ts +2 -1
  80. package/interfaces/module-config.interface.d.ts +6 -0
  81. package/interfaces/permission.interface.d.ts +0 -1
  82. package/modules/utils/utils.service.d.ts +10 -4
  83. package/package.json +4 -4
  84. package/utils/error-handler.util.d.ts +23 -13
  85. package/utils/html-sanitizer.util.d.ts +3 -0
  86. package/utils/index.d.ts +2 -0
  87. package/utils/query-helpers.util.d.ts +16 -0
  88. package/cjs/interfaces/base-query.interface.js +0 -6
  89. package/fesm/interfaces/base-query.interface.js +0 -3
  90. package/interfaces/base-query.interface.d.ts +0 -7
@@ -0,0 +1,121 @@
1
+ /**
2
+ * Centralized Permission Codes
3
+ *
4
+ * Single source of truth for all permission codes used across the application.
5
+ * Use these constants instead of hardcoded strings to prevent typos and enable easy refactoring.
6
+ *
7
+ * Naming Convention: <entity>.<action>
8
+ * - entity: The resource being accessed (e.g., user, role, company)
9
+ * - action: The operation being performed (create, read, update, delete, assign)
10
+ */ // ==================== AUTH MODULE ====================
11
+ export const USER_PERMISSIONS = {
12
+ CREATE: 'user.create',
13
+ READ: 'user.read',
14
+ UPDATE: 'user.update',
15
+ DELETE: 'user.delete'
16
+ };
17
+ export const COMPANY_PERMISSIONS = {
18
+ CREATE: 'company.create',
19
+ READ: 'company.read',
20
+ UPDATE: 'company.update',
21
+ DELETE: 'company.delete'
22
+ };
23
+ export const BRANCH_PERMISSIONS = {
24
+ CREATE: 'branch.create',
25
+ READ: 'branch.read',
26
+ UPDATE: 'branch.update',
27
+ DELETE: 'branch.delete'
28
+ };
29
+ // ==================== IAM MODULE ====================
30
+ export const ACTION_PERMISSIONS = {
31
+ CREATE: 'action.create',
32
+ READ: 'action.read',
33
+ UPDATE: 'action.update',
34
+ DELETE: 'action.delete'
35
+ };
36
+ export const ROLE_PERMISSIONS = {
37
+ CREATE: 'role.create',
38
+ READ: 'role.read',
39
+ UPDATE: 'role.update',
40
+ DELETE: 'role.delete'
41
+ };
42
+ export const ROLE_ACTION_PERMISSIONS = {
43
+ READ: 'role-action.read',
44
+ ASSIGN: 'role-action.assign'
45
+ };
46
+ export const USER_ROLE_PERMISSIONS = {
47
+ READ: 'user-role.read',
48
+ ASSIGN: 'user-role.assign'
49
+ };
50
+ export const USER_ACTION_PERMISSIONS = {
51
+ READ: 'user-action.read',
52
+ ASSIGN: 'user-action.assign'
53
+ };
54
+ export const COMPANY_ACTION_PERMISSIONS = {
55
+ READ: 'company-action.read',
56
+ ASSIGN: 'company-action.assign'
57
+ };
58
+ // ==================== STORAGE MODULE ====================
59
+ export const FILE_PERMISSIONS = {
60
+ CREATE: 'file.create',
61
+ READ: 'file.read',
62
+ UPDATE: 'file.update',
63
+ DELETE: 'file.delete'
64
+ };
65
+ export const FOLDER_PERMISSIONS = {
66
+ CREATE: 'folder.create',
67
+ READ: 'folder.read',
68
+ UPDATE: 'folder.update',
69
+ DELETE: 'folder.delete'
70
+ };
71
+ export const STORAGE_CONFIG_PERMISSIONS = {
72
+ CREATE: 'storage-config.create',
73
+ READ: 'storage-config.read',
74
+ UPDATE: 'storage-config.update',
75
+ DELETE: 'storage-config.delete'
76
+ };
77
+ // ==================== EMAIL MODULE ====================
78
+ export const EMAIL_CONFIG_PERMISSIONS = {
79
+ CREATE: 'email-config.create',
80
+ READ: 'email-config.read',
81
+ UPDATE: 'email-config.update',
82
+ DELETE: 'email-config.delete'
83
+ };
84
+ export const EMAIL_TEMPLATE_PERMISSIONS = {
85
+ CREATE: 'email-template.create',
86
+ READ: 'email-template.read',
87
+ UPDATE: 'email-template.update',
88
+ DELETE: 'email-template.delete'
89
+ };
90
+ // ==================== FORM BUILDER MODULE ====================
91
+ export const FORM_PERMISSIONS = {
92
+ CREATE: 'form.create',
93
+ READ: 'form.read',
94
+ UPDATE: 'form.update',
95
+ DELETE: 'form.delete'
96
+ };
97
+ // ==================== AGGREGATED EXPORTS ====================
98
+ /**
99
+ * All permission codes grouped by module
100
+ */ export const PERMISSIONS = {
101
+ // Auth
102
+ USER: USER_PERMISSIONS,
103
+ COMPANY: COMPANY_PERMISSIONS,
104
+ BRANCH: BRANCH_PERMISSIONS,
105
+ // IAM
106
+ ACTION: ACTION_PERMISSIONS,
107
+ ROLE: ROLE_PERMISSIONS,
108
+ ROLE_ACTION: ROLE_ACTION_PERMISSIONS,
109
+ USER_ROLE: USER_ROLE_PERMISSIONS,
110
+ USER_ACTION: USER_ACTION_PERMISSIONS,
111
+ COMPANY_ACTION: COMPANY_ACTION_PERMISSIONS,
112
+ // Storage
113
+ FILE: FILE_PERMISSIONS,
114
+ FOLDER: FOLDER_PERMISSIONS,
115
+ STORAGE_CONFIG: STORAGE_CONFIG_PERMISSIONS,
116
+ // Email
117
+ EMAIL_CONFIG: EMAIL_CONFIG_PERMISSIONS,
118
+ EMAIL_TEMPLATE: EMAIL_TEMPLATE_PERMISSIONS,
119
+ // Form Builder
120
+ FORM: FORM_PERMISSIONS
121
+ };
@@ -1,4 +1,4 @@
1
- import { BulkMetaDto, BulkResponseDto, ListResponseDto, PaginationMetaDto, SingleResponseDto } from '@flusys/nestjs-shared/dtos';
1
+ import { BulkMetaDto, BulkResponseDto, ListResponseDto, PaginationMetaDto, SingleResponseDto } from '../dtos';
2
2
  import { applyDecorators } from '@nestjs/common';
3
3
  import { ApiExtraModels, ApiOkResponse, getSchemaPath } from '@nestjs/swagger';
4
4
  /**
@@ -2,3 +2,4 @@ export * from './api-response.decorator';
2
2
  export * from './current-user.decorator';
3
3
  export * from './public.decorator';
4
4
  export * from './require-permission.decorator';
5
+ export * from './sanitize-html.decorator';
@@ -0,0 +1,45 @@
1
+ import { Transform } from 'class-transformer';
2
+ import { escapeHtml } from '../utils/html-sanitizer.util';
3
+ /**
4
+ * Decorator that sanitizes HTML content in string fields to prevent XSS attacks.
5
+ * Applies HTML escaping during transformation (when class-transformer processes the DTO).
6
+ *
7
+ * @example
8
+ * ```typescript
9
+ * class CreateCommentDto {
10
+ * @SanitizeHtml()
11
+ * @IsString()
12
+ * content: string;
13
+ * }
14
+ * ```
15
+ *
16
+ * Input: '<script>alert("xss")</script>'
17
+ * Output: '&lt;script&gt;alert(&quot;xss&quot;)&lt;/script&gt;'
18
+ */ export function SanitizeHtml() {
19
+ return Transform(({ value })=>{
20
+ if (typeof value === 'string') {
21
+ return escapeHtml(value);
22
+ }
23
+ return value;
24
+ });
25
+ }
26
+ /**
27
+ * Decorator that sanitizes HTML and trims whitespace from string fields.
28
+ * Combines sanitization with trimming for cleaner input handling.
29
+ *
30
+ * @example
31
+ * ```typescript
32
+ * class CreatePostDto {
33
+ * @SanitizeAndTrim()
34
+ * @IsString()
35
+ * title: string;
36
+ * }
37
+ * ```
38
+ */ export function SanitizeAndTrim() {
39
+ return Transform(({ value })=>{
40
+ if (typeof value === 'string') {
41
+ return escapeHtml(value.trim());
42
+ }
43
+ return value;
44
+ });
45
+ }
@@ -22,27 +22,15 @@ function _ts_metadata(k, v) {
22
22
  }
23
23
  import { ApiPropertyOptional } from '@nestjs/swagger';
24
24
  import { Type } from 'class-transformer';
25
- import { IsArray, IsBoolean, IsObject, IsOptional, ValidateNested } from 'class-validator';
25
+ import { IsArray, IsBoolean, IsObject, IsOptional, IsString, Matches, MaxLength, ValidateNested } from 'class-validator';
26
26
  import { PaginationDto } from './pagination.dto';
27
- /**
28
- * DTO for filtering and pagination in get-all requests
29
- *
30
- * @example
31
- * {
32
- * "filter": { "isActive": true, "category": "electronics" },
33
- * "pagination": { "currentPage": 0, "pageSize": 10 },
34
- * "sort": { "createdAt": "DESC" },
35
- * "select": ["id", "name", "price"],
36
- * "withDeleted": false
37
- * }
38
- */ export class FilterAndPaginationDto {
27
+ export class FilterAndPaginationDto {
39
28
  constructor(){
40
29
  _define_property(this, "filter", void 0);
41
30
  _define_property(this, "pagination", void 0);
42
31
  _define_property(this, "sort", void 0);
43
32
  _define_property(this, "select", void 0);
44
33
  _define_property(this, "withDeleted", void 0);
45
- _define_property(this, "extraKey", void 0);
46
34
  }
47
35
  }
48
36
  _ts_decorate([
@@ -90,7 +78,7 @@ _ts_decorate([
90
78
  type: [
91
79
  String
92
80
  ],
93
- description: 'Fields to return. If empty, returns all fields.',
81
+ description: 'Fields to return. Must be valid field names (alphanumeric, underscores only). If empty, returns all fields.',
94
82
  example: [
95
83
  'id',
96
84
  'name',
@@ -100,6 +88,17 @@ _ts_decorate([
100
88
  }),
101
89
  IsOptional(),
102
90
  IsArray(),
91
+ IsString({
92
+ each: true
93
+ }),
94
+ Matches(/^[a-zA-Z_][a-zA-Z0-9_]*$/, {
95
+ each: true,
96
+ message: 'Select fields must be valid identifiers (alphanumeric and underscores only)'
97
+ }),
98
+ MaxLength(64, {
99
+ each: true,
100
+ message: 'Field names must be 64 characters or less'
101
+ }),
103
102
  _ts_metadata("design:type", Array)
104
103
  ], FilterAndPaginationDto.prototype, "select", void 0);
105
104
  _ts_decorate([
@@ -112,27 +111,11 @@ _ts_decorate([
112
111
  IsBoolean(),
113
112
  _ts_metadata("design:type", Boolean)
114
113
  ], FilterAndPaginationDto.prototype, "withDeleted", void 0);
115
- _ts_decorate([
116
- ApiPropertyOptional({
117
- type: [
118
- String
119
- ],
120
- description: 'Additional relation keys to include',
121
- example: [
122
- 'category',
123
- 'createdBy'
124
- ]
125
- }),
126
- IsOptional(),
127
- IsArray(),
128
- _ts_metadata("design:type", Array)
129
- ], FilterAndPaginationDto.prototype, "extraKey", void 0);
130
114
  /**
131
115
  * DTO for get-by-id request body
132
116
  */ export class GetByIdBodyDto {
133
117
  constructor(){
134
118
  _define_property(this, "select", void 0);
135
- _define_property(this, "extraKey", void 0);
136
119
  }
137
120
  }
138
121
  _ts_decorate([
@@ -140,7 +123,7 @@ _ts_decorate([
140
123
  type: [
141
124
  String
142
125
  ],
143
- description: 'Fields to return. If empty, returns all fields.',
126
+ description: 'Fields to return. Must be valid field names (alphanumeric, underscores only). If empty, returns all fields.',
144
127
  example: [
145
128
  'id',
146
129
  'name',
@@ -150,20 +133,16 @@ _ts_decorate([
150
133
  }),
151
134
  IsOptional(),
152
135
  IsArray(),
153
- _ts_metadata("design:type", Array)
154
- ], GetByIdBodyDto.prototype, "select", void 0);
155
- _ts_decorate([
156
- ApiPropertyOptional({
157
- type: [
158
- String
159
- ],
160
- description: 'Additional relation keys to include',
161
- example: [
162
- 'category',
163
- 'createdBy'
164
- ]
136
+ IsString({
137
+ each: true
138
+ }),
139
+ Matches(/^[a-zA-Z_][a-zA-Z0-9_]*$/, {
140
+ each: true,
141
+ message: 'Select fields must be valid identifiers (alphanumeric and underscores only)'
142
+ }),
143
+ MaxLength(64, {
144
+ each: true,
145
+ message: 'Field names must be 64 characters or less'
165
146
  }),
166
- IsOptional(),
167
- IsArray(),
168
147
  _ts_metadata("design:type", Array)
169
- ], GetByIdBodyDto.prototype, "extraKey", void 0);
148
+ ], GetByIdBodyDto.prototype, "select", void 0);
@@ -25,17 +25,13 @@ import { Transform } from 'class-transformer';
25
25
  import { IsNumber, IsOptional } from 'class-validator';
26
26
  export class PaginationDto {
27
27
  constructor(){
28
- /**
29
- * Number of items per page. Defaults to 10 when not provided or invalid.
30
- */ _define_property(this, "pageSize", 10);
31
- /**
32
- * Zero-based page index. Defaults to 0 when not provided or invalid.
33
- */ _define_property(this, "currentPage", 0);
28
+ _define_property(this, "pageSize", 10);
29
+ _define_property(this, "currentPage", 0);
34
30
  }
35
31
  }
36
32
  _ts_decorate([
37
33
  ApiPropertyOptional({
38
- description: 'Number of items per page. Defaults to 10 when not provided or invalid.',
34
+ description: 'Number of items per page (default: 10)',
39
35
  example: 10
40
36
  }),
41
37
  IsOptional(),
@@ -48,7 +44,7 @@ _ts_decorate([
48
44
  ], PaginationDto.prototype, "pageSize", void 0);
49
45
  _ts_decorate([
50
46
  ApiPropertyOptional({
51
- description: 'Zero-based page index. Defaults to 0 when not provided or invalid.',
47
+ description: 'Zero-based page index (default: 0)',
52
48
  example: 0
53
49
  }),
54
50
  IsOptional(),
@@ -331,41 +331,3 @@ _ts_decorate([
331
331
  ErrorResponseDto = _ts_decorate([
332
332
  ApiExtraModels()
333
333
  ], ErrorResponseDto);
334
- export class ResponsePayloadDto {
335
- constructor(){
336
- _define_property(this, "success", void 0);
337
- _define_property(this, "message", void 0);
338
- _define_property(this, "data", void 0);
339
- _define_property(this, "meta", void 0);
340
- _define_property(this, "_meta", void 0);
341
- }
342
- }
343
- _ts_decorate([
344
- ApiProperty({
345
- example: true
346
- }),
347
- _ts_metadata("design:type", Boolean)
348
- ], ResponsePayloadDto.prototype, "success", void 0);
349
- _ts_decorate([
350
- ApiProperty({
351
- example: 'Operation successful'
352
- }),
353
- _ts_metadata("design:type", String)
354
- ], ResponsePayloadDto.prototype, "message", void 0);
355
- _ts_decorate([
356
- ApiPropertyOptional(),
357
- _ts_metadata("design:type", typeof T === "undefined" ? Object : T)
358
- ], ResponsePayloadDto.prototype, "data", void 0);
359
- _ts_decorate([
360
- ApiPropertyOptional(),
361
- _ts_metadata("design:type", Object)
362
- ], ResponsePayloadDto.prototype, "meta", void 0);
363
- _ts_decorate([
364
- ApiPropertyOptional({
365
- type: RequestMetaDto
366
- }),
367
- _ts_metadata("design:type", typeof RequestMetaDto === "undefined" ? Object : RequestMetaDto)
368
- ], ResponsePayloadDto.prototype, "_meta", void 0);
369
- ResponsePayloadDto = _ts_decorate([
370
- ApiExtraModels()
371
- ], ResponsePayloadDto);
@@ -26,10 +26,10 @@ export class Identity {
26
26
  _define_property(this, "id", void 0);
27
27
  _define_property(this, "createdAt", void 0);
28
28
  _define_property(this, "updatedAt", void 0);
29
- _define_property(this, "deletedAt", void 0);
30
- _define_property(this, "createdById", void 0);
31
- _define_property(this, "updatedById", void 0);
32
- _define_property(this, "deletedById", void 0);
29
+ _define_property(this, "deletedAt", null);
30
+ _define_property(this, "createdById", null);
31
+ _define_property(this, "updatedById", null);
32
+ _define_property(this, "deletedById", null);
33
33
  }
34
34
  }
35
35
  _ts_decorate([
@@ -24,23 +24,22 @@ import { Column, CreateDateColumn, DeleteDateColumn, PrimaryGeneratedColumn, Upd
24
24
  export class UserRoot {
25
25
  constructor(){
26
26
  _define_property(this, "id", void 0);
27
- _define_property(this, "name", void 0);
28
- _define_property(this, "password", void 0);
27
+ _define_property(this, "name", null);
28
+ _define_property(this, "password", null);
29
29
  _define_property(this, "email", void 0);
30
- _define_property(this, "phone", void 0);
31
- _define_property(this, "isActive", void 0);
32
- _define_property(this, "emailVerified", void 0);
33
- _define_property(this, "phoneVerified", void 0);
34
- _define_property(this, "profilePictureId", void 0);
35
- _define_property(this, "lastLoginAt", void 0);
36
- _define_property(this, "additionalFields", void 0);
30
+ _define_property(this, "phone", null);
31
+ _define_property(this, "isActive", true);
32
+ _define_property(this, "emailVerified", false);
33
+ _define_property(this, "phoneVerified", false);
34
+ _define_property(this, "profilePictureId", null);
35
+ _define_property(this, "lastLoginAt", null);
36
+ _define_property(this, "additionalFields", null);
37
37
  _define_property(this, "createdAt", void 0);
38
38
  _define_property(this, "updatedAt", void 0);
39
- _define_property(this, "deletedAt", void 0);
40
- // Audit columns (for ApiService compatibility)
41
- _define_property(this, "createdById", void 0);
42
- _define_property(this, "updatedById", void 0);
43
- _define_property(this, "deletedById", void 0);
39
+ _define_property(this, "deletedAt", null);
40
+ _define_property(this, "createdById", null);
41
+ _define_property(this, "updatedById", null);
42
+ _define_property(this, "deletedById", null);
44
43
  }
45
44
  }
46
45
  _ts_decorate([
@@ -35,106 +35,79 @@ import { ILogger } from '../interfaces/logger.interface';
35
35
  import { PermissionGuardConfig } from '../interfaces/permission.interface';
36
36
  export class PermissionGuard {
37
37
  async canActivate(context) {
38
- // Check if route is marked as public
39
38
  const isPublic = this.reflector.getAllAndOverride(IS_PUBLIC_KEY, [
40
39
  context.getHandler(),
41
40
  context.getClass()
42
41
  ]);
43
42
  if (isPublic) return true;
44
- // Get required permissions from decorator
45
43
  const permissionConfig = this.reflector.getAllAndOverride(PERMISSIONS_KEY, [
46
44
  context.getHandler(),
47
45
  context.getClass()
48
46
  ]);
49
- // If no permissions required, allow access
50
- if (!permissionConfig) {
51
- return true;
52
- }
53
- // Normalize permission config (support old format: string[])
47
+ if (!permissionConfig) return true;
54
48
  const { permissions: requiredPermissions, operator } = this.normalizePermissionConfig(permissionConfig);
55
- // If no permissions required, allow access
56
- if (!requiredPermissions || requiredPermissions.length === 0) {
57
- return true;
58
- }
49
+ if (!requiredPermissions || requiredPermissions.length === 0) return true;
59
50
  const request = context.switchToHttp().getRequest();
60
51
  const user = request.user;
61
- // User must be authenticated
62
- if (!user) {
63
- throw new UnauthorizedException('Authentication required');
64
- }
65
- // Cache is required for permission checks - fail securely if unavailable
52
+ if (!user) throw new UnauthorizedException('Authentication required');
66
53
  if (!this.cache) {
67
- // Log error (in production, this should be monitored)
68
- this.logger.error(`Cache not available - permission system unavailable (userId: ${user.id})`, undefined, 'PermissionGuard');
69
- // Fail securely - deny access rather than allowing without permission check
54
+ this.logger.error(`Cache not available (userId: ${user.id})`, undefined, 'PermissionGuard');
70
55
  throw new PermissionSystemUnavailableException();
71
56
  }
72
- // Get user's permissions from cache
73
57
  const userPermissions = await this.getUserPermissions(user);
74
- // If no permissions found in cache, deny access
75
58
  if (!userPermissions || userPermissions.length === 0) {
76
- this.logger.warn(`No permissions found for user (userId: ${user.id})`, 'PermissionGuard');
59
+ this.logger.warn(`No permissions found (userId: ${user.id})`, 'PermissionGuard');
77
60
  throw new NoPermissionsFoundException();
78
61
  }
79
- // Check if this is a nested condition or simple permission list
80
62
  if (this.isNestedCondition(permissionConfig)) {
81
- // Complex nested permission check
82
63
  const result = this.evaluateCondition(permissionConfig, userPermissions);
83
64
  if (!result.passed) {
84
- this.logger.warn(`Permission check failed (userId: ${user.id}, missing: ${result.missingPermissions.join(', ')})`, 'PermissionGuard');
65
+ this.logger.warn(`Permission denied (userId: ${user.id})`, 'PermissionGuard');
85
66
  throw new InsufficientPermissionsException(result.missingPermissions, result.operator);
86
67
  }
87
68
  } else {
88
- // Simple permission check (backward compatible)
89
- let hasRequiredPermissions;
69
+ let hasRequired;
90
70
  if (operator === 'or') {
91
- // OR: User must have at least ONE permission
92
- hasRequiredPermissions = requiredPermissions.some((permission)=>this.hasPermission(userPermissions, permission));
93
- if (!hasRequiredPermissions) {
94
- throw new InsufficientPermissionsException(requiredPermissions, 'or');
95
- }
71
+ hasRequired = requiredPermissions.some((p)=>this.hasPermission(userPermissions, p));
72
+ if (!hasRequired) throw new InsufficientPermissionsException(requiredPermissions, 'or');
96
73
  } else {
97
- // AND (default): User must have ALL permissions
98
- hasRequiredPermissions = requiredPermissions.every((permission)=>this.hasPermission(userPermissions, permission));
99
- if (!hasRequiredPermissions) {
100
- const missing = requiredPermissions.filter((permission)=>!this.hasPermission(userPermissions, permission));
74
+ hasRequired = requiredPermissions.every((p)=>this.hasPermission(userPermissions, p));
75
+ if (!hasRequired) {
76
+ const missing = requiredPermissions.filter((p)=>!this.hasPermission(userPermissions, p));
101
77
  throw new InsufficientPermissionsException(missing, 'and');
102
78
  }
103
79
  }
104
80
  }
105
81
  return true;
106
82
  }
107
- /**
108
- * Normalize permission config to handle both old and new formats
109
- */ normalizePermissionConfig(config) {
110
- // Old format: string[]
111
- if (Array.isArray(config)) {
112
- return {
113
- permissions: config,
114
- operator: 'and'
115
- };
116
- }
117
- // New format: PermissionConfig
83
+ normalizePermissionConfig(config) {
84
+ if (Array.isArray(config)) return {
85
+ permissions: config,
86
+ operator: 'and'
87
+ };
118
88
  return {
119
89
  permissions: config.permissions || [],
120
90
  operator: config.operator || 'and'
121
91
  };
122
92
  }
123
- /**
124
- * Check if config is a nested condition (has children)
125
- */ isNestedCondition(config) {
93
+ isNestedCondition(config) {
126
94
  if (Array.isArray(config)) return false;
127
95
  return 'children' in config && Array.isArray(config.children) && config.children.length > 0;
128
96
  }
129
- /**
130
- * Evaluate a nested permission condition recursively
131
- */ evaluateCondition(condition, userPermissions) {
97
+ evaluateCondition(condition, userPermissions) {
132
98
  const { permissions = [], operator, children = [] } = condition;
133
- // Results for this level
99
+ // SECURITY: Fail-closed - deny access when no permissions configured (empty condition)
100
+ if (permissions.length === 0 && children.length === 0) {
101
+ return {
102
+ passed: false,
103
+ message: 'No permissions configured - access denied by default',
104
+ missingPermissions: [],
105
+ operator
106
+ };
107
+ }
134
108
  const results = [];
135
109
  const failureDetails = [];
136
110
  const missingPermissions = [];
137
- // Check permissions at this level
138
111
  if (permissions.length > 0) {
139
112
  if (operator === 'or') {
140
113
  const hasAny = permissions.some((p)=>this.hasPermission(userPermissions, p));
@@ -153,7 +126,6 @@ export class PermissionGuard {
153
126
  }
154
127
  }
155
128
  }
156
- // Evaluate children recursively
157
129
  for (const child of children){
158
130
  const childResult = this.evaluateCondition(child, userPermissions);
159
131
  results.push(childResult.passed);
@@ -162,14 +134,9 @@ export class PermissionGuard {
162
134
  missingPermissions.push(...childResult.missingPermissions);
163
135
  }
164
136
  }
165
- // Combine results based on operator
166
- let passed;
167
- if (operator === 'or') {
168
- passed = results.length === 0 || results.some((r)=>r);
169
- } else {
170
- passed = results.length === 0 || results.every((r)=>r);
171
- }
172
- const message = passed ? 'Permission granted' : `Permission denied: ${failureDetails.join(` ${operator.toUpperCase()} `)}`;
137
+ // Evaluate based on operator - empty results already handled above
138
+ const passed = operator === 'or' ? results.some((r)=>r) : results.every((r)=>r);
139
+ const message = passed ? 'OK' : `Denied: ${failureDetails.join(` ${operator.toUpperCase()} `)}`;
173
140
  return {
174
141
  passed,
175
142
  message,
@@ -177,48 +144,28 @@ export class PermissionGuard {
177
144
  operator
178
145
  };
179
146
  }
180
- /**
181
- * Get user's permissions from cache
182
- */ async getUserPermissions(user) {
183
- if (!this.cache) {
184
- throw new PermissionSystemUnavailableException();
185
- }
147
+ async getUserPermissions(user) {
148
+ if (!this.cache) throw new PermissionSystemUnavailableException();
186
149
  let cacheKey;
187
150
  if (this.config.enableCompanyFeature && user.companyId) {
188
- // Company-based permissions (includes branchId for branch-scoped DIRECT permissions)
189
151
  const format = this.config.companyPermissionKeyFormat || `${PERMISSIONS_CACHE_PREFIX}:company:{companyId}:branch:{branchId}:user:{userId}`;
190
152
  cacheKey = format.replace('{userId}', user.id).replace('{companyId}', user.companyId).replace('{branchId}', user.branchId || 'null');
191
153
  } else {
192
- // User-based permissions
193
154
  const format = this.config.userPermissionKeyFormat || `${PERMISSIONS_CACHE_PREFIX}:user:{userId}`;
194
155
  cacheKey = format.replace('{userId}', user.id);
195
156
  }
196
- const permissions = await this.cache.get(cacheKey);
197
- return permissions || [];
157
+ return await this.cache.get(cacheKey) || [];
198
158
  }
199
- /**
200
- * Check if user has a specific permission
201
- * Supports wildcard matching (e.g., 'admin.*' matches 'admin.users.read')
202
- */ hasPermission(userPermissions, requiredPermission) {
203
- // Direct match
204
- if (userPermissions.includes(requiredPermission)) {
205
- return true;
206
- }
207
- // Wildcard match (e.g., '*' or 'admin.*')
159
+ hasPermission(userPermissions, requiredPermission) {
160
+ if (userPermissions.includes(requiredPermission)) return true;
208
161
  for (const permission of userPermissions){
209
- if (permission === '*') {
210
- return true; // Super admin
211
- }
212
- if (permission.endsWith('.*')) {
213
- const prefix = permission.slice(0, -1); // Remove '*'
214
- if (requiredPermission.startsWith(prefix)) {
215
- return true;
216
- }
162
+ if (permission === '*') return true;
163
+ if (permission.endsWith('.*') && requiredPermission.startsWith(permission.slice(0, -1))) {
164
+ return true;
217
165
  }
218
166
  }
219
167
  return false;
220
168
  }
221
- // NOTE: @Inject(Reflector) required for bundled code - external classes need explicit injection
222
169
  constructor(reflector, cache, config, logger){
223
170
  _define_property(this, "reflector", void 0);
224
171
  _define_property(this, "cache", void 0);
@@ -228,12 +175,10 @@ export class PermissionGuard {
228
175
  this.cache = cache;
229
176
  this.config = {
230
177
  enableCompanyFeature: false,
231
- cacheKeyPrefix: PERMISSIONS_CACHE_PREFIX,
232
178
  userPermissionKeyFormat: `${PERMISSIONS_CACHE_PREFIX}:user:{userId}`,
233
179
  companyPermissionKeyFormat: `${PERMISSIONS_CACHE_PREFIX}:company:{companyId}:branch:{branchId}:user:{userId}`,
234
180
  ...config
235
181
  };
236
- // Use provided logger or fallback to NestJS Logger wrapped in adapter
237
182
  this.logger = logger || new NestLoggerAdapter(new Logger(PermissionGuard.name));
238
183
  }
239
184
  }