@flusys/nestjs-shared 0.1.0-beta.3 → 1.0.0-rc
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +106 -138
- package/cjs/classes/api-controller.class.js +9 -45
- package/cjs/classes/api-service.class.js +4 -41
- package/cjs/classes/index.js +1 -0
- package/cjs/classes/request-scoped-api.service.js +4 -53
- package/cjs/classes/winston.logger.class.js +5 -15
- package/cjs/constants/index.js +16 -11
- package/cjs/constants/permissions.js +174 -0
- package/cjs/decorators/api-response.decorator.js +1 -1
- package/cjs/decorators/index.js +1 -0
- package/cjs/decorators/sanitize-html.decorator.js +36 -0
- package/cjs/dtos/filter-and-pagination.dto.js +24 -34
- package/cjs/dtos/pagination.dto.js +4 -8
- package/cjs/dtos/response-payload.dto.js +35 -121
- package/cjs/entities/identity.js +4 -4
- package/cjs/entities/user-root.js +13 -14
- package/cjs/guards/permission.guard.js +39 -94
- package/cjs/interceptors/index.js +1 -0
- package/cjs/interceptors/set-create-by-on-body.interceptor.js +2 -30
- package/cjs/interceptors/set-delete-by-on-body.interceptor.js +2 -30
- package/cjs/interceptors/set-update-by-on-body.interceptor.js +2 -30
- package/cjs/interceptors/set-user-field-on-body.interceptor.js +43 -0
- package/cjs/interceptors/slug.interceptor.js +30 -9
- package/cjs/interfaces/datasource.interface.js +4 -0
- package/cjs/interfaces/index.js +2 -1
- package/cjs/interfaces/logged-user-info.interface.js +1 -2
- package/cjs/interfaces/module-config.interface.js +4 -0
- package/cjs/interfaces/permission.interface.js +1 -10
- package/cjs/middlewares/logger.middleware.js +2 -6
- package/cjs/modules/cache/cache.module.js +3 -3
- package/cjs/modules/datasource/multi-tenant-datasource.service.js +31 -111
- package/cjs/modules/utils/utils.service.js +63 -145
- package/cjs/utils/error-handler.util.js +91 -13
- package/cjs/utils/html-sanitizer.util.js +74 -0
- package/cjs/utils/index.js +2 -0
- package/cjs/utils/query-helpers.util.js +53 -0
- package/classes/api-controller.class.d.ts +5 -5
- package/classes/api-service.class.d.ts +5 -5
- package/classes/index.d.ts +1 -0
- package/classes/request-scoped-api.service.d.ts +3 -2
- package/constants/index.d.ts +1 -0
- package/constants/permissions.d.ts +167 -0
- package/decorators/index.d.ts +1 -0
- package/decorators/sanitize-html.decorator.d.ts +2 -0
- package/dtos/filter-and-pagination.dto.d.ts +0 -2
- package/dtos/response-payload.dto.d.ts +0 -7
- package/fesm/classes/api-controller.class.js +10 -93
- package/fesm/classes/api-service.class.js +5 -46
- package/fesm/classes/index.js +2 -0
- package/fesm/classes/request-scoped-api.service.js +4 -53
- package/fesm/classes/winston.logger.class.js +6 -18
- package/fesm/constants/index.js +16 -29
- package/fesm/constants/permissions.js +121 -0
- package/fesm/decorators/api-response.decorator.js +1 -1
- package/fesm/decorators/index.js +1 -0
- package/fesm/decorators/sanitize-html.decorator.js +45 -0
- package/fesm/dtos/filter-and-pagination.dto.js +26 -47
- package/fesm/dtos/pagination.dto.js +4 -8
- package/fesm/dtos/response-payload.dto.js +39 -142
- package/fesm/entities/identity.js +4 -4
- package/fesm/entities/user-root.js +13 -14
- package/fesm/guards/permission.guard.js +39 -94
- package/fesm/interceptors/index.js +1 -0
- package/fesm/interceptors/set-create-by-on-body.interceptor.js +4 -30
- package/fesm/interceptors/set-delete-by-on-body.interceptor.js +4 -30
- package/fesm/interceptors/set-update-by-on-body.interceptor.js +4 -30
- package/fesm/interceptors/set-user-field-on-body.interceptor.js +36 -0
- package/fesm/interceptors/slug.interceptor.js +31 -10
- package/fesm/interfaces/datasource.interface.js +20 -0
- package/fesm/interfaces/index.js +2 -1
- package/fesm/interfaces/logged-user-info.interface.js +1 -2
- package/fesm/interfaces/module-config.interface.js +5 -0
- package/fesm/interfaces/permission.interface.js +0 -12
- package/fesm/middlewares/logger.middleware.js +2 -6
- package/fesm/modules/cache/cache.module.js +2 -2
- package/fesm/modules/datasource/multi-tenant-datasource.service.js +31 -111
- package/fesm/modules/utils/utils.service.js +50 -143
- package/fesm/utils/error-handler.util.js +93 -14
- package/fesm/utils/html-sanitizer.util.js +82 -0
- package/fesm/utils/index.js +2 -0
- package/fesm/utils/query-helpers.util.js +78 -0
- package/interceptors/index.d.ts +1 -0
- package/interceptors/set-create-by-on-body.interceptor.d.ts +1 -5
- package/interceptors/set-delete-by-on-body.interceptor.d.ts +1 -5
- package/interceptors/set-update-by-on-body.interceptor.d.ts +1 -5
- package/interceptors/set-user-field-on-body.interceptor.d.ts +2 -0
- package/interceptors/slug.interceptor.d.ts +2 -1
- package/interfaces/api.interface.d.ts +2 -2
- package/interfaces/datasource.interface.d.ts +5 -0
- package/interfaces/identity.interface.d.ts +4 -4
- package/interfaces/index.d.ts +2 -1
- package/interfaces/module-config.interface.d.ts +6 -0
- package/interfaces/permission.interface.d.ts +0 -1
- package/modules/utils/utils.service.d.ts +10 -4
- package/package.json +4 -4
- package/utils/error-handler.util.d.ts +23 -13
- package/utils/html-sanitizer.util.d.ts +3 -0
- package/utils/index.d.ts +2 -0
- package/utils/query-helpers.util.d.ts +16 -0
- package/cjs/interfaces/base-query.interface.js +0 -6
- package/fesm/interfaces/base-query.interface.js +0 -3
- package/interfaces/base-query.interface.d.ts +0 -7
|
@@ -2,31 +2,55 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", {
|
|
3
3
|
value: true
|
|
4
4
|
});
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
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 DEFAULT_PHONE_COUNTRY_CODE () {
|
|
13
|
+
return DEFAULT_PHONE_COUNTRY_CODE;
|
|
14
|
+
},
|
|
15
|
+
get DEFAULT_PHONE_REGEX () {
|
|
16
|
+
return DEFAULT_PHONE_REGEX;
|
|
17
|
+
},
|
|
18
|
+
get UtilsService () {
|
|
8
19
|
return UtilsService;
|
|
9
20
|
}
|
|
10
21
|
});
|
|
11
22
|
const _common = require("@nestjs/common");
|
|
12
23
|
const _classvalidator = require("class-validator");
|
|
24
|
+
function _define_property(obj, key, value) {
|
|
25
|
+
if (key in obj) {
|
|
26
|
+
Object.defineProperty(obj, key, {
|
|
27
|
+
value: value,
|
|
28
|
+
enumerable: true,
|
|
29
|
+
configurable: true,
|
|
30
|
+
writable: true
|
|
31
|
+
});
|
|
32
|
+
} else {
|
|
33
|
+
obj[key] = value;
|
|
34
|
+
}
|
|
35
|
+
return obj;
|
|
36
|
+
}
|
|
13
37
|
function _ts_decorate(decorators, target, key, desc) {
|
|
14
38
|
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
15
39
|
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
16
40
|
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
41
|
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
18
42
|
}
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
}
|
|
43
|
+
const DEFAULT_PHONE_REGEX = /^((\+880)|0)?(13|15|16|17|18|19)\d{8}$/;
|
|
44
|
+
const DEFAULT_PHONE_COUNTRY_CODE = '+88';
|
|
22
45
|
let UtilsService = class UtilsService {
|
|
46
|
+
/**
|
|
47
|
+
* Configure phone validation regex and country code
|
|
48
|
+
*/ setPhoneValidationConfig(config) {
|
|
49
|
+
this.phoneConfig = config;
|
|
50
|
+
}
|
|
23
51
|
// ---------------- CACHE HELPERS ----------------
|
|
24
52
|
/**
|
|
25
53
|
* 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
54
|
*/ getCacheKey(entityName, params, entityId, tenantId) {
|
|
31
55
|
const tenantPrefix = tenantId ? `tenant_${tenantId}_` : '';
|
|
32
56
|
if (entityId) return `${tenantPrefix}entity_${entityName}_id_${entityId}${params ? '_select_' + JSON.stringify(params) : ''}`;
|
|
@@ -49,48 +73,44 @@ let UtilsService = class UtilsService {
|
|
|
49
73
|
await cacheManager.set(trackingKey, allKeys);
|
|
50
74
|
}
|
|
51
75
|
} catch (error) {
|
|
52
|
-
|
|
53
|
-
console.error(`Cache tracking failed for ${entityName}:`, error);
|
|
76
|
+
this.logger.error(`Cache tracking failed for ${entityName}`, error instanceof Error ? error.stack : String(error));
|
|
54
77
|
}
|
|
55
78
|
}
|
|
56
79
|
/**
|
|
57
80
|
* Clear cache for entity with optional tenant prefix
|
|
58
|
-
* Uses Promise.allSettled to ensure all deletions are attempted even if some fail
|
|
59
81
|
*/ async clearCache(entityName, cacheManager, entityId, tenantId) {
|
|
60
82
|
const tenantPrefix = tenantId ? `tenant_${tenantId}_` : '';
|
|
61
83
|
try {
|
|
62
84
|
if (entityId) {
|
|
63
85
|
const trackingKey = `${tenantPrefix}entity_${entityName}_id_${entityId}_keys`;
|
|
64
86
|
const idKeys = await cacheManager.get(trackingKey) || [];
|
|
65
|
-
// Use Promise.allSettled to ensure all deletions are attempted
|
|
66
87
|
await Promise.allSettled(idKeys.map((key)=>cacheManager.del(key)));
|
|
67
88
|
await cacheManager.del(trackingKey);
|
|
68
89
|
} else {
|
|
69
90
|
const trackingKey = `${tenantPrefix}entity_${entityName}_keys`;
|
|
70
91
|
const keySet = await cacheManager.get(trackingKey) || [];
|
|
71
|
-
// Use Promise.allSettled to ensure all deletions are attempted
|
|
72
92
|
await Promise.allSettled(keySet.map((key)=>cacheManager.del(key)));
|
|
73
93
|
await cacheManager.del(trackingKey);
|
|
74
94
|
}
|
|
75
95
|
} catch (error) {
|
|
76
|
-
|
|
77
|
-
console.error(`Cache invalidation failed for ${entityName}:`, error);
|
|
96
|
+
this.logger.error(`Cache invalidation failed for ${entityName}`, error instanceof Error ? error.stack : String(error));
|
|
78
97
|
}
|
|
79
98
|
}
|
|
99
|
+
// ---------------- VALIDATION HELPERS ----------------
|
|
80
100
|
/**
|
|
81
|
-
* Check
|
|
101
|
+
* Check if value is phone or email and normalize phone number
|
|
82
102
|
*/ checkPhoneOrEmail(value) {
|
|
83
103
|
if ((0, _classvalidator.isEmail)(value)) {
|
|
84
104
|
return {
|
|
85
|
-
value
|
|
105
|
+
value,
|
|
86
106
|
type: 'email'
|
|
87
107
|
};
|
|
88
108
|
}
|
|
89
|
-
const phoneMatch = value.match(
|
|
109
|
+
const phoneMatch = value.match(this.phoneConfig.regex);
|
|
90
110
|
if (phoneMatch) {
|
|
91
111
|
let phone = phoneMatch[0];
|
|
92
|
-
if (!phone.startsWith(
|
|
93
|
-
phone =
|
|
112
|
+
if (!phone.startsWith(this.phoneConfig.countryCode)) {
|
|
113
|
+
phone = this.phoneConfig.countryCode + phone;
|
|
94
114
|
}
|
|
95
115
|
return {
|
|
96
116
|
value: phone,
|
|
@@ -102,22 +122,22 @@ let UtilsService = class UtilsService {
|
|
|
102
122
|
type: null
|
|
103
123
|
};
|
|
104
124
|
}
|
|
125
|
+
// ---------------- STRING HELPERS ----------------
|
|
105
126
|
/**
|
|
106
|
-
*
|
|
107
|
-
* transformToSlug
|
|
127
|
+
* Transform string to URL-friendly slug
|
|
108
128
|
*/ transformToSlug(value, salt) {
|
|
109
129
|
const slug = value?.trim().replace(/[^A-Z0-9]+/gi, '-').toLowerCase();
|
|
110
130
|
return salt ? `${slug}-${this.getRandomInt(1, 100)}` : slug;
|
|
111
131
|
}
|
|
132
|
+
// ---------------- RANDOM HELPERS ----------------
|
|
112
133
|
/**
|
|
113
|
-
*
|
|
114
|
-
* getRandomInt
|
|
115
|
-
* generateRandomId
|
|
116
|
-
* getRandomOtpCode
|
|
134
|
+
* Generate random integer between min and max (inclusive)
|
|
117
135
|
*/ getRandomInt(min, max) {
|
|
118
136
|
return Math.floor(Math.random() * (max - min + 1)) + min;
|
|
119
137
|
}
|
|
120
|
-
|
|
138
|
+
/**
|
|
139
|
+
* Generate random alphanumeric string
|
|
140
|
+
*/ generateRandomId(length) {
|
|
121
141
|
const characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
|
|
122
142
|
let result = '';
|
|
123
143
|
for(let i = 0; i < length; i++){
|
|
@@ -125,131 +145,29 @@ let UtilsService = class UtilsService {
|
|
|
125
145
|
}
|
|
126
146
|
return result;
|
|
127
147
|
}
|
|
128
|
-
|
|
148
|
+
/**
|
|
149
|
+
* Generate 4-digit OTP code
|
|
150
|
+
*/ getRandomOtpCode() {
|
|
129
151
|
return Math.floor(Math.random() * (9999 - 1000 + 1)) + 1000;
|
|
130
152
|
}
|
|
131
|
-
|
|
153
|
+
// ---------------- ERROR HELPERS ----------------
|
|
154
|
+
/**
|
|
155
|
+
* Extract column name and value from PostgreSQL error detail
|
|
156
|
+
*/ extractColumnNameFromError(detail) {
|
|
132
157
|
const match = detail.match(/\((.*?)\)=\((.*?)\)/);
|
|
133
158
|
return {
|
|
134
159
|
columnName: match ? match[1] : 'unknown',
|
|
135
160
|
value: match ? match[2] : 'unknown'
|
|
136
161
|
};
|
|
137
162
|
}
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
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
|
-
`;
|
|
193
|
-
}
|
|
194
|
-
getResetPasswordEmailFormat(resetLink, userName) {
|
|
195
|
-
return `
|
|
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
|
-
`;
|
|
163
|
+
constructor(){
|
|
164
|
+
_define_property(this, "logger", new _common.Logger(UtilsService.name));
|
|
165
|
+
_define_property(this, "phoneConfig", {
|
|
166
|
+
regex: DEFAULT_PHONE_REGEX,
|
|
167
|
+
countryCode: DEFAULT_PHONE_COUNTRY_CODE
|
|
168
|
+
});
|
|
248
169
|
}
|
|
249
|
-
constructor(){}
|
|
250
170
|
};
|
|
251
171
|
UtilsService = _ts_decorate([
|
|
252
|
-
(0, _common.Injectable)()
|
|
253
|
-
_ts_metadata("design:type", Function),
|
|
254
|
-
_ts_metadata("design:paramtypes", [])
|
|
172
|
+
(0, _common.Injectable)()
|
|
255
173
|
], UtilsService);
|
|
@@ -8,28 +8,104 @@ Object.defineProperty(exports, "ErrorHandler", {
|
|
|
8
8
|
return ErrorHandler;
|
|
9
9
|
}
|
|
10
10
|
});
|
|
11
|
+
const _common = require("@nestjs/common");
|
|
12
|
+
/** Check if running in production environment */ const IS_PRODUCTION = process.env.NODE_ENV === 'production';
|
|
13
|
+
/** Sensitive keys that should be redacted from logs */ const SENSITIVE_KEYS = [
|
|
14
|
+
'password',
|
|
15
|
+
'secret',
|
|
16
|
+
'token',
|
|
17
|
+
'apiKey',
|
|
18
|
+
'credential',
|
|
19
|
+
'authorization'
|
|
20
|
+
];
|
|
21
|
+
/** Patterns that indicate sensitive data in error messages */ const SENSITIVE_PATTERNS = [
|
|
22
|
+
/password/i,
|
|
23
|
+
/secret/i,
|
|
24
|
+
/token/i,
|
|
25
|
+
/key/i,
|
|
26
|
+
/credential/i,
|
|
27
|
+
/authorization/i,
|
|
28
|
+
/bearer/i
|
|
29
|
+
];
|
|
11
30
|
let ErrorHandler = class ErrorHandler {
|
|
12
31
|
/**
|
|
13
|
-
* Safely extract error message from unknown error
|
|
14
|
-
|
|
32
|
+
* Safely extract error message from unknown error.
|
|
33
|
+
* @param error - The error to extract message from
|
|
34
|
+
* @param sanitizeForClient - If true, redacts potentially sensitive info in production
|
|
35
|
+
*/ static getErrorMessage(error, sanitizeForClient = false) {
|
|
36
|
+
let message = 'Unknown error occurred';
|
|
15
37
|
if (error instanceof Error) {
|
|
16
|
-
|
|
38
|
+
message = error.message;
|
|
39
|
+
} else if (typeof error === 'string') {
|
|
40
|
+
message = error;
|
|
17
41
|
}
|
|
18
|
-
|
|
19
|
-
|
|
42
|
+
// Sanitize for client responses in production
|
|
43
|
+
if (sanitizeForClient && IS_PRODUCTION) {
|
|
44
|
+
if (this.containsSensitiveData(message)) {
|
|
45
|
+
return 'An unexpected error occurred. Please try again later.';
|
|
46
|
+
}
|
|
20
47
|
}
|
|
21
|
-
return
|
|
48
|
+
return message;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Check if a string contains potentially sensitive data.
|
|
52
|
+
*/ static containsSensitiveData(text) {
|
|
53
|
+
return SENSITIVE_PATTERNS.some((pattern)=>pattern.test(text));
|
|
22
54
|
}
|
|
23
55
|
/**
|
|
24
|
-
* Safely extract error stack from unknown error
|
|
25
|
-
|
|
56
|
+
* Safely extract error stack from unknown error.
|
|
57
|
+
* Returns undefined in production for client responses.
|
|
58
|
+
* @param error - The error to extract stack from
|
|
59
|
+
* @param forClient - If true, never returns stack in production
|
|
60
|
+
*/ static getErrorStack(error, forClient = false) {
|
|
61
|
+
// Never expose stack traces to clients in production
|
|
62
|
+
if (forClient && IS_PRODUCTION) {
|
|
63
|
+
return undefined;
|
|
64
|
+
}
|
|
26
65
|
if (error instanceof Error) {
|
|
27
66
|
return error.stack;
|
|
28
67
|
}
|
|
29
68
|
return undefined;
|
|
30
69
|
}
|
|
31
70
|
/**
|
|
32
|
-
* Create error
|
|
71
|
+
* Create a sanitized error response for clients.
|
|
72
|
+
* In production, sensitive data is redacted and stack traces removed.
|
|
73
|
+
*/ static createClientError(error, statusCode = _common.HttpStatus.INTERNAL_SERVER_ERROR, code) {
|
|
74
|
+
const sanitizedError = {
|
|
75
|
+
message: this.getErrorMessage(error, true),
|
|
76
|
+
statusCode
|
|
77
|
+
};
|
|
78
|
+
if (code) {
|
|
79
|
+
sanitizedError.code = code;
|
|
80
|
+
}
|
|
81
|
+
// Include stack only in development
|
|
82
|
+
if (!IS_PRODUCTION) {
|
|
83
|
+
sanitizedError.stack = this.getErrorStack(error);
|
|
84
|
+
}
|
|
85
|
+
return sanitizedError;
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Sanitize context data to redact sensitive fields from logs.
|
|
89
|
+
*/ static sanitizeContextForLogging(context) {
|
|
90
|
+
const sanitized = {};
|
|
91
|
+
for (const [key, value] of Object.entries(context)){
|
|
92
|
+
// Check if key contains sensitive words
|
|
93
|
+
const isSensitive = SENSITIVE_KEYS.some((sk)=>key.toLowerCase().includes(sk.toLowerCase()));
|
|
94
|
+
if (isSensitive) {
|
|
95
|
+
sanitized[key] = '[REDACTED]';
|
|
96
|
+
} else if (Array.isArray(value)) {
|
|
97
|
+
sanitized[key] = value.map((item)=>typeof item === 'object' && item !== null ? this.sanitizeContextForLogging(item) : item);
|
|
98
|
+
} else if (typeof value === 'object' && value !== null) {
|
|
99
|
+
sanitized[key] = this.sanitizeContextForLogging(value);
|
|
100
|
+
} else {
|
|
101
|
+
sanitized[key] = value;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
return sanitized;
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Create error context object for internal logging.
|
|
108
|
+
* Context data is sanitized to redact sensitive fields.
|
|
33
109
|
*/ static createErrorContext(error, context) {
|
|
34
110
|
const errorContext = {
|
|
35
111
|
error: {
|
|
@@ -41,20 +117,22 @@ let ErrorHandler = class ErrorHandler {
|
|
|
41
117
|
errorContext.error.name = error.name;
|
|
42
118
|
}
|
|
43
119
|
if (context && Object.keys(context).length > 0) {
|
|
44
|
-
|
|
120
|
+
// Sanitize context to redact sensitive fields
|
|
121
|
+
errorContext.context = this.sanitizeContextForLogging(context);
|
|
45
122
|
}
|
|
46
123
|
return errorContext;
|
|
47
124
|
}
|
|
48
125
|
/**
|
|
49
|
-
* Log error with consistent format
|
|
126
|
+
* Log error with consistent format.
|
|
127
|
+
* Sensitive data in context is automatically redacted.
|
|
50
128
|
*/ static logError(logger, error, operation, context) {
|
|
51
129
|
const errorContext = this.createErrorContext(error, {
|
|
52
130
|
operation,
|
|
53
131
|
...context
|
|
54
132
|
});
|
|
55
133
|
const errorMessage = `Failed to ${operation}: ${errorContext.error.message}`;
|
|
56
|
-
|
|
57
|
-
logger.error(errorMessage, errorContext.error.stack,
|
|
134
|
+
// Log full details internally (stack traces are fine for internal logs)
|
|
135
|
+
logger.error(errorMessage, errorContext.error.stack, errorContext);
|
|
58
136
|
}
|
|
59
137
|
/**
|
|
60
138
|
* Re-throw error with proper type checking
|
|
@@ -0,0 +1,74 @@
|
|
|
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 containsHtmlContent () {
|
|
20
|
+
return containsHtmlContent;
|
|
21
|
+
},
|
|
22
|
+
get escapeHtml () {
|
|
23
|
+
return escapeHtml;
|
|
24
|
+
},
|
|
25
|
+
get escapeHtmlVariables () {
|
|
26
|
+
return escapeHtmlVariables;
|
|
27
|
+
}
|
|
28
|
+
});
|
|
29
|
+
const HTML_ESCAPE_MAP = {
|
|
30
|
+
'&': '&',
|
|
31
|
+
'<': '<',
|
|
32
|
+
'>': '>',
|
|
33
|
+
'"': '"',
|
|
34
|
+
"'": ''',
|
|
35
|
+
'/': '/',
|
|
36
|
+
'`': '`',
|
|
37
|
+
'=': '='
|
|
38
|
+
};
|
|
39
|
+
/**
|
|
40
|
+
* Regex pattern matching characters that need HTML escaping
|
|
41
|
+
*/ const HTML_ESCAPE_REGEX = /[&<>"'`=/]/g;
|
|
42
|
+
function escapeHtml(str) {
|
|
43
|
+
if (!str || typeof str !== 'string') {
|
|
44
|
+
return str ?? '';
|
|
45
|
+
}
|
|
46
|
+
return str.replace(HTML_ESCAPE_REGEX, (char)=>HTML_ESCAPE_MAP[char] || char);
|
|
47
|
+
}
|
|
48
|
+
function escapeHtmlVariables(variables) {
|
|
49
|
+
if (!variables || typeof variables !== 'object') {
|
|
50
|
+
return {};
|
|
51
|
+
}
|
|
52
|
+
const escaped = {};
|
|
53
|
+
for (const [key, value] of Object.entries(variables)){
|
|
54
|
+
if (value === null || value === undefined) {
|
|
55
|
+
escaped[key] = '';
|
|
56
|
+
} else if (typeof value === 'string') {
|
|
57
|
+
escaped[key] = escapeHtml(value);
|
|
58
|
+
} else if (typeof value === 'object') {
|
|
59
|
+
// For objects/arrays, stringify and escape
|
|
60
|
+
escaped[key] = escapeHtml(JSON.stringify(value));
|
|
61
|
+
} else {
|
|
62
|
+
// For numbers, booleans, etc., convert to string (no escaping needed)
|
|
63
|
+
escaped[key] = String(value);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
return escaped;
|
|
67
|
+
}
|
|
68
|
+
function containsHtmlContent(str) {
|
|
69
|
+
if (!str || typeof str !== 'string') {
|
|
70
|
+
return false;
|
|
71
|
+
}
|
|
72
|
+
// Check for common HTML patterns
|
|
73
|
+
return /<[a-z][\s\S]*>/i.test(str) || /javascript:/i.test(str) || /on\w+=/i.test(str);
|
|
74
|
+
}
|
package/cjs/utils/index.js
CHANGED
|
@@ -3,6 +3,8 @@ 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);
|
|
6
8
|
function _export_star(from, to) {
|
|
7
9
|
Object.keys(from).forEach(function(k) {
|
|
8
10
|
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
|
+
}
|
|
@@ -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>>;
|
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
import { DeleteDto, FilterAndPaginationDto } from '
|
|
2
|
-
import { Identity } from '
|
|
3
|
-
import { ILoggedUserInfo, IService } from '
|
|
4
|
-
import { UtilsService } from '
|
|
1
|
+
import { DeleteDto, FilterAndPaginationDto } from '../dtos';
|
|
2
|
+
import { Identity } from '../entities';
|
|
3
|
+
import { ILoggedUserInfo, IService } from '../interfaces';
|
|
4
|
+
import { UtilsService } from '../modules/utils/utils.service';
|
|
5
5
|
import { Logger } from '@nestjs/common';
|
|
6
6
|
import { QueryRunner, Repository, SelectQueryBuilder } from 'typeorm';
|
|
7
7
|
import { HybridCache } from './hybrid-cache.class';
|
|
8
|
-
export declare abstract class ApiService<CreateDtoT extends
|
|
8
|
+
export declare abstract class ApiService<CreateDtoT extends object, UpdateDtoT extends {
|
|
9
9
|
id: string;
|
|
10
10
|
}, InterfaceT extends Identity, EntityT extends Identity, RepositoryT extends Repository<EntityT>> implements IService<CreateDtoT, UpdateDtoT, InterfaceT> {
|
|
11
11
|
protected entityName: string;
|
package/classes/index.d.ts
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
import { DataSource, EntityTarget, Repository } from 'typeorm';
|
|
2
2
|
import { Identity } from '../entities';
|
|
3
|
+
import { IDataSourceProvider } from '../interfaces';
|
|
3
4
|
import { ApiService } from './api-service.class';
|
|
4
|
-
export declare abstract class RequestScopedApiService<CreateDtoT extends
|
|
5
|
+
export declare abstract class RequestScopedApiService<CreateDtoT extends object, UpdateDtoT extends {
|
|
5
6
|
id: string;
|
|
6
7
|
}, InterfaceT extends Identity, EntityT extends Identity, RepositoryT extends Repository<EntityT>> extends ApiService<CreateDtoT, UpdateDtoT, InterfaceT, EntityT, RepositoryT> {
|
|
7
8
|
private repositoryInitialized;
|
|
8
9
|
protected abstract resolveEntity(): EntityTarget<EntityT>;
|
|
9
|
-
protected abstract getDataSourceProvider():
|
|
10
|
+
protected abstract getDataSourceProvider(): IDataSourceProvider;
|
|
10
11
|
protected ensureRepositoryInitialized(): Promise<void>;
|
|
11
12
|
protected initializeAdditionalRepositories(entities: EntityTarget<any>[]): Promise<Repository<any>[]>;
|
|
12
13
|
protected getDataSourceForService(): Promise<DataSource>;
|
package/constants/index.d.ts
CHANGED
|
@@ -8,3 +8,4 @@ export declare const REQUEST_ID_HEADER = "x-request-id";
|
|
|
8
8
|
export declare const CLIENT_TYPE_HEADER = "x-client-type";
|
|
9
9
|
export declare const PERMISSIONS_CACHE_PREFIX = "permissions";
|
|
10
10
|
export declare const IDEMPOTENCY_CACHE_PREFIX = "idempotency";
|
|
11
|
+
export * from './permissions';
|