@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
|
@@ -9,247 +9,84 @@ Object.defineProperty(exports, "UtilsService", {
|
|
|
9
9
|
}
|
|
10
10
|
});
|
|
11
11
|
const _common = require("@nestjs/common");
|
|
12
|
-
|
|
12
|
+
function _define_property(obj, key, value) {
|
|
13
|
+
if (key in obj) {
|
|
14
|
+
Object.defineProperty(obj, key, {
|
|
15
|
+
value: value,
|
|
16
|
+
enumerable: true,
|
|
17
|
+
configurable: true,
|
|
18
|
+
writable: true
|
|
19
|
+
});
|
|
20
|
+
} else {
|
|
21
|
+
obj[key] = value;
|
|
22
|
+
}
|
|
23
|
+
return obj;
|
|
24
|
+
}
|
|
13
25
|
function _ts_decorate(decorators, target, key, desc) {
|
|
14
26
|
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
15
27
|
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
16
28
|
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;
|
|
17
29
|
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
18
30
|
}
|
|
19
|
-
function _ts_metadata(k, v) {
|
|
20
|
-
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
|
|
21
|
-
}
|
|
22
31
|
let UtilsService = class UtilsService {
|
|
23
32
|
// ---------------- CACHE HELPERS ----------------
|
|
24
33
|
/**
|
|
25
34
|
* Generate cache key with optional tenant prefix for multi-tenant isolation
|
|
26
|
-
* @param entityName - Name of the entity being cached
|
|
27
|
-
* @param params - Query parameters for cache key uniqueness
|
|
28
|
-
* @param entityId - Optional entity ID for single entity caching
|
|
29
|
-
* @param tenantId - Optional tenant ID for multi-tenant isolation
|
|
30
35
|
*/ getCacheKey(entityName, params, entityId, tenantId) {
|
|
31
|
-
const
|
|
32
|
-
if (entityId)
|
|
33
|
-
|
|
36
|
+
const prefix = this.buildTenantPrefix(tenantId);
|
|
37
|
+
if (entityId) {
|
|
38
|
+
return `${prefix}entity_${entityName}_id_${entityId}${params ? '_select_' + JSON.stringify(params) : ''}`;
|
|
39
|
+
}
|
|
40
|
+
return `${prefix}entity_${entityName}_all_${JSON.stringify(params)}`;
|
|
34
41
|
}
|
|
35
42
|
/**
|
|
36
43
|
* Track cache key for later invalidation with optional tenant prefix
|
|
37
44
|
*/ async trackCacheKey(cacheKey, entityName, cacheManager, entityId, tenantId) {
|
|
38
|
-
const tenantPrefix = tenantId ? `tenant_${tenantId}_` : '';
|
|
39
45
|
try {
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
await cacheManager.set(trackingKey, idKeys);
|
|
45
|
-
} else {
|
|
46
|
-
const trackingKey = `${tenantPrefix}entity_${entityName}_keys`;
|
|
47
|
-
const allKeys = await cacheManager.get(trackingKey) || [];
|
|
48
|
-
if (!allKeys.includes(cacheKey)) allKeys.push(cacheKey);
|
|
49
|
-
await cacheManager.set(trackingKey, allKeys);
|
|
50
|
-
}
|
|
46
|
+
const trackingKey = this.buildTrackingKey(entityName, entityId, tenantId);
|
|
47
|
+
const keys = await cacheManager.get(trackingKey) || [];
|
|
48
|
+
if (!keys.includes(cacheKey)) keys.push(cacheKey);
|
|
49
|
+
await cacheManager.set(trackingKey, keys);
|
|
51
50
|
} catch (error) {
|
|
52
|
-
|
|
53
|
-
console.error(`Cache tracking failed for ${entityName}:`, error);
|
|
51
|
+
this.logger.error(`Cache tracking failed for ${entityName}`, error instanceof Error ? error.stack : String(error));
|
|
54
52
|
}
|
|
55
53
|
}
|
|
56
54
|
/**
|
|
57
55
|
* Clear cache for entity with optional tenant prefix
|
|
58
|
-
* Uses Promise.allSettled to ensure all deletions are attempted even if some fail
|
|
59
56
|
*/ async clearCache(entityName, cacheManager, entityId, tenantId) {
|
|
60
|
-
const tenantPrefix = tenantId ? `tenant_${tenantId}_` : '';
|
|
61
57
|
try {
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
await Promise.allSettled(idKeys.map((key)=>cacheManager.del(key)));
|
|
67
|
-
await cacheManager.del(trackingKey);
|
|
68
|
-
} else {
|
|
69
|
-
const trackingKey = `${tenantPrefix}entity_${entityName}_keys`;
|
|
70
|
-
const keySet = await cacheManager.get(trackingKey) || [];
|
|
71
|
-
// Use Promise.allSettled to ensure all deletions are attempted
|
|
72
|
-
await Promise.allSettled(keySet.map((key)=>cacheManager.del(key)));
|
|
73
|
-
await cacheManager.del(trackingKey);
|
|
74
|
-
}
|
|
58
|
+
const trackingKey = this.buildTrackingKey(entityName, entityId, tenantId);
|
|
59
|
+
const keys = await cacheManager.get(trackingKey) || [];
|
|
60
|
+
await Promise.allSettled(keys.map((key)=>cacheManager.del(key)));
|
|
61
|
+
await cacheManager.del(trackingKey);
|
|
75
62
|
} catch (error) {
|
|
76
|
-
|
|
77
|
-
console.error(`Cache invalidation failed for ${entityName}:`, error);
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
/**
|
|
81
|
-
* Check Phone or Email
|
|
82
|
-
*/ checkPhoneOrEmail(value) {
|
|
83
|
-
if ((0, _classvalidator.isEmail)(value)) {
|
|
84
|
-
return {
|
|
85
|
-
value: value,
|
|
86
|
-
type: 'email'
|
|
87
|
-
};
|
|
88
|
-
}
|
|
89
|
-
const phoneMatch = value.match(/^((\+880)|0)?(13|15|16|17|18|19)\d{8}$/);
|
|
90
|
-
if (phoneMatch) {
|
|
91
|
-
let phone = phoneMatch[0];
|
|
92
|
-
if (!phone.startsWith('+88')) {
|
|
93
|
-
phone = '+88' + phone;
|
|
94
|
-
}
|
|
95
|
-
return {
|
|
96
|
-
value: phone,
|
|
97
|
-
type: 'phone'
|
|
98
|
-
};
|
|
63
|
+
this.logger.error(`Cache invalidation failed for ${entityName}`, error instanceof Error ? error.stack : String(error));
|
|
99
64
|
}
|
|
100
|
-
return {
|
|
101
|
-
value: null,
|
|
102
|
-
type: null
|
|
103
|
-
};
|
|
104
65
|
}
|
|
66
|
+
// ---------------- STRING HELPERS ----------------
|
|
105
67
|
/**
|
|
106
|
-
*
|
|
107
|
-
* transformToSlug
|
|
68
|
+
* Transform string to URL-friendly slug
|
|
108
69
|
*/ transformToSlug(value, salt) {
|
|
109
70
|
const slug = value?.trim().replace(/[^A-Z0-9]+/gi, '-').toLowerCase();
|
|
110
71
|
return salt ? `${slug}-${this.getRandomInt(1, 100)}` : slug;
|
|
111
72
|
}
|
|
112
73
|
/**
|
|
113
|
-
*
|
|
114
|
-
* getRandomInt
|
|
115
|
-
* generateRandomId
|
|
116
|
-
* getRandomOtpCode
|
|
74
|
+
* Generate random integer between min and max (inclusive)
|
|
117
75
|
*/ getRandomInt(min, max) {
|
|
118
76
|
return Math.floor(Math.random() * (max - min + 1)) + min;
|
|
119
77
|
}
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
for(let i = 0; i < length; i++){
|
|
124
|
-
result += characters.charAt(Math.floor(Math.random() * characters.length));
|
|
125
|
-
}
|
|
126
|
-
return result;
|
|
127
|
-
}
|
|
128
|
-
getRandomOtpCode() {
|
|
129
|
-
return Math.floor(Math.random() * (9999 - 1000 + 1)) + 1000;
|
|
78
|
+
// ---------------- PRIVATE HELPERS ----------------
|
|
79
|
+
buildTenantPrefix(tenantId) {
|
|
80
|
+
return tenantId ? `tenant_${tenantId}_` : '';
|
|
130
81
|
}
|
|
131
|
-
|
|
132
|
-
const
|
|
133
|
-
return {
|
|
134
|
-
columnName: match ? match[1] : 'unknown',
|
|
135
|
-
value: match ? match[2] : 'unknown'
|
|
136
|
-
};
|
|
137
|
-
}
|
|
138
|
-
getOtpEmailFormat(otp, userName) {
|
|
139
|
-
return `
|
|
140
|
-
<!DOCTYPE html>
|
|
141
|
-
<html lang="en">
|
|
142
|
-
<head>
|
|
143
|
-
<meta charset="UTF-8" />
|
|
144
|
-
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
|
|
145
|
-
<title>Your OTP Code</title>
|
|
146
|
-
<style>
|
|
147
|
-
body {
|
|
148
|
-
font-family: Arial, sans-serif;
|
|
149
|
-
background-color: #f4f4f7;
|
|
150
|
-
margin: 0;
|
|
151
|
-
padding: 0;
|
|
152
|
-
}
|
|
153
|
-
.email-container {
|
|
154
|
-
max-width: 500px;
|
|
155
|
-
margin: 40px auto;
|
|
156
|
-
background-color: #ffffff;
|
|
157
|
-
padding: 30px;
|
|
158
|
-
border-radius: 8px;
|
|
159
|
-
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.1);
|
|
160
|
-
text-align: center;
|
|
161
|
-
}
|
|
162
|
-
h1 {
|
|
163
|
-
color: #333;
|
|
164
|
-
}
|
|
165
|
-
p {
|
|
166
|
-
font-size: 16px;
|
|
167
|
-
color: #555;
|
|
168
|
-
}
|
|
169
|
-
.otp-box {
|
|
170
|
-
display: inline-block;
|
|
171
|
-
margin: 20px 0;
|
|
172
|
-
padding: 14px 28px;
|
|
173
|
-
font-size: 24px;
|
|
174
|
-
letter-spacing: 6px;
|
|
175
|
-
background-color: #007BFF;
|
|
176
|
-
color: white;
|
|
177
|
-
border-radius: 8px;
|
|
178
|
-
font-weight: bold;
|
|
179
|
-
}
|
|
180
|
-
</style>
|
|
181
|
-
</head>
|
|
182
|
-
<body>
|
|
183
|
-
<div class="email-container">
|
|
184
|
-
<p>Hi ${userName || 'Sir/Madam'},</p>
|
|
185
|
-
<p>Use the code below to verify your identity:</p>
|
|
186
|
-
<div class="otp-box">${otp}</div>
|
|
187
|
-
<p>This OTP is valid for a limited time. Do not share it with anyone.</p>
|
|
188
|
-
<p>If you didn’t request this, please ignore this email.</p>
|
|
189
|
-
</div>
|
|
190
|
-
</body>
|
|
191
|
-
</html>
|
|
192
|
-
`;
|
|
82
|
+
buildTrackingKey(entityName, entityId, tenantId) {
|
|
83
|
+
const prefix = this.buildTenantPrefix(tenantId);
|
|
84
|
+
return entityId ? `${prefix}entity_${entityName}_id_${entityId}_keys` : `${prefix}entity_${entityName}_keys`;
|
|
193
85
|
}
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
<!DOCTYPE html>
|
|
197
|
-
<html lang="en">
|
|
198
|
-
<head>
|
|
199
|
-
<meta charset="UTF-8" />
|
|
200
|
-
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
|
|
201
|
-
<title>Reset Your Password</title>
|
|
202
|
-
<style>
|
|
203
|
-
body {
|
|
204
|
-
font-family: Arial, sans-serif;
|
|
205
|
-
background-color: #f4f4f7;
|
|
206
|
-
margin: 0;
|
|
207
|
-
padding: 0;
|
|
208
|
-
}
|
|
209
|
-
.email-container {
|
|
210
|
-
max-width: 500px;
|
|
211
|
-
margin: 40px auto;
|
|
212
|
-
background-color: #ffffff;
|
|
213
|
-
padding: 30px;
|
|
214
|
-
border-radius: 8px;
|
|
215
|
-
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.1);
|
|
216
|
-
text-align: center;
|
|
217
|
-
}
|
|
218
|
-
h1 {
|
|
219
|
-
color: #333;
|
|
220
|
-
}
|
|
221
|
-
p {
|
|
222
|
-
font-size: 16px;
|
|
223
|
-
color: #555;
|
|
224
|
-
}
|
|
225
|
-
.reset-button {
|
|
226
|
-
display: inline-block;
|
|
227
|
-
margin: 20px 0;
|
|
228
|
-
padding: 14px 28px;
|
|
229
|
-
font-size: 16px;
|
|
230
|
-
background-color: #007BFF;
|
|
231
|
-
color: #ffffff !important;
|
|
232
|
-
text-decoration: none;
|
|
233
|
-
border-radius: 6px;
|
|
234
|
-
font-weight: bold;
|
|
235
|
-
}
|
|
236
|
-
</style>
|
|
237
|
-
</head>
|
|
238
|
-
<body>
|
|
239
|
-
<div class="email-container">
|
|
240
|
-
<p>Hi ${userName || 'Sir/Madam'},</p>
|
|
241
|
-
<p>We received a request to reset your password. Click the button below to reset it:</p>
|
|
242
|
-
<a href="${resetLink}" class="reset-button" target="_blank">Reset Password</a>
|
|
243
|
-
<p>If you didn’t request a password reset, you can safely ignore this email.</p>
|
|
244
|
-
</div>
|
|
245
|
-
</body>
|
|
246
|
-
</html>
|
|
247
|
-
`;
|
|
86
|
+
constructor(){
|
|
87
|
+
_define_property(this, "logger", new _common.Logger(UtilsService.name));
|
|
248
88
|
}
|
|
249
|
-
constructor(){}
|
|
250
89
|
};
|
|
251
90
|
UtilsService = _ts_decorate([
|
|
252
|
-
(0, _common.Injectable)()
|
|
253
|
-
_ts_metadata("design:type", Function),
|
|
254
|
-
_ts_metadata("design:paramtypes", [])
|
|
91
|
+
(0, _common.Injectable)()
|
|
255
92
|
], UtilsService);
|
|
@@ -8,9 +8,17 @@ Object.defineProperty(exports, "ErrorHandler", {
|
|
|
8
8
|
return ErrorHandler;
|
|
9
9
|
}
|
|
10
10
|
});
|
|
11
|
+
/** Sensitive keys that should be redacted from logs */ const SENSITIVE_KEYS = [
|
|
12
|
+
'password',
|
|
13
|
+
'secret',
|
|
14
|
+
'token',
|
|
15
|
+
'apiKey',
|
|
16
|
+
'credential',
|
|
17
|
+
'authorization'
|
|
18
|
+
];
|
|
11
19
|
let ErrorHandler = class ErrorHandler {
|
|
12
20
|
/**
|
|
13
|
-
* Safely extract error message from unknown error
|
|
21
|
+
* Safely extract error message from unknown error.
|
|
14
22
|
*/ static getErrorMessage(error) {
|
|
15
23
|
if (error instanceof Error) {
|
|
16
24
|
return error.message;
|
|
@@ -21,15 +29,25 @@ let ErrorHandler = class ErrorHandler {
|
|
|
21
29
|
return 'Unknown error occurred';
|
|
22
30
|
}
|
|
23
31
|
/**
|
|
24
|
-
*
|
|
25
|
-
*/ static
|
|
26
|
-
|
|
27
|
-
|
|
32
|
+
* Sanitize context data to redact sensitive fields from logs.
|
|
33
|
+
*/ static sanitizeContextForLogging(context) {
|
|
34
|
+
const sanitized = {};
|
|
35
|
+
for (const [key, value] of Object.entries(context)){
|
|
36
|
+
const isSensitive = SENSITIVE_KEYS.some((sk)=>key.toLowerCase().includes(sk.toLowerCase()));
|
|
37
|
+
if (isSensitive) {
|
|
38
|
+
sanitized[key] = '[REDACTED]';
|
|
39
|
+
} else if (Array.isArray(value)) {
|
|
40
|
+
sanitized[key] = value.map((item)=>typeof item === 'object' && item !== null ? this.sanitizeContextForLogging(item) : item);
|
|
41
|
+
} else if (typeof value === 'object' && value !== null) {
|
|
42
|
+
sanitized[key] = this.sanitizeContextForLogging(value);
|
|
43
|
+
} else {
|
|
44
|
+
sanitized[key] = value;
|
|
45
|
+
}
|
|
28
46
|
}
|
|
29
|
-
return
|
|
47
|
+
return sanitized;
|
|
30
48
|
}
|
|
31
49
|
/**
|
|
32
|
-
* Create error context object for logging
|
|
50
|
+
* Create error context object for internal logging.
|
|
33
51
|
*/ static createErrorContext(error, context) {
|
|
34
52
|
const errorContext = {
|
|
35
53
|
error: {
|
|
@@ -41,27 +59,32 @@ let ErrorHandler = class ErrorHandler {
|
|
|
41
59
|
errorContext.error.name = error.name;
|
|
42
60
|
}
|
|
43
61
|
if (context && Object.keys(context).length > 0) {
|
|
44
|
-
errorContext.context = context;
|
|
62
|
+
errorContext.context = this.sanitizeContextForLogging(context);
|
|
45
63
|
}
|
|
46
64
|
return errorContext;
|
|
47
65
|
}
|
|
48
66
|
/**
|
|
49
|
-
* Log error with consistent format
|
|
67
|
+
* Log error with consistent format.
|
|
50
68
|
*/ static logError(logger, error, operation, context) {
|
|
51
69
|
const errorContext = this.createErrorContext(error, {
|
|
52
70
|
operation,
|
|
53
71
|
...context
|
|
54
72
|
});
|
|
55
73
|
const errorMessage = `Failed to ${operation}: ${errorContext.error.message}`;
|
|
56
|
-
|
|
57
|
-
logger.error(errorMessage, errorContext.error.stack, loggerContext, errorContext);
|
|
74
|
+
logger.error(errorMessage, errorContext.error.stack, errorContext);
|
|
58
75
|
}
|
|
59
76
|
/**
|
|
60
|
-
* Re-throw error with proper type checking
|
|
77
|
+
* Re-throw error with proper type checking.
|
|
61
78
|
*/ static rethrowError(error) {
|
|
62
79
|
if (error instanceof Error) {
|
|
63
80
|
throw error;
|
|
64
81
|
}
|
|
65
82
|
throw new Error(`Unexpected error: ${String(error)}`);
|
|
66
83
|
}
|
|
84
|
+
/**
|
|
85
|
+
* Log error and re-throw (common pattern).
|
|
86
|
+
*/ static logAndRethrow(logger, error, operation, context) {
|
|
87
|
+
this.logError(logger, error, operation, context);
|
|
88
|
+
this.rethrowError(error);
|
|
89
|
+
}
|
|
67
90
|
};
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HTML Sanitizer Utilities
|
|
3
|
+
*
|
|
4
|
+
* Provides functions for escaping HTML content to prevent XSS attacks.
|
|
5
|
+
* Use these utilities when interpolating user-provided variables into HTML content.
|
|
6
|
+
*/ /**
|
|
7
|
+
* HTML entity mapping for escaping special characters
|
|
8
|
+
*/ "use strict";
|
|
9
|
+
Object.defineProperty(exports, "__esModule", {
|
|
10
|
+
value: true
|
|
11
|
+
});
|
|
12
|
+
function _export(target, all) {
|
|
13
|
+
for(var name in all)Object.defineProperty(target, name, {
|
|
14
|
+
enumerable: true,
|
|
15
|
+
get: Object.getOwnPropertyDescriptor(all, name).get
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
_export(exports, {
|
|
19
|
+
get escapeHtml () {
|
|
20
|
+
return escapeHtml;
|
|
21
|
+
},
|
|
22
|
+
get escapeHtmlVariables () {
|
|
23
|
+
return escapeHtmlVariables;
|
|
24
|
+
}
|
|
25
|
+
});
|
|
26
|
+
const HTML_ESCAPE_MAP = {
|
|
27
|
+
'&': '&',
|
|
28
|
+
'<': '<',
|
|
29
|
+
'>': '>',
|
|
30
|
+
'"': '"',
|
|
31
|
+
"'": ''',
|
|
32
|
+
'/': '/',
|
|
33
|
+
'`': '`',
|
|
34
|
+
'=': '='
|
|
35
|
+
};
|
|
36
|
+
/**
|
|
37
|
+
* Regex pattern matching characters that need HTML escaping
|
|
38
|
+
*/ const HTML_ESCAPE_REGEX = /[&<>"'`=/]/g;
|
|
39
|
+
function escapeHtml(str) {
|
|
40
|
+
if (!str || typeof str !== 'string') {
|
|
41
|
+
return str ?? '';
|
|
42
|
+
}
|
|
43
|
+
return str.replace(HTML_ESCAPE_REGEX, (char)=>HTML_ESCAPE_MAP[char]);
|
|
44
|
+
}
|
|
45
|
+
function escapeHtmlVariables(variables) {
|
|
46
|
+
if (!variables || typeof variables !== 'object') {
|
|
47
|
+
return {};
|
|
48
|
+
}
|
|
49
|
+
const escaped = {};
|
|
50
|
+
for (const [key, value] of Object.entries(variables)){
|
|
51
|
+
if (value === null || value === undefined) {
|
|
52
|
+
escaped[key] = '';
|
|
53
|
+
} else if (typeof value === 'string') {
|
|
54
|
+
escaped[key] = escapeHtml(value);
|
|
55
|
+
} else if (typeof value === 'object') {
|
|
56
|
+
// For objects/arrays, stringify and escape
|
|
57
|
+
escaped[key] = escapeHtml(JSON.stringify(value));
|
|
58
|
+
} else {
|
|
59
|
+
// For numbers, booleans, etc., convert to string (no escaping needed)
|
|
60
|
+
escaped[key] = String(value);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
return escaped;
|
|
64
|
+
}
|
package/cjs/utils/index.js
CHANGED
|
@@ -3,6 +3,10 @@ Object.defineProperty(exports, "__esModule", {
|
|
|
3
3
|
value: true
|
|
4
4
|
});
|
|
5
5
|
_export_star(require("./error-handler.util"), exports);
|
|
6
|
+
_export_star(require("./html-sanitizer.util"), exports);
|
|
7
|
+
_export_star(require("./query-helpers.util"), exports);
|
|
8
|
+
_export_star(require("./request.util"), exports);
|
|
9
|
+
_export_star(require("./string.util"), exports);
|
|
6
10
|
function _export_star(from, to) {
|
|
7
11
|
Object.keys(from).forEach(function(k) {
|
|
8
12
|
if (k !== "default" && !Object.prototype.hasOwnProperty.call(to, k)) {
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", {
|
|
3
|
+
value: true
|
|
4
|
+
});
|
|
5
|
+
function _export(target, all) {
|
|
6
|
+
for(var name in all)Object.defineProperty(target, name, {
|
|
7
|
+
enumerable: true,
|
|
8
|
+
get: Object.getOwnPropertyDescriptor(all, name).get
|
|
9
|
+
});
|
|
10
|
+
}
|
|
11
|
+
_export(exports, {
|
|
12
|
+
get applyCompanyFilter () {
|
|
13
|
+
return applyCompanyFilter;
|
|
14
|
+
},
|
|
15
|
+
get buildCompanyWhereCondition () {
|
|
16
|
+
return buildCompanyWhereCondition;
|
|
17
|
+
},
|
|
18
|
+
get hasCompanyId () {
|
|
19
|
+
return hasCompanyId;
|
|
20
|
+
},
|
|
21
|
+
get validateCompanyOwnership () {
|
|
22
|
+
return validateCompanyOwnership;
|
|
23
|
+
}
|
|
24
|
+
});
|
|
25
|
+
const _common = require("@nestjs/common");
|
|
26
|
+
function applyCompanyFilter(query, config, user) {
|
|
27
|
+
const columnName = config.columnName ?? 'companyId';
|
|
28
|
+
if (config.isCompanyFeatureEnabled && user?.companyId) {
|
|
29
|
+
query.andWhere(`${config.entityAlias}.${columnName} = :companyId`, {
|
|
30
|
+
companyId: user.companyId
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
return query;
|
|
34
|
+
}
|
|
35
|
+
function buildCompanyWhereCondition(baseWhere, isCompanyFeatureEnabled, user) {
|
|
36
|
+
if (isCompanyFeatureEnabled && user?.companyId) {
|
|
37
|
+
return {
|
|
38
|
+
...baseWhere,
|
|
39
|
+
companyId: user.companyId
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
return baseWhere;
|
|
43
|
+
}
|
|
44
|
+
function hasCompanyId(entity) {
|
|
45
|
+
return entity !== null && typeof entity === 'object' && 'companyId' in entity;
|
|
46
|
+
}
|
|
47
|
+
function validateCompanyOwnership(entity, user, isCompanyFeatureEnabled, entityName) {
|
|
48
|
+
if (isCompanyFeatureEnabled && user?.companyId && hasCompanyId(entity)) {
|
|
49
|
+
if (entity.companyId && entity.companyId !== user.companyId) {
|
|
50
|
+
throw new _common.BadRequestException(`${entityName} belongs to another company`);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", {
|
|
3
|
+
value: true
|
|
4
|
+
});
|
|
5
|
+
function _export(target, all) {
|
|
6
|
+
for(var name in all)Object.defineProperty(target, name, {
|
|
7
|
+
enumerable: true,
|
|
8
|
+
get: Object.getOwnPropertyDescriptor(all, name).get
|
|
9
|
+
});
|
|
10
|
+
}
|
|
11
|
+
_export(exports, {
|
|
12
|
+
get buildCookieOptions () {
|
|
13
|
+
return buildCookieOptions;
|
|
14
|
+
},
|
|
15
|
+
get isBrowserRequest () {
|
|
16
|
+
return isBrowserRequest;
|
|
17
|
+
},
|
|
18
|
+
get parseDurationToMs () {
|
|
19
|
+
return parseDurationToMs;
|
|
20
|
+
}
|
|
21
|
+
});
|
|
22
|
+
const _constants = require("../constants");
|
|
23
|
+
/** Time unit multipliers in milliseconds */ const TIME_UNIT_MS = {
|
|
24
|
+
s: 1000,
|
|
25
|
+
m: 60 * 1000,
|
|
26
|
+
h: 60 * 60 * 1000,
|
|
27
|
+
d: 24 * 60 * 60 * 1000,
|
|
28
|
+
w: 7 * 24 * 60 * 60 * 1000
|
|
29
|
+
};
|
|
30
|
+
/** Get normalized client type from request header */ function getClientType(req) {
|
|
31
|
+
const clientType = req.headers[_constants.CLIENT_TYPE_HEADER];
|
|
32
|
+
return clientType ? clientType.toLowerCase() : null;
|
|
33
|
+
}
|
|
34
|
+
function isBrowserRequest(req) {
|
|
35
|
+
const clientType = getClientType(req);
|
|
36
|
+
if (clientType) {
|
|
37
|
+
return clientType === 'browser' || clientType === 'web';
|
|
38
|
+
}
|
|
39
|
+
const accept = req.headers['accept'] || '';
|
|
40
|
+
if (accept.includes('text/html')) return true;
|
|
41
|
+
const userAgent = req.headers['user-agent'] || '';
|
|
42
|
+
const browserPatterns = /mozilla|chrome|safari|firefox|edge|opera|msie/i;
|
|
43
|
+
return browserPatterns.test(userAgent) && !userAgent.includes('Postman');
|
|
44
|
+
}
|
|
45
|
+
function buildCookieOptions(req) {
|
|
46
|
+
const hostname = req.hostname || '';
|
|
47
|
+
const origin = req.headers.origin || '';
|
|
48
|
+
const isProduction = process.env.NODE_ENV === 'production';
|
|
49
|
+
const forwardedProto = req.headers['x-forwarded-proto'];
|
|
50
|
+
const isHttps = isProduction || forwardedProto === 'https' || origin.startsWith('https://') || req.secure;
|
|
51
|
+
let domain;
|
|
52
|
+
const domainParts = hostname.split('.');
|
|
53
|
+
if (domainParts.length > 2 && !hostname.includes('localhost')) {
|
|
54
|
+
domain = '.' + domainParts.slice(-2).join('.');
|
|
55
|
+
}
|
|
56
|
+
return {
|
|
57
|
+
secure: isHttps,
|
|
58
|
+
sameSite: isHttps ? 'strict' : 'lax',
|
|
59
|
+
...domain && {
|
|
60
|
+
domain
|
|
61
|
+
}
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
function parseDurationToMs(duration, defaultMs = TIME_UNIT_MS.w) {
|
|
65
|
+
const match = duration.match(/^(\d+)(s|m|h|d|w)$/);
|
|
66
|
+
if (!match) return defaultMs;
|
|
67
|
+
const value = parseInt(match[1], 10);
|
|
68
|
+
const unit = match[2];
|
|
69
|
+
return value * (TIME_UNIT_MS[unit] ?? TIME_UNIT_MS.d);
|
|
70
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* String utility functions
|
|
3
|
+
*/ /**
|
|
4
|
+
* Generate URL-friendly slug from a string
|
|
5
|
+
* @param text - The text to convert to slug
|
|
6
|
+
* @param maxLength - Maximum length of the slug (default: 100)
|
|
7
|
+
* @returns URL-friendly slug
|
|
8
|
+
*
|
|
9
|
+
* @example
|
|
10
|
+
* generateSlug('My Company Name') // 'my-company-name'
|
|
11
|
+
* generateSlug('Hello World!') // 'hello-world'
|
|
12
|
+
*/ "use strict";
|
|
13
|
+
Object.defineProperty(exports, "__esModule", {
|
|
14
|
+
value: true
|
|
15
|
+
});
|
|
16
|
+
function _export(target, all) {
|
|
17
|
+
for(var name in all)Object.defineProperty(target, name, {
|
|
18
|
+
enumerable: true,
|
|
19
|
+
get: Object.getOwnPropertyDescriptor(all, name).get
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
_export(exports, {
|
|
23
|
+
get generateSlug () {
|
|
24
|
+
return generateSlug;
|
|
25
|
+
},
|
|
26
|
+
get generateUniqueSlug () {
|
|
27
|
+
return generateUniqueSlug;
|
|
28
|
+
}
|
|
29
|
+
});
|
|
30
|
+
function generateSlug(text, maxLength = 100) {
|
|
31
|
+
return text.toLowerCase().trim().replace(/[^a-z0-9\s-]/g, '') // Remove special characters
|
|
32
|
+
.replace(/\s+/g, '-') // Replace spaces with hyphens
|
|
33
|
+
.replace(/-+/g, '-') // Replace multiple hyphens with single
|
|
34
|
+
.replace(/^-+|-+$/g, '') // Remove leading/trailing hyphens
|
|
35
|
+
.substring(0, maxLength);
|
|
36
|
+
}
|
|
37
|
+
async function generateUniqueSlug(text, findMatchingSlugs, maxLength = 100) {
|
|
38
|
+
const baseSlug = generateSlug(text, maxLength);
|
|
39
|
+
// Single query to get all matching slugs
|
|
40
|
+
const existingSlugs = await findMatchingSlugs(baseSlug);
|
|
41
|
+
// No collisions - base slug is available
|
|
42
|
+
if (existingSlugs.length === 0) {
|
|
43
|
+
return baseSlug;
|
|
44
|
+
}
|
|
45
|
+
// Build set for O(1) lookup
|
|
46
|
+
const existingSet = new Set(existingSlugs);
|
|
47
|
+
// Base slug not taken (only suffixed versions exist)
|
|
48
|
+
if (!existingSet.has(baseSlug)) {
|
|
49
|
+
return baseSlug;
|
|
50
|
+
}
|
|
51
|
+
// Find next available suffix in memory (fast)
|
|
52
|
+
let counter = 1;
|
|
53
|
+
while(counter < 10000){
|
|
54
|
+
const slugWithSuffix = `${baseSlug}-${counter}`;
|
|
55
|
+
if (!existingSet.has(slugWithSuffix)) {
|
|
56
|
+
return slugWithSuffix;
|
|
57
|
+
}
|
|
58
|
+
counter++;
|
|
59
|
+
}
|
|
60
|
+
// Fallback: append random string (extremely rare edge case)
|
|
61
|
+
const randomSuffix = Math.random().toString(36).substring(2, 8);
|
|
62
|
+
return `${baseSlug}-${randomSuffix}`;
|
|
63
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { BulkResponseDto, DeleteDto, FilterAndPaginationDto, GetByIdBodyDto, ListResponseDto, MessageResponseDto, SingleResponseDto } from '
|
|
2
|
-
import { Identity } from '
|
|
3
|
-
import { ILoggedUserInfo, IService, PermissionCondition, PermissionOperator } from '
|
|
1
|
+
import { BulkResponseDto, DeleteDto, FilterAndPaginationDto, GetByIdBodyDto, ListResponseDto, MessageResponseDto, SingleResponseDto } from '../dtos';
|
|
2
|
+
import { Identity } from '../entities';
|
|
3
|
+
import { ILoggedUserInfo, IService, PermissionCondition, PermissionOperator } from '../interfaces';
|
|
4
4
|
import { Type } from '@nestjs/common';
|
|
5
5
|
export type ApiEndpoint = 'insert' | 'insertMany' | 'getById' | 'getAll' | 'update' | 'updateMany' | 'delete';
|
|
6
6
|
export type SecurityLevel = 'public' | 'jwt' | 'permission';
|
|
@@ -16,9 +16,9 @@ export type ApiSecurityConfig = {
|
|
|
16
16
|
export interface ApiControllerOptions {
|
|
17
17
|
security?: ApiSecurityConfig | EndpointSecurity | SecurityLevel;
|
|
18
18
|
}
|
|
19
|
-
export declare function createApiController<CreateDtoT extends
|
|
19
|
+
export declare function createApiController<CreateDtoT extends object, UpdateDtoT extends {
|
|
20
20
|
id: string;
|
|
21
|
-
}, ResponseDtoT extends
|
|
21
|
+
}, ResponseDtoT extends object, InterfaceT extends Identity, ServiceT extends IService<CreateDtoT, UpdateDtoT, InterfaceT>>(createDtoClass: Type<CreateDtoT>, updateDtoClass: Type<UpdateDtoT>, responseDtoClass: Type<ResponseDtoT>, options?: ApiControllerOptions): abstract new (service: ServiceT) => {
|
|
22
22
|
service: ServiceT;
|
|
23
23
|
insert(addDto: CreateDtoT, user: ILoggedUserInfo | null): Promise<SingleResponseDto<ResponseDtoT>>;
|
|
24
24
|
insertMany(addDto: CreateDtoT[], user: ILoggedUserInfo | null): Promise<BulkResponseDto<ResponseDtoT>>;
|