@flusys/nestjs-shared 1.1.0 → 1.2.0-rc.1
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 +139 -1103
- package/cjs/classes/api-controller.class.js +320 -29
- package/cjs/classes/api-service.class.js +219 -64
- package/cjs/classes/handle-custome-db.js +37 -0
- package/cjs/classes/index.js +1 -2
- package/cjs/classes/winston.logger.class.js +103 -70
- package/cjs/constants/index.js +1 -4
- package/cjs/constants/message-keys.js +68 -0
- package/cjs/constants/permissions.js +114 -11
- package/cjs/decorators/api-response.decorator.js +1 -2
- package/cjs/decorators/index.js +1 -1
- package/cjs/decorators/log-action.decorator.js +149 -0
- package/cjs/decorators/require-permission.decorator.js +5 -5
- package/cjs/dtos/get-by-ids.dto.js +65 -0
- package/cjs/dtos/identity-response.dto.js +4 -8
- package/cjs/dtos/index.js +1 -0
- package/cjs/dtos/response-payload.dto.js +72 -0
- package/cjs/entities/identity.js +8 -2
- package/cjs/entities/index.js +1 -0
- package/cjs/entities/raw-type.js +4 -0
- package/cjs/entities/user-root.js +2 -1
- package/cjs/enums/index.js +20 -0
- package/cjs/enums/notification-type.enum.js +17 -0
- package/cjs/enums/participant-status.enum.js +17 -0
- package/cjs/enums/recurrence-type.enum.js +18 -0
- package/cjs/exceptions/base-app.exception.js +151 -0
- package/cjs/exceptions/index.js +1 -0
- package/cjs/exceptions/permission.exception.js +13 -9
- package/cjs/filters/global-exception.filter.js +279 -0
- package/cjs/filters/index.js +18 -0
- package/cjs/guards/jwt-auth.guard.js +4 -1
- package/cjs/guards/permission.guard.js +71 -77
- package/cjs/index.js +2 -0
- package/cjs/interceptors/idempotency.interceptor.js +1 -1
- package/cjs/interceptors/index.js +0 -1
- package/cjs/interfaces/event-manager-adapter.interface.js +11 -0
- package/cjs/interfaces/index.js +2 -1
- package/cjs/interfaces/notification-adapter.interface.js +11 -0
- package/cjs/interfaces/permission.interface.js +1 -1
- package/cjs/middlewares/logger.middleware.js +83 -26
- package/cjs/modules/datasource/index.js +0 -1
- package/cjs/modules/datasource/multi-tenant-datasource.service.js +46 -85
- package/cjs/modules/utils/utils.service.js +4 -20
- package/cjs/utils/date-time.util.js +94 -0
- package/cjs/utils/index.js +1 -1
- package/cjs/utils/query-helpers.util.js +8 -1
- package/cjs/utils/request.util.js +25 -0
- package/classes/api-controller.class.d.ts +14 -6
- package/classes/api-service.class.d.ts +25 -22
- package/classes/handle-custome-db.d.ts +3 -0
- package/classes/index.d.ts +1 -2
- package/classes/winston.logger.class.d.ts +1 -0
- package/constants/index.d.ts +1 -1
- package/constants/message-keys.d.ts +45 -0
- package/constants/permissions.d.ts +127 -0
- package/decorators/api-response.decorator.d.ts +1 -1
- package/decorators/current-user.decorator.d.ts +1 -1
- package/decorators/index.d.ts +1 -1
- package/decorators/log-action.decorator.d.ts +8 -0
- package/decorators/require-permission.decorator.d.ts +2 -2
- package/dtos/get-by-ids.dto.d.ts +4 -0
- package/dtos/index.d.ts +1 -0
- package/dtos/response-payload.dto.d.ts +8 -0
- package/entities/index.d.ts +1 -0
- package/entities/raw-type.d.ts +1 -0
- package/enums/index.d.ts +3 -0
- package/enums/notification-type.enum.d.ts +6 -0
- package/enums/participant-status.enum.d.ts +6 -0
- package/enums/recurrence-type.enum.d.ts +7 -0
- package/exceptions/base-app.exception.d.ts +41 -0
- package/exceptions/index.d.ts +1 -0
- package/exceptions/permission.exception.d.ts +2 -2
- package/fesm/classes/api-controller.class.js +321 -30
- package/fesm/classes/api-service.class.js +222 -67
- package/fesm/classes/handle-custome-db.js +19 -0
- package/fesm/classes/index.js +1 -2
- package/fesm/classes/winston.logger.class.js +100 -68
- package/fesm/constants/index.js +2 -1
- package/fesm/constants/message-keys.js +49 -0
- package/fesm/constants/permissions.js +86 -14
- package/fesm/decorators/api-response.decorator.js +2 -20
- package/fesm/decorators/index.js +1 -1
- package/fesm/decorators/log-action.decorator.js +139 -0
- package/fesm/decorators/require-permission.decorator.js +17 -65
- package/fesm/dtos/get-by-ids.dto.js +55 -0
- package/fesm/dtos/identity-response.dto.js +4 -8
- package/fesm/dtos/index.js +1 -0
- package/fesm/dtos/response-payload.dto.js +72 -0
- package/fesm/entities/identity.js +8 -2
- package/fesm/entities/index.js +1 -0
- package/fesm/entities/raw-type.js +1 -0
- package/fesm/entities/user-root.js +2 -1
- package/fesm/enums/index.js +3 -0
- package/fesm/enums/notification-type.enum.js +7 -0
- package/fesm/enums/participant-status.enum.js +7 -0
- package/fesm/enums/recurrence-type.enum.js +8 -0
- package/fesm/exceptions/base-app.exception.js +115 -0
- package/fesm/exceptions/index.js +1 -0
- package/fesm/exceptions/permission.exception.js +16 -18
- package/fesm/filters/global-exception.filter.js +269 -0
- package/fesm/filters/index.js +1 -0
- package/fesm/guards/jwt-auth.guard.js +5 -2
- package/fesm/guards/permission.guard.js +73 -79
- package/fesm/index.js +2 -0
- package/fesm/interceptors/idempotency.interceptor.js +2 -2
- package/fesm/interceptors/index.js +0 -1
- package/fesm/interfaces/datasource.interface.js +1 -20
- package/fesm/interfaces/event-manager-adapter.interface.js +1 -0
- package/fesm/interfaces/index.js +2 -1
- package/fesm/interfaces/notification-adapter.interface.js +1 -0
- package/fesm/interfaces/permission.interface.js +1 -3
- package/fesm/middlewares/logger.middleware.js +83 -26
- package/fesm/modules/datasource/index.js +0 -1
- package/fesm/modules/datasource/multi-tenant-datasource.service.js +47 -86
- package/fesm/modules/utils/utils.service.js +5 -21
- package/fesm/utils/date-time.util.js +70 -0
- package/fesm/utils/index.js +1 -1
- package/fesm/utils/query-helpers.util.js +8 -1
- package/fesm/utils/request.util.js +22 -0
- package/filters/global-exception.filter.d.ts +9 -0
- package/filters/index.d.ts +1 -0
- package/guards/permission.guard.d.ts +3 -7
- package/index.d.ts +2 -0
- package/interceptors/idempotency.interceptor.d.ts +1 -1
- package/interceptors/index.d.ts +0 -1
- package/interfaces/api.interface.d.ts +2 -0
- package/interfaces/event-manager-adapter.interface.d.ts +43 -0
- package/interfaces/index.d.ts +2 -1
- package/interfaces/notification-adapter.interface.d.ts +21 -0
- package/interfaces/permission.interface.d.ts +13 -8
- package/modules/datasource/index.d.ts +0 -1
- package/modules/datasource/multi-tenant-datasource.service.d.ts +5 -15
- package/modules/utils/utils.service.d.ts +0 -1
- package/package.json +21 -14
- package/utils/date-time.util.d.ts +8 -0
- package/utils/index.d.ts +1 -1
- package/utils/request.util.d.ts +2 -0
- package/cjs/classes/request-scoped-api.service.js +0 -61
- package/cjs/classes/winston-logger-adapter.class.js +0 -104
- package/cjs/decorators/sanitize-html.decorator.js +0 -36
- package/cjs/interceptors/query-performance.interceptor.js +0 -66
- package/cjs/interfaces/logger.interface.js +0 -7
- package/cjs/modules/datasource/datasource.module.js +0 -117
- package/cjs/utils/error-handler.util.js +0 -90
- package/classes/request-scoped-api.service.d.ts +0 -14
- package/classes/winston-logger-adapter.class.d.ts +0 -22
- package/decorators/sanitize-html.decorator.d.ts +0 -2
- package/fesm/classes/request-scoped-api.service.js +0 -51
- package/fesm/classes/winston-logger-adapter.class.js +0 -107
- package/fesm/decorators/sanitize-html.decorator.js +0 -45
- package/fesm/interceptors/query-performance.interceptor.js +0 -56
- package/fesm/interfaces/logger.interface.js +0 -4
- package/fesm/modules/datasource/datasource.module.js +0 -107
- package/fesm/utils/error-handler.util.js +0 -82
- package/interceptors/query-performance.interceptor.d.ts +0 -8
- package/interfaces/logger.interface.d.ts +0 -7
- package/modules/datasource/datasource.module.d.ts +0 -19
- package/utils/error-handler.util.d.ts +0 -19
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
function _define_property(obj, key, value) {
|
|
2
|
+
if (key in obj) {
|
|
3
|
+
Object.defineProperty(obj, key, {
|
|
4
|
+
value: value,
|
|
5
|
+
enumerable: true,
|
|
6
|
+
configurable: true,
|
|
7
|
+
writable: true
|
|
8
|
+
});
|
|
9
|
+
} else {
|
|
10
|
+
obj[key] = value;
|
|
11
|
+
}
|
|
12
|
+
return obj;
|
|
13
|
+
}
|
|
14
|
+
function _ts_decorate(decorators, target, key, desc) {
|
|
15
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
16
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
17
|
+
else for(var i = decorators.length - 1; i >= 0; i--)if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
18
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
19
|
+
}
|
|
20
|
+
import { Catch, HttpException, HttpStatus, Logger } from '@nestjs/common';
|
|
21
|
+
import { QueryFailedError } from 'typeorm';
|
|
22
|
+
import { ERROR_MESSAGES } from '../constants/message-keys';
|
|
23
|
+
import { BaseAppException, ConflictException } from '../exceptions/base-app.exception';
|
|
24
|
+
import { getCompanyId, getRequestId, getTenantId, getUserId } from '../middlewares/logger.middleware';
|
|
25
|
+
export class GlobalExceptionFilter {
|
|
26
|
+
catch(exception, host) {
|
|
27
|
+
const ctx = host.switchToHttp();
|
|
28
|
+
const request = ctx.getRequest();
|
|
29
|
+
const response = ctx.getResponse();
|
|
30
|
+
const responseTime = Date.now() - (request._startTime || Date.now());
|
|
31
|
+
const requestId = getRequestId() || request.headers['x-request-id'] || 'no-request-id';
|
|
32
|
+
const { statusCode, errorResponse } = this.resolveException(exception, requestId, responseTime);
|
|
33
|
+
const logContext = {
|
|
34
|
+
requestId,
|
|
35
|
+
userId: getUserId(),
|
|
36
|
+
tenantId: getTenantId(),
|
|
37
|
+
companyId: getCompanyId(),
|
|
38
|
+
method: request.method,
|
|
39
|
+
url: request.originalUrl,
|
|
40
|
+
path: request.path,
|
|
41
|
+
statusCode,
|
|
42
|
+
duration: `${responseTime}ms`,
|
|
43
|
+
messageKey: errorResponse.messageKey
|
|
44
|
+
};
|
|
45
|
+
const logMessage = `Exception caught "HTTP/1.1" "${request.method}" "${request.originalUrl}" - ${statusCode} "${errorResponse.messageKey}" ${responseTime}ms | ${errorResponse.message}`;
|
|
46
|
+
if (statusCode >= 500) {
|
|
47
|
+
this.logger.error(logMessage, exception instanceof Error ? exception.stack : undefined, logContext);
|
|
48
|
+
} else if (statusCode >= 400) {
|
|
49
|
+
this.logger.warn(logMessage, logContext);
|
|
50
|
+
}
|
|
51
|
+
response.status(statusCode).json(errorResponse);
|
|
52
|
+
}
|
|
53
|
+
resolveException(exception, requestId, responseTime) {
|
|
54
|
+
const meta = {
|
|
55
|
+
requestId,
|
|
56
|
+
timestamp: new Date().toISOString(),
|
|
57
|
+
responseTime
|
|
58
|
+
};
|
|
59
|
+
if (exception instanceof BaseAppException) {
|
|
60
|
+
return {
|
|
61
|
+
statusCode: exception.getStatus(),
|
|
62
|
+
errorResponse: {
|
|
63
|
+
success: false,
|
|
64
|
+
message: exception.message,
|
|
65
|
+
messageKey: exception.messageKey,
|
|
66
|
+
messageVariables: exception.messageVariables,
|
|
67
|
+
errors: exception.errors,
|
|
68
|
+
_meta: meta
|
|
69
|
+
}
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
if (exception instanceof HttpException) {
|
|
73
|
+
const resolved = this.resolveHttpException(exception);
|
|
74
|
+
return {
|
|
75
|
+
statusCode: resolved.statusCode,
|
|
76
|
+
errorResponse: {
|
|
77
|
+
success: false,
|
|
78
|
+
message: resolved.message,
|
|
79
|
+
messageKey: resolved.messageKey,
|
|
80
|
+
messageVariables: resolved.messageVariables,
|
|
81
|
+
errors: resolved.errors,
|
|
82
|
+
_meta: meta
|
|
83
|
+
}
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
if (exception instanceof QueryFailedError) {
|
|
87
|
+
const resolved = this.resolveQueryFailedError(exception);
|
|
88
|
+
if (resolved) {
|
|
89
|
+
return {
|
|
90
|
+
statusCode: resolved.getStatus(),
|
|
91
|
+
errorResponse: {
|
|
92
|
+
success: false,
|
|
93
|
+
message: resolved.message,
|
|
94
|
+
messageKey: resolved.messageKey,
|
|
95
|
+
messageVariables: resolved.messageVariables,
|
|
96
|
+
_meta: meta
|
|
97
|
+
}
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
return {
|
|
102
|
+
statusCode: HttpStatus.INTERNAL_SERVER_ERROR,
|
|
103
|
+
errorResponse: {
|
|
104
|
+
success: false,
|
|
105
|
+
message: 'Internal server error',
|
|
106
|
+
messageKey: ERROR_MESSAGES.INTERNAL,
|
|
107
|
+
_meta: meta
|
|
108
|
+
}
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
resolveHttpException(exception) {
|
|
112
|
+
const statusCode = exception.getStatus();
|
|
113
|
+
const response = exception.getResponse();
|
|
114
|
+
if (typeof response === 'string') {
|
|
115
|
+
return {
|
|
116
|
+
statusCode,
|
|
117
|
+
message: response,
|
|
118
|
+
messageKey: ERROR_MESSAGES.HTTP
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
if (typeof response === 'object' && response !== null) {
|
|
122
|
+
const obj = response;
|
|
123
|
+
if (Array.isArray(obj.message)) {
|
|
124
|
+
const errors = obj.message.map((msg)=>{
|
|
125
|
+
const [field, ...rest] = msg.split(' ');
|
|
126
|
+
return {
|
|
127
|
+
field: field || 'unknown',
|
|
128
|
+
message: rest.join(' ') || msg
|
|
129
|
+
};
|
|
130
|
+
});
|
|
131
|
+
return {
|
|
132
|
+
statusCode,
|
|
133
|
+
message: 'Validation failed',
|
|
134
|
+
messageKey: ERROR_MESSAGES.VALIDATION,
|
|
135
|
+
errors
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
return {
|
|
139
|
+
statusCode,
|
|
140
|
+
message: String(obj.message || 'Unknown error'),
|
|
141
|
+
messageKey: String(obj.messageKey || ERROR_MESSAGES.HTTP),
|
|
142
|
+
messageVariables: obj.messageVariables,
|
|
143
|
+
errors: obj.errors
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
return {
|
|
147
|
+
statusCode,
|
|
148
|
+
message: 'Unknown error',
|
|
149
|
+
messageKey: ERROR_MESSAGES.UNKNOWN
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
resolveQueryFailedError(exception) {
|
|
153
|
+
const dbError = exception;
|
|
154
|
+
const code = dbError.driverError?.code ?? dbError.code;
|
|
155
|
+
const number = dbError.driverError?.number ?? dbError.number;
|
|
156
|
+
const errno = dbError.driverError?.errno ?? dbError.errno;
|
|
157
|
+
const sqlMessage = dbError.driverError?.sqlMessage ?? '';
|
|
158
|
+
const detail = dbError.driverError?.detail ?? dbError.detail ?? '';
|
|
159
|
+
const table = dbError.driverError?.table ?? dbError.table ?? '';
|
|
160
|
+
const column = dbError.driverError?.column ?? dbError.column;
|
|
161
|
+
const pgField = detail.match(/Key \(([^)]+)\)/)?.[1] ?? column;
|
|
162
|
+
// DUPLICATE / UNIQUE VIOLATION
|
|
163
|
+
// PG: 23505 | MSSQL: 2627 (constraint), 2601 (index) | MySQL: ER_DUP_ENTRY / errno 1062
|
|
164
|
+
if (code === '23505' || number === 2627 || number === 2601 || code === 'ER_DUP_ENTRY' || errno === 1062) {
|
|
165
|
+
const mysqlTable = sqlMessage.match(/for key '([^'.]+)\./)?.[1];
|
|
166
|
+
const mysqlIndex = sqlMessage.match(/for key '(?:[^.]+\.)?([^']+)'/)?.[1];
|
|
167
|
+
const resolvedTable = table || mysqlTable || 'record';
|
|
168
|
+
const resolvedField = pgField ? this.toHumanReadable(pgField) : mysqlIndex ? this.toHumanReadable(mysqlIndex) : undefined;
|
|
169
|
+
return new ConflictException(this.toHumanReadable(resolvedTable), resolvedField);
|
|
170
|
+
}
|
|
171
|
+
// FOREIGN KEY VIOLATION
|
|
172
|
+
// PG: 23503 | MSSQL: 547 | MySQL: ER_NO_REFERENCED_ROW_2/1452 (insert), ER_ROW_IS_REFERENCED_2/1451 (delete)
|
|
173
|
+
if (code === '23503' || number === 547 || code === 'ER_NO_REFERENCED_ROW_2' || code === 'ER_ROW_IS_REFERENCED_2' || errno === 1452 || errno === 1451) {
|
|
174
|
+
const mysqlColumn = sqlMessage.match(/FOREIGN KEY \(`([^`]+)`\)/)?.[1];
|
|
175
|
+
const raw = pgField ?? mysqlColumn;
|
|
176
|
+
const humanField = raw ? this.toHumanReadable(raw) : undefined;
|
|
177
|
+
return new BaseAppException({
|
|
178
|
+
message: humanField ? `Invalid reference for '${humanField}'` : 'Invalid reference',
|
|
179
|
+
messageKey: ERROR_MESSAGES.DB_FOREIGN_KEY,
|
|
180
|
+
messageVariables: humanField ? {
|
|
181
|
+
field: humanField
|
|
182
|
+
} : undefined,
|
|
183
|
+
status: HttpStatus.BAD_REQUEST
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
// NOT NULL VIOLATION
|
|
187
|
+
// PG: 23502 | MSSQL: 515 | MySQL: ER_BAD_NULL_ERROR / errno 1048
|
|
188
|
+
if (code === '23502' || number === 515 || code === 'ER_BAD_NULL_ERROR' || errno === 1048) {
|
|
189
|
+
const mysqlColumn = sqlMessage.match(/Column '([^']+)' cannot be null/)?.[1];
|
|
190
|
+
const humanField = this.toHumanReadable(column ?? mysqlColumn ?? 'field');
|
|
191
|
+
return new BaseAppException({
|
|
192
|
+
message: `Required field '${humanField}' cannot be null`,
|
|
193
|
+
messageKey: ERROR_MESSAGES.DB_NOT_NULL,
|
|
194
|
+
messageVariables: {
|
|
195
|
+
field: humanField
|
|
196
|
+
},
|
|
197
|
+
status: HttpStatus.BAD_REQUEST
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
// INVALID INPUT SYNTAX — PostgreSQL only (MySQL silently coerces types)
|
|
201
|
+
if (code === '22P02') {
|
|
202
|
+
const typeMatch = exception.message.match(/invalid input syntax for(?:\s+type)?\s+(\w+)/i);
|
|
203
|
+
const type = typeMatch?.[1] ?? 'value';
|
|
204
|
+
const humanField = column ? this.toHumanReadable(column) : undefined;
|
|
205
|
+
return new BaseAppException({
|
|
206
|
+
message: humanField ? `Invalid ${type} value for '${humanField}'` : `Invalid ${type} value`,
|
|
207
|
+
messageKey: ERROR_MESSAGES.DB_INVALID_INPUT,
|
|
208
|
+
messageVariables: {
|
|
209
|
+
type,
|
|
210
|
+
...humanField !== undefined && {
|
|
211
|
+
field: humanField
|
|
212
|
+
}
|
|
213
|
+
},
|
|
214
|
+
status: HttpStatus.BAD_REQUEST
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
// VALUE TOO LONG
|
|
218
|
+
// PG: 22001 | MySQL: ER_DATA_TOO_LONG / errno 1406
|
|
219
|
+
if (code === '22001' || code === 'ER_DATA_TOO_LONG' || errno === 1406) {
|
|
220
|
+
const mysqlColumn = sqlMessage.match(/Data too long for column '([^']+)'/)?.[1];
|
|
221
|
+
const resolvedColumn = column ?? mysqlColumn;
|
|
222
|
+
const humanField = resolvedColumn ? this.toHumanReadable(resolvedColumn) : undefined;
|
|
223
|
+
return new BaseAppException({
|
|
224
|
+
message: humanField ? `Value too long for '${humanField}'` : 'Value exceeds maximum length',
|
|
225
|
+
messageKey: ERROR_MESSAGES.DB_VALUE_TOO_LONG,
|
|
226
|
+
messageVariables: humanField !== undefined ? {
|
|
227
|
+
field: humanField
|
|
228
|
+
} : undefined,
|
|
229
|
+
status: HttpStatus.BAD_REQUEST
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
// CHECK CONSTRAINT VIOLATION
|
|
233
|
+
// PG: 23514 | MySQL 8.0+: ER_CHECK_CONSTRAINT_VIOLATED / errno 3819
|
|
234
|
+
if (code === '23514' || code === 'ER_CHECK_CONSTRAINT_VIOLATED' || errno === 3819) {
|
|
235
|
+
const constraint = dbError.driverError?.constraint ?? dbError.constraint;
|
|
236
|
+
const humanTable = this.toHumanReadable(table || 'record');
|
|
237
|
+
return new BaseAppException({
|
|
238
|
+
message: `${humanTable} violates a validation rule`,
|
|
239
|
+
messageKey: ERROR_MESSAGES.DB_CHECK_CONSTRAINT,
|
|
240
|
+
messageVariables: {
|
|
241
|
+
entity: humanTable,
|
|
242
|
+
...constraint !== undefined && {
|
|
243
|
+
constraint
|
|
244
|
+
}
|
|
245
|
+
},
|
|
246
|
+
status: HttpStatus.BAD_REQUEST
|
|
247
|
+
});
|
|
248
|
+
}
|
|
249
|
+
// DEADLOCK
|
|
250
|
+
// PG: 40P01 | MSSQL: 1205 | MySQL: ER_LOCK_DEADLOCK / errno 1213
|
|
251
|
+
if (code === '40P01' || number === 1205 || code === 'ER_LOCK_DEADLOCK' || errno === 1213) {
|
|
252
|
+
return new BaseAppException({
|
|
253
|
+
message: 'The request could not be completed due to a conflict, please try again',
|
|
254
|
+
messageKey: ERROR_MESSAGES.DB_DEADLOCK,
|
|
255
|
+
status: HttpStatus.SERVICE_UNAVAILABLE
|
|
256
|
+
});
|
|
257
|
+
}
|
|
258
|
+
return null;
|
|
259
|
+
}
|
|
260
|
+
toHumanReadable(dbName) {
|
|
261
|
+
return dbName.split(', ').map((part)=>part.split('_').map((word)=>word.toLowerCase() === 'id' ? 'ID' : word.charAt(0).toUpperCase() + word.slice(1)).join(' ')).join(', ');
|
|
262
|
+
}
|
|
263
|
+
constructor(){
|
|
264
|
+
_define_property(this, "logger", new Logger('ExceptionFilter'));
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
GlobalExceptionFilter = _ts_decorate([
|
|
268
|
+
Catch()
|
|
269
|
+
], GlobalExceptionFilter);
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './global-exception.filter';
|
|
@@ -25,7 +25,7 @@ function _ts_param(paramIndex, decorator) {
|
|
|
25
25
|
decorator(target, key, paramIndex);
|
|
26
26
|
};
|
|
27
27
|
}
|
|
28
|
-
import { IS_PUBLIC_KEY } from '../constants';
|
|
28
|
+
import { AUTH_MESSAGES, IS_PUBLIC_KEY } from '../constants';
|
|
29
29
|
import { Inject, Injectable, UnauthorizedException } from '@nestjs/common';
|
|
30
30
|
import { Reflector } from '@nestjs/core';
|
|
31
31
|
import { AuthGuard } from '@nestjs/passport';
|
|
@@ -43,7 +43,10 @@ export class JwtAuthGuard extends AuthGuard('jwt') {
|
|
|
43
43
|
}
|
|
44
44
|
handleRequest(err, user) {
|
|
45
45
|
if (err || !user) {
|
|
46
|
-
throw err || new UnauthorizedException(
|
|
46
|
+
throw err || new UnauthorizedException({
|
|
47
|
+
message: 'Invalid or expired token',
|
|
48
|
+
messageKey: AUTH_MESSAGES.TOKEN_INVALID
|
|
49
|
+
});
|
|
47
50
|
}
|
|
48
51
|
return user;
|
|
49
52
|
}
|
|
@@ -25,13 +25,11 @@ function _ts_param(paramIndex, decorator) {
|
|
|
25
25
|
decorator(target, key, paramIndex);
|
|
26
26
|
};
|
|
27
27
|
}
|
|
28
|
-
import { Inject, Injectable,
|
|
28
|
+
import { Inject, Injectable, Optional, UnauthorizedException } from '@nestjs/common';
|
|
29
29
|
import { Reflector } from '@nestjs/core';
|
|
30
30
|
import { HybridCache } from '../classes/hybrid-cache.class';
|
|
31
|
-
import {
|
|
32
|
-
import { CACHE_INSTANCE, IS_PUBLIC_KEY, LOGGER_INSTANCE, PERMISSION_GUARD_CONFIG, PERMISSIONS_CACHE_PREFIX, PERMISSIONS_KEY } from '../constants';
|
|
31
|
+
import { AUTH_MESSAGES, CACHE_INSTANCE, IS_PUBLIC_KEY, PERMISSION_GUARD_CONFIG, PERMISSIONS_CACHE_PREFIX, PERMISSIONS_KEY } from '../constants';
|
|
33
32
|
import { InsufficientPermissionsException, NoPermissionsFoundException, PermissionSystemUnavailableException } from '../exceptions/permission.exception';
|
|
34
|
-
import { ILogger } from '../interfaces/logger.interface';
|
|
35
33
|
import { PermissionGuardConfig } from '../interfaces/permission.interface';
|
|
36
34
|
export class PermissionGuard {
|
|
37
35
|
async canActivate(context) {
|
|
@@ -45,104 +43,105 @@ export class PermissionGuard {
|
|
|
45
43
|
context.getClass()
|
|
46
44
|
]);
|
|
47
45
|
if (!permissionConfig) return true;
|
|
48
|
-
const { permissions: requiredPermissions, operator } = this.normalizePermissionConfig(permissionConfig);
|
|
49
|
-
if (!requiredPermissions || requiredPermissions.length === 0) return true;
|
|
50
46
|
const request = context.switchToHttp().getRequest();
|
|
51
47
|
const user = request.user;
|
|
52
|
-
if (!user) throw new UnauthorizedException(
|
|
48
|
+
if (!user) throw new UnauthorizedException({
|
|
49
|
+
message: 'Authentication required',
|
|
50
|
+
messageKey: AUTH_MESSAGES.TOKEN_REQUIRED
|
|
51
|
+
});
|
|
53
52
|
if (!this.cache) {
|
|
54
|
-
this.logger.error(`Cache not available (userId: ${user.id})`, undefined, 'PermissionGuard');
|
|
55
53
|
throw new PermissionSystemUnavailableException();
|
|
56
54
|
}
|
|
57
55
|
const userPermissions = await this.getUserPermissions(user);
|
|
58
56
|
if (!userPermissions || userPermissions.length === 0) {
|
|
59
|
-
this.logger.warn(`No permissions found (userId: ${user.id})`, 'PermissionGuard');
|
|
60
57
|
throw new NoPermissionsFoundException();
|
|
61
58
|
}
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
}
|
|
68
|
-
} else {
|
|
69
|
-
this.validateSimplePermissions(requiredPermissions, userPermissions, operator);
|
|
59
|
+
const logicNode = this.normalizeToLogicNode(permissionConfig);
|
|
60
|
+
if (!logicNode) return true;
|
|
61
|
+
const result = this.evaluateLogicNode(logicNode, userPermissions);
|
|
62
|
+
if (!result.passed) {
|
|
63
|
+
throw new InsufficientPermissionsException(result.missingPermissions, result.operator);
|
|
70
64
|
}
|
|
71
65
|
return true;
|
|
72
66
|
}
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
operator: config.operator || 'and'
|
|
81
|
-
};
|
|
82
|
-
}
|
|
83
|
-
validateSimplePermissions(requiredPermissions, userPermissions, operator) {
|
|
84
|
-
if (operator === 'or') {
|
|
85
|
-
const hasAny = requiredPermissions.some((p)=>this.hasPermission(userPermissions, p));
|
|
86
|
-
if (!hasAny) throw new InsufficientPermissionsException(requiredPermissions, 'or');
|
|
87
|
-
} else {
|
|
88
|
-
const hasAll = requiredPermissions.every((p)=>this.hasPermission(userPermissions, p));
|
|
89
|
-
if (!hasAll) {
|
|
90
|
-
const missing = requiredPermissions.filter((p)=>!this.hasPermission(userPermissions, p));
|
|
91
|
-
throw new InsufficientPermissionsException(missing, 'and');
|
|
92
|
-
}
|
|
67
|
+
normalizeToLogicNode(config) {
|
|
68
|
+
// string - single permission
|
|
69
|
+
if (typeof config === 'string') {
|
|
70
|
+
return config ? {
|
|
71
|
+
type: 'action',
|
|
72
|
+
actionId: config
|
|
73
|
+
} : null;
|
|
93
74
|
}
|
|
75
|
+
// string[] - treat as AND of multiple permissions
|
|
76
|
+
if (Array.isArray(config)) {
|
|
77
|
+
if (config.length === 0) return null;
|
|
78
|
+
if (config.length === 1) return {
|
|
79
|
+
type: 'action',
|
|
80
|
+
actionId: config[0]
|
|
81
|
+
};
|
|
82
|
+
return {
|
|
83
|
+
type: 'group',
|
|
84
|
+
operator: 'AND',
|
|
85
|
+
children: config.map((p)=>({
|
|
86
|
+
type: 'action',
|
|
87
|
+
actionId: p
|
|
88
|
+
}))
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
// SimplePermissionConfig - { permissions: string[], operator: 'AND'|'OR' }
|
|
92
|
+
if ('permissions' in config && Array.isArray(config.permissions)) {
|
|
93
|
+
const simple = config;
|
|
94
|
+
if (simple.permissions.length === 0) return null;
|
|
95
|
+
if (simple.permissions.length === 1) return {
|
|
96
|
+
type: 'action',
|
|
97
|
+
actionId: simple.permissions[0]
|
|
98
|
+
};
|
|
99
|
+
return {
|
|
100
|
+
type: 'group',
|
|
101
|
+
operator: simple.operator || 'AND',
|
|
102
|
+
children: simple.permissions.map((p)=>({
|
|
103
|
+
type: 'action',
|
|
104
|
+
actionId: p
|
|
105
|
+
}))
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
// ILogicNode
|
|
109
|
+
return config;
|
|
94
110
|
}
|
|
95
|
-
|
|
96
|
-
if (
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
111
|
+
evaluateLogicNode(node, userPermissions) {
|
|
112
|
+
if (node.type === 'action') {
|
|
113
|
+
const passed = this.hasPermission(userPermissions, node.actionId);
|
|
114
|
+
return {
|
|
115
|
+
passed,
|
|
116
|
+
missingPermissions: passed ? [] : [
|
|
117
|
+
node.actionId
|
|
118
|
+
],
|
|
119
|
+
operator: 'AND'
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
// Group node
|
|
123
|
+
const { operator, children } = node;
|
|
124
|
+
// SECURITY: Fail-closed - deny access when no children configured
|
|
125
|
+
if (!children || children.length === 0) {
|
|
103
126
|
return {
|
|
104
127
|
passed: false,
|
|
105
|
-
message: 'No permissions configured - access denied by default',
|
|
106
128
|
missingPermissions: [],
|
|
107
129
|
operator
|
|
108
130
|
};
|
|
109
131
|
}
|
|
110
132
|
const results = [];
|
|
111
|
-
const failureDetails = [];
|
|
112
133
|
const missingPermissions = [];
|
|
113
|
-
if (permissions.length > 0) {
|
|
114
|
-
if (operator === 'or') {
|
|
115
|
-
const hasAny = permissions.some((p)=>this.hasPermission(userPermissions, p));
|
|
116
|
-
results.push(hasAny);
|
|
117
|
-
if (!hasAny) {
|
|
118
|
-
failureDetails.push(`needs one of: [${permissions.join(', ')}]`);
|
|
119
|
-
missingPermissions.push(...permissions);
|
|
120
|
-
}
|
|
121
|
-
} else {
|
|
122
|
-
const hasAll = permissions.every((p)=>this.hasPermission(userPermissions, p));
|
|
123
|
-
results.push(hasAll);
|
|
124
|
-
if (!hasAll) {
|
|
125
|
-
const missing = permissions.filter((p)=>!this.hasPermission(userPermissions, p));
|
|
126
|
-
failureDetails.push(`missing: [${missing.join(', ')}]`);
|
|
127
|
-
missingPermissions.push(...missing);
|
|
128
|
-
}
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
134
|
for (const child of children){
|
|
132
|
-
const childResult = this.
|
|
135
|
+
const childResult = this.evaluateLogicNode(child, userPermissions);
|
|
133
136
|
results.push(childResult.passed);
|
|
134
137
|
if (!childResult.passed) {
|
|
135
|
-
failureDetails.push(`(${childResult.message})`);
|
|
136
138
|
missingPermissions.push(...childResult.missingPermissions);
|
|
137
139
|
}
|
|
138
140
|
}
|
|
139
|
-
|
|
140
|
-
const passed = operator === 'or' ? results.some((r)=>r) : results.every((r)=>r);
|
|
141
|
-
const message = passed ? 'OK' : `Denied: ${failureDetails.join(` ${operator.toUpperCase()} `)}`;
|
|
141
|
+
const passed = operator === 'OR' ? results.some((r)=>r) : results.every((r)=>r);
|
|
142
142
|
return {
|
|
143
143
|
passed,
|
|
144
|
-
|
|
145
|
-
missingPermissions,
|
|
144
|
+
missingPermissions: passed ? [] : missingPermissions,
|
|
146
145
|
operator
|
|
147
146
|
};
|
|
148
147
|
}
|
|
@@ -167,11 +166,10 @@ export class PermissionGuard {
|
|
|
167
166
|
}
|
|
168
167
|
return false;
|
|
169
168
|
}
|
|
170
|
-
constructor(reflector, cache, config
|
|
169
|
+
constructor(reflector, cache, config){
|
|
171
170
|
_define_property(this, "reflector", void 0);
|
|
172
171
|
_define_property(this, "cache", void 0);
|
|
173
172
|
_define_property(this, "config", void 0);
|
|
174
|
-
_define_property(this, "logger", void 0);
|
|
175
173
|
this.reflector = reflector;
|
|
176
174
|
this.cache = cache;
|
|
177
175
|
this.config = {
|
|
@@ -180,7 +178,6 @@ export class PermissionGuard {
|
|
|
180
178
|
companyPermissionKeyFormat: `${PERMISSIONS_CACHE_PREFIX}:company:{companyId}:branch:{branchId}:user:{userId}`,
|
|
181
179
|
...config
|
|
182
180
|
};
|
|
183
|
-
this.logger = logger || new NestLoggerAdapter(new Logger(PermissionGuard.name));
|
|
184
181
|
}
|
|
185
182
|
}
|
|
186
183
|
PermissionGuard = _ts_decorate([
|
|
@@ -190,13 +187,10 @@ PermissionGuard = _ts_decorate([
|
|
|
190
187
|
_ts_param(1, Inject(CACHE_INSTANCE)),
|
|
191
188
|
_ts_param(2, Optional()),
|
|
192
189
|
_ts_param(2, Inject(PERMISSION_GUARD_CONFIG)),
|
|
193
|
-
_ts_param(3, Optional()),
|
|
194
|
-
_ts_param(3, Inject(LOGGER_INSTANCE)),
|
|
195
190
|
_ts_metadata("design:type", Function),
|
|
196
191
|
_ts_metadata("design:paramtypes", [
|
|
197
192
|
typeof Reflector === "undefined" ? Object : Reflector,
|
|
198
193
|
typeof HybridCache === "undefined" ? Object : HybridCache,
|
|
199
|
-
typeof PermissionGuardConfig === "undefined" ? Object : PermissionGuardConfig
|
|
200
|
-
typeof ILogger === "undefined" ? Object : ILogger
|
|
194
|
+
typeof PermissionGuardConfig === "undefined" ? Object : PermissionGuardConfig
|
|
201
195
|
])
|
|
202
196
|
], PermissionGuard);
|
package/fesm/index.js
CHANGED
|
@@ -3,7 +3,9 @@ export * from './constants';
|
|
|
3
3
|
export * from './decorators';
|
|
4
4
|
export * from './dtos';
|
|
5
5
|
export * from './entities';
|
|
6
|
+
export * from './enums';
|
|
6
7
|
export * from './exceptions';
|
|
8
|
+
export * from './filters';
|
|
7
9
|
export * from './guards';
|
|
8
10
|
export * from './interceptors';
|
|
9
11
|
export * from './interfaces';
|
|
@@ -29,7 +29,7 @@ import { ConflictException, Inject, Injectable, Optional } from '@nestjs/common'
|
|
|
29
29
|
import { of } from 'rxjs';
|
|
30
30
|
import { tap } from 'rxjs/operators';
|
|
31
31
|
import { HybridCache } from '../classes/hybrid-cache.class';
|
|
32
|
-
import { CACHE_INSTANCE, IDEMPOTENCY_CACHE_PREFIX, IDEMPOTENCY_KEY_HEADER } from '../constants';
|
|
32
|
+
import { CACHE_INSTANCE, IDEMPOTENCY_CACHE_PREFIX, IDEMPOTENCY_KEY_HEADER, SYSTEM_MESSAGES } from '../constants';
|
|
33
33
|
export class IdempotencyInterceptor {
|
|
34
34
|
async intercept(context, next) {
|
|
35
35
|
// Skip if no cache available
|
|
@@ -51,7 +51,7 @@ export class IdempotencyInterceptor {
|
|
|
51
51
|
throw new ConflictException({
|
|
52
52
|
success: false,
|
|
53
53
|
message: 'Request is already being processed',
|
|
54
|
-
|
|
54
|
+
messageKey: SYSTEM_MESSAGES.DUPLICATE_REQUEST
|
|
55
55
|
});
|
|
56
56
|
}
|
|
57
57
|
// Return cached response
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
export * from './delete-empty-id-from-body.interceptor';
|
|
2
2
|
export * from './idempotency.interceptor';
|
|
3
|
-
export * from './query-performance.interceptor';
|
|
4
3
|
export * from './response-meta.interceptor';
|
|
5
4
|
export * from './set-user-field-on-body.interceptor';
|
|
6
5
|
export * from './slug.interceptor';
|
|
@@ -1,20 +1 @@
|
|
|
1
|
-
|
|
2
|
-
* Interface for DataSource providers used by RequestScopedApiService.
|
|
3
|
-
* Implement this interface in each module's DataSourceProvider to ensure type safety.
|
|
4
|
-
*
|
|
5
|
-
* @example
|
|
6
|
-
* ```typescript
|
|
7
|
-
* @Injectable()
|
|
8
|
-
* export class IAMDataSourceProvider implements IDataSourceProvider {
|
|
9
|
-
* constructor(private readonly dataSource: DataSource) {}
|
|
10
|
-
*
|
|
11
|
-
* async getRepository<T extends ObjectLiteral>(entity: EntityTarget<T>): Promise<Repository<T>> {
|
|
12
|
-
* return this.dataSource.getRepository(entity);
|
|
13
|
-
* }
|
|
14
|
-
*
|
|
15
|
-
* async getDataSource(): Promise<DataSource> {
|
|
16
|
-
* return this.dataSource;
|
|
17
|
-
* }
|
|
18
|
-
* }
|
|
19
|
-
* ```
|
|
20
|
-
*/ export { };
|
|
1
|
+
export { };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const EVENT_MANAGER_ADAPTER = 'EVENT_MANAGER_ADAPTER';
|
package/fesm/interfaces/index.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
export * from './api.interface';
|
|
2
2
|
export * from './datasource.interface';
|
|
3
|
+
export * from './event-manager-adapter.interface';
|
|
3
4
|
export * from './identity.interface';
|
|
4
5
|
export * from './logged-user-info.interface';
|
|
5
|
-
export * from './logger.interface';
|
|
6
6
|
export * from './module-config.interface';
|
|
7
|
+
export * from './notification-adapter.interface';
|
|
7
8
|
export * from './permission.interface';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const NOTIFICATION_ADAPTER = 'NOTIFICATION_ADAPTER';
|