@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.
Files changed (102) hide show
  1. package/README.md +106 -138
  2. package/cjs/classes/api-controller.class.js +9 -45
  3. package/cjs/classes/api-service.class.js +4 -41
  4. package/cjs/classes/index.js +1 -0
  5. package/cjs/classes/request-scoped-api.service.js +4 -53
  6. package/cjs/classes/winston.logger.class.js +5 -15
  7. package/cjs/constants/index.js +16 -11
  8. package/cjs/constants/permissions.js +174 -0
  9. package/cjs/decorators/api-response.decorator.js +1 -1
  10. package/cjs/decorators/index.js +1 -0
  11. package/cjs/decorators/sanitize-html.decorator.js +36 -0
  12. package/cjs/dtos/filter-and-pagination.dto.js +24 -34
  13. package/cjs/dtos/pagination.dto.js +4 -8
  14. package/cjs/dtos/response-payload.dto.js +35 -121
  15. package/cjs/entities/identity.js +4 -4
  16. package/cjs/entities/user-root.js +13 -14
  17. package/cjs/guards/permission.guard.js +39 -94
  18. package/cjs/interceptors/index.js +1 -0
  19. package/cjs/interceptors/set-create-by-on-body.interceptor.js +2 -30
  20. package/cjs/interceptors/set-delete-by-on-body.interceptor.js +2 -30
  21. package/cjs/interceptors/set-update-by-on-body.interceptor.js +2 -30
  22. package/cjs/interceptors/set-user-field-on-body.interceptor.js +43 -0
  23. package/cjs/interceptors/slug.interceptor.js +30 -9
  24. package/cjs/interfaces/datasource.interface.js +4 -0
  25. package/cjs/interfaces/index.js +2 -1
  26. package/cjs/interfaces/logged-user-info.interface.js +1 -2
  27. package/cjs/interfaces/module-config.interface.js +4 -0
  28. package/cjs/interfaces/permission.interface.js +1 -10
  29. package/cjs/middlewares/logger.middleware.js +2 -6
  30. package/cjs/modules/cache/cache.module.js +3 -3
  31. package/cjs/modules/datasource/multi-tenant-datasource.service.js +31 -111
  32. package/cjs/modules/utils/utils.service.js +63 -145
  33. package/cjs/utils/error-handler.util.js +91 -13
  34. package/cjs/utils/html-sanitizer.util.js +74 -0
  35. package/cjs/utils/index.js +2 -0
  36. package/cjs/utils/query-helpers.util.js +53 -0
  37. package/classes/api-controller.class.d.ts +5 -5
  38. package/classes/api-service.class.d.ts +5 -5
  39. package/classes/index.d.ts +1 -0
  40. package/classes/request-scoped-api.service.d.ts +3 -2
  41. package/constants/index.d.ts +1 -0
  42. package/constants/permissions.d.ts +167 -0
  43. package/decorators/index.d.ts +1 -0
  44. package/decorators/sanitize-html.decorator.d.ts +2 -0
  45. package/dtos/filter-and-pagination.dto.d.ts +0 -2
  46. package/dtos/response-payload.dto.d.ts +0 -7
  47. package/fesm/classes/api-controller.class.js +10 -93
  48. package/fesm/classes/api-service.class.js +5 -46
  49. package/fesm/classes/index.js +2 -0
  50. package/fesm/classes/request-scoped-api.service.js +4 -53
  51. package/fesm/classes/winston.logger.class.js +6 -18
  52. package/fesm/constants/index.js +16 -29
  53. package/fesm/constants/permissions.js +121 -0
  54. package/fesm/decorators/api-response.decorator.js +1 -1
  55. package/fesm/decorators/index.js +1 -0
  56. package/fesm/decorators/sanitize-html.decorator.js +45 -0
  57. package/fesm/dtos/filter-and-pagination.dto.js +26 -47
  58. package/fesm/dtos/pagination.dto.js +4 -8
  59. package/fesm/dtos/response-payload.dto.js +39 -142
  60. package/fesm/entities/identity.js +4 -4
  61. package/fesm/entities/user-root.js +13 -14
  62. package/fesm/guards/permission.guard.js +39 -94
  63. package/fesm/interceptors/index.js +1 -0
  64. package/fesm/interceptors/set-create-by-on-body.interceptor.js +4 -30
  65. package/fesm/interceptors/set-delete-by-on-body.interceptor.js +4 -30
  66. package/fesm/interceptors/set-update-by-on-body.interceptor.js +4 -30
  67. package/fesm/interceptors/set-user-field-on-body.interceptor.js +36 -0
  68. package/fesm/interceptors/slug.interceptor.js +31 -10
  69. package/fesm/interfaces/datasource.interface.js +20 -0
  70. package/fesm/interfaces/index.js +2 -1
  71. package/fesm/interfaces/logged-user-info.interface.js +1 -2
  72. package/fesm/interfaces/module-config.interface.js +5 -0
  73. package/fesm/interfaces/permission.interface.js +0 -12
  74. package/fesm/middlewares/logger.middleware.js +2 -6
  75. package/fesm/modules/cache/cache.module.js +2 -2
  76. package/fesm/modules/datasource/multi-tenant-datasource.service.js +31 -111
  77. package/fesm/modules/utils/utils.service.js +50 -143
  78. package/fesm/utils/error-handler.util.js +93 -14
  79. package/fesm/utils/html-sanitizer.util.js +82 -0
  80. package/fesm/utils/index.js +2 -0
  81. package/fesm/utils/query-helpers.util.js +78 -0
  82. package/interceptors/index.d.ts +1 -0
  83. package/interceptors/set-create-by-on-body.interceptor.d.ts +1 -5
  84. package/interceptors/set-delete-by-on-body.interceptor.d.ts +1 -5
  85. package/interceptors/set-update-by-on-body.interceptor.d.ts +1 -5
  86. package/interceptors/set-user-field-on-body.interceptor.d.ts +2 -0
  87. package/interceptors/slug.interceptor.d.ts +2 -1
  88. package/interfaces/api.interface.d.ts +2 -2
  89. package/interfaces/datasource.interface.d.ts +5 -0
  90. package/interfaces/identity.interface.d.ts +4 -4
  91. package/interfaces/index.d.ts +2 -1
  92. package/interfaces/module-config.interface.d.ts +6 -0
  93. package/interfaces/permission.interface.d.ts +0 -1
  94. package/modules/utils/utils.service.d.ts +10 -4
  95. package/package.json +4 -4
  96. package/utils/error-handler.util.d.ts +23 -13
  97. package/utils/html-sanitizer.util.d.ts +3 -0
  98. package/utils/index.d.ts +2 -0
  99. package/utils/query-helpers.util.d.ts +16 -0
  100. package/cjs/interfaces/base-query.interface.js +0 -6
  101. package/fesm/interfaces/base-query.interface.js +0 -3
  102. 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
