diff --git a/app.js b/app.js index c83b7ff..67ee5fe 100644 --- a/app.js +++ b/app.js @@ -8,7 +8,8 @@ const helmet = require("helmet"); const compression = require("compression"); const unknownEndpoint = require("./middleware/unKnownEndpoint"); const { handleError } = require("./helpers/error"); -const { checkConnection } = require("./config"); +const { checkConnection, mqttClient } = require("./config"); +const { onNotification } = require("./services/notifikasi-wa.service"); const app = express(); @@ -47,4 +48,10 @@ app.get("/check-db", async (req, res) => { app.use(unknownEndpoint); 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; diff --git a/config/index.js b/config/index.js index 15c4e5a..116d59e 100644 --- a/config/index.js +++ b/config/index.js @@ -1,8 +1,11 @@ require("dotenv").config(); +const { default: mqtt } = require("mqtt"); const sql = require("mssql"); const isProduction = process.env.NODE_ENV === "production"; +const endPointWhatsapp = process.env.ENDPOINT_WHATSAPP; + // Config SQL Server const config = { user: process.env.SQL_USERNAME, @@ -284,6 +287,34 @@ async function generateKode(prefix, tableName, columnName) { 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 = { checkConnection, query, @@ -293,4 +324,6 @@ module.exports = { buildDynamicInsert, buildDynamicUpdate, generateKode, + endPointWhatsapp, + mqttClient }; diff --git a/controllers/auth.controller.js b/controllers/auth.controller.js index de1a9dd..1c75f84 100644 --- a/controllers/auth.controller.js +++ b/controllers/auth.controller.js @@ -2,6 +2,9 @@ const AuthService = require('../services/auth.service'); const { setResponse, checkValidate } = require('../helpers/utils'); const { registerSchema, loginSchema } = require('../validate/auth.schema'); const { createCaptcha } = require('../utils/captcha'); +const JWTService = require('../utils/jwt'); + +const CryptoJS = require('crypto-js'); class AuthController { // Register @@ -94,6 +97,41 @@ class AuthController { const response = await setResponse({ svg, text }, 'Captcha generated'); 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; diff --git a/db/contact.db.js b/db/contact.db.js index f8c3b65..2b1cff5 100644 --- a/db/contact.db.js +++ b/db/contact.db.js @@ -24,6 +24,7 @@ const getAllContactDb = async (searchParams = {}) => { [ { column: "a.contact_name", param: searchParams.name, type: "string" }, { column: "a.contact_type", param: searchParams.code, type: "string" }, + { column: "a.is_active", param: searchParams.active, type: "boolean" }, ], queryParams ); diff --git a/db/history_value.db.js b/db/history_value.db.js index 41032b5..aea197d 100644 --- a/db/history_value.db.js +++ b/db/history_value.db.js @@ -349,7 +349,7 @@ const getHistoryValueTrendingPivotDb = async (tableName, searchParams = {}) => { const tagList = Object.keys(rows[0]).filter(k => k !== timeKey); const nivoData = tagList.map(tag => ({ - id: tag, + name: tag, data: rows.map(row => ({ x: row[timeKey], y: row[tag] !== null ? Number(row[tag]) : null diff --git a/db/notification_wa.db.js b/db/notification_wa.db.js new file mode 100644 index 0000000..aa2d368 --- /dev/null +++ b/db/notification_wa.db.js @@ -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, +}; diff --git a/routes/auth.route.js b/routes/auth.route.js index 166d68d..e84b420 100644 --- a/routes/auth.route.js +++ b/routes/auth.route.js @@ -7,5 +7,6 @@ router.post('/login', AuthController.login); router.post('/register', AuthController.register); router.get('/generate-captcha', AuthController.generateCaptcha); router.post('/refresh-token', AuthController.refreshToken); +router.post('/verify-redirect', AuthController.verifyTokenRedirect); module.exports = router; \ No newline at end of file diff --git a/services/notifikasi-wa.service.js b/services/notifikasi-wa.service.js new file mode 100644 index 0000000..210b454 --- /dev/null +++ b/services/notifikasi-wa.service.js @@ -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(); diff --git a/validate/contact.schema.js b/validate/contact.schema.js index db362e6..04aaa0a 100644 --- a/validate/contact.schema.js +++ b/validate/contact.schema.js @@ -13,7 +13,7 @@ const insertContactSchema = Joi.object({ "Phone number must be a valid Indonesian number in format +628XXXXXXXXX", }), 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({ @@ -26,7 +26,7 @@ const updateContactSchema = Joi.object({ "Phone number must be a valid Indonesian number in format +628XXXXXXXXX", }), is_active: Joi.boolean().optional(), - contact_type: Joi.string().max(255).optional() + contact_type: Joi.string().max(255).optional().allow(null) }); module.exports = {