@flusys/nestjs-shared 1.1.0-beta → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +501 -720
- package/cjs/classes/api-controller.class.js +9 -24
- package/cjs/classes/api-service.class.js +59 -92
- package/cjs/classes/index.js +1 -0
- package/cjs/classes/winston-logger-adapter.class.js +23 -40
- package/cjs/constants/index.js +14 -0
- package/cjs/constants/permissions.js +184 -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/delete.dto.js +10 -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 -116
- package/cjs/entities/identity.js +4 -4
- package/cjs/entities/user-root.js +13 -14
- package/cjs/guards/permission.guard.js +51 -105
- package/cjs/interceptors/index.js +1 -3
- package/cjs/interceptors/set-user-field-on-body.interceptor.js +60 -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/middlewares/logger.middleware.js +50 -89
- package/cjs/modules/cache/cache.module.js +3 -3
- package/cjs/modules/datasource/datasource.module.js +11 -14
- package/cjs/modules/datasource/multi-tenant-datasource.service.js +29 -113
- package/cjs/modules/utils/utils.service.js +40 -203
- package/cjs/utils/error-handler.util.js +35 -12
- package/cjs/utils/html-sanitizer.util.js +64 -0
- package/cjs/utils/index.js +4 -0
- package/cjs/utils/query-helpers.util.js +53 -0
- package/cjs/utils/request.util.js +70 -0
- package/cjs/utils/string.util.js +63 -0
- package/classes/api-controller.class.d.ts +5 -5
- package/classes/api-service.class.d.ts +7 -5
- package/classes/index.d.ts +1 -0
- package/classes/request-scoped-api.service.d.ts +3 -2
- package/classes/winston-logger-adapter.class.d.ts +2 -0
- package/constants/index.d.ts +1 -0
- package/constants/permissions.d.ts +179 -0
- package/decorators/index.d.ts +1 -0
- package/decorators/sanitize-html.decorator.d.ts +2 -0
- package/dtos/delete.dto.d.ts +1 -0
- package/dtos/filter-and-pagination.dto.d.ts +0 -2
- package/dtos/response-payload.dto.d.ts +0 -20
- package/fesm/classes/api-controller.class.js +9 -24
- package/fesm/classes/api-service.class.js +59 -92
- package/fesm/classes/index.js +2 -0
- package/fesm/classes/winston-logger-adapter.class.js +23 -40
- package/fesm/constants/index.js +2 -0
- package/fesm/constants/permissions.js +128 -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/delete.dto.js +12 -2
- 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 -107
- package/fesm/entities/identity.js +4 -4
- package/fesm/entities/user-root.js +13 -14
- package/fesm/guards/permission.guard.js +51 -105
- package/fesm/interceptors/index.js +1 -3
- package/fesm/interceptors/set-user-field-on-body.interceptor.js +39 -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/middlewares/logger.middleware.js +50 -83
- package/fesm/modules/cache/cache.module.js +2 -2
- package/fesm/modules/datasource/datasource.module.js +11 -14
- package/fesm/modules/datasource/multi-tenant-datasource.service.js +29 -113
- package/fesm/modules/utils/utils.service.js +41 -204
- package/fesm/utils/error-handler.util.js +36 -13
- package/fesm/utils/html-sanitizer.util.js +69 -0
- package/fesm/utils/index.js +4 -0
- package/fesm/utils/query-helpers.util.js +78 -0
- package/fesm/utils/request.util.js +58 -0
- package/fesm/utils/string.util.js +71 -0
- package/guards/permission.guard.d.ts +2 -0
- package/interceptors/index.d.ts +1 -3
- package/interceptors/set-user-field-on-body.interceptor.d.ts +5 -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/logged-user-info.interface.d.ts +0 -2
- package/interfaces/module-config.interface.d.ts +6 -0
- package/interfaces/permission.interface.d.ts +0 -1
- package/middlewares/logger.middleware.d.ts +2 -2
- package/modules/datasource/datasource.module.d.ts +1 -0
- package/modules/datasource/multi-tenant-datasource.service.d.ts +0 -1
- package/modules/utils/utils.service.d.ts +4 -14
- package/package.json +4 -4
- package/utils/error-handler.util.d.ts +14 -19
- package/utils/html-sanitizer.util.d.ts +2 -0
- package/utils/index.d.ts +4 -0
- package/utils/query-helpers.util.d.ts +16 -0
- package/utils/request.util.d.ts +4 -0
- package/utils/string.util.d.ts +2 -0
- package/cjs/interceptors/set-create-by-on-body.interceptor.js +0 -40
- package/cjs/interceptors/set-delete-by-on-body.interceptor.js +0 -40
- package/cjs/interceptors/set-update-by-on-body.interceptor.js +0 -40
- package/cjs/interfaces/base-query.interface.js +0 -6
- package/fesm/interceptors/set-create-by-on-body.interceptor.js +0 -30
- package/fesm/interceptors/set-delete-by-on-body.interceptor.js +0 -30
- package/fesm/interceptors/set-update-by-on-body.interceptor.js +0 -30
- package/fesm/interfaces/base-query.interface.js +0 -3
- package/interceptors/set-create-by-on-body.interceptor.d.ts +0 -5
- package/interceptors/set-delete-by-on-body.interceptor.d.ts +0 -5
- package/interceptors/set-update-by-on-body.interceptor.d.ts +0 -5
- package/interfaces/base-query.interface.d.ts +0 -7
|
@@ -35,106 +35,81 @@ 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;
|
|
90
|
-
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
|
-
}
|
|
96
|
-
} 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));
|
|
101
|
-
throw new InsufficientPermissionsException(missing, 'and');
|
|
102
|
-
}
|
|
103
|
-
}
|
|
69
|
+
this.validateSimplePermissions(requiredPermissions, userPermissions, operator);
|
|
104
70
|
}
|
|
105
71
|
return true;
|
|
106
72
|
}
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
return {
|
|
113
|
-
permissions: config,
|
|
114
|
-
operator: 'and'
|
|
115
|
-
};
|
|
116
|
-
}
|
|
117
|
-
// New format: PermissionConfig
|
|
73
|
+
normalizePermissionConfig(config) {
|
|
74
|
+
if (Array.isArray(config)) return {
|
|
75
|
+
permissions: config,
|
|
76
|
+
operator: 'and'
|
|
77
|
+
};
|
|
118
78
|
return {
|
|
119
79
|
permissions: config.permissions || [],
|
|
120
80
|
operator: config.operator || 'and'
|
|
121
81
|
};
|
|
122
82
|
}
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
83
|
+
validateSimplePermissions(requiredPermissions, userPermissions, operator) {
|
|
84
|
+
if (operator === 'or') {
|
|
85
|
+
const hasAny = requiredPermissions.some((p)=>this.hasPermission(userPermissions, p));
|
|
86
|
+
if (!hasAny) throw new InsufficientPermissionsException(requiredPermissions, 'or');
|
|
87
|
+
} else {
|
|
88
|
+
const hasAll = requiredPermissions.every((p)=>this.hasPermission(userPermissions, p));
|
|
89
|
+
if (!hasAll) {
|
|
90
|
+
const missing = requiredPermissions.filter((p)=>!this.hasPermission(userPermissions, p));
|
|
91
|
+
throw new InsufficientPermissionsException(missing, 'and');
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
isNestedCondition(config) {
|
|
126
96
|
if (Array.isArray(config)) return false;
|
|
127
97
|
return 'children' in config && Array.isArray(config.children) && config.children.length > 0;
|
|
128
98
|
}
|
|
129
|
-
|
|
130
|
-
* Evaluate a nested permission condition recursively
|
|
131
|
-
*/ evaluateCondition(condition, userPermissions) {
|
|
99
|
+
evaluateCondition(condition, userPermissions) {
|
|
132
100
|
const { permissions = [], operator, children = [] } = condition;
|
|
133
|
-
//
|
|
101
|
+
// SECURITY: Fail-closed - deny access when no permissions configured (empty condition)
|
|
102
|
+
if (permissions.length === 0 && children.length === 0) {
|
|
103
|
+
return {
|
|
104
|
+
passed: false,
|
|
105
|
+
message: 'No permissions configured - access denied by default',
|
|
106
|
+
missingPermissions: [],
|
|
107
|
+
operator
|
|
108
|
+
};
|
|
109
|
+
}
|
|
134
110
|
const results = [];
|
|
135
111
|
const failureDetails = [];
|
|
136
112
|
const missingPermissions = [];
|
|
137
|
-
// Check permissions at this level
|
|
138
113
|
if (permissions.length > 0) {
|
|
139
114
|
if (operator === 'or') {
|
|
140
115
|
const hasAny = permissions.some((p)=>this.hasPermission(userPermissions, p));
|
|
@@ -153,7 +128,6 @@ export class PermissionGuard {
|
|
|
153
128
|
}
|
|
154
129
|
}
|
|
155
130
|
}
|
|
156
|
-
// Evaluate children recursively
|
|
157
131
|
for (const child of children){
|
|
158
132
|
const childResult = this.evaluateCondition(child, userPermissions);
|
|
159
133
|
results.push(childResult.passed);
|
|
@@ -162,14 +136,9 @@ export class PermissionGuard {
|
|
|
162
136
|
missingPermissions.push(...childResult.missingPermissions);
|
|
163
137
|
}
|
|
164
138
|
}
|
|
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()} `)}`;
|
|
139
|
+
// Evaluate based on operator - empty results already handled above
|
|
140
|
+
const passed = operator === 'or' ? results.some((r)=>r) : results.every((r)=>r);
|
|
141
|
+
const message = passed ? 'OK' : `Denied: ${failureDetails.join(` ${operator.toUpperCase()} `)}`;
|
|
173
142
|
return {
|
|
174
143
|
passed,
|
|
175
144
|
message,
|
|
@@ -177,48 +146,27 @@ export class PermissionGuard {
|
|
|
177
146
|
operator
|
|
178
147
|
};
|
|
179
148
|
}
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
let cacheKey;
|
|
149
|
+
async getUserPermissions(user) {
|
|
150
|
+
if (!this.cache) throw new PermissionSystemUnavailableException();
|
|
151
|
+
const cacheKey = this.buildPermissionCacheKey(user);
|
|
152
|
+
return await this.cache.get(cacheKey) || [];
|
|
153
|
+
}
|
|
154
|
+
buildPermissionCacheKey(user) {
|
|
187
155
|
if (this.config.enableCompanyFeature && user.companyId) {
|
|
188
|
-
|
|
189
|
-
const format = this.config.companyPermissionKeyFormat || `${PERMISSIONS_CACHE_PREFIX}:company:{companyId}:branch:{branchId}:user:{userId}`;
|
|
190
|
-
cacheKey = format.replace('{userId}', user.id).replace('{companyId}', user.companyId).replace('{branchId}', user.branchId || 'null');
|
|
191
|
-
} else {
|
|
192
|
-
// User-based permissions
|
|
193
|
-
const format = this.config.userPermissionKeyFormat || `${PERMISSIONS_CACHE_PREFIX}:user:{userId}`;
|
|
194
|
-
cacheKey = format.replace('{userId}', user.id);
|
|
156
|
+
return (this.config.companyPermissionKeyFormat || `${PERMISSIONS_CACHE_PREFIX}:company:{companyId}:branch:{branchId}:user:{userId}`).replace('{userId}', user.id).replace('{companyId}', user.companyId).replace('{branchId}', user.branchId || 'null');
|
|
195
157
|
}
|
|
196
|
-
|
|
197
|
-
return permissions || [];
|
|
158
|
+
return (this.config.userPermissionKeyFormat || `${PERMISSIONS_CACHE_PREFIX}:user:{userId}`).replace('{userId}', user.id);
|
|
198
159
|
}
|
|
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.*')
|
|
160
|
+
hasPermission(userPermissions, requiredPermission) {
|
|
161
|
+
if (userPermissions.includes(requiredPermission)) return true;
|
|
208
162
|
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
|
-
}
|
|
163
|
+
if (permission === '*') return true;
|
|
164
|
+
if (permission.endsWith('.*') && requiredPermission.startsWith(permission.slice(0, -1))) {
|
|
165
|
+
return true;
|
|
217
166
|
}
|
|
218
167
|
}
|
|
219
168
|
return false;
|
|
220
169
|
}
|
|
221
|
-
// NOTE: @Inject(Reflector) required for bundled code - external classes need explicit injection
|
|
222
170
|
constructor(reflector, cache, config, logger){
|
|
223
171
|
_define_property(this, "reflector", void 0);
|
|
224
172
|
_define_property(this, "cache", void 0);
|
|
@@ -228,12 +176,10 @@ export class PermissionGuard {
|
|
|
228
176
|
this.cache = cache;
|
|
229
177
|
this.config = {
|
|
230
178
|
enableCompanyFeature: false,
|
|
231
|
-
cacheKeyPrefix: PERMISSIONS_CACHE_PREFIX,
|
|
232
179
|
userPermissionKeyFormat: `${PERMISSIONS_CACHE_PREFIX}:user:{userId}`,
|
|
233
180
|
companyPermissionKeyFormat: `${PERMISSIONS_CACHE_PREFIX}:company:{companyId}:branch:{branchId}:user:{userId}`,
|
|
234
181
|
...config
|
|
235
182
|
};
|
|
236
|
-
// Use provided logger or fallback to NestJS Logger wrapped in adapter
|
|
237
183
|
this.logger = logger || new NestLoggerAdapter(new Logger(PermissionGuard.name));
|
|
238
184
|
}
|
|
239
185
|
}
|
|
@@ -2,7 +2,5 @@ export * from './delete-empty-id-from-body.interceptor';
|
|
|
2
2
|
export * from './idempotency.interceptor';
|
|
3
3
|
export * from './query-performance.interceptor';
|
|
4
4
|
export * from './response-meta.interceptor';
|
|
5
|
-
export * from './set-
|
|
6
|
-
export * from './set-delete-by-on-body.interceptor';
|
|
7
|
-
export * from './set-update-by-on-body.interceptor';
|
|
5
|
+
export * from './set-user-field-on-body.interceptor';
|
|
8
6
|
export * from './slug.interceptor';
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
function _ts_decorate(decorators, target, key, desc) {
|
|
2
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
3
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
4
|
+
else for(var i = decorators.length - 1; i >= 0; i--)if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
5
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
6
|
+
}
|
|
7
|
+
import { Injectable } from '@nestjs/common';
|
|
8
|
+
/**
|
|
9
|
+
* Factory function to create interceptors that set user ID fields on request body
|
|
10
|
+
* @param fieldName - The field name to set (e.g., 'createdById', 'updatedById', 'deletedById')
|
|
11
|
+
*/ export function createSetUserFieldInterceptor(fieldName) {
|
|
12
|
+
let SetUserFieldOnBody = class SetUserFieldOnBody {
|
|
13
|
+
intercept(context, next) {
|
|
14
|
+
const request = context.switchToHttp().getRequest();
|
|
15
|
+
const user = request?.user;
|
|
16
|
+
if (user) {
|
|
17
|
+
if (Array.isArray(request.body)) {
|
|
18
|
+
request.body = request.body.map((item)=>({
|
|
19
|
+
...item,
|
|
20
|
+
[fieldName]: user.id
|
|
21
|
+
}));
|
|
22
|
+
} else if (typeof request.body === 'object' && request.body !== null) {
|
|
23
|
+
request.body = {
|
|
24
|
+
...request.body,
|
|
25
|
+
[fieldName]: user.id
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
return next.handle();
|
|
30
|
+
}
|
|
31
|
+
};
|
|
32
|
+
SetUserFieldOnBody = _ts_decorate([
|
|
33
|
+
Injectable()
|
|
34
|
+
], SetUserFieldOnBody);
|
|
35
|
+
return SetUserFieldOnBody;
|
|
36
|
+
}
|
|
37
|
+
/** Sets createdById field on request body from authenticated user */ export const SetCreatedByOnBody = createSetUserFieldInterceptor('createdById');
|
|
38
|
+
/** Sets updatedById field on request body from authenticated user */ export const SetUpdateByOnBody = createSetUserFieldInterceptor('updatedById');
|
|
39
|
+
/** Sets deletedById field on request body from authenticated user */ export const SetDeletedByOnBody = createSetUserFieldInterceptor('deletedById');
|
|
@@ -17,28 +17,49 @@ function _ts_decorate(decorators, target, key, desc) {
|
|
|
17
17
|
else for(var i = decorators.length - 1; i >= 0; i--)if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
18
18
|
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
19
19
|
}
|
|
20
|
-
|
|
20
|
+
function _ts_metadata(k, v) {
|
|
21
|
+
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
|
|
22
|
+
}
|
|
23
|
+
function _ts_param(paramIndex, decorator) {
|
|
24
|
+
return function(target, key) {
|
|
25
|
+
decorator(target, key, paramIndex);
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
import { Inject, Injectable } from '@nestjs/common';
|
|
21
29
|
import { UtilsService } from '../modules/utils/utils.service';
|
|
22
30
|
export class Slug {
|
|
23
31
|
intercept(context, next) {
|
|
24
32
|
const request = context.switchToHttp().getRequest();
|
|
25
33
|
if (Array.isArray(request.body)) {
|
|
26
|
-
request.body = request.body.map((item)=>
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
34
|
+
request.body = request.body.map((item)=>{
|
|
35
|
+
// Only generate slug if not provided and name exists
|
|
36
|
+
if (!item.slug && item?.name) {
|
|
37
|
+
return {
|
|
38
|
+
...item,
|
|
39
|
+
slug: this.utilsService.transformToSlug(item.name)
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
return item;
|
|
43
|
+
});
|
|
44
|
+
} else if (!request.body?.slug && request.body?.name) {
|
|
45
|
+
// Only generate slug if not provided and name exists
|
|
31
46
|
request.body = {
|
|
32
47
|
...request.body,
|
|
33
|
-
slug:
|
|
48
|
+
slug: this.utilsService.transformToSlug(request.body.name)
|
|
34
49
|
};
|
|
35
50
|
}
|
|
36
51
|
return next.handle();
|
|
37
52
|
}
|
|
38
|
-
constructor(){
|
|
39
|
-
_define_property(this, "utilsService",
|
|
53
|
+
constructor(utilsService){
|
|
54
|
+
_define_property(this, "utilsService", void 0);
|
|
55
|
+
this.utilsService = utilsService;
|
|
40
56
|
}
|
|
41
57
|
}
|
|
42
58
|
Slug = _ts_decorate([
|
|
43
|
-
Injectable()
|
|
59
|
+
Injectable(),
|
|
60
|
+
_ts_param(0, Inject(UtilsService)),
|
|
61
|
+
_ts_metadata("design:type", Function),
|
|
62
|
+
_ts_metadata("design:paramtypes", [
|
|
63
|
+
typeof UtilsService === "undefined" ? Object : UtilsService
|
|
64
|
+
])
|
|
44
65
|
], Slug);
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Interface for DataSource providers used by RequestScopedApiService.
|
|
3
|
+
* Implement this interface in each module's DataSourceProvider to ensure type safety.
|
|
4
|
+
*
|
|
5
|
+
* @example
|
|
6
|
+
* ```typescript
|
|
7
|
+
* @Injectable()
|
|
8
|
+
* export class IAMDataSourceProvider implements IDataSourceProvider {
|
|
9
|
+
* constructor(private readonly dataSource: DataSource) {}
|
|
10
|
+
*
|
|
11
|
+
* async getRepository<T extends ObjectLiteral>(entity: EntityTarget<T>): Promise<Repository<T>> {
|
|
12
|
+
* return this.dataSource.getRepository(entity);
|
|
13
|
+
* }
|
|
14
|
+
*
|
|
15
|
+
* async getDataSource(): Promise<DataSource> {
|
|
16
|
+
* return this.dataSource;
|
|
17
|
+
* }
|
|
18
|
+
* }
|
|
19
|
+
* ```
|
|
20
|
+
*/ export { };
|
package/fesm/interfaces/index.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
export * from './api.interface';
|
|
2
|
-
export * from './
|
|
2
|
+
export * from './datasource.interface';
|
|
3
3
|
export * from './identity.interface';
|
|
4
4
|
export * from './logged-user-info.interface';
|
|
5
5
|
export * from './logger.interface';
|
|
6
|
+
export * from './module-config.interface';
|
|
6
7
|
export * from './permission.interface';
|
|
@@ -43,14 +43,6 @@ export const getRequestId = ()=>requestContext.getStore()?.requestId;
|
|
|
43
43
|
export const getTenantId = ()=>requestContext.getStore()?.tenantId;
|
|
44
44
|
export const getUserId = ()=>requestContext.getStore()?.userId;
|
|
45
45
|
export const getCompanyId = ()=>requestContext.getStore()?.companyId;
|
|
46
|
-
export const setUserId = (userId)=>{
|
|
47
|
-
const store = requestContext.getStore();
|
|
48
|
-
if (store) store.userId = userId;
|
|
49
|
-
};
|
|
50
|
-
export const setCompanyId = (companyId)=>{
|
|
51
|
-
const store = requestContext.getStore();
|
|
52
|
-
if (store) store.companyId = companyId;
|
|
53
|
-
};
|
|
54
46
|
// Helper Functions
|
|
55
47
|
function sanitizeHeaders(headers) {
|
|
56
48
|
const sanitized = {};
|
|
@@ -72,84 +64,62 @@ export class LoggerMiddleware {
|
|
|
72
64
|
const requestId = req.headers[REQUEST_ID_HEADER] || uuidv4();
|
|
73
65
|
const tenantId = req.headers[TENANT_ID_HEADER];
|
|
74
66
|
const startTime = Date.now();
|
|
75
|
-
// Set response header
|
|
76
67
|
res.setHeader(REQUEST_ID_HEADER, requestId);
|
|
77
|
-
// Create context
|
|
78
68
|
const context = {
|
|
79
69
|
requestId,
|
|
80
70
|
tenantId,
|
|
81
71
|
startTime
|
|
82
72
|
};
|
|
83
|
-
// Run in AsyncLocalStorage context
|
|
84
73
|
requestContext.run(context, ()=>{
|
|
85
|
-
// Check if we should skip logging
|
|
86
74
|
const shouldSkipLogging = EXCLUDED_PATHS.some((path)=>req.originalUrl.startsWith(path));
|
|
87
75
|
if (!shouldSkipLogging) {
|
|
88
76
|
this.logRequest(req, requestId, tenantId);
|
|
89
|
-
|
|
90
|
-
let responseLogged = false;
|
|
91
|
-
// Capture response using multiple hooks to ensure we catch it
|
|
92
|
-
const originalSend = res.send;
|
|
93
|
-
const originalJson = res.json;
|
|
94
|
-
const originalEnd = res.end;
|
|
95
|
-
// Store reference to this for use in callbacks
|
|
96
|
-
const self = this;
|
|
97
|
-
// Override res.send
|
|
98
|
-
res.send = function(body) {
|
|
99
|
-
if (!responseLogged) {
|
|
100
|
-
responseLogged = true;
|
|
101
|
-
self.logResponse(req, res, startTime, body, requestId, tenantId);
|
|
102
|
-
}
|
|
103
|
-
return originalSend.call(this, body);
|
|
104
|
-
};
|
|
105
|
-
// Override res.json
|
|
106
|
-
res.json = function(body) {
|
|
107
|
-
if (!responseLogged) {
|
|
108
|
-
responseLogged = true;
|
|
109
|
-
self.logResponse(req, res, startTime, body, requestId, tenantId);
|
|
110
|
-
}
|
|
111
|
-
return originalJson.call(this, body);
|
|
112
|
-
};
|
|
113
|
-
// Override res.end as fallback
|
|
114
|
-
res.end = function(...args) {
|
|
115
|
-
if (!responseLogged) {
|
|
116
|
-
responseLogged = true;
|
|
117
|
-
self.logResponse(req, res, startTime, args[0], requestId, tenantId);
|
|
118
|
-
}
|
|
119
|
-
return originalEnd.apply(this, args);
|
|
120
|
-
};
|
|
121
|
-
// Handle errors
|
|
122
|
-
res.on('error', (error)=>{
|
|
123
|
-
this.logger.error('Response error', {
|
|
124
|
-
context: 'HTTP',
|
|
125
|
-
requestId,
|
|
126
|
-
tenantId,
|
|
127
|
-
method: req.method,
|
|
128
|
-
url: req.originalUrl,
|
|
129
|
-
path: req.path,
|
|
130
|
-
error: error.message,
|
|
131
|
-
stack: error.stack
|
|
132
|
-
});
|
|
133
|
-
});
|
|
77
|
+
this.setupResponseLogging(req, res, startTime, requestId, tenantId);
|
|
134
78
|
}
|
|
135
79
|
next();
|
|
136
80
|
});
|
|
137
81
|
}
|
|
82
|
+
setupResponseLogging(req, res, startTime, requestId, tenantId) {
|
|
83
|
+
let responseLogged = false;
|
|
84
|
+
const originalSend = res.send;
|
|
85
|
+
const originalJson = res.json;
|
|
86
|
+
const originalEnd = res.end;
|
|
87
|
+
const self = this;
|
|
88
|
+
const logOnce = (body)=>{
|
|
89
|
+
if (!responseLogged) {
|
|
90
|
+
responseLogged = true;
|
|
91
|
+
self.logResponse(req, res, startTime, body, requestId, tenantId);
|
|
92
|
+
}
|
|
93
|
+
};
|
|
94
|
+
res.send = function(body) {
|
|
95
|
+
logOnce(body);
|
|
96
|
+
return originalSend.call(this, body);
|
|
97
|
+
};
|
|
98
|
+
res.json = function(body) {
|
|
99
|
+
logOnce(body);
|
|
100
|
+
return originalJson.call(this, body);
|
|
101
|
+
};
|
|
102
|
+
res.end = function(...args) {
|
|
103
|
+
logOnce(args[0]);
|
|
104
|
+
return originalEnd.apply(this, args);
|
|
105
|
+
};
|
|
106
|
+
res.on('error', (error)=>{
|
|
107
|
+
this.logger.error('Response error', {
|
|
108
|
+
...this.buildBaseLogData(req, requestId, tenantId),
|
|
109
|
+
error: error.message,
|
|
110
|
+
stack: error.stack
|
|
111
|
+
});
|
|
112
|
+
});
|
|
113
|
+
}
|
|
138
114
|
logRequest(req, requestId, tenantId) {
|
|
139
115
|
const logData = {
|
|
140
|
-
|
|
141
|
-
requestId,
|
|
142
|
-
tenantId,
|
|
143
|
-
method: req.method,
|
|
144
|
-
url: req.originalUrl,
|
|
145
|
-
path: req.path,
|
|
116
|
+
...this.buildBaseLogData(req, requestId, tenantId),
|
|
146
117
|
query: Object.keys(req.query).length > 0 ? req.query : undefined,
|
|
147
118
|
ip: getClientIp(req),
|
|
148
119
|
userAgent: req.headers['user-agent'],
|
|
149
120
|
contentType: req.headers['content-type'],
|
|
150
121
|
contentLength: req.headers['content-length']
|
|
151
122
|
};
|
|
152
|
-
// Add debug details if enabled
|
|
153
123
|
if (IS_DEBUG) {
|
|
154
124
|
logData.headers = sanitizeHeaders(req.headers);
|
|
155
125
|
logData.body = truncateBody(req.body);
|
|
@@ -161,13 +131,12 @@ export class LoggerMiddleware {
|
|
|
161
131
|
const duration = Date.now() - startTime;
|
|
162
132
|
const statusCode = res.statusCode;
|
|
163
133
|
const level = statusCode >= 500 ? 'error' : statusCode >= 400 ? 'warn' : 'info';
|
|
134
|
+
const userId = getUserId();
|
|
135
|
+
const companyId = getCompanyId();
|
|
164
136
|
const logData = {
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
method: req.method,
|
|
169
|
-
url: req.originalUrl,
|
|
170
|
-
path: req.path,
|
|
137
|
+
...this.buildBaseLogData(req, requestId, tenantId),
|
|
138
|
+
userId,
|
|
139
|
+
companyId,
|
|
171
140
|
statusCode,
|
|
172
141
|
statusMessage: res.statusMessage,
|
|
173
142
|
duration: `${duration}ms`,
|
|
@@ -175,33 +144,31 @@ export class LoggerMiddleware {
|
|
|
175
144
|
contentType: res.getHeader('content-type'),
|
|
176
145
|
contentLength: res.getHeader('content-length')
|
|
177
146
|
};
|
|
178
|
-
// Add user context if available
|
|
179
|
-
const userId = getUserId();
|
|
180
|
-
const companyId = getCompanyId();
|
|
181
|
-
if (userId) logData.userId = userId;
|
|
182
|
-
if (companyId) logData.companyId = companyId;
|
|
183
|
-
// Add response body for errors or debug mode
|
|
184
147
|
if (statusCode >= 400 || IS_DEBUG) {
|
|
185
148
|
logData.responseBody = truncateBody(body);
|
|
186
149
|
}
|
|
187
150
|
this.logger.log(level, `Response [${statusCode}]`, logData);
|
|
188
|
-
// Log slow requests separately
|
|
189
151
|
if (duration > 3000 && statusCode < 400) {
|
|
190
152
|
this.logger.warn('Slow request detected', {
|
|
191
|
-
|
|
192
|
-
requestId,
|
|
193
|
-
tenantId,
|
|
153
|
+
...this.buildBaseLogData(req, requestId, tenantId),
|
|
194
154
|
userId,
|
|
195
155
|
companyId,
|
|
196
|
-
method: req.method,
|
|
197
|
-
url: req.originalUrl,
|
|
198
|
-
path: req.path,
|
|
199
156
|
duration: `${duration}ms`,
|
|
200
157
|
durationMs: duration,
|
|
201
158
|
threshold: '3000ms'
|
|
202
159
|
});
|
|
203
160
|
}
|
|
204
161
|
}
|
|
162
|
+
buildBaseLogData(req, requestId, tenantId) {
|
|
163
|
+
return {
|
|
164
|
+
context: 'HTTP',
|
|
165
|
+
requestId,
|
|
166
|
+
tenantId,
|
|
167
|
+
method: req.method,
|
|
168
|
+
url: req.originalUrl,
|
|
169
|
+
path: req.path
|
|
170
|
+
};
|
|
171
|
+
}
|
|
205
172
|
constructor(){
|
|
206
173
|
_define_property(this, "logger", winstonLogger);
|
|
207
174
|
}
|
|
@@ -4,8 +4,8 @@ function _ts_decorate(decorators, target, key, desc) {
|
|
|
4
4
|
else for(var i = decorators.length - 1; i >= 0; i--)if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
5
5
|
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
6
6
|
}
|
|
7
|
-
import { HybridCache } from '
|
|
8
|
-
import { CACHE_INSTANCE } from '
|
|
7
|
+
import { HybridCache } from '../../classes/hybrid-cache.class';
|
|
8
|
+
import { CACHE_INSTANCE } from '../../constants';
|
|
9
9
|
import { Module } from '@nestjs/common';
|
|
10
10
|
export class CacheModule {
|
|
11
11
|
static forRoot(isGlobal = true, memoryTtl = 60_000, memorySize = 5000) {
|
|
@@ -77,24 +77,12 @@ export class DataSourceModule {
|
|
|
77
77
|
provide: options.useClass,
|
|
78
78
|
useClass: options.useClass
|
|
79
79
|
},
|
|
80
|
-
|
|
81
|
-
provide: MODULE_OPTIONS,
|
|
82
|
-
useFactory: async (factory)=>factory.createOptions(),
|
|
83
|
-
inject: [
|
|
84
|
-
options.useClass
|
|
85
|
-
]
|
|
86
|
-
}
|
|
80
|
+
this.createFactoryProvider(options.useClass)
|
|
87
81
|
];
|
|
88
82
|
}
|
|
89
83
|
if (options.useExisting) {
|
|
90
84
|
return [
|
|
91
|
-
|
|
92
|
-
provide: MODULE_OPTIONS,
|
|
93
|
-
useFactory: async (factory)=>factory.createOptions(),
|
|
94
|
-
inject: [
|
|
95
|
-
options.useExisting
|
|
96
|
-
]
|
|
97
|
-
}
|
|
85
|
+
this.createFactoryProvider(options.useExisting)
|
|
98
86
|
];
|
|
99
87
|
}
|
|
100
88
|
return [
|
|
@@ -104,6 +92,15 @@ export class DataSourceModule {
|
|
|
104
92
|
}
|
|
105
93
|
];
|
|
106
94
|
}
|
|
95
|
+
static createFactoryProvider(factoryClass) {
|
|
96
|
+
return {
|
|
97
|
+
provide: MODULE_OPTIONS,
|
|
98
|
+
useFactory: async (factory)=>factory.createOptions(),
|
|
99
|
+
inject: [
|
|
100
|
+
factoryClass
|
|
101
|
+
]
|
|
102
|
+
};
|
|
103
|
+
}
|
|
107
104
|
}
|
|
108
105
|
DataSourceModule = _ts_decorate([
|
|
109
106
|
Module({})
|