notification wa by mqtt broker

This commit is contained in:
2025-12-18 10:05:50 +07:00
parent 2b93baa648
commit 907f5767c1
9 changed files with 248 additions and 4 deletions

9
app.js
View File

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

View File

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

View File

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

View File

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

View File

@@ -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
View 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,
};

View File

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

View 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();

View File

@@ -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 = {