diff --git a/server/database.js b/server/database.js index a4247946..2f6c1c5f 100644 --- a/server/database.js +++ b/server/database.js @@ -5,10 +5,23 @@ const { debug, sleep } = require("../src/util"); const dayjs = require("dayjs"); const knex = require("knex"); +/** + * Database & App Data Folder + */ class Database { static templatePath = "./db/kuma.db"; + + /** + * Data Dir (Default: ./data) + */ static dataDir; + + /** + * User Upload Dir (Default: ./data/upload) + */ + static uploadDir; + static path; /** @@ -52,6 +65,13 @@ class Database { if (! fs.existsSync(Database.dataDir)) { fs.mkdirSync(Database.dataDir, { recursive: true }); } + + Database.uploadDir = Database.dataDir + "upload/"; + + if (! fs.existsSync(Database.uploadDir)) { + fs.mkdirSync(Database.uploadDir, { recursive: true }); + } + console.log(`Data Dir: ${Database.dataDir}`); } diff --git a/server/image-data-uri.js b/server/image-data-uri.js new file mode 100644 index 00000000..3ccaab7d --- /dev/null +++ b/server/image-data-uri.js @@ -0,0 +1,57 @@ +/* + From https://github.com/DiegoZoracKy/image-data-uri/blob/master/lib/image-data-uri.js + Modified with 0 dependencies + */ +let fs = require("fs"); + +let ImageDataURI = (() => { + + function decode(dataURI) { + if (!/data:image\//.test(dataURI)) { + console.log("ImageDataURI :: Error :: It seems that it is not an Image Data URI. Couldn't match \"data:image/\""); + return null; + } + + let regExMatches = dataURI.match("data:(image/.*);base64,(.*)"); + return { + imageType: regExMatches[1], + dataBase64: regExMatches[2], + dataBuffer: new Buffer(regExMatches[2], "base64") + }; + } + + function encode(data, mediaType) { + if (!data || !mediaType) { + console.log("ImageDataURI :: Error :: Missing some of the required params: data, mediaType "); + return null; + } + + mediaType = (/\//.test(mediaType)) ? mediaType : "image/" + mediaType; + let dataBase64 = (Buffer.isBuffer(data)) ? data.toString("base64") : new Buffer(data).toString("base64"); + let dataImgBase64 = "data:" + mediaType + ";base64," + dataBase64; + + return dataImgBase64; + } + + function outputFile(dataURI, filePath) { + filePath = filePath || "./"; + return new Promise((resolve, reject) => { + let imageDecoded = decode(dataURI); + + fs.writeFile(filePath, imageDecoded.dataBuffer, err => { + if (err) { + return reject("ImageDataURI :: Error :: " + JSON.stringify(err, null, 4)); + } + resolve(filePath); + }); + }); + } + + return { + decode: decode, + encode: encode, + outputFile: outputFile, + }; +})(); + +module.exports = ImageDataURI; diff --git a/server/server.js b/server/server.js index 4f2fa5a0..51095365 100644 --- a/server/server.js +++ b/server/server.js @@ -8,12 +8,12 @@ console.log("Node Env: " + process.env.NODE_ENV); const { sleep, debug, TimeLogger, getRandomInt } = require("../src/util"); -console.log("Importing Node libraries") +console.log("Importing Node libraries"); const fs = require("fs"); const http = require("http"); const https = require("https"); -console.log("Importing 3rd-party libraries") +console.log("Importing 3rd-party libraries"); debug("Importing express"); const express = require("express"); debug("Importing socket.io"); @@ -70,7 +70,7 @@ if (! fs.existsSync(Database.dataDir)) { } console.log(`Data Dir: ${Database.dataDir}`); -console.log("Creating express and socket.io instance") +console.log("Creating express and socket.io instance"); const app = express(); let server; @@ -157,6 +157,9 @@ exports.entryPage = "dashboard"; app.use("/", express.static("dist")); + // ./data/upload + app.use("/upload", express.static(Database.uploadDir)); + app.get("/.well-known/change-password", async (_, response) => { response.redirect("https://github.com/louislam/uptime-kuma/wiki/Reset-Password-via-CLI"); }); @@ -167,22 +170,26 @@ exports.entryPage = "dashboard"; // Universal Route Handler, must be at the end of all express route. app.get("*", async (_request, response) => { - response.send(indexHTML); + if (_request.originalUrl.startsWith("/upload/")) { + response.status(404).send("File not found."); + } else { + response.send(indexHTML); + } }); - console.log("Adding socket handler") + console.log("Adding socket handler"); io.on("connection", async (socket) => { socket.emit("info", { version: checkVersion.version, latestVersion: checkVersion.latestVersion, - }) + }); totalClient++; if (needSetup) { - console.log("Redirect to setup page") - socket.emit("setup") + console.log("Redirect to setup page"); + socket.emit("setup"); } socket.on("disconnect", () => { @@ -198,44 +205,44 @@ exports.entryPage = "dashboard"; try { let decoded = jwt.verify(token, jwtSecret); - console.log("Username from JWT: " + decoded.username) + console.log("Username from JWT: " + decoded.username); let user = await R.findOne("user", " username = ? AND active = 1 ", [ decoded.username, - ]) + ]); if (user) { - debug("afterLogin") + debug("afterLogin"); - afterLogin(socket, user) + afterLogin(socket, user); - debug("afterLogin ok") + debug("afterLogin ok"); callback({ ok: true, - }) + }); } else { callback({ ok: false, msg: "The user is inactive or deleted.", - }) + }); } } catch (error) { callback({ ok: false, msg: "Invalid token.", - }) + }); } }); socket.on("login", async (data, callback) => { - console.log("Login") + console.log("Login"); - let user = await login(data.username, data.password) + let user = await login(data.username, data.password); if (user) { - afterLogin(socket, user) + afterLogin(socket, user); if (user.twofaStatus == 0) { callback({ @@ -243,13 +250,13 @@ exports.entryPage = "dashboard"; token: jwt.sign({ username: data.username, }, jwtSecret), - }) + }); } if (user.twofaStatus == 1 && !data.token) { callback({ tokenRequired: true, - }) + }); } if (data.token) { @@ -261,39 +268,39 @@ exports.entryPage = "dashboard"; token: jwt.sign({ username: data.username, }, jwtSecret), - }) + }); } else { callback({ ok: false, msg: "Invalid Token!", - }) + }); } } } else { callback({ ok: false, msg: "Incorrect username or password.", - }) + }); } }); socket.on("logout", async (callback) => { - socket.leave(socket.userID) + socket.leave(socket.userID); socket.userID = null; callback(); }); socket.on("prepare2FA", async (callback) => { try { - checkLogin(socket) + checkLogin(socket); let user = await R.findOne("user", " id = ? AND active = 1 ", [ socket.userID, - ]) + ]); if (user.twofa_status == 0) { - let newSecret = await genSecret() + let newSecret = await genSecret(); let encodedSecret = base32.encode(newSecret); let uri = `otpauth://totp/Uptime%20Kuma:${user.username}?secret=${encodedSecret}`; @@ -305,24 +312,24 @@ exports.entryPage = "dashboard"; callback({ ok: true, uri: uri, - }) + }); } else { callback({ ok: false, msg: "2FA is already enabled.", - }) + }); } } catch (error) { callback({ ok: false, msg: "Error while trying to prepare 2FA.", - }) + }); } }); socket.on("save2FA", async (callback) => { try { - checkLogin(socket) + checkLogin(socket); await R.exec("UPDATE `user` SET twofa_status = 1 WHERE id = ? ", [ socket.userID, @@ -331,18 +338,18 @@ exports.entryPage = "dashboard"; callback({ ok: true, msg: "2FA Enabled.", - }) + }); } catch (error) { callback({ ok: false, msg: "Error while trying to change 2FA.", - }) + }); } }); socket.on("disable2FA", async (callback) => { try { - checkLogin(socket) + checkLogin(socket); await R.exec("UPDATE `user` SET twofa_status = 0 WHERE id = ? ", [ socket.userID, @@ -351,19 +358,19 @@ exports.entryPage = "dashboard"; callback({ ok: true, msg: "2FA Disabled.", - }) + }); } catch (error) { callback({ ok: false, msg: "Error while trying to change 2FA.", - }) + }); } }); socket.on("verifyToken", async (token, callback) => { let user = await R.findOne("user", " id = ? AND active = 1 ", [ socket.userID, - ]) + ]); let verify = notp.totp.verify(token, user.twofa_secret); @@ -371,40 +378,40 @@ exports.entryPage = "dashboard"; callback({ ok: true, valid: true, - }) + }); } else { callback({ ok: false, msg: "Invalid Token.", valid: false, - }) + }); } }); socket.on("twoFAStatus", async (callback) => { - checkLogin(socket) + checkLogin(socket); try { let user = await R.findOne("user", " id = ? AND active = 1 ", [ socket.userID, - ]) + ]); if (user.twofa_status == 1) { callback({ ok: true, status: true, - }) + }); } else { callback({ ok: true, status: false, - }) + }); } } catch (error) { callback({ ok: false, msg: "Error while trying to get 2FA status.", - }) + }); } }); @@ -415,13 +422,13 @@ exports.entryPage = "dashboard"; socket.on("setup", async (username, password, callback) => { try { if ((await R.count("user")) !== 0) { - throw new Error("Uptime Kuma has been setup. If you want to setup again, please delete the database.") + throw new Error("Uptime Kuma has been setup. If you want to setup again, please delete the database."); } - let user = R.dispense("user") + let user = R.dispense("user"); user.username = username; - user.password = passwordHash.generate(password) - await R.store(user) + user.password = passwordHash.generate(password); + await R.store(user); needSetup = false; @@ -445,8 +452,8 @@ exports.entryPage = "dashboard"; // Add a new monitor socket.on("add", async (monitor, callback) => { try { - checkLogin(socket) - let bean = R.dispense("monitor") + checkLogin(socket); + let bean = R.dispense("monitor"); let notificationIDList = monitor.notificationIDList; delete monitor.notificationIDList; @@ -454,11 +461,11 @@ exports.entryPage = "dashboard"; monitor.accepted_statuscodes_json = JSON.stringify(monitor.accepted_statuscodes); delete monitor.accepted_statuscodes; - bean.import(monitor) - bean.user_id = socket.userID - await R.store(bean) + bean.import(monitor); + bean.user_id = socket.userID; + await R.store(bean); - await updateMonitorNotification(bean.id, notificationIDList) + await updateMonitorNotification(bean.id, notificationIDList); await startMonitor(socket.userID, bean.id); await sendMonitorList(socket); @@ -480,18 +487,18 @@ exports.entryPage = "dashboard"; // Edit a monitor socket.on("editMonitor", async (monitor, callback) => { try { - checkLogin(socket) + checkLogin(socket); - let bean = await R.findOne("monitor", " id = ? ", [ monitor.id ]) + let bean = await R.findOne("monitor", " id = ? ", [ monitor.id ]); if (bean.user_id !== socket.userID) { - throw new Error("Permission denied.") + throw new Error("Permission denied."); } - bean.name = monitor.name - bean.type = monitor.type - bean.url = monitor.url - bean.interval = monitor.interval + bean.name = monitor.name; + bean.type = monitor.type; + bean.url = monitor.url; + bean.interval = monitor.interval; bean.retryInterval = monitor.retryInterval; bean.hostname = monitor.hostname; bean.maxretries = monitor.maxretries; @@ -504,12 +511,12 @@ exports.entryPage = "dashboard"; bean.dns_resolve_type = monitor.dns_resolve_type; bean.dns_resolve_server = monitor.dns_resolve_server; - await R.store(bean) + await R.store(bean); - await updateMonitorNotification(bean.id, monitor.notificationIDList) + await updateMonitorNotification(bean.id, monitor.notificationIDList); if (bean.active) { - await restartMonitor(socket.userID, bean.id) + await restartMonitor(socket.userID, bean.id); } await sendMonitorList(socket); @@ -521,7 +528,7 @@ exports.entryPage = "dashboard"; }); } catch (e) { - console.error(e) + console.error(e); callback({ ok: false, msg: e.message, @@ -531,13 +538,13 @@ exports.entryPage = "dashboard"; socket.on("getMonitorList", async (callback) => { try { - checkLogin(socket) + checkLogin(socket); await sendMonitorList(socket); callback({ ok: true, }); } catch (e) { - console.error(e) + console.error(e); callback({ ok: false, msg: e.message, @@ -547,14 +554,14 @@ exports.entryPage = "dashboard"; socket.on("getMonitor", async (monitorID, callback) => { try { - checkLogin(socket) + checkLogin(socket); - console.log(`Get Monitor: ${monitorID} User ID: ${socket.userID}`) + console.log(`Get Monitor: ${monitorID} User ID: ${socket.userID}`); let bean = await R.findOne("monitor", " id = ? AND user_id = ? ", [ monitorID, socket.userID, - ]) + ]); callback({ ok: true, @@ -572,7 +579,7 @@ exports.entryPage = "dashboard"; // Start or Resume the monitor socket.on("resumeMonitor", async (monitorID, callback) => { try { - checkLogin(socket) + checkLogin(socket); await startMonitor(socket.userID, monitorID); await sendMonitorList(socket); @@ -591,8 +598,8 @@ exports.entryPage = "dashboard"; socket.on("pauseMonitor", async (monitorID, callback) => { try { - checkLogin(socket) - await pauseMonitor(socket.userID, monitorID) + checkLogin(socket); + await pauseMonitor(socket.userID, monitorID); await sendMonitorList(socket); callback({ @@ -610,13 +617,13 @@ exports.entryPage = "dashboard"; socket.on("deleteMonitor", async (monitorID, callback) => { try { - checkLogin(socket) + checkLogin(socket); - console.log(`Delete Monitor: ${monitorID} User ID: ${socket.userID}`) + console.log(`Delete Monitor: ${monitorID} User ID: ${socket.userID}`); if (monitorID in monitorList) { monitorList[monitorID].stop(); - delete monitorList[monitorID] + delete monitorList[monitorID]; } await R.exec("DELETE FROM monitor WHERE id = ? AND user_id = ? ", [ @@ -641,9 +648,9 @@ exports.entryPage = "dashboard"; socket.on("getTags", async (callback) => { try { - checkLogin(socket) + checkLogin(socket); - const list = await R.findAll("tag") + const list = await R.findAll("tag"); callback({ ok: true, @@ -660,12 +667,12 @@ exports.entryPage = "dashboard"; socket.on("addTag", async (tag, callback) => { try { - checkLogin(socket) + checkLogin(socket); - let bean = R.dispense("tag") - bean.name = tag.name - bean.color = tag.color - await R.store(bean) + let bean = R.dispense("tag"); + bean.name = tag.name; + bean.color = tag.color; + await R.store(bean); callback({ ok: true, @@ -682,12 +689,12 @@ exports.entryPage = "dashboard"; socket.on("editTag", async (tag, callback) => { try { - checkLogin(socket) + checkLogin(socket); - let bean = await R.findOne("monitor", " id = ? ", [ tag.id ]) - bean.name = tag.name - bean.color = tag.color - await R.store(bean) + let bean = await R.findOne("monitor", " id = ? ", [ tag.id ]); + bean.name = tag.name; + bean.color = tag.color; + await R.store(bean); callback({ ok: true, @@ -704,9 +711,9 @@ exports.entryPage = "dashboard"; socket.on("deleteTag", async (tagID, callback) => { try { - checkLogin(socket) + checkLogin(socket); - await R.exec("DELETE FROM tag WHERE id = ? ", [ tagID ]) + await R.exec("DELETE FROM tag WHERE id = ? ", [ tagID ]); callback({ ok: true, @@ -723,13 +730,13 @@ exports.entryPage = "dashboard"; socket.on("addMonitorTag", async (tagID, monitorID, value, callback) => { try { - checkLogin(socket) + checkLogin(socket); await R.exec("INSERT INTO monitor_tag (tag_id, monitor_id, value) VALUES (?, ?, ?)", [ tagID, monitorID, value, - ]) + ]); callback({ ok: true, @@ -746,13 +753,13 @@ exports.entryPage = "dashboard"; socket.on("editMonitorTag", async (tagID, monitorID, value, callback) => { try { - checkLogin(socket) + checkLogin(socket); await R.exec("UPDATE monitor_tag SET value = ? WHERE tag_id = ? AND monitor_id = ?", [ value, tagID, monitorID, - ]) + ]); callback({ ok: true, @@ -769,13 +776,13 @@ exports.entryPage = "dashboard"; socket.on("deleteMonitorTag", async (tagID, monitorID, value, callback) => { try { - checkLogin(socket) + checkLogin(socket); await R.exec("DELETE FROM monitor_tag WHERE tag_id = ? AND monitor_id = ? AND value = ?", [ tagID, monitorID, value, - ]) + ]); // Cleanup unused Tags await R.exec("delete from tag where ( select count(*) from monitor_tag mt where tag.id = mt.tag_id ) = 0"); @@ -795,15 +802,15 @@ exports.entryPage = "dashboard"; socket.on("changePassword", async (password, callback) => { try { - checkLogin(socket) + checkLogin(socket); if (! password.currentPassword) { - throw new Error("Invalid new password") + throw new Error("Invalid new password"); } let user = await R.findOne("user", " id = ? AND active = 1 ", [ socket.userID, - ]) + ]); if (user && passwordHash.verify(password.currentPassword, user.password)) { @@ -812,9 +819,9 @@ exports.entryPage = "dashboard"; callback({ ok: true, msg: "Password has been updated successfully.", - }) + }); } else { - throw new Error("Incorrect current password") + throw new Error("Incorrect current password"); } } catch (e) { @@ -827,7 +834,7 @@ exports.entryPage = "dashboard"; socket.on("getSettings", async (callback) => { try { - checkLogin(socket) + checkLogin(socket); callback({ ok: true, @@ -844,7 +851,7 @@ exports.entryPage = "dashboard"; socket.on("setSettings", async (data, callback) => { try { - checkLogin(socket) + checkLogin(socket); await setSettings("general", data); exports.entryPage = data.entryPage; @@ -865,10 +872,10 @@ exports.entryPage = "dashboard"; // Add or Edit socket.on("addNotification", async (notification, notificationID, callback) => { try { - checkLogin(socket) + checkLogin(socket); - let notificationBean = await Notification.save(notification, notificationID, socket.userID) - await sendNotificationList(socket) + let notificationBean = await Notification.save(notification, notificationID, socket.userID); + await sendNotificationList(socket); callback({ ok: true, @@ -886,10 +893,10 @@ exports.entryPage = "dashboard"; socket.on("deleteNotification", async (notificationID, callback) => { try { - checkLogin(socket) + checkLogin(socket); - await Notification.delete(notificationID, socket.userID) - await sendNotificationList(socket) + await Notification.delete(notificationID, socket.userID); + await sendNotificationList(socket); callback({ ok: true, @@ -906,9 +913,9 @@ exports.entryPage = "dashboard"; socket.on("testNotification", async (notification, callback) => { try { - checkLogin(socket) + checkLogin(socket); - let msg = await Notification.send(notification, notification.name + " Testing") + let msg = await Notification.send(notification, notification.name + " Testing"); callback({ ok: true, @@ -916,7 +923,7 @@ exports.entryPage = "dashboard"; }); } catch (e) { - console.error(e) + console.error(e); callback({ ok: false, @@ -927,7 +934,7 @@ exports.entryPage = "dashboard"; socket.on("checkApprise", async (callback) => { try { - checkLogin(socket) + checkLogin(socket); callback(Notification.checkApprise()); } catch (e) { callback(false); @@ -936,19 +943,19 @@ exports.entryPage = "dashboard"; socket.on("uploadBackup", async (uploadedJSON, importHandle, callback) => { try { - checkLogin(socket) + checkLogin(socket); let backupData = JSON.parse(uploadedJSON); - console.log(`Importing Backup, User ID: ${socket.userID}, Version: ${backupData.version}`) + console.log(`Importing Backup, User ID: ${socket.userID}, Version: ${backupData.version}`); let notificationListData = backupData.notificationList; let monitorListData = backupData.monitorList; if (importHandle == "overwrite") { for (let id in monitorList) { - let monitor = monitorList[id] - await monitor.stop() + let monitor = monitorList[id]; + await monitor.stop(); } await R.exec("DELETE FROM heartbeat"); await R.exec("DELETE FROM monitor_notification"); @@ -965,7 +972,7 @@ exports.entryPage = "dashboard"; if ((importHandle == "skip" && notificationNameListString.includes(notificationListData[i].name) == false) || importHandle == "keep" || importHandle == "overwrite") { let notification = JSON.parse(notificationListData[i].config); - await Notification.save(notification, null, socket.userID) + await Notification.save(notification, null, socket.userID); } } @@ -994,9 +1001,9 @@ exports.entryPage = "dashboard"; dns_resolve_type: monitorListData[i].dns_resolve_type, dns_resolve_server: monitorListData[i].dns_resolve_server, notificationIDList: {}, - } + }; - let bean = R.dispense("monitor") + let bean = R.dispense("monitor"); let notificationIDList = monitor.notificationIDList; delete monitor.notificationIDList; @@ -1004,11 +1011,11 @@ exports.entryPage = "dashboard"; monitor.accepted_statuscodes_json = JSON.stringify(monitor.accepted_statuscodes); delete monitor.accepted_statuscodes; - bean.import(monitor) - bean.user_id = socket.userID - await R.store(bean) + bean.import(monitor); + bean.user_id = socket.userID; + await R.store(bean); - await updateMonitorNotification(bean.id, notificationIDList) + await updateMonitorNotification(bean.id, notificationIDList); if (monitorListData[i].active == 1) { await startMonitor(socket.userID, bean.id); @@ -1019,7 +1026,7 @@ exports.entryPage = "dashboard"; } } - await sendNotificationList(socket) + await sendNotificationList(socket); await sendMonitorList(socket); } @@ -1038,9 +1045,9 @@ exports.entryPage = "dashboard"; socket.on("clearEvents", async (monitorID, callback) => { try { - checkLogin(socket) + checkLogin(socket); - console.log(`Clear Events Monitor: ${monitorID} User ID: ${socket.userID}`) + console.log(`Clear Events Monitor: ${monitorID} User ID: ${socket.userID}`); await R.exec("UPDATE heartbeat SET msg = ?, important = ? WHERE monitor_id = ? ", [ "", @@ -1064,9 +1071,9 @@ exports.entryPage = "dashboard"; socket.on("clearHeartbeats", async (monitorID, callback) => { try { - checkLogin(socket) + checkLogin(socket); - console.log(`Clear Heartbeats Monitor: ${monitorID} User ID: ${socket.userID}`) + console.log(`Clear Heartbeats Monitor: ${monitorID} User ID: ${socket.userID}`); await R.exec("DELETE FROM heartbeat WHERE monitor_id = ?", [ monitorID @@ -1088,9 +1095,9 @@ exports.entryPage = "dashboard"; socket.on("clearStatistics", async (callback) => { try { - checkLogin(socket) + checkLogin(socket); - console.log(`Clear Statistics User ID: ${socket.userID}`) + console.log(`Clear Statistics User ID: ${socket.userID}`); await R.exec("DELETE FROM heartbeat"); @@ -1115,18 +1122,18 @@ exports.entryPage = "dashboard"; // Better do anything after added all socket handlers here // *************************** - debug("check auto login") + debug("check auto login"); if (await setting("disableAuth")) { - console.log("Disabled Auth: auto login to admin") - afterLogin(socket, await R.findOne("user")) - socket.emit("autoLogin") + console.log("Disabled Auth: auto login to admin"); + afterLogin(socket, await R.findOne("user")); + socket.emit("autoLogin"); } else { - debug("need auth") + debug("need auth"); } }); - console.log("Init the server") + console.log("Init the server"); server.once("error", async (err) => { console.error("Cannot listen: " + err.message); @@ -1148,14 +1155,14 @@ exports.entryPage = "dashboard"; async function updateMonitorNotification(monitorID, notificationIDList) { await R.exec("DELETE FROM monitor_notification WHERE monitor_id = ? ", [ monitorID, - ]) + ]); for (let notificationID in notificationIDList) { if (notificationIDList[notificationID]) { let relation = R.dispense("monitor_notification"); relation.monitor_id = monitorID; relation.notification_id = notificationID; - await R.store(relation) + await R.store(relation); } } } @@ -1164,7 +1171,7 @@ async function checkOwner(userID, monitorID) { let row = await R.getRow("SELECT id FROM monitor WHERE id = ? AND user_id = ? ", [ monitorID, userID, - ]) + ]); if (! row) { throw new Error("You do not own this monitor."); @@ -1173,16 +1180,16 @@ async function checkOwner(userID, monitorID) { async function sendMonitorList(socket) { let list = await getMonitorJSONList(socket.userID); - io.to(socket.userID).emit("monitorList", list) + io.to(socket.userID).emit("monitorList", list); return list; } async function afterLogin(socket, user) { socket.userID = user.id; - socket.join(user.id) + socket.join(user.id); - let monitorList = await sendMonitorList(socket) - sendNotificationList(socket) + let monitorList = await sendMonitorList(socket); + sendNotificationList(socket); await sleep(500); @@ -1195,7 +1202,7 @@ async function afterLogin(socket, user) { } for (let monitorID in monitorList) { - await Monitor.sendStats(io, monitorID, user.id) + await Monitor.sendStats(io, monitorID, user.id); } } @@ -1204,7 +1211,7 @@ async function getMonitorJSONList(userID) { let monitorList = await R.find("monitor", " user_id = ? ORDER BY weight DESC, name", [ userID, - ]) + ]); for (let monitor of monitorList) { result[monitor.id] = await monitor.toJSON(); @@ -1215,16 +1222,16 @@ async function getMonitorJSONList(userID) { async function initDatabase() { if (! fs.existsSync(Database.path)) { - console.log("Copying Database") + console.log("Copying Database"); fs.copyFileSync(Database.templatePath, Database.path); } - console.log("Connecting to Database") + console.log("Connecting to Database"); await Database.connect(); - console.log("Connected") + console.log("Connected"); // Patch the database - await Database.patch() + await Database.patch(); let jwtSecretBean = await R.findOne("setting", " `key` = ? ", [ "jwtSecret", @@ -1240,7 +1247,7 @@ async function initDatabase() { // If there is no record in user table, it is a new Uptime Kuma instance, need to setup if ((await R.count("user")) === 0) { - console.log("No user, need setup") + console.log("No user, need setup"); needSetup = true; } @@ -1248,9 +1255,9 @@ async function initDatabase() { } async function startMonitor(userID, monitorID) { - await checkOwner(userID, monitorID) + await checkOwner(userID, monitorID); - console.log(`Resume Monitor: ${monitorID} User ID: ${userID}`) + console.log(`Resume Monitor: ${monitorID} User ID: ${userID}`); await R.exec("UPDATE monitor SET active = 1 WHERE id = ? AND user_id = ? ", [ monitorID, @@ -1259,24 +1266,24 @@ async function startMonitor(userID, monitorID) { let monitor = await R.findOne("monitor", " id = ? ", [ monitorID, - ]) + ]); if (monitor.id in monitorList) { monitorList[monitor.id].stop(); } monitorList[monitor.id] = monitor; - monitor.start(io) + monitor.start(io); } async function restartMonitor(userID, monitorID) { - return await startMonitor(userID, monitorID) + return await startMonitor(userID, monitorID); } async function pauseMonitor(userID, monitorID) { - await checkOwner(userID, monitorID) + await checkOwner(userID, monitorID); - console.log(`Pause Monitor: ${monitorID} User ID: ${userID}`) + console.log(`Pause Monitor: ${monitorID} User ID: ${userID}`); await R.exec("UPDATE monitor SET active = 0 WHERE id = ? AND user_id = ? ", [ monitorID, @@ -1292,7 +1299,7 @@ async function pauseMonitor(userID, monitorID) { * Resume active monitors */ async function startMonitors() { - let list = await R.find("monitor", " active = 1 ") + let list = await R.find("monitor", " active = 1 "); for (let monitor of list) { monitorList[monitor.id] = monitor; @@ -1309,10 +1316,10 @@ async function shutdownFunction(signal) { console.log("Shutdown requested"); console.log("Called signal: " + signal); - console.log("Stopping all monitors") + console.log("Stopping all monitors"); for (let id in monitorList) { - let monitor = monitorList[id] - monitor.stop() + let monitor = monitorList[id]; + monitor.stop(); } await sleep(2000); await Database.close(); diff --git a/server/socket-handlers/status-page-socket-handler.js b/server/socket-handlers/status-page-socket-handler.js index 5ba173f3..16d17831 100644 --- a/server/socket-handlers/status-page-socket-handler.js +++ b/server/socket-handlers/status-page-socket-handler.js @@ -2,6 +2,9 @@ const { R } = require("redbean-node"); const { checkLogin, setSettings } = require("../util-server"); const dayjs = require("dayjs"); const { debug } = require("../../src/util"); +const ImageDataURI = require("../image-data-uri"); +const Database = require("../database"); +const fs = require("fs"); module.exports.statusPageSocketHandler = (socket) => { @@ -67,18 +70,35 @@ module.exports.statusPageSocketHandler = (socket) => { }); // Save Status Page + // imgDataUrl Only Accept PNG! socket.on("saveStatusPage", async (config, imgDataUrl, publicGroupList, callback) => { try { checkLogin(socket); + const header = "data:image/png;base64,"; + + // Check logo format + // If is image data url, convert to png file + // Else assume it is a url, nothing to do + if (imgDataUrl.startsWith("data:")) { + if (! imgDataUrl.startsWith(header)) { + throw new Error("Only allowed PNG logo."); + } + + // Convert to file + await ImageDataURI.outputFile(imgDataUrl, Database.uploadDir + "logo.png"); + config.logo = "/upload/logo.png?t=" + Date.now(); + + } else { + config.icon = imgDataUrl; + } + // Save Config await setSettings("statusPage", config); await R.transaction(async (trx) => { - // Save Icon - // Save Public Group List const groupIDList = []; let groupOrder = 1; diff --git a/src/icon.js b/src/icon.js index ea11c2b5..67eb2a76 100644 --- a/src/icon.js +++ b/src/icon.js @@ -26,6 +26,7 @@ import { faArrowsAltV, faUnlink, faQuestionCircle, + faImages, faUpload, } from "@fortawesome/free-solid-svg-icons"; library.add( @@ -51,6 +52,8 @@ library.add( faArrowsAltV, faUnlink, faQuestionCircle, + faImages, + faUpload, ); export { FontAwesomeIcon }; diff --git a/src/mixins/socket.js b/src/mixins/socket.js index 6010d072..0757dead 100644 --- a/src/mixins/socket.js +++ b/src/mixins/socket.js @@ -1,7 +1,6 @@ import { io } from "socket.io-client"; import { useToast } from "vue-toastification"; -import axios from "axios"; -const toast = useToast() +const toast = useToast(); let socket; @@ -32,7 +31,7 @@ export default { certInfoList: {}, notificationList: [], connectionErrorMsg: "Cannot connect to the socket server. Reconnecting...", - } + }; }, created() { @@ -74,7 +73,7 @@ export default { }); socket.on("setup", (monitorID, data) => { - this.$router.push("/setup") + this.$router.push("/setup"); }); socket.on("autoLogin", (monitorID, data) => { @@ -132,7 +131,7 @@ export default { this.importantHeartbeatList[data.monitorID] = []; } - this.importantHeartbeatList[data.monitorID].unshift(data) + this.importantHeartbeatList[data.monitorID].unshift(data); } }); @@ -140,27 +139,27 @@ export default { if (! (monitorID in this.heartbeatList) || overwrite) { this.heartbeatList[monitorID] = data; } else { - this.heartbeatList[monitorID] = data.concat(this.heartbeatList[monitorID]) + this.heartbeatList[monitorID] = data.concat(this.heartbeatList[monitorID]); } }); socket.on("avgPing", (monitorID, data) => { - this.avgPingList[monitorID] = data + this.avgPingList[monitorID] = data; }); socket.on("uptime", (monitorID, type, data) => { - this.uptimeList[`${monitorID}_${type}`] = data + this.uptimeList[`${monitorID}_${type}`] = data; }); socket.on("certInfo", (monitorID, data) => { - this.certInfoList[monitorID] = JSON.parse(data) + this.certInfoList[monitorID] = JSON.parse(data); }); socket.on("importantHeartbeatList", (monitorID, data, overwrite) => { if (! (monitorID in this.importantHeartbeatList) || overwrite) { this.importantHeartbeatList[monitorID] = data; } else { - this.importantHeartbeatList[monitorID] = data.concat(this.importantHeartbeatList[monitorID]) + this.importantHeartbeatList[monitorID] = data.concat(this.importantHeartbeatList[monitorID]); } }); @@ -172,26 +171,26 @@ export default { }); socket.on("disconnect", () => { - console.log("disconnect") + console.log("disconnect"); this.connectionErrorMsg = "Lost connection to the socket server. Reconnecting..."; this.socket.connected = false; }); socket.on("connect", () => { - console.log("connect") + console.log("connect"); this.socket.connectCount++; this.socket.connected = true; // Reset Heartbeat list if it is re-connect if (this.socket.connectCount >= 2) { - this.clearData() + this.clearData(); } let token = this.storage().token; if (token) { if (token !== "autoLogin") { - this.loginByToken(token) + this.loginByToken(token); } else { // Timeout if it is not actually auto login @@ -235,7 +234,7 @@ export default { token, }, (res) => { if (res.tokenRequired) { - callback(res) + callback(res); } if (res.ok) { @@ -244,11 +243,11 @@ export default { this.loggedIn = true; // Trigger Chrome Save Password - history.pushState({}, "") + history.pushState({}, ""); } - callback(res) - }) + callback(res); + }); }, loginByToken(token) { @@ -256,11 +255,11 @@ export default { this.allowLoginDialog = true; if (! res.ok) { - this.logout() + this.logout(); } else { this.loggedIn = true; } - }) + }); }, logout() { @@ -268,68 +267,68 @@ export default { this.socket.token = null; this.loggedIn = false; - this.clearData() + this.clearData(); }, prepare2FA(callback) { - socket.emit("prepare2FA", callback) + socket.emit("prepare2FA", callback); }, save2FA(secret, callback) { - socket.emit("save2FA", callback) + socket.emit("save2FA", callback); }, disable2FA(callback) { - socket.emit("disable2FA", callback) + socket.emit("disable2FA", callback); }, verifyToken(token, callback) { - socket.emit("verifyToken", token, callback) + socket.emit("verifyToken", token, callback); }, twoFAStatus(callback) { - socket.emit("twoFAStatus", callback) + socket.emit("twoFAStatus", callback); }, getMonitorList(callback) { - socket.emit("getMonitorList", callback) + socket.emit("getMonitorList", callback); }, add(monitor, callback) { - socket.emit("add", monitor, callback) + socket.emit("add", monitor, callback); }, deleteMonitor(monitorID, callback) { - socket.emit("deleteMonitor", monitorID, callback) + socket.emit("deleteMonitor", monitorID, callback); }, clearData() { - console.log("reset heartbeat list") - this.heartbeatList = {} - this.importantHeartbeatList = {} + console.log("reset heartbeat list"); + this.heartbeatList = {}; + this.importantHeartbeatList = {}; }, uploadBackup(uploadedJSON, importHandle, callback) { - socket.emit("uploadBackup", uploadedJSON, importHandle, callback) + socket.emit("uploadBackup", uploadedJSON, importHandle, callback); }, clearEvents(monitorID, callback) { - socket.emit("clearEvents", monitorID, callback) + socket.emit("clearEvents", monitorID, callback); }, clearHeartbeats(monitorID, callback) { - socket.emit("clearHeartbeats", monitorID, callback) + socket.emit("clearHeartbeats", monitorID, callback); }, clearStatistics(callback) { - socket.emit("clearStatistics", callback) + socket.emit("clearStatistics", callback); }, }, computed: { lastHeartbeatList() { - let result = {} + let result = {}; for (let monitorID in this.heartbeatList) { let index = this.heartbeatList[monitorID].length - 1; @@ -340,15 +339,15 @@ export default { }, statusList() { - let result = {} + let result = {}; let unknown = { text: "Unknown", color: "secondary", - } + }; for (let monitorID in this.lastHeartbeatList) { - let lastHeartBeat = this.lastHeartbeatList[monitorID] + let lastHeartBeat = this.lastHeartbeatList[monitorID]; if (! lastHeartBeat) { result[monitorID] = unknown; @@ -381,12 +380,12 @@ export default { // Reload the SPA if the server version is changed. "info.version"(to, from) { if (from && from !== to) { - window.location.reload() + window.location.reload(); } }, remember() { - localStorage.remember = (this.remember) ? "1" : "0" + localStorage.remember = (this.remember) ? "1" : "0"; }, // Reconnect the socket io, if status-page to dashboard @@ -399,4 +398,4 @@ export default { }, -} +}; diff --git a/src/pages/StatusPage.vue b/src/pages/StatusPage.vue index 38cadeb3..ab396959 100644 --- a/src/pages/StatusPage.vue +++ b/src/pages/StatusPage.vue @@ -3,7 +3,10 @@

- +
+ + +
@@ -376,6 +379,16 @@ export default { axios.get("/api/status-page/config").then((res) => { this.config = res.data; + if (this.config.logo) { + this.imgDataUrl = this.config.logo; + + // Special handle for dev + const env = process.env.NODE_ENV; + if (env === "development" || localStorage.dev === "dev") { + let baseURL = location.protocol + "//" + location.hostname + ":3001"; + this.imgDataUrl = baseURL + this.imgDataUrl; + } + } }); axios.get("/api/status-page/incident").then((res) => { @@ -557,6 +570,30 @@ footer { min-width: 50px; } +.logo-wrapper { + display: inline-block; + position: relative; + + &:hover { + .icon-upload { + transform: scale(1.2); + } + } + + .icon-upload { + transition: all $easing-in 0.2s; + position: absolute; + bottom: 6px; + font-size: 20px; + left: -14px; + background-color: white; + padding: 5px; + border-radius: 10px; + cursor: pointer; + box-shadow: 0 15px 70px rgba(0, 0, 0, 0.9); + } +} + .logo { transition: all $easing-in 0.2s; @@ -577,7 +614,7 @@ footer { } .date { - font-size: 14px; + font-size: 12px; } }