@just-tracking/shared 1.0.2 → 1.0.3
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/.eslintrc.json +20 -0
- package/.prettierrc +6 -0
- package/package.json +11 -2
- package/src/errors/errors.js +186 -109
- package/src/logger/logger.js +18 -13
- package/src/middlewares/auth.middleware.js +55 -41
- package/src/response/response.js +16 -16
package/.eslintrc.json
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
{
|
|
2
|
+
"env": {
|
|
3
|
+
"browser": true,
|
|
4
|
+
"es2021": true,
|
|
5
|
+
"node": true
|
|
6
|
+
},
|
|
7
|
+
"extends": ["eslint:recommended", "prettier"],
|
|
8
|
+
"overrides": [],
|
|
9
|
+
"parserOptions": {
|
|
10
|
+
"ecmaVersion": 2020,
|
|
11
|
+
"sourceType": "module"
|
|
12
|
+
},
|
|
13
|
+
"plugins": ["prettier", "import"],
|
|
14
|
+
"rules": {
|
|
15
|
+
"prettier/prettier": "error",
|
|
16
|
+
"camelcase": "error",
|
|
17
|
+
"no-multiple-empty-lines": ["error", { "max": 4, "maxEOF": 1 }],
|
|
18
|
+
"object-shorthand": ["error"]
|
|
19
|
+
}
|
|
20
|
+
}
|
package/.prettierrc
ADDED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@just-tracking/shared",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.3",
|
|
4
4
|
"main": "index.js",
|
|
5
5
|
"author": "Sargis Yeritsyan <sargis021996@gmail.com>",
|
|
6
6
|
"license": "MIT",
|
|
@@ -21,5 +21,14 @@
|
|
|
21
21
|
"winston": "^3.11.0",
|
|
22
22
|
"winston-daily-rotate-file": "^5.0.0"
|
|
23
23
|
},
|
|
24
|
-
"devDependencies": {
|
|
24
|
+
"devDependencies": {
|
|
25
|
+
"@babel/eslint-parser": "7.19.1",
|
|
26
|
+
"babel-eslint": "10.1.0",
|
|
27
|
+
"eslint": "8.27.0",
|
|
28
|
+
"eslint-config-prettier": "8.5.0",
|
|
29
|
+
"eslint-plugin-import": "2.26.0",
|
|
30
|
+
"eslint-plugin-prettier": "4.2.1",
|
|
31
|
+
"nodemon": "3.1.9",
|
|
32
|
+
"prettier": "2.7.1"
|
|
33
|
+
}
|
|
25
34
|
}
|
package/src/errors/errors.js
CHANGED
|
@@ -1,135 +1,212 @@
|
|
|
1
1
|
const logger = require('../logger/logger');
|
|
2
2
|
const Slack = require('@slack/bolt');
|
|
3
3
|
|
|
4
|
-
const SLACK_ENABLED =process?.env?.SLACK_ENABLED === 'true'
|
|
5
|
-
const SLACK_SIGNING_SECRET = process?.env?.SLACK_SIGNING_SECRET
|
|
6
|
-
const SLACK_BOT_TOKEN = process?.env?.SLACK_BOT_TOKEN
|
|
7
|
-
const SLACK_CHANNEL = process?.env?.SLACK_CHANNEL
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
4
|
+
const SLACK_ENABLED = process?.env?.SLACK_ENABLED === 'true';
|
|
5
|
+
const SLACK_SIGNING_SECRET = process?.env?.SLACK_SIGNING_SECRET;
|
|
6
|
+
const SLACK_BOT_TOKEN = process?.env?.SLACK_BOT_TOKEN;
|
|
7
|
+
const SLACK_CHANNEL = process?.env?.SLACK_CHANNEL;
|
|
8
|
+
const NODE_ENV = process?.env?.NODE_ENV || 'development';
|
|
9
|
+
|
|
10
|
+
let app;
|
|
11
|
+
|
|
12
|
+
if (SLACK_SIGNING_SECRET && SLACK_BOT_TOKEN && SLACK_ENABLED) {
|
|
13
|
+
app = new Slack.App({
|
|
14
|
+
signingSecret: SLACK_SIGNING_SECRET,
|
|
15
|
+
token: SLACK_BOT_TOKEN,
|
|
16
|
+
});
|
|
16
17
|
}
|
|
17
18
|
|
|
18
19
|
const ERRORS = {
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
}
|
|
20
|
+
AUTHENTICATION: {
|
|
21
|
+
label: 'AUTHENTICATION',
|
|
22
|
+
status: 401,
|
|
23
|
+
},
|
|
24
|
+
UNAUTHORIZED: {
|
|
25
|
+
label: 'UNAUTHORIZED',
|
|
26
|
+
status: 403,
|
|
27
|
+
},
|
|
28
|
+
VALIDATION: {
|
|
29
|
+
label: 'VALIDATION',
|
|
30
|
+
status: 400,
|
|
31
|
+
},
|
|
32
|
+
};
|
|
32
33
|
|
|
33
34
|
class CustomError extends Error {
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
this.type = type;
|
|
43
|
-
}
|
|
35
|
+
constructor(message, data = {}, type = 'GENERAL', status) {
|
|
36
|
+
super(message);
|
|
37
|
+
this.name = this.constructor.name;
|
|
38
|
+
this.data = data;
|
|
39
|
+
this.status = status;
|
|
40
|
+
this.type = type;
|
|
41
|
+
Error.captureStackTrace(this, this.constructor);
|
|
42
|
+
}
|
|
44
43
|
}
|
|
45
44
|
|
|
46
45
|
class AuthenticationError extends CustomError {
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
46
|
+
constructor(message, data = {}) {
|
|
47
|
+
super(
|
|
48
|
+
message,
|
|
49
|
+
data,
|
|
50
|
+
ERRORS['AUTHENTICATION'].label,
|
|
51
|
+
ERRORS['AUTHENTICATION'].status
|
|
52
|
+
);
|
|
53
|
+
}
|
|
50
54
|
}
|
|
51
55
|
|
|
52
56
|
class ValidationError extends CustomError {
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
57
|
+
constructor(message, data = {}) {
|
|
58
|
+
super(
|
|
59
|
+
message,
|
|
60
|
+
data,
|
|
61
|
+
ERRORS['VALIDATION'].label,
|
|
62
|
+
ERRORS['VALIDATION'].status
|
|
63
|
+
);
|
|
64
|
+
}
|
|
56
65
|
}
|
|
57
66
|
|
|
58
67
|
class UnauthorizedError extends CustomError {
|
|
59
|
-
|
|
60
|
-
|
|
68
|
+
constructor(message, data = {}) {
|
|
69
|
+
super(
|
|
70
|
+
message,
|
|
71
|
+
data,
|
|
72
|
+
ERRORS['UNAUTHORIZED'].label,
|
|
73
|
+
ERRORS['UNAUTHORIZED'].status
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Helper to sanitize sensitive data
|
|
79
|
+
function sanitizeData(data) {
|
|
80
|
+
if (!data || typeof data !== 'object') return data;
|
|
81
|
+
|
|
82
|
+
const sensitive = [
|
|
83
|
+
'password',
|
|
84
|
+
'token',
|
|
85
|
+
'apiKey',
|
|
86
|
+
'secret',
|
|
87
|
+
'authorization',
|
|
88
|
+
'cookie',
|
|
89
|
+
];
|
|
90
|
+
const sanitized = Array.isArray(data) ? [...data] : { ...data };
|
|
91
|
+
|
|
92
|
+
for (const key in sanitized) {
|
|
93
|
+
if (sensitive.some((s) => key.toLowerCase().includes(s))) {
|
|
94
|
+
sanitized[key] = '[REDACTED]';
|
|
95
|
+
} else if (typeof sanitized[key] === 'object' && sanitized[key] !== null) {
|
|
96
|
+
sanitized[key] = sanitizeData(sanitized[key]);
|
|
61
97
|
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return sanitized;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Helper to send Slack notification with retry logic
|
|
104
|
+
async function sendSlackNotification(errorBody) {
|
|
105
|
+
if (!app || !SLACK_ENABLED) return;
|
|
106
|
+
|
|
107
|
+
try {
|
|
108
|
+
const message = `
|
|
109
|
+
*🚨 Production Error Report*
|
|
110
|
+
*Status:* ${errorBody.status}
|
|
111
|
+
*Method:* ${errorBody.method}
|
|
112
|
+
*URL:* ${errorBody.url}
|
|
113
|
+
*Message:* ${errorBody.message}
|
|
114
|
+
*Stack Trace:*
|
|
115
|
+
\`\`\`
|
|
116
|
+
${errorBody.stack?.substring(0, 2000) || 'No stack trace available'}
|
|
117
|
+
\`\`\`
|
|
118
|
+
`.trim();
|
|
119
|
+
|
|
120
|
+
await app.client.chat.postMessage({
|
|
121
|
+
token: SLACK_BOT_TOKEN,
|
|
122
|
+
channel: SLACK_CHANNEL,
|
|
123
|
+
text: message,
|
|
124
|
+
});
|
|
125
|
+
} catch (slackError) {
|
|
126
|
+
// Log Slack notification failure but don't throw
|
|
127
|
+
logger.error('Failed to send Slack notification', {
|
|
128
|
+
error: slackError.message,
|
|
129
|
+
originalError: errorBody.message,
|
|
130
|
+
});
|
|
131
|
+
}
|
|
62
132
|
}
|
|
63
133
|
|
|
64
134
|
async function errorHandler(err, req, res, next) {
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
*Body:* ${JSON.stringify(errorBody.body)}
|
|
92
|
-
*Stack Trace:* ${errorBody.stack}
|
|
93
|
-
`;
|
|
94
|
-
|
|
95
|
-
await app.client.chat.postMessage({
|
|
96
|
-
token: SLACK_BOT_TOKEN,
|
|
97
|
-
channel: SLACK_CHANNEL,
|
|
98
|
-
text: message,
|
|
99
|
-
});
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
if (err.name === 'ValidationError') {
|
|
103
|
-
console.log("ValidationError err", err);
|
|
104
|
-
if (err.details && err.details.length) {
|
|
105
|
-
|
|
106
|
-
const errors = {};
|
|
107
|
-
|
|
108
|
-
err.details.forEach((item) => {
|
|
109
|
-
errors[item.context.key] = item.message
|
|
110
|
-
})
|
|
111
|
-
|
|
112
|
-
return res.status(ERRORS['VALIDATION'].status).json({
|
|
113
|
-
success: false,
|
|
114
|
-
status: ERRORS['VALIDATION'].status,
|
|
115
|
-
error: ERRORS['VALIDATION'].label,
|
|
116
|
-
data: errors
|
|
117
|
-
})
|
|
118
|
-
}
|
|
119
|
-
}
|
|
135
|
+
const status = err.status || 500;
|
|
136
|
+
const isProduction = NODE_ENV === 'production';
|
|
137
|
+
|
|
138
|
+
// Build error body with sanitized data
|
|
139
|
+
const errorBody = {
|
|
140
|
+
status,
|
|
141
|
+
message: err.message || 'Internal server error',
|
|
142
|
+
method: req.method,
|
|
143
|
+
url: req.url,
|
|
144
|
+
timestamp: new Date().toISOString(),
|
|
145
|
+
...(isProduction
|
|
146
|
+
? {}
|
|
147
|
+
: {
|
|
148
|
+
headers: sanitizeData(req.headers),
|
|
149
|
+
body: sanitizeData(req.body),
|
|
150
|
+
stack: err.stack,
|
|
151
|
+
}),
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
// Log appropriately based on status
|
|
155
|
+
if (status >= 500) {
|
|
156
|
+
logger.error(errorBody.message, {
|
|
157
|
+
...errorBody,
|
|
158
|
+
errorType: err.type,
|
|
159
|
+
errorData: err.data,
|
|
160
|
+
});
|
|
120
161
|
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
162
|
+
// Send Slack notification for 500 errors only
|
|
163
|
+
if (isProduction && status === 500) {
|
|
164
|
+
// Don't await - fire and forget to not block response
|
|
165
|
+
sendSlackNotification({
|
|
166
|
+
...errorBody,
|
|
167
|
+
stack: err.stack,
|
|
168
|
+
}).catch(() => {}); // Silently fail if Slack notification fails
|
|
169
|
+
}
|
|
170
|
+
} else if (status >= 400 && status < 500) {
|
|
171
|
+
logger.warn(errorBody.message, {
|
|
172
|
+
...errorBody,
|
|
173
|
+
errorType: err.type,
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// Handle Joi/validation errors
|
|
178
|
+
if (err.name === 'ValidationError' && err.details?.length) {
|
|
179
|
+
const errors = err.details.reduce((acc, item) => {
|
|
180
|
+
acc[item.context.key] = item.message;
|
|
181
|
+
return acc;
|
|
182
|
+
}, {});
|
|
183
|
+
|
|
184
|
+
return res.status(ERRORS['VALIDATION'].status).json({
|
|
185
|
+
success: false,
|
|
186
|
+
status: ERRORS['VALIDATION'].status,
|
|
187
|
+
error: ERRORS['VALIDATION'].label,
|
|
188
|
+
data: errors,
|
|
127
189
|
});
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// Build response based on environment
|
|
193
|
+
const response = {
|
|
194
|
+
success: false,
|
|
195
|
+
status,
|
|
196
|
+
error:
|
|
197
|
+
isProduction && status === 500 ? 'Internal server error' : err.message,
|
|
198
|
+
...(err.type && { type: err.type }),
|
|
199
|
+
...(err.data && Object.keys(err.data).length > 0 && { data: err.data }),
|
|
200
|
+
...(!isProduction && err.stack && { stack: err.stack }),
|
|
201
|
+
};
|
|
202
|
+
|
|
203
|
+
return res.status(status).json(response);
|
|
128
204
|
}
|
|
129
205
|
|
|
130
206
|
module.exports = {
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
207
|
+
AuthenticationError,
|
|
208
|
+
UnauthorizedError,
|
|
209
|
+
ValidationError,
|
|
210
|
+
CustomError,
|
|
211
|
+
errorHandler,
|
|
212
|
+
};
|
package/src/logger/logger.js
CHANGED
|
@@ -7,21 +7,26 @@ const DailyRotateFile = require('winston-daily-rotate-file');
|
|
|
7
7
|
const logDirectory = path.resolve(__dirname, '../../../../logs');
|
|
8
8
|
|
|
9
9
|
if (!fs.existsSync(logDirectory)) {
|
|
10
|
-
|
|
10
|
+
fs.mkdirSync(logDirectory);
|
|
11
11
|
}
|
|
12
12
|
|
|
13
13
|
const logger = winston.createLogger({
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
14
|
+
level: 'debug',
|
|
15
|
+
format: combine(
|
|
16
|
+
errors({ stack: true }),
|
|
17
|
+
timestamp({ format: 'YYYY-MM-DD HH:mm:ss' }),
|
|
18
|
+
json(),
|
|
19
|
+
prettyPrint()
|
|
20
|
+
),
|
|
21
|
+
transports: [
|
|
22
|
+
new winston.transports.Console(),
|
|
23
|
+
new DailyRotateFile({
|
|
24
|
+
level: 'error',
|
|
25
|
+
filename: path.join(logDirectory, 'app-%DATE%.log'),
|
|
26
|
+
datePattern: 'YYYY-MM-DD',
|
|
27
|
+
zippedArchive: true,
|
|
28
|
+
}),
|
|
29
|
+
],
|
|
25
30
|
});
|
|
26
31
|
|
|
27
|
-
module.exports = logger;
|
|
32
|
+
module.exports = logger;
|
|
@@ -1,57 +1,71 @@
|
|
|
1
1
|
const axios = require('axios');
|
|
2
2
|
const { UnauthorizedError, AuthenticationError } = require('../errors/errors');
|
|
3
|
+
const NODE_ENV = process.env.NODE_ENV;
|
|
3
4
|
|
|
4
5
|
function formatCookies(cookies) {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
6
|
+
return Object.keys(cookies)
|
|
7
|
+
.map((key) => `${key}=${cookies[key]}`)
|
|
8
|
+
.join(';');
|
|
8
9
|
}
|
|
9
10
|
|
|
10
11
|
async function auth(req, res, next) {
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
12
|
+
try {
|
|
13
|
+
const { data } = await axios.get(
|
|
14
|
+
`${process.env.AUTH_MICROSERVICE_URL}/user`,
|
|
15
|
+
{
|
|
16
|
+
headers: {
|
|
17
|
+
Authorization: req.headers?.authorization
|
|
18
|
+
? req.headers['authorization']
|
|
19
|
+
: '',
|
|
20
|
+
Cookie: formatCookies(req?.cookies ? req.cookies : {}),
|
|
21
|
+
},
|
|
22
|
+
}
|
|
23
|
+
);
|
|
24
|
+
req.user = data;
|
|
25
|
+
return next();
|
|
26
|
+
} catch (error) {
|
|
27
|
+
console.error(error.message);
|
|
28
|
+
return next(
|
|
29
|
+
new AuthenticationError(
|
|
30
|
+
error.response?.data?.message || error.response?.data?.error
|
|
31
|
+
)
|
|
32
|
+
);
|
|
33
|
+
}
|
|
27
34
|
}
|
|
28
35
|
|
|
29
36
|
function can(permission) {
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
}
|
|
43
|
-
);
|
|
44
|
-
|
|
45
|
-
if (!data) return next(new UnauthorizedError("You are not authorized to access this endpoint"));
|
|
46
|
-
else return next();
|
|
47
|
-
} catch (error) {
|
|
48
|
-
return next(new UnauthorizedError("You are not authorized to access this endpoint"))
|
|
37
|
+
return async (req, res, next) => {
|
|
38
|
+
try {
|
|
39
|
+
const { data } = await axios.get(
|
|
40
|
+
`${process.env.AUTH_MICROSERVICE_URL}/user/can`,
|
|
41
|
+
{
|
|
42
|
+
params: {
|
|
43
|
+
name: permission,
|
|
44
|
+
},
|
|
45
|
+
headers: {
|
|
46
|
+
Authorization: req.headers['authorization'],
|
|
47
|
+
Cookie: formatCookies(req.cookies),
|
|
48
|
+
},
|
|
49
49
|
}
|
|
50
|
+
);
|
|
50
51
|
|
|
52
|
+
if (!data) {
|
|
53
|
+
return next(
|
|
54
|
+
new UnauthorizedError(
|
|
55
|
+
'You are not authorized to access this endpoint'
|
|
56
|
+
)
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
return next();
|
|
60
|
+
} catch (error) {
|
|
61
|
+
return next(
|
|
62
|
+
new UnauthorizedError('You are not authorized to access this endpoint')
|
|
63
|
+
);
|
|
51
64
|
}
|
|
65
|
+
};
|
|
52
66
|
}
|
|
53
67
|
|
|
54
68
|
module.exports = {
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
};
|
|
69
|
+
auth,
|
|
70
|
+
can,
|
|
71
|
+
};
|
package/src/response/response.js
CHANGED
|
@@ -1,18 +1,18 @@
|
|
|
1
1
|
module.exports = formatResponse = (req, res, next) => {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
};
|
|
7
|
-
if (data) {
|
|
8
|
-
response.data = data;
|
|
9
|
-
}
|
|
10
|
-
if (message && success) {
|
|
11
|
-
response.message = message;
|
|
12
|
-
} else if (message) {
|
|
13
|
-
response.error = message;
|
|
14
|
-
}
|
|
15
|
-
res.status(status).json(response);
|
|
2
|
+
res.apiResponse = (status, success, message = null, data = null) => {
|
|
3
|
+
const response = {
|
|
4
|
+
status,
|
|
5
|
+
success,
|
|
16
6
|
};
|
|
17
|
-
|
|
18
|
-
|
|
7
|
+
if (data) {
|
|
8
|
+
response.data = data;
|
|
9
|
+
}
|
|
10
|
+
if (message && success) {
|
|
11
|
+
response.message = message;
|
|
12
|
+
} else if (message) {
|
|
13
|
+
response.error = message;
|
|
14
|
+
}
|
|
15
|
+
res.status(status).json(response);
|
|
16
|
+
};
|
|
17
|
+
next();
|
|
18
|
+
};
|