@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 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;