notification wa by mqtt broker
This commit is contained in:
9
app.js
9
app.js
@@ -8,7 +8,8 @@ const helmet = require("helmet");
|
|||||||
const compression = require("compression");
|
const compression = require("compression");
|
||||||
const unknownEndpoint = require("./middleware/unKnownEndpoint");
|
const unknownEndpoint = require("./middleware/unKnownEndpoint");
|
||||||
const { handleError } = require("./helpers/error");
|
const { handleError } = require("./helpers/error");
|
||||||
const { checkConnection } = require("./config");
|
const { checkConnection, mqttClient } = require("./config");
|
||||||
|
const { onNotification } = require("./services/notifikasi-wa.service");
|
||||||
|
|
||||||
const app = express();
|
const app = express();
|
||||||
|
|
||||||
@@ -47,4 +48,10 @@ app.get("/check-db", async (req, res) => {
|
|||||||
app.use(unknownEndpoint);
|
app.use(unknownEndpoint);
|
||||||
app.use(handleError);
|
app.use(handleError);
|
||||||
|
|
||||||
|
// Saat pesan diterima
|
||||||
|
mqttClient.on('message', (topic, message) => {
|
||||||
|
console.log(`Received message on topic "${topic}":`, message.toString());
|
||||||
|
onNotification(topic, message);
|
||||||
|
});
|
||||||
|
|
||||||
module.exports = app;
|
module.exports = app;
|
||||||
|
|||||||
@@ -1,8 +1,11 @@
|
|||||||
require("dotenv").config();
|
require("dotenv").config();
|
||||||
|
const { default: mqtt } = require("mqtt");
|
||||||
const sql = require("mssql");
|
const sql = require("mssql");
|
||||||
|
|
||||||
const isProduction = process.env.NODE_ENV === "production";
|
const isProduction = process.env.NODE_ENV === "production";
|
||||||
|
|
||||||
|
const endPointWhatsapp = process.env.ENDPOINT_WHATSAPP;
|
||||||
|
|
||||||
// Config SQL Server
|
// Config SQL Server
|
||||||
const config = {
|
const config = {
|
||||||
user: process.env.SQL_USERNAME,
|
user: process.env.SQL_USERNAME,
|
||||||
@@ -284,6 +287,34 @@ async function generateKode(prefix, tableName, columnName) {
|
|||||||
return prefix + String(nextNumber).padStart(3, "0");
|
return prefix + String(nextNumber).padStart(3, "0");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Koneksi ke broker MQTT
|
||||||
|
const mqttOptions = {
|
||||||
|
clientId: 'express_mqtt_client_' + Math.random().toString(16).substr(2, 8),
|
||||||
|
clean: true,
|
||||||
|
connectTimeout: 4000,
|
||||||
|
username: '', // jika ada
|
||||||
|
password: '', // jika ada
|
||||||
|
};
|
||||||
|
|
||||||
|
const mqttUrl = 'ws://localhost:1884'; // Ganti dengan broker kamu
|
||||||
|
const topic = 'morek';
|
||||||
|
|
||||||
|
const mqttClient = mqtt.connect(mqttUrl, mqttOptions);
|
||||||
|
|
||||||
|
// Saat terkoneksi
|
||||||
|
mqttClient.on('connect', () => {
|
||||||
|
console.log('MQTT connected');
|
||||||
|
|
||||||
|
// Subscribe ke topik tertentu
|
||||||
|
mqttClient.subscribe(topic, (err) => {
|
||||||
|
if (!err) {
|
||||||
|
console.log(`Subscribed to topic "${topic}"`);
|
||||||
|
} else {
|
||||||
|
console.error('Subscribe error:', err);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
checkConnection,
|
checkConnection,
|
||||||
query,
|
query,
|
||||||
@@ -293,4 +324,6 @@ module.exports = {
|
|||||||
buildDynamicInsert,
|
buildDynamicInsert,
|
||||||
buildDynamicUpdate,
|
buildDynamicUpdate,
|
||||||
generateKode,
|
generateKode,
|
||||||
|
endPointWhatsapp,
|
||||||
|
mqttClient
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -2,6 +2,9 @@ const AuthService = require('../services/auth.service');
|
|||||||
const { setResponse, checkValidate } = require('../helpers/utils');
|
const { setResponse, checkValidate } = require('../helpers/utils');
|
||||||
const { registerSchema, loginSchema } = require('../validate/auth.schema');
|
const { registerSchema, loginSchema } = require('../validate/auth.schema');
|
||||||
const { createCaptcha } = require('../utils/captcha');
|
const { createCaptcha } = require('../utils/captcha');
|
||||||
|
const JWTService = require('../utils/jwt');
|
||||||
|
|
||||||
|
const CryptoJS = require('crypto-js');
|
||||||
|
|
||||||
class AuthController {
|
class AuthController {
|
||||||
// Register
|
// Register
|
||||||
@@ -94,6 +97,41 @@ class AuthController {
|
|||||||
const response = await setResponse({ svg, text }, 'Captcha generated');
|
const response = await setResponse({ svg, text }, 'Captcha generated');
|
||||||
res.status(response.statusCode).json(response);
|
res.status(response.statusCode).json(response);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static async verifyTokenRedirect(req, res) {
|
||||||
|
const { tokenRedirect } = req.body;
|
||||||
|
|
||||||
|
const bytes = CryptoJS.AES.decrypt(tokenRedirect, process.env.VITE_KEY_SESSION);
|
||||||
|
const decrypted = JSON.parse(bytes.toString(CryptoJS.enc.Utf8));
|
||||||
|
|
||||||
|
const userPhone = decrypted?.user_phone
|
||||||
|
const userName = decrypted?.user_name
|
||||||
|
const idData = decrypted?.id
|
||||||
|
|
||||||
|
const payload = {
|
||||||
|
user_id: userPhone,
|
||||||
|
user_fullname: userName,
|
||||||
|
};
|
||||||
|
|
||||||
|
const tokens = JWTService.generateTokenPair(payload);
|
||||||
|
|
||||||
|
// Simpan refresh token di cookie
|
||||||
|
res.cookie('refreshToken', tokens.refreshToken, {
|
||||||
|
httpOnly: true,
|
||||||
|
secure: false,
|
||||||
|
sameSite: 'lax',
|
||||||
|
maxAge: 7 * 24 * 60 * 60 * 1000
|
||||||
|
});
|
||||||
|
|
||||||
|
const response = await setResponse(
|
||||||
|
{
|
||||||
|
accessToken: tokens.accessToken
|
||||||
|
},
|
||||||
|
'Verify successful'
|
||||||
|
);
|
||||||
|
|
||||||
|
res.status(response.statusCode).json(response);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = AuthController;
|
module.exports = AuthController;
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ const getAllContactDb = async (searchParams = {}) => {
|
|||||||
[
|
[
|
||||||
{ column: "a.contact_name", param: searchParams.name, type: "string" },
|
{ column: "a.contact_name", param: searchParams.name, type: "string" },
|
||||||
{ column: "a.contact_type", param: searchParams.code, type: "string" },
|
{ column: "a.contact_type", param: searchParams.code, type: "string" },
|
||||||
|
{ column: "a.is_active", param: searchParams.active, type: "boolean" },
|
||||||
],
|
],
|
||||||
queryParams
|
queryParams
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -349,7 +349,7 @@ const getHistoryValueTrendingPivotDb = async (tableName, searchParams = {}) => {
|
|||||||
const tagList = Object.keys(rows[0]).filter(k => k !== timeKey);
|
const tagList = Object.keys(rows[0]).filter(k => k !== timeKey);
|
||||||
|
|
||||||
const nivoData = tagList.map(tag => ({
|
const nivoData = tagList.map(tag => ({
|
||||||
id: tag,
|
name: tag,
|
||||||
data: rows.map(row => ({
|
data: rows.map(row => ({
|
||||||
x: row[timeKey],
|
x: row[timeKey],
|
||||||
y: row[tag] !== null ? Number(row[tag]) : null
|
y: row[tag] !== null ? Number(row[tag]) : null
|
||||||
|
|||||||
59
db/notification_wa.db.js
Normal file
59
db/notification_wa.db.js
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
const { default: axios } = require('axios');
|
||||||
|
const CryptoJS = require('crypto-js');
|
||||||
|
|
||||||
|
const generateTokenRedirect = async (userPhone, userName, id) => {
|
||||||
|
|
||||||
|
const plain = {
|
||||||
|
user_phone: userPhone,
|
||||||
|
user_name: userName,
|
||||||
|
id
|
||||||
|
}
|
||||||
|
|
||||||
|
const tokenCrypt = CryptoJS.AES.encrypt(JSON.stringify(plain), process.env.VITE_KEY_SESSION).toString();
|
||||||
|
return tokenCrypt
|
||||||
|
}
|
||||||
|
|
||||||
|
const shortUrltiny = async (encodedToken) => {
|
||||||
|
const url = `${process.env.ENDPOINT_FE}/redirect?token=${encodedToken}`
|
||||||
|
|
||||||
|
const encodedUrl = encodeURIComponent(url); // ⬅️ Encode dulu!
|
||||||
|
|
||||||
|
const response = await axios.get(`https://tinyurl.com/api-create.php?url=${encodedUrl}`);
|
||||||
|
|
||||||
|
let shortUrl = response.data;
|
||||||
|
if (!shortUrl.startsWith('http')) {
|
||||||
|
shortUrl = 'https://' + shortUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
return shortUrl
|
||||||
|
}
|
||||||
|
|
||||||
|
const sendNotifikasi = async (phone, message) => {
|
||||||
|
const payload = {
|
||||||
|
phone: phone,
|
||||||
|
message: message
|
||||||
|
};
|
||||||
|
|
||||||
|
// console.log('payload', payload);
|
||||||
|
|
||||||
|
const endPointWhatsapp = process.env.ENDPOINT_WHATSAPP;
|
||||||
|
|
||||||
|
const response = await axios.post(endPointWhatsapp, payload);
|
||||||
|
// console.log('response', response);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await axios.post(endPointWhatsapp, payload);
|
||||||
|
// console.log(response.data);
|
||||||
|
return response?.data
|
||||||
|
} catch (error) {
|
||||||
|
// console.error(error.response?.data || error.message);
|
||||||
|
return error.response?.data || error.message
|
||||||
|
}
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
generateTokenRedirect,
|
||||||
|
shortUrltiny,
|
||||||
|
sendNotifikasi,
|
||||||
|
};
|
||||||
@@ -7,5 +7,6 @@ router.post('/login', AuthController.login);
|
|||||||
router.post('/register', AuthController.register);
|
router.post('/register', AuthController.register);
|
||||||
router.get('/generate-captcha', AuthController.generateCaptcha);
|
router.get('/generate-captcha', AuthController.generateCaptcha);
|
||||||
router.post('/refresh-token', AuthController.refreshToken);
|
router.post('/refresh-token', AuthController.refreshToken);
|
||||||
|
router.post('/verify-redirect', AuthController.verifyTokenRedirect);
|
||||||
|
|
||||||
module.exports = router;
|
module.exports = router;
|
||||||
105
services/notifikasi-wa.service.js
Normal file
105
services/notifikasi-wa.service.js
Normal file
@@ -0,0 +1,105 @@
|
|||||||
|
const { getAllContactDb } = require('../db/contact.db');
|
||||||
|
const { InsertNotificationErrorDb } = require('../db/notification_error.db');
|
||||||
|
const { createNotificationErrorUserDb, updateNotificationErrorUserDb } = require('../db/notification_error_user.db');
|
||||||
|
const { generateTokenRedirect, shortUrltiny, sendNotifikasi } = require('../db/notification_wa.db');
|
||||||
|
|
||||||
|
class NotifikasiWaService {
|
||||||
|
|
||||||
|
async onNotification(topic, message) {
|
||||||
|
|
||||||
|
try {
|
||||||
|
const paramDb = {
|
||||||
|
limit: 100,
|
||||||
|
page: 1,
|
||||||
|
criteria: '',
|
||||||
|
active: 1
|
||||||
|
}
|
||||||
|
|
||||||
|
// const chanel = {
|
||||||
|
// "time": "2025-12-11 11:10:58",
|
||||||
|
// "c_4501": 4,
|
||||||
|
// "c_5501": 3,
|
||||||
|
// "c_6501": 0
|
||||||
|
// }
|
||||||
|
|
||||||
|
if (topic === 'morek') {
|
||||||
|
|
||||||
|
const dataMqtt = JSON.parse(message);
|
||||||
|
|
||||||
|
const resultChanel = [];
|
||||||
|
|
||||||
|
Object.entries(dataMqtt).forEach(([key, value]) => {
|
||||||
|
if (key.startsWith('c_')) {
|
||||||
|
resultChanel.push({
|
||||||
|
chanel_id: Number(key.slice(2)),
|
||||||
|
value
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const results = await getAllContactDb(paramDb);
|
||||||
|
|
||||||
|
const bodyMessage = `Hai Operator\n` +
|
||||||
|
`Terjadi peringatan pada device, silahkan cek detail pada link berikut :\n`;
|
||||||
|
|
||||||
|
const dataUsers = results.data;
|
||||||
|
|
||||||
|
for (const chanel of resultChanel) {
|
||||||
|
const data = {
|
||||||
|
"error_code_id": chanel.value,
|
||||||
|
"error_chanel": chanel.chanel_id,
|
||||||
|
"message_error_issue": bodyMessage,
|
||||||
|
"is_send": false,
|
||||||
|
"is_delivered": false,
|
||||||
|
"is_read": false,
|
||||||
|
"is_active": true
|
||||||
|
}
|
||||||
|
|
||||||
|
const resultNotificationError = await InsertNotificationErrorDb(data)
|
||||||
|
|
||||||
|
for (const dataUser of dataUsers) {
|
||||||
|
if (dataUser.is_active) {
|
||||||
|
|
||||||
|
const param = {
|
||||||
|
idData: resultNotificationError.notification_error_id,
|
||||||
|
userPhone: dataUser.contact_phone,
|
||||||
|
userName: dataUser.contact_name,
|
||||||
|
bodyMessage: bodyMessage,
|
||||||
|
}
|
||||||
|
|
||||||
|
const tokenRedirect = await generateTokenRedirect(param.userPhone, param.userName, param.idData)
|
||||||
|
|
||||||
|
const encodedToken = encodeURIComponent(tokenRedirect);
|
||||||
|
|
||||||
|
const shortUrl = await shortUrltiny(encodedToken)
|
||||||
|
|
||||||
|
let bodyWithUrl = `${param.bodyMessage}\n🔗 ${shortUrl}`;
|
||||||
|
|
||||||
|
param.bodyMessage = bodyWithUrl
|
||||||
|
|
||||||
|
const resultNotificationErrorUser = await createNotificationErrorUserDb({
|
||||||
|
notification_error_id: resultNotificationError.notification_error_id,
|
||||||
|
contact_phone: param.userPhone,
|
||||||
|
contact_name: param.userName,
|
||||||
|
is_send: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
const resultSend = await sendNotifikasi(param.userPhone, param.bodyMessage);
|
||||||
|
|
||||||
|
await updateNotificationErrorUserDb(resultNotificationErrorUser[0].notification_error_user_id, {
|
||||||
|
is_send: resultSend?.error ? false : true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
// throw new ErrorHandler(error.statusCode, error.message);
|
||||||
|
return error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = new NotifikasiWaService();
|
||||||
@@ -13,7 +13,7 @@ const insertContactSchema = Joi.object({
|
|||||||
"Phone number must be a valid Indonesian number in format +628XXXXXXXXX",
|
"Phone number must be a valid Indonesian number in format +628XXXXXXXXX",
|
||||||
}),
|
}),
|
||||||
is_active: Joi.boolean().required(),
|
is_active: Joi.boolean().required(),
|
||||||
contact_type: Joi.string().max(255).optional()
|
contact_type: Joi.string().max(255).optional().allow(null)
|
||||||
});
|
});
|
||||||
|
|
||||||
const updateContactSchema = Joi.object({
|
const updateContactSchema = Joi.object({
|
||||||
@@ -26,7 +26,7 @@ const updateContactSchema = Joi.object({
|
|||||||
"Phone number must be a valid Indonesian number in format +628XXXXXXXXX",
|
"Phone number must be a valid Indonesian number in format +628XXXXXXXXX",
|
||||||
}),
|
}),
|
||||||
is_active: Joi.boolean().optional(),
|
is_active: Joi.boolean().optional(),
|
||||||
contact_type: Joi.string().max(255).optional()
|
contact_type: Joi.string().max(255).optional().allow(null)
|
||||||
});
|
});
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
|
|||||||
Reference in New Issue
Block a user