@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.
- package/README.md +56 -110
- package/cjs/classes/api-controller.class.js +9 -24
- package/cjs/classes/index.js +1 -0
- package/cjs/constants/index.js +14 -0
- package/cjs/constants/permissions.js +174 -0
- package/cjs/decorators/api-response.decorator.js +1 -1
- package/cjs/decorators/index.js +1 -0
- package/cjs/decorators/sanitize-html.decorator.js +36 -0
- package/cjs/dtos/filter-and-pagination.dto.js +24 -34
- package/cjs/dtos/pagination.dto.js +4 -8
- package/cjs/dtos/response-payload.dto.js +0 -41
- package/cjs/entities/identity.js +4 -4
- package/cjs/entities/user-root.js +13 -14
- package/cjs/guards/permission.guard.js +39 -94
- package/cjs/interceptors/index.js +1 -0
- package/cjs/interceptors/set-create-by-on-body.interceptor.js +2 -30
- package/cjs/interceptors/set-delete-by-on-body.interceptor.js +2 -30
- package/cjs/interceptors/set-update-by-on-body.interceptor.js +2 -30
- package/cjs/interceptors/set-user-field-on-body.interceptor.js +43 -0
- package/cjs/interceptors/slug.interceptor.js +30 -9
- package/cjs/interfaces/datasource.interface.js +4 -0
- package/cjs/interfaces/index.js +2 -1
- package/cjs/interfaces/module-config.interface.js +4 -0
- package/cjs/modules/cache/cache.module.js +3 -3
- package/cjs/modules/datasource/multi-tenant-datasource.service.js +30 -110
- package/cjs/modules/utils/utils.service.js +63 -145
- package/cjs/utils/error-handler.util.js +91 -13
- package/cjs/utils/html-sanitizer.util.js +74 -0
- package/cjs/utils/index.js +2 -0
- package/cjs/utils/query-helpers.util.js +53 -0
- package/classes/api-controller.class.d.ts +5 -5
- package/classes/api-service.class.d.ts +5 -5
- package/classes/index.d.ts +1 -0
- package/classes/request-scoped-api.service.d.ts +3 -2
- package/constants/index.d.ts +1 -0
- package/constants/permissions.d.ts +167 -0
- package/decorators/index.d.ts +1 -0
- package/decorators/sanitize-html.decorator.d.ts +2 -0
- package/dtos/filter-and-pagination.dto.d.ts +0 -2
- package/dtos/response-payload.dto.d.ts +0 -7
- package/fesm/classes/api-controller.class.js +9 -24
- package/fesm/classes/index.js +2 -0
- package/fesm/constants/index.js +2 -0
- package/fesm/constants/permissions.js +121 -0
- package/fesm/decorators/api-response.decorator.js +1 -1
- package/fesm/decorators/index.js +1 -0
- package/fesm/decorators/sanitize-html.decorator.js +45 -0
- package/fesm/dtos/filter-and-pagination.dto.js +26 -47
- package/fesm/dtos/pagination.dto.js +4 -8
- package/fesm/dtos/response-payload.dto.js +0 -38
- package/fesm/entities/identity.js +4 -4
- package/fesm/entities/user-root.js +13 -14
- package/fesm/guards/permission.guard.js +39 -94
- package/fesm/interceptors/index.js +1 -0
- package/fesm/interceptors/set-create-by-on-body.interceptor.js +4 -30
- package/fesm/interceptors/set-delete-by-on-body.interceptor.js +4 -30
- package/fesm/interceptors/set-update-by-on-body.interceptor.js +4 -30
- package/fesm/interceptors/set-user-field-on-body.interceptor.js +36 -0
- package/fesm/interceptors/slug.interceptor.js +31 -10
- package/fesm/interfaces/datasource.interface.js +20 -0
- package/fesm/interfaces/index.js +2 -1
- package/fesm/interfaces/module-config.interface.js +5 -0
- package/fesm/modules/cache/cache.module.js +2 -2
- package/fesm/modules/datasource/multi-tenant-datasource.service.js +30 -110
- package/fesm/modules/utils/utils.service.js +50 -143
- package/fesm/utils/error-handler.util.js +93 -14
- package/fesm/utils/html-sanitizer.util.js +82 -0
- package/fesm/utils/index.js +2 -0
- package/fesm/utils/query-helpers.util.js +78 -0
- package/interceptors/index.d.ts +1 -0
- package/interceptors/set-create-by-on-body.interceptor.d.ts +1 -5
- package/interceptors/set-delete-by-on-body.interceptor.d.ts +1 -5
- package/interceptors/set-update-by-on-body.interceptor.d.ts +1 -5
- package/interceptors/set-user-field-on-body.interceptor.d.ts +2 -0
- package/interceptors/slug.interceptor.d.ts +2 -1
- package/interfaces/api.interface.d.ts +2 -2
- package/interfaces/datasource.interface.d.ts +5 -0
- package/interfaces/identity.interface.d.ts +4 -4
- package/interfaces/index.d.ts +2 -1
- package/interfaces/module-config.interface.d.ts +6 -0
- package/interfaces/permission.interface.d.ts +0 -1
- package/modules/utils/utils.service.d.ts +10 -4
- package/package.json +4 -4
- package/utils/error-handler.util.d.ts +23 -13
- package/utils/html-sanitizer.util.d.ts +3 -0
- package/utils/index.d.ts +2 -0
- package/utils/query-helpers.util.d.ts +16 -0
- package/cjs/interfaces/base-query.interface.js +0 -6
- package/fesm/interfaces/base-query.interface.js +0 -3
- 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 '
|
|
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
|
/**
|
package/fesm/decorators/index.js
CHANGED
|
@@ -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: '<script>alert("xss")</script>'
|
|
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
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
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, "
|
|
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
|
-
|
|
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
|
|
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
|
|
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",
|
|
30
|
-
_define_property(this, "createdById",
|
|
31
|
-
_define_property(this, "updatedById",
|
|
32
|
-
_define_property(this, "deletedById",
|
|
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",
|
|
28
|
-
_define_property(this, "password",
|
|
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",
|
|
31
|
-
_define_property(this, "isActive",
|
|
32
|
-
_define_property(this, "emailVerified",
|
|
33
|
-
_define_property(this, "phoneVerified",
|
|
34
|
-
_define_property(this, "profilePictureId",
|
|
35
|
-
_define_property(this, "lastLoginAt",
|
|
36
|
-
_define_property(this, "additionalFields",
|
|
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",
|
|
40
|
-
|
|
41
|
-
_define_property(this, "
|
|
42
|
-
_define_property(this, "
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
89
|
-
let hasRequiredPermissions;
|
|
69
|
+
let hasRequired;
|
|
90
70
|
if (operator === 'or') {
|
|
91
|
-
|
|
92
|
-
|
|
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
|
-
|
|
98
|
-
|
|
99
|
-
|
|
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
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
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
|
-
//
|
|
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
|
-
//
|
|
166
|
-
|
|
167
|
-
|
|
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
|
-
|
|
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
|
-
|
|
197
|
-
return permissions || [];
|
|
157
|
+
return await this.cache.get(cacheKey) || [];
|
|
198
158
|
}
|
|
199
|
-
|
|
200
|
-
|
|
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
|
-
|
|
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
|
}
|