- Object.defineProperty(exports, "UtilsService", {
6
- enumerable: true,
7
- get: function() {
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
- function _ts_metadata(k, v) {
20
- if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
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
- // Log but don't throw - cache tracking failure shouldn't break the request
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
- // Log but don't throw - cache invalidation failure shouldn't break the request
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 Phone or Email
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: value,
105
+ value,
86
106
  type: 'email'
87
107
  };
88
108
  }
89
- const phoneMatch = value.match(/^((\+880)|0)?(13|15|16|17|18|19)\d{8}$/);
109
+ const phoneMatch = value.match(this.phoneConfig.regex);
90
110
  if (phoneMatch) {
91
111
  let phone = phoneMatch[0];
92
- if (!phone.startsWith('+88')) {
93
- phone = '+88' + 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
- * STRING FUNCTIONS
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
- * RANDOM FUNCTIONS
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
- generateRandomId(length) {
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
- getRandomOtpCode() {
148
+ /**
149
+ * Generate 4-digit OTP code
150
+ */ getRandomOtpCode() {
129
151
  return Math.floor(Math.random() * (9999 - 1000 + 1)) + 1000;
130
152
  }
131
- extractColumnNameFromError(detail) {
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
- 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
- `;
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
- */ static getErrorMessage(error) {
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
- return error.message;
38
+ message = error.message;
39
+ } else if (typeof error === 'string') {
40
+ message = error;
17
41
  }
18
- if (typeof error === 'string') {
19
- return error;
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 'Unknown error occurred';
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
- */ static getErrorStack(error) {
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 context object for logging
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
- errorContext.context = context;
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
- const loggerContext = logger.context || 'ErrorHandler';
57
- logger.error(errorMessage, errorContext.error.stack, loggerContext, errorContext);
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
+ '&': '&amp;',
31
+ '<': '&lt;',
32
+ '>': '&gt;',
33
+ '"': '&quot;',
34
+ "'": '&#x27;',
35
+ '/': '&#x2F;',
36
+ '`': '&#x60;',
37
+ '=': '&#x3D;'
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
+ }
@@ -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 '@flusys/nestjs-shared/dtos';
2
- import { Identity } from '@flusys/nestjs-shared/entities';
3
- import { ILoggedUserInfo, IService, PermissionCondition, PermissionOperator } from '@flusys/nestjs-shared/interfaces';
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 Record<string, unknown>, UpdateDtoT extends {
19
+ export declare function createApiController<CreateDtoT extends object, UpdateDtoT extends {
20
20
  id: string;
21
- }, ResponseDtoT extends Record<string, unknown>, InterfaceT extends Identity, ServiceT extends IService<CreateDtoT, UpdateDtoT, InterfaceT>>(createDtoClass: Type<CreateDtoT>, updateDtoClass: Type<UpdateDtoT>, responseDtoClass: Type<ResponseDtoT>, options?: ApiControllerOptions): abstract new (service: ServiceT) => {
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 '@flusys/nestjs-shared/dtos';
2
- import { Identity } from '@flusys/nestjs-shared/entities';
3
- import { ILoggedUserInfo, IService } from '@flusys/nestjs-shared/interfaces';
4
- import { UtilsService } from '@flusys/nestjs-shared/modules';
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 Record<string, unknown>, UpdateDtoT 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;
@@ -4,3 +4,4 @@ export * from './request-scoped-api.service';
4
4
  export * from './hybrid-cache.class';
5
5
  export * from './winston-logger-adapter.class';
6
6
  export * from './winston.logger.class';
7
+ export * from '../constants/permissions';
@@ -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 Record<string, unknown>, UpdateDtoT 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(): any;
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>;
@@ -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';