@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 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
@@ -0,0 +1,6 @@
1
+ {
2
+ "trailingComma": "es5",
3
+ "tabWidth": 2,
4
+ "semi": true,
5
+ "singleQuote": true
6
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@just-tracking/shared",
3
- "version": "1.0.2",
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
  }
@@ -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
- let app
10
-
11
- if(SLACK_SIGNING_SECRET && SLACK_BOT_TOKEN && SLACK_ENABLED) {
12
- app = new Slack.App({
13
- signingSecret: process.env.SLACK_SIGNING_SECRET,
14
- token: process.env.SLACK_BOT_TOKEN,
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
- AUTHENTICATION: {
20
- label: 'AUTHENTICATION',
21
- status: 401
22
- },
23
- UNAUTHORIZED: {
24
- label: 'UNAUTHORIZED',
25
- status: 403
26
- },
27
- VALIDATION: {
28
- label: 'VALIDATION',
29
- status: 400
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
- data;
35
- type;
36
- status;
37
-
38
- constructor(message, data = {}, type = 'GENERAL', status) {
39
- super(message);
40
- this.data = data;
41
- this.status = status;
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
- constructor(message, status, data = {}) {
48
- super(message, data, ERRORS['AUTHENTICATION'].label, ERRORS['AUTHENTICATION'].status);
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
- constructor(message, data = {}) {
54
- super(message, data, ERRORS['VALIDATION'].label, ERRORS['VALIDATION'].status);
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
- constructor(message, data = {}) {
60
- super(message, data, ERRORS['UNAUTHORIZED'].label, ERRORS['UNAUTHORIZED'].status);
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
- console.log('err', err)
66
- const status = err.status || 500;
67
- const { status: errStatus, ...errData } = typeof err === 'object' ? err : {};
68
-
69
- const errorBody = {
70
- status,
71
- method: req.method,
72
- url: req.url,
73
- headers: req.headers,
74
- body: req.body,
75
- stack: err instanceof Error ? err.stack : undefined,
76
- };
77
- console.log('errorBody', errorBody)
78
-
79
- if(status !== 401 && status !== 403) {
80
- logger.error(err?.message || err?.data?.error, { ...errorBody, err });
81
- }
82
- console.log('status', status)
83
-
84
- if(app && SLACK_ENABLED && status === 500) {
85
- const message = `
86
- *Error Report*
87
- *Status:* ${errorBody.status}
88
- *Method:* ${errorBody.method}
89
- *URL:* ${errorBody.url}
90
- *Headers:* ${JSON.stringify(errorBody.headers)}
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
- return res.status(status).json({
122
- success: false,
123
- status: status,
124
- error: err.message,
125
- data: err.data,
126
- type: err.type
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
- AuthenticationError,
132
- UnauthorizedError,
133
- ValidationError,
134
- errorHandler
135
- }
207
+ AuthenticationError,
208
+ UnauthorizedError,
209
+ ValidationError,
210
+ CustomError,
211
+ errorHandler,
212
+ };
@@ -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
- fs.mkdirSync(logDirectory);
10
+ fs.mkdirSync(logDirectory);
11
11
  }
12
12
 
13
13
  const logger = winston.createLogger({
14
- level: 'debug',
15
- format: combine(errors({ stack: true }), timestamp({ format: 'YYYY-MM-DD HH:mm:ss' }), json(), prettyPrint()),
16
- transports: [
17
- new winston.transports.Console(),
18
- new DailyRotateFile({
19
- level: 'error',
20
- filename: path.join(logDirectory, 'app-%DATE%.log'),
21
- datePattern: 'YYYY-MM-DD',
22
- zippedArchive: true
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
- return Object.keys(cookies)
6
- .map((key) => `${key}=${cookies[key]}`)
7
- .join(';');
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
- try {
12
- const { data } = await axios.get(
13
- `${process.env.AUTH_MICROSERVICE_URL}/user`,
14
- {
15
- headers: {
16
- Authorization: req.headers?.authorization ? req.headers['authorization'] : '',
17
- Cookie: formatCookies(req?.cookies ? req.cookies : {})
18
- },
19
- }
20
- );
21
- req.user = data;
22
- return next();
23
- } catch (error) {
24
- console.error(error);
25
- return next(new AuthenticationError(error.response?.data?.message || error.response?.data?.error));
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
- return async (req, res, next) => {
31
- try {
32
- const { data } = await axios.get(
33
- `${process.env.AUTH_MICROSERVICE_URL}/user/can`,
34
- {
35
- params: {
36
- name: permission,
37
- },
38
- headers: {
39
- Authorization: req.headers['authorization'],
40
- Cookie: formatCookies(req.cookies)
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
- auth,
56
- can
57
- };
69
+ auth,
70
+ can,
71
+ };
@@ -1,18 +1,18 @@
1
1
  module.exports = formatResponse = (req, res, next) => {
2
- res.apiResponse = (status, success, message = null, data = null) => {
3
- const response = {
4
- status,
5
- success
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
- next();
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
+ };