@flusys/nestjs-shared 1.0.0-rc → 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 +493 -658
- package/cjs/classes/api-service.class.js +59 -92
- package/cjs/classes/winston-logger-adapter.class.js +23 -40
- package/cjs/constants/permissions.js +11 -1
- package/cjs/dtos/delete.dto.js +10 -0
- package/cjs/dtos/response-payload.dto.js +0 -75
- package/cjs/guards/permission.guard.js +19 -18
- package/cjs/interceptors/index.js +0 -3
- package/cjs/interceptors/set-user-field-on-body.interceptor.js +20 -3
- package/cjs/middlewares/logger.middleware.js +50 -89
- package/cjs/modules/datasource/datasource.module.js +11 -14
- package/cjs/modules/datasource/multi-tenant-datasource.service.js +0 -4
- package/cjs/modules/utils/utils.service.js +22 -103
- package/cjs/utils/error-handler.util.js +12 -67
- package/cjs/utils/html-sanitizer.util.js +1 -11
- package/cjs/utils/index.js +2 -0
- package/cjs/utils/request.util.js +70 -0
- package/cjs/utils/string.util.js +63 -0
- package/classes/api-service.class.d.ts +2 -0
- package/classes/winston-logger-adapter.class.d.ts +2 -0
- package/constants/permissions.d.ts +12 -0
- package/dtos/delete.dto.d.ts +1 -0
- package/dtos/response-payload.dto.d.ts +0 -13
- package/fesm/classes/api-service.class.js +59 -92
- package/fesm/classes/winston-logger-adapter.class.js +23 -40
- package/fesm/constants/permissions.js +8 -1
- package/fesm/dtos/delete.dto.js +12 -2
- package/fesm/dtos/response-payload.dto.js +0 -69
- package/fesm/guards/permission.guard.js +19 -18
- package/fesm/interceptors/index.js +0 -3
- package/fesm/interceptors/set-user-field-on-body.interceptor.js +3 -0
- package/fesm/middlewares/logger.middleware.js +50 -83
- package/fesm/modules/datasource/datasource.module.js +11 -14
- package/fesm/modules/datasource/multi-tenant-datasource.service.js +0 -4
- package/fesm/modules/utils/utils.service.js +19 -89
- package/fesm/utils/error-handler.util.js +12 -68
- package/fesm/utils/html-sanitizer.util.js +1 -14
- package/fesm/utils/index.js +2 -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 +0 -3
- package/interceptors/set-user-field-on-body.interceptor.d.ts +3 -0
- package/interfaces/logged-user-info.interface.d.ts +0 -2
- 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 +2 -18
- package/package.json +2 -2
- package/utils/error-handler.util.d.ts +3 -18
- package/utils/html-sanitizer.util.d.ts +0 -1
- package/utils/index.d.ts +2 -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 -12
- package/cjs/interceptors/set-delete-by-on-body.interceptor.js +0 -12
- package/cjs/interceptors/set-update-by-on-body.interceptor.js +0 -12
- package/fesm/interceptors/set-create-by-on-body.interceptor.js +0 -4
- package/fesm/interceptors/set-delete-by-on-body.interceptor.js +0 -4
- package/fesm/interceptors/set-update-by-on-body.interceptor.js +0 -4
- package/interceptors/set-create-by-on-body.interceptor.d.ts +0 -1
- package/interceptors/set-delete-by-on-body.interceptor.d.ts +0 -1
- package/interceptors/set-update-by-on-body.interceptor.d.ts +0 -1
|
@@ -66,17 +66,7 @@ export class PermissionGuard {
|
|
|
66
66
|
throw new InsufficientPermissionsException(result.missingPermissions, result.operator);
|
|
67
67
|
}
|
|
68
68
|
} else {
|
|
69
|
-
|
|
70
|
-
if (operator === 'or') {
|
|
71
|
-
hasRequired = requiredPermissions.some((p)=>this.hasPermission(userPermissions, p));
|
|
72
|
-
if (!hasRequired) throw new InsufficientPermissionsException(requiredPermissions, 'or');
|
|
73
|
-
} else {
|
|
74
|
-
hasRequired = requiredPermissions.every((p)=>this.hasPermission(userPermissions, p));
|
|
75
|
-
if (!hasRequired) {
|
|
76
|
-
const missing = requiredPermissions.filter((p)=>!this.hasPermission(userPermissions, p));
|
|
77
|
-
throw new InsufficientPermissionsException(missing, 'and');
|
|
78
|
-
}
|
|
79
|
-
}
|
|
69
|
+
this.validateSimplePermissions(requiredPermissions, userPermissions, operator);
|
|
80
70
|
}
|
|
81
71
|
return true;
|
|
82
72
|
}
|
|
@@ -90,6 +80,18 @@ export class PermissionGuard {
|
|
|
90
80
|
operator: config.operator || 'and'
|
|
91
81
|
};
|
|
92
82
|
}
|
|
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
|
+
}
|
|
93
95
|
isNestedCondition(config) {
|
|
94
96
|
if (Array.isArray(config)) return false;
|
|
95
97
|
return 'children' in config && Array.isArray(config.children) && config.children.length > 0;
|
|
@@ -146,15 +148,14 @@ export class PermissionGuard {
|
|
|
146
148
|
}
|
|
147
149
|
async getUserPermissions(user) {
|
|
148
150
|
if (!this.cache) throw new PermissionSystemUnavailableException();
|
|
149
|
-
|
|
151
|
+
const cacheKey = this.buildPermissionCacheKey(user);
|
|
152
|
+
return await this.cache.get(cacheKey) || [];
|
|
153
|
+
}
|
|
154
|
+
buildPermissionCacheKey(user) {
|
|
150
155
|
if (this.config.enableCompanyFeature && user.companyId) {
|
|
151
|
-
|
|
152
|
-
cacheKey = format.replace('{userId}', user.id).replace('{companyId}', user.companyId).replace('{branchId}', user.branchId || 'null');
|
|
153
|
-
} else {
|
|
154
|
-
const format = this.config.userPermissionKeyFormat || `${PERMISSIONS_CACHE_PREFIX}:user:{userId}`;
|
|
155
|
-
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');
|
|
156
157
|
}
|
|
157
|
-
return
|
|
158
|
+
return (this.config.userPermissionKeyFormat || `${PERMISSIONS_CACHE_PREFIX}:user:{userId}`).replace('{userId}', user.id);
|
|
158
159
|
}
|
|
159
160
|
hasPermission(userPermissions, requiredPermission) {
|
|
160
161
|
if (userPermissions.includes(requiredPermission)) return true;
|
|
@@ -3,7 +3,4 @@ export * from './idempotency.interceptor';
|
|
|
3
3
|
export * from './query-performance.interceptor';
|
|
4
4
|
export * from './response-meta.interceptor';
|
|
5
5
|
export * from './set-user-field-on-body.interceptor';
|
|
6
|
-
export * from './set-create-by-on-body.interceptor';
|
|
7
|
-
export * from './set-delete-by-on-body.interceptor';
|
|
8
|
-
export * from './set-update-by-on-body.interceptor';
|
|
9
6
|
export * from './slug.interceptor';
|
|
@@ -34,3 +34,6 @@ import { Injectable } from '@nestjs/common';
|
|
|
34
34
|
], SetUserFieldOnBody);
|
|
35
35
|
return SetUserFieldOnBody;
|
|
36
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');
|
|
@@ -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
|
}
|
|
@@ -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({})
|
|
@@ -90,10 +90,6 @@ export class MultiTenantDataSourceService {
|
|
|
90
90
|
const dataSource = await this.getDataSource();
|
|
91
91
|
return dataSource.getRepository(entity);
|
|
92
92
|
}
|
|
93
|
-
async getRepositoryForTenant(entity, tenantId) {
|
|
94
|
-
const dataSource = await this.getDataSourceForTenant(tenantId);
|
|
95
|
-
return dataSource.getRepository(entity);
|
|
96
|
-
}
|
|
97
93
|
async withTenant(tenantId, callback) {
|
|
98
94
|
const dataSource = await this.getDataSourceForTenant(tenantId);
|
|
99
95
|
return callback(dataSource);
|
|
@@ -18,39 +18,25 @@ function _ts_decorate(decorators, target, key, desc) {
|
|
|
18
18
|
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
19
19
|
}
|
|
20
20
|
import { Injectable, Logger } from '@nestjs/common';
|
|
21
|
-
import { isEmail } from 'class-validator';
|
|
22
|
-
/** Default phone regex for Bangladesh (+880) */ export const DEFAULT_PHONE_REGEX = /^((\+880)|0)?(13|15|16|17|18|19)\d{8}$/;
|
|
23
|
-
/** Default phone country code */ export const DEFAULT_PHONE_COUNTRY_CODE = '+88';
|
|
24
21
|
export class UtilsService {
|
|
25
|
-
/**
|
|
26
|
-
* Configure phone validation regex and country code
|
|
27
|
-
*/ setPhoneValidationConfig(config) {
|
|
28
|
-
this.phoneConfig = config;
|
|
29
|
-
}
|
|
30
22
|
// ---------------- CACHE HELPERS ----------------
|
|
31
23
|
/**
|
|
32
24
|
* Generate cache key with optional tenant prefix for multi-tenant isolation
|
|
33
25
|
*/ getCacheKey(entityName, params, entityId, tenantId) {
|
|
34
|
-
const
|
|
35
|
-
if (entityId)
|
|
36
|
-
|
|
26
|
+
const prefix = this.buildTenantPrefix(tenantId);
|
|
27
|
+
if (entityId) {
|
|
28
|
+
return `${prefix}entity_${entityName}_id_${entityId}${params ? '_select_' + JSON.stringify(params) : ''}`;
|
|
29
|
+
}
|
|
30
|
+
return `${prefix}entity_${entityName}_all_${JSON.stringify(params)}`;
|
|
37
31
|
}
|
|
38
32
|
/**
|
|
39
33
|
* Track cache key for later invalidation with optional tenant prefix
|
|
40
34
|
*/ async trackCacheKey(cacheKey, entityName, cacheManager, entityId, tenantId) {
|
|
41
|
-
const tenantPrefix = tenantId ? `tenant_${tenantId}_` : '';
|
|
42
35
|
try {
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
await cacheManager.set(trackingKey, idKeys);
|
|
48
|
-
} else {
|
|
49
|
-
const trackingKey = `${tenantPrefix}entity_${entityName}_keys`;
|
|
50
|
-
const allKeys = await cacheManager.get(trackingKey) || [];
|
|
51
|
-
if (!allKeys.includes(cacheKey)) allKeys.push(cacheKey);
|
|
52
|
-
await cacheManager.set(trackingKey, allKeys);
|
|
53
|
-
}
|
|
36
|
+
const trackingKey = this.buildTrackingKey(entityName, entityId, tenantId);
|
|
37
|
+
const keys = await cacheManager.get(trackingKey) || [];
|
|
38
|
+
if (!keys.includes(cacheKey)) keys.push(cacheKey);
|
|
39
|
+
await cacheManager.set(trackingKey, keys);
|
|
54
40
|
} catch (error) {
|
|
55
41
|
this.logger.error(`Cache tracking failed for ${entityName}`, error instanceof Error ? error.stack : String(error));
|
|
56
42
|
}
|
|
@@ -58,49 +44,15 @@ export class UtilsService {
|
|
|
58
44
|
/**
|
|
59
45
|
* Clear cache for entity with optional tenant prefix
|
|
60
46
|
*/ async clearCache(entityName, cacheManager, entityId, tenantId) {
|
|
61
|
-
const tenantPrefix = tenantId ? `tenant_${tenantId}_` : '';
|
|
62
47
|
try {
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
await cacheManager.del(trackingKey);
|
|
68
|
-
} else {
|
|
69
|
-
const trackingKey = `${tenantPrefix}entity_${entityName}_keys`;
|
|
70
|
-
const keySet = await cacheManager.get(trackingKey) || [];
|
|
71
|
-
await Promise.allSettled(keySet.map((key)=>cacheManager.del(key)));
|
|
72
|
-
await cacheManager.del(trackingKey);
|
|
73
|
-
}
|
|
48
|
+
const trackingKey = this.buildTrackingKey(entityName, entityId, tenantId);
|
|
49
|
+
const keys = await cacheManager.get(trackingKey) || [];
|
|
50
|
+
await Promise.allSettled(keys.map((key)=>cacheManager.del(key)));
|
|
51
|
+
await cacheManager.del(trackingKey);
|
|
74
52
|
} catch (error) {
|
|
75
53
|
this.logger.error(`Cache invalidation failed for ${entityName}`, error instanceof Error ? error.stack : String(error));
|
|
76
54
|
}
|
|
77
55
|
}
|
|
78
|
-
// ---------------- VALIDATION HELPERS ----------------
|
|
79
|
-
/**
|
|
80
|
-
* Check if value is phone or email and normalize phone number
|
|
81
|
-
*/ checkPhoneOrEmail(value) {
|
|
82
|
-
if (isEmail(value)) {
|
|
83
|
-
return {
|
|
84
|
-
value,
|
|
85
|
-
type: 'email'
|
|
86
|
-
};
|
|
87
|
-
}
|
|
88
|
-
const phoneMatch = value.match(this.phoneConfig.regex);
|
|
89
|
-
if (phoneMatch) {
|
|
90
|
-
let phone = phoneMatch[0];
|
|
91
|
-
if (!phone.startsWith(this.phoneConfig.countryCode)) {
|
|
92
|
-
phone = this.phoneConfig.countryCode + phone;
|
|
93
|
-
}
|
|
94
|
-
return {
|
|
95
|
-
value: phone,
|
|
96
|
-
type: 'phone'
|
|
97
|
-
};
|
|
98
|
-
}
|
|
99
|
-
return {
|
|
100
|
-
value: null,
|
|
101
|
-
type: null
|
|
102
|
-
};
|
|
103
|
-
}
|
|
104
56
|
// ---------------- STRING HELPERS ----------------
|
|
105
57
|
/**
|
|
106
58
|
* Transform string to URL-friendly slug
|
|
@@ -108,43 +60,21 @@ export class UtilsService {
|
|
|
108
60
|
const slug = value?.trim().replace(/[^A-Z0-9]+/gi, '-').toLowerCase();
|
|
109
61
|
return salt ? `${slug}-${this.getRandomInt(1, 100)}` : slug;
|
|
110
62
|
}
|
|
111
|
-
// ---------------- RANDOM HELPERS ----------------
|
|
112
63
|
/**
|
|
113
64
|
* Generate random integer between min and max (inclusive)
|
|
114
65
|
*/ getRandomInt(min, max) {
|
|
115
66
|
return Math.floor(Math.random() * (max - min + 1)) + min;
|
|
116
67
|
}
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
const characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
|
|
121
|
-
let result = '';
|
|
122
|
-
for(let i = 0; i < length; i++){
|
|
123
|
-
result += characters.charAt(Math.floor(Math.random() * characters.length));
|
|
124
|
-
}
|
|
125
|
-
return result;
|
|
126
|
-
}
|
|
127
|
-
/**
|
|
128
|
-
* Generate 4-digit OTP code
|
|
129
|
-
*/ getRandomOtpCode() {
|
|
130
|
-
return Math.floor(Math.random() * (9999 - 1000 + 1)) + 1000;
|
|
68
|
+
// ---------------- PRIVATE HELPERS ----------------
|
|
69
|
+
buildTenantPrefix(tenantId) {
|
|
70
|
+
return tenantId ? `tenant_${tenantId}_` : '';
|
|
131
71
|
}
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
*/ extractColumnNameFromError(detail) {
|
|
136
|
-
const match = detail.match(/\((.*?)\)=\((.*?)\)/);
|
|
137
|
-
return {
|
|
138
|
-
columnName: match ? match[1] : 'unknown',
|
|
139
|
-
value: match ? match[2] : 'unknown'
|
|
140
|
-
};
|
|
72
|
+
buildTrackingKey(entityName, entityId, tenantId) {
|
|
73
|
+
const prefix = this.buildTenantPrefix(tenantId);
|
|
74
|
+
return entityId ? `${prefix}entity_${entityName}_id_${entityId}_keys` : `${prefix}entity_${entityName}_keys`;
|
|
141
75
|
}
|
|
142
76
|
constructor(){
|
|
143
77
|
_define_property(this, "logger", new Logger(UtilsService.name));
|
|
144
|
-
_define_property(this, "phoneConfig", {
|
|
145
|
-
regex: DEFAULT_PHONE_REGEX,
|
|
146
|
-
countryCode: DEFAULT_PHONE_COUNTRY_CODE
|
|
147
|
-
});
|
|
148
78
|
}
|
|
149
79
|
}
|
|
150
80
|
UtilsService = _ts_decorate([
|
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
import { HttpStatus } from '@nestjs/common';
|
|
2
|
-
/** Check if running in production environment */ const IS_PRODUCTION = process.env.NODE_ENV === 'production';
|
|
3
1
|
/** Sensitive keys that should be redacted from logs */ const SENSITIVE_KEYS = [
|
|
4
2
|
'password',
|
|
5
3
|
'secret',
|
|
@@ -8,81 +6,25 @@ import { HttpStatus } from '@nestjs/common';
|
|
|
8
6
|
'credential',
|
|
9
7
|
'authorization'
|
|
10
8
|
];
|
|
11
|
-
/** Patterns that indicate sensitive data in error messages */ const SENSITIVE_PATTERNS = [
|
|
12
|
-
/password/i,
|
|
13
|
-
/secret/i,
|
|
14
|
-
/token/i,
|
|
15
|
-
/key/i,
|
|
16
|
-
/credential/i,
|
|
17
|
-
/authorization/i,
|
|
18
|
-
/bearer/i
|
|
19
|
-
];
|
|
20
9
|
/**
|
|
21
10
|
* Error handling utility for consistent error logging and handling.
|
|
22
|
-
* Provides production-aware error sanitization to prevent sensitive data leakage.
|
|
23
11
|
*/ export class ErrorHandler {
|
|
24
12
|
/**
|
|
25
13
|
* Safely extract error message from unknown error.
|
|
26
|
-
|
|
27
|
-
* @param sanitizeForClient - If true, redacts potentially sensitive info in production
|
|
28
|
-
*/ static getErrorMessage(error, sanitizeForClient = false) {
|
|
29
|
-
let message = 'Unknown error occurred';
|
|
30
|
-
if (error instanceof Error) {
|
|
31
|
-
message = error.message;
|
|
32
|
-
} else if (typeof error === 'string') {
|
|
33
|
-
message = error;
|
|
34
|
-
}
|
|
35
|
-
// Sanitize for client responses in production
|
|
36
|
-
if (sanitizeForClient && IS_PRODUCTION) {
|
|
37
|
-
if (this.containsSensitiveData(message)) {
|
|
38
|
-
return 'An unexpected error occurred. Please try again later.';
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
return message;
|
|
42
|
-
}
|
|
43
|
-
/**
|
|
44
|
-
* Check if a string contains potentially sensitive data.
|
|
45
|
-
*/ static containsSensitiveData(text) {
|
|
46
|
-
return SENSITIVE_PATTERNS.some((pattern)=>pattern.test(text));
|
|
47
|
-
}
|
|
48
|
-
/**
|
|
49
|
-
* Safely extract error stack from unknown error.
|
|
50
|
-
* Returns undefined in production for client responses.
|
|
51
|
-
* @param error - The error to extract stack from
|
|
52
|
-
* @param forClient - If true, never returns stack in production
|
|
53
|
-
*/ static getErrorStack(error, forClient = false) {
|
|
54
|
-
// Never expose stack traces to clients in production
|
|
55
|
-
if (forClient && IS_PRODUCTION) {
|
|
56
|
-
return undefined;
|
|
57
|
-
}
|
|
14
|
+
*/ static getErrorMessage(error) {
|
|
58
15
|
if (error instanceof Error) {
|
|
59
|
-
return error.
|
|
60
|
-
}
|
|
61
|
-
return undefined;
|
|
62
|
-
}
|
|
63
|
-
/**
|
|
64
|
-
* Create a sanitized error response for clients.
|
|
65
|
-
* In production, sensitive data is redacted and stack traces removed.
|
|
66
|
-
*/ static createClientError(error, statusCode = HttpStatus.INTERNAL_SERVER_ERROR, code) {
|
|
67
|
-
const sanitizedError = {
|
|
68
|
-
message: this.getErrorMessage(error, true),
|
|
69
|
-
statusCode
|
|
70
|
-
};
|
|
71
|
-
if (code) {
|
|
72
|
-
sanitizedError.code = code;
|
|
16
|
+
return error.message;
|
|
73
17
|
}
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
sanitizedError.stack = this.getErrorStack(error);
|
|
18
|
+
if (typeof error === 'string') {
|
|
19
|
+
return error;
|
|
77
20
|
}
|
|
78
|
-
return
|
|
21
|
+
return 'Unknown error occurred';
|
|
79
22
|
}
|
|
80
23
|
/**
|
|
81
24
|
* Sanitize context data to redact sensitive fields from logs.
|
|
82
25
|
*/ static sanitizeContextForLogging(context) {
|
|
83
26
|
const sanitized = {};
|
|
84
27
|
for (const [key, value] of Object.entries(context)){
|
|
85
|
-
// Check if key contains sensitive words
|
|
86
28
|
const isSensitive = SENSITIVE_KEYS.some((sk)=>key.toLowerCase().includes(sk.toLowerCase()));
|
|
87
29
|
if (isSensitive) {
|
|
88
30
|
sanitized[key] = '[REDACTED]';
|
|
@@ -98,7 +40,6 @@ import { HttpStatus } from '@nestjs/common';
|
|
|
98
40
|
}
|
|
99
41
|
/**
|
|
100
42
|
* Create error context object for internal logging.
|
|
101
|
-
* Context data is sanitized to redact sensitive fields.
|
|
102
43
|
*/ static createErrorContext(error, context) {
|
|
103
44
|
const errorContext = {
|
|
104
45
|
error: {
|
|
@@ -110,29 +51,32 @@ import { HttpStatus } from '@nestjs/common';
|
|
|
110
51
|
errorContext.error.name = error.name;
|
|
111
52
|
}
|
|
112
53
|
if (context && Object.keys(context).length > 0) {
|
|
113
|
-
// Sanitize context to redact sensitive fields
|
|
114
54
|
errorContext.context = this.sanitizeContextForLogging(context);
|
|
115
55
|
}
|
|
116
56
|
return errorContext;
|
|
117
57
|
}
|
|
118
58
|
/**
|
|
119
59
|
* Log error with consistent format.
|
|
120
|
-
* Sensitive data in context is automatically redacted.
|
|
121
60
|
*/ static logError(logger, error, operation, context) {
|
|
122
61
|
const errorContext = this.createErrorContext(error, {
|
|
123
62
|
operation,
|
|
124
63
|
...context
|
|
125
64
|
});
|
|
126
65
|
const errorMessage = `Failed to ${operation}: ${errorContext.error.message}`;
|
|
127
|
-
// Log full details internally (stack traces are fine for internal logs)
|
|
128
66
|
logger.error(errorMessage, errorContext.error.stack, errorContext);
|
|
129
67
|
}
|
|
130
68
|
/**
|
|
131
|
-
* Re-throw error with proper type checking
|
|
69
|
+
* Re-throw error with proper type checking.
|
|
132
70
|
*/ static rethrowError(error) {
|
|
133
71
|
if (error instanceof Error) {
|
|
134
72
|
throw error;
|
|
135
73
|
}
|
|
136
74
|
throw new Error(`Unexpected error: ${String(error)}`);
|
|
137
75
|
}
|
|
76
|
+
/**
|
|
77
|
+
* Log error and re-throw (common pattern).
|
|
78
|
+
*/ static logAndRethrow(logger, error, operation, context) {
|
|
79
|
+
this.logError(logger, error, operation, context);
|
|
80
|
+
this.rethrowError(error);
|
|
81
|
+
}
|
|
138
82
|
}
|
|
@@ -33,7 +33,7 @@
|
|
|
33
33
|
if (!str || typeof str !== 'string') {
|
|
34
34
|
return str ?? '';
|
|
35
35
|
}
|
|
36
|
-
return str.replace(HTML_ESCAPE_REGEX, (char)=>HTML_ESCAPE_MAP[char]
|
|
36
|
+
return str.replace(HTML_ESCAPE_REGEX, (char)=>HTML_ESCAPE_MAP[char]);
|
|
37
37
|
}
|
|
38
38
|
/**
|
|
39
39
|
* Escapes all string values in a variables object for safe HTML interpolation.
|
|
@@ -67,16 +67,3 @@
|
|
|
67
67
|
}
|
|
68
68
|
return escaped;
|
|
69
69
|
}
|
|
70
|
-
/**
|
|
71
|
-
* Checks if a string contains potential HTML/script injection.
|
|
72
|
-
* Useful for logging or validation purposes.
|
|
73
|
-
*
|
|
74
|
-
* @param str - The string to check
|
|
75
|
-
* @returns True if the string contains HTML-like content
|
|
76
|
-
*/ export function containsHtmlContent(str) {
|
|
77
|
-
if (!str || typeof str !== 'string') {
|
|
78
|
-
return false;
|
|
79
|
-
}
|
|
80
|
-
// Check for common HTML patterns
|
|
81
|
-
return /<[a-z][\s\S]*>/i.test(str) || /javascript:/i.test(str) || /on\w+=/i.test(str);
|
|
82
|
-
}
|