@just-tracking/shared 1.0.0
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/.env.example +1 -0
- package/.github/workflows/publish.yml +26 -0
- package/index.js +12 -0
- package/package.json +25 -0
- package/src/errors/errors.js +135 -0
- package/src/kafka/topics.js +21 -0
- package/src/logger/logger.js +27 -0
- package/src/middlewares/auth.middleware.js +57 -0
- package/src/response/response.js +18 -0
- package/src/wrappers/rabbitBroker.js +150 -0
package/.env.example
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
AUTH_MICROSERVICE_URL=""
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
name: Publish to npm
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches:
|
|
6
|
+
- master
|
|
7
|
+
workflow_dispatch:
|
|
8
|
+
|
|
9
|
+
jobs:
|
|
10
|
+
publish:
|
|
11
|
+
runs-on: ubuntu-latest
|
|
12
|
+
steps:
|
|
13
|
+
- name: Checkout
|
|
14
|
+
uses: actions/checkout@v4
|
|
15
|
+
|
|
16
|
+
- name: Setup Node.js
|
|
17
|
+
uses: actions/setup-node@v4
|
|
18
|
+
with:
|
|
19
|
+
node-version: "18"
|
|
20
|
+
registry-url: "https://registry.npmjs.org"
|
|
21
|
+
|
|
22
|
+
- name: Publish package
|
|
23
|
+
run: npm publish --access public
|
|
24
|
+
env:
|
|
25
|
+
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
|
26
|
+
|
package/index.js
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
const errors = require("./src/errors/errors");
|
|
2
|
+
const topics = require("./src/kafka/topics");
|
|
3
|
+
const authMiddleware = require("./src/middlewares/auth.middleware");
|
|
4
|
+
const formatResponse = require('./src/response/response');
|
|
5
|
+
const logger = require('./src/logger/logger');
|
|
6
|
+
const rabbitBroker = require('./src/wrappers/rabbitBroker');
|
|
7
|
+
|
|
8
|
+
module.exports.Errors = errors;
|
|
9
|
+
module.exports.Topics = topics;
|
|
10
|
+
module.exports.AuthMiddleware = authMiddleware;
|
|
11
|
+
module.exports.formatResponse = formatResponse;
|
|
12
|
+
module.exports.rabbitBroker = rabbitBroker;
|
package/package.json
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@just-tracking/shared",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"main": "index.js",
|
|
5
|
+
"author": "Sargis Yeritsyan <sargis021996@gmail.com>",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"description": "Just Tracking shared lib",
|
|
8
|
+
"publishConfig": {
|
|
9
|
+
"access": "public"
|
|
10
|
+
},
|
|
11
|
+
"engines": {
|
|
12
|
+
"node": ">=18.12.1",
|
|
13
|
+
"npm": ">=8.19.2"
|
|
14
|
+
},
|
|
15
|
+
"dependencies": {
|
|
16
|
+
"@slack/bolt": "^3.17.1",
|
|
17
|
+
"amqplib": "^0.10.4",
|
|
18
|
+
"axios": "^1.4.0",
|
|
19
|
+
"dotenv": "^16.3.1",
|
|
20
|
+
"lodash": "^4.17.21",
|
|
21
|
+
"winston": "^3.11.0",
|
|
22
|
+
"winston-daily-rotate-file": "^5.0.0"
|
|
23
|
+
},
|
|
24
|
+
"devDependencies": {}
|
|
25
|
+
}
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
const logger = require('../logger/logger');
|
|
2
|
+
const Slack = require('@slack/bolt');
|
|
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
|
+
});
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
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
|
+
}
|
|
32
|
+
|
|
33
|
+
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
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
class AuthenticationError extends CustomError {
|
|
47
|
+
constructor(message, status, data = {}) {
|
|
48
|
+
super(message, data, ERRORS['AUTHENTICATION'].label, ERRORS['AUTHENTICATION'].status);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
class ValidationError extends CustomError {
|
|
53
|
+
constructor(message, data = {}) {
|
|
54
|
+
super(message, data, ERRORS['VALIDATION'].label, ERRORS['VALIDATION'].status);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
class UnauthorizedError extends CustomError {
|
|
59
|
+
constructor(message, data = {}) {
|
|
60
|
+
super(message, data, ERRORS['UNAUTHORIZED'].label, ERRORS['UNAUTHORIZED'].status);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
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
|
+
}
|
|
120
|
+
|
|
121
|
+
return res.status(status).json({
|
|
122
|
+
success: false,
|
|
123
|
+
status: status,
|
|
124
|
+
error: err.message,
|
|
125
|
+
data: err.data,
|
|
126
|
+
type: err.type
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
module.exports = {
|
|
131
|
+
AuthenticationError,
|
|
132
|
+
UnauthorizedError,
|
|
133
|
+
ValidationError,
|
|
134
|
+
errorHandler
|
|
135
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
const topics = {
|
|
2
|
+
USER_CREATED: 'user.created',
|
|
3
|
+
USER_EDITED: 'user.edited',
|
|
4
|
+
USER_DELETED: 'user.deleted',
|
|
5
|
+
TRIP_CREATED: 'trip.created',
|
|
6
|
+
TRIP_EDITED: 'trip.edited',
|
|
7
|
+
TRIP_DELETED: 'trip.deleted',
|
|
8
|
+
TRIP_FINISHED: 'trip.finished',
|
|
9
|
+
DRIVER_ASSIGNED: 'driver.assigned',
|
|
10
|
+
DRIVER_CREATED: 'driver.created',
|
|
11
|
+
DRIVER_EDITED: 'driver.edited',
|
|
12
|
+
DRIVER_DELETED: 'driver.deleted',
|
|
13
|
+
SEND_EMAIL_MESSAGE: 'send.email.message',
|
|
14
|
+
SEND_PHONE_MESSAGE: 'send.phone.message',
|
|
15
|
+
SEND_CHAT_MESSAGE: 'send.chat.message',
|
|
16
|
+
SEND_NOTIFICATION: 'send.notification'
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
module.exports = {
|
|
20
|
+
topics
|
|
21
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
const path = require('path');
|
|
2
|
+
const fs = require('fs');
|
|
3
|
+
const winston = require('winston');
|
|
4
|
+
const { combine, timestamp, json, prettyPrint, errors } = winston.format;
|
|
5
|
+
const DailyRotateFile = require('winston-daily-rotate-file');
|
|
6
|
+
|
|
7
|
+
const logDirectory = path.resolve(__dirname, '../../../../logs');
|
|
8
|
+
|
|
9
|
+
if (!fs.existsSync(logDirectory)) {
|
|
10
|
+
fs.mkdirSync(logDirectory);
|
|
11
|
+
}
|
|
12
|
+
|
|
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
|
+
],
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
module.exports = logger;
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
const axios = require('axios');
|
|
2
|
+
const { UnauthorizedError, AuthenticationError } = require('../errors/errors');
|
|
3
|
+
|
|
4
|
+
function formatCookies(cookies) {
|
|
5
|
+
return Object.keys(cookies)
|
|
6
|
+
.map((key) => `${key}=${cookies[key]}`)
|
|
7
|
+
.join(';');
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
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
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
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"))
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
module.exports = {
|
|
55
|
+
auth,
|
|
56
|
+
can
|
|
57
|
+
};
|
|
@@ -0,0 +1,18 @@
|
|
|
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);
|
|
16
|
+
};
|
|
17
|
+
next();
|
|
18
|
+
};
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
const amqp = require('amqplib');
|
|
2
|
+
const _ = require('lodash');
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* @var {Promise<MessageBroker>}
|
|
6
|
+
*/
|
|
7
|
+
let instance;
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Broker for async messaging
|
|
11
|
+
*/
|
|
12
|
+
class MessageBroker {
|
|
13
|
+
/**
|
|
14
|
+
* Trigger init connection method
|
|
15
|
+
*/
|
|
16
|
+
constructor() {
|
|
17
|
+
this.queues = {};
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Initialize connection to rabbitMQ
|
|
22
|
+
*/
|
|
23
|
+
async init() {
|
|
24
|
+
try {
|
|
25
|
+
console.log('Connecting to RabbitMQ:', process.env.RABBITMQ_URL);
|
|
26
|
+
this.connection = await amqp.connect(process.env.RABBITMQ_URL || 'amqp://localhost');
|
|
27
|
+
this.channel = await this.connection.createChannel();
|
|
28
|
+
console.log('Connected to RabbitMQ');
|
|
29
|
+
|
|
30
|
+
this.connection.on('close', async () => {
|
|
31
|
+
console.warn('RabbitMQ connection closed, attempting to reconnect...');
|
|
32
|
+
await this.init();
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
this.connection.on('error', (error) => {
|
|
36
|
+
console.error('RabbitMQ connection error:', error);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
return this;
|
|
40
|
+
} catch (error) {
|
|
41
|
+
console.error('Error connecting to RabbitMQ:', error);
|
|
42
|
+
throw error;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Send message to queue
|
|
48
|
+
* @param {String} queue Queue name
|
|
49
|
+
* @param {Object} msg Message as any
|
|
50
|
+
*/
|
|
51
|
+
async send(queue, msg) {
|
|
52
|
+
if (!this.connection) {
|
|
53
|
+
await this.init();
|
|
54
|
+
}
|
|
55
|
+
await this.channel.assertQueue(queue, { durable: true });
|
|
56
|
+
this.channel.sendToQueue(queue, Buffer.from(JSON.stringify(msg)), { persistent: true });
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
async sendExchange(exchange, queue, msg) {
|
|
60
|
+
if (!this.connection) {
|
|
61
|
+
await this.init();
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
await this.channel.assertExchange(exchange, 'fanout', { durable: true });
|
|
65
|
+
this.channel.publish(exchange, queue, Buffer.from(JSON.stringify(msg)), { deliveryMode: 2 });
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* @param {String} queue Queue name
|
|
70
|
+
* @param {Function} handler Handler that will be invoked with given message and acknowledge function (msg, ack)
|
|
71
|
+
*/
|
|
72
|
+
async subscribe(queue, handler) {
|
|
73
|
+
if (!this.connection) {
|
|
74
|
+
await this.init();
|
|
75
|
+
}
|
|
76
|
+
if (this.queues[queue]) {
|
|
77
|
+
const existingHandler = _.find(this.queues[queue], (h) => h === handler);
|
|
78
|
+
if (existingHandler) {
|
|
79
|
+
return () => this.unsubscribe(queue, existingHandler);
|
|
80
|
+
}
|
|
81
|
+
this.queues[queue].push(handler);
|
|
82
|
+
return () => this.unsubscribe(queue, handler);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
await this.channel.assertQueue(queue, { durable: true });
|
|
86
|
+
await this.channel.prefetch(1);
|
|
87
|
+
this.queues[queue] = [handler];
|
|
88
|
+
this.channel.consume(queue, async (msg) => {
|
|
89
|
+
const ack = _.once(() => this.channel.ack(msg));
|
|
90
|
+
const nack = _.once(() => this.channel.nack(msg, false, true));
|
|
91
|
+
|
|
92
|
+
try {
|
|
93
|
+
await Promise.all(this.queues[queue].map((h) => h(msg, ack)));
|
|
94
|
+
ack(); // Acknowledge if all handlers succeed
|
|
95
|
+
} catch (error) {
|
|
96
|
+
console.error('Error processing message:', error);
|
|
97
|
+
nack(); // Reject message on failure
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
});
|
|
101
|
+
return () => this.unsubscribe(queue, handler);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* @param {String} exchange Queue name
|
|
106
|
+
* @param {String} routing Queue name
|
|
107
|
+
* @param {Function} handler Handler that will be invoked with given message and acknowledge function (msg, ack)
|
|
108
|
+
*/
|
|
109
|
+
async subscribeExchange(exchange, routing, handler) {
|
|
110
|
+
if (!this.connection) {
|
|
111
|
+
await this.init();
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
await this.channel.assertExchange(exchange, 'fanout', {
|
|
115
|
+
durable: true,
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
const { queue } = await this.channel.assertQueue('', {
|
|
119
|
+
exclusive: true,
|
|
120
|
+
durable: true
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
await this.channel.bindQueue(queue, exchange, routing);
|
|
124
|
+
|
|
125
|
+
this.channel.consume(
|
|
126
|
+
queue,
|
|
127
|
+
async (msg) => {
|
|
128
|
+
const ack = _.once(() => this.channel.ack(msg));
|
|
129
|
+
handler(msg, ack);
|
|
130
|
+
}
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
async unsubscribe(queue, handler) {
|
|
135
|
+
_.pull(this.queues[queue], handler);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* @return {Promise<MessageBroker>}
|
|
141
|
+
*/
|
|
142
|
+
MessageBroker.getInstance = async function () {
|
|
143
|
+
if (!instance) {
|
|
144
|
+
const broker = new MessageBroker();
|
|
145
|
+
instance = broker.init();
|
|
146
|
+
}
|
|
147
|
+
return instance;
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
module.exports = MessageBroker;
|