From d5db40f40d1ab12c6549c48e38540a677b4f6e34 Mon Sep 17 00:00:00 2001 From: M1CK431 Date: Mon, 31 Jul 2023 23:08:03 +0200 Subject: [PATCH 01/11] API: make all resources users-agnostic --- server/client.js | 10 ++-- server/docker.js | 7 ++- server/notification.js | 20 ++------ server/proxy.js | 12 ++--- server/server.js | 46 ++----------------- .../socket-handlers/api-key-socket-handler.js | 5 +- .../maintenance-socket-handler.js | 14 +----- server/uptime-kuma-server.js | 18 +++----- 8 files changed, 30 insertions(+), 102 deletions(-) diff --git a/server/client.js b/server/client.js index 72f0a4e8e..0890ec8be 100644 --- a/server/client.js +++ b/server/client.js @@ -19,9 +19,7 @@ async function sendNotificationList(socket) { const timeLogger = new TimeLogger(); let result = []; - let list = await R.find("notification", " user_id = ? ", [ - socket.userID, - ]); + let list = await R.findAll("notification"); for (let bean of list) { let notificationObject = bean.export(); @@ -102,7 +100,7 @@ async function sendImportantHeartbeatList(socket, monitorID, toUser = false, ove async function sendProxyList(socket) { const timeLogger = new TimeLogger(); - const list = await R.find("proxy", " user_id = ? ", [ socket.userID ]); + const list = await R.findAll("proxy"); io.to(socket.userID).emit("proxyList", list.map(bean => bean.export())); timeLogger.print("Send Proxy List"); @@ -174,9 +172,7 @@ async function sendDockerHostList(socket) { const timeLogger = new TimeLogger(); let result = []; - let list = await R.find("docker_host", " user_id = ? ", [ - socket.userID, - ]); + let list = await R.findAll("docker_host"); for (let bean of list) { result.push(bean.toJSON()); diff --git a/server/docker.js b/server/docker.js index ee6051dfa..8330c2019 100644 --- a/server/docker.js +++ b/server/docker.js @@ -23,7 +23,7 @@ class DockerHost { let bean; if (dockerHostID) { - bean = await R.findOne("docker_host", " id = ? AND user_id = ? ", [ dockerHostID, userID ]); + bean = await R.findOne("docker_host", " id = ? ", [ dockerHostID ]); if (!bean) { throw new Error("docker host not found"); @@ -46,11 +46,10 @@ class DockerHost { /** * Delete a Docker host * @param {number} dockerHostID ID of the Docker host to delete - * @param {number} userID ID of the user who created the Docker host * @returns {Promise} */ - static async delete(dockerHostID, userID) { - let bean = await R.findOne("docker_host", " id = ? AND user_id = ? ", [ dockerHostID, userID ]); + static async delete(dockerHostID) { + let bean = await R.findOne("docker_host", " id = ? ", [ dockerHostID ]); if (!bean) { throw new Error("docker host not found"); diff --git a/server/notification.js b/server/notification.js index 26daeb042..6ed6f6053 100644 --- a/server/notification.js +++ b/server/notification.js @@ -194,10 +194,7 @@ class Notification { let bean; if (notificationID) { - bean = await R.findOne("notification", " id = ? AND user_id = ? ", [ - notificationID, - userID, - ]); + bean = await R.findOne("notification", " id = ? ", [ notificationID ]); if (! bean) { throw new Error("notification not found"); @@ -223,14 +220,10 @@ class Notification { /** * Delete a notification * @param {number} notificationID ID of notification to delete - * @param {number} userID ID of user who created notification * @returns {Promise} */ - static async delete(notificationID, userID) { - let bean = await R.findOne("notification", " id = ? AND user_id = ? ", [ - notificationID, - userID, - ]); + static async delete(notificationID) { + let bean = await R.findOne("notification", " id = ? ", [ notificationID ]); if (! bean) { throw new Error("notification not found"); @@ -254,13 +247,10 @@ class Notification { /** * Apply the notification to every monitor * @param {number} notificationID ID of notification to apply - * @param {number} userID ID of user who created notification * @returns {Promise} */ -async function applyNotificationEveryMonitor(notificationID, userID) { - let monitors = await R.getAll("SELECT id FROM monitor WHERE user_id = ?", [ - userID - ]); +async function applyNotificationEveryMonitor(notificationID) { + let monitors = await R.getAll("SELECT id FROM monitor"); for (let i = 0; i < monitors.length; i++) { let checkNotification = await R.findOne("monitor_notification", " monitor_id = ? AND notification_id = ? ", [ diff --git a/server/proxy.js b/server/proxy.js index 3f3771ab9..2100b3e9c 100644 --- a/server/proxy.js +++ b/server/proxy.js @@ -22,7 +22,7 @@ class Proxy { let bean; if (proxyID) { - bean = await R.findOne("proxy", " id = ? AND user_id = ? ", [ proxyID, userID ]); + bean = await R.findOne("proxy", " id = ? ", [ proxyID ]); if (!bean) { throw new Error("proxy not found"); @@ -67,11 +67,10 @@ class Proxy { /** * Deletes proxy with given id and removes it from monitors * @param {number} proxyID ID of proxy to delete - * @param {number} userID ID of proxy owner * @returns {Promise} */ - static async delete(proxyID, userID) { - const bean = await R.findOne("proxy", " id = ? AND user_id = ? ", [ proxyID, userID ]); + static async delete(proxyID) { + const bean = await R.findOne("proxy", " id = ? ", [ proxyID ]); if (!bean) { throw new Error("proxy not found"); @@ -182,12 +181,11 @@ class Proxy { /** * Applies given proxy id to monitors * @param {number} proxyID ID of proxy to apply - * @param {number} userID ID of proxy owner * @returns {Promise} */ -async function applyProxyEveryMonitor(proxyID, userID) { +async function applyProxyEveryMonitor(proxyID) { // Find all monitors with id and proxy id - const monitors = await R.getAll("SELECT id, proxy_id FROM monitor WHERE user_id = ?", [ userID ]); + const monitors = await R.getAll("SELECT id, proxy_id FROM monitor"); // Update proxy id not match with given proxy id for (const monitor of monitors) { diff --git a/server/server.js b/server/server.js index db58ae829..009dffc38 100644 --- a/server/server.js +++ b/server/server.js @@ -761,10 +761,6 @@ let needSetup = false; let bean = await R.findOne("monitor", " id = ? ", [ monitor.id ]); - if (bean.user_id !== socket.userID) { - throw new Error("Permission denied."); - } - // Check if Parent is Descendant (would cause endless loop) if (monitor.parent !== null) { const childIDs = await Monitor.getAllChildrenIDs(monitor.id); @@ -924,10 +920,7 @@ let needSetup = false; log.info("monitor", `Get Monitor: ${monitorID} User ID: ${socket.userID}`); - let monitor = await R.findOne("monitor", " id = ? AND user_id = ? ", [ - monitorID, - socket.userID, - ]); + let monitor = await R.findOne("monitor", " id = ? ", [ monitorID ]); const monitorData = [{ id: monitor.id, active: monitor.active }]; @@ -1034,10 +1027,7 @@ let needSetup = false; const startTime = Date.now(); - await R.exec("DELETE FROM monitor WHERE id = ? AND user_id = ? ", [ - monitorID, - socket.userID, - ]); + await R.exec("DELETE FROM monitor WHERE id = ? ", [ monitorID ]); // Fix #2880 apicache.clear(); @@ -1638,24 +1628,6 @@ async function updateMonitorNotification(monitorID, notificationIDList) { } } -/** - * Check if a given user owns a specific monitor - * @param {number} userID ID of user to check - * @param {number} monitorID ID of monitor to check - * @returns {Promise} - * @throws {Error} The specified user does not own the monitor - */ -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."); - } -} - /** * Function called after user login * This function is used to send the heartbeat list of a monitor. @@ -1739,14 +1711,9 @@ async function initDatabase(testMode = false) { * @returns {Promise} */ async function startMonitor(userID, monitorID) { - await checkOwner(userID, monitorID); - log.info("manage", `Resume Monitor: ${monitorID} User ID: ${userID}`); - await R.exec("UPDATE monitor SET active = 1 WHERE id = ? AND user_id = ? ", [ - monitorID, - userID, - ]); + await R.exec("UPDATE monitor SET active = 1 WHERE id = ? ", [ monitorID ]); let monitor = await R.findOne("monitor", " id = ? ", [ monitorID, @@ -1777,14 +1744,9 @@ async function restartMonitor(userID, monitorID) { * @returns {Promise} */ async function pauseMonitor(userID, monitorID) { - await checkOwner(userID, monitorID); - log.info("manage", `Pause Monitor: ${monitorID} User ID: ${userID}`); - await R.exec("UPDATE monitor SET active = 0 WHERE id = ? AND user_id = ? ", [ - monitorID, - userID, - ]); + await R.exec("UPDATE monitor SET active = 0 WHERE id = ? ", [ monitorID ]); if (monitorID in server.monitorList) { await server.monitorList[monitorID].stop(); diff --git a/server/socket-handlers/api-key-socket-handler.js b/server/socket-handlers/api-key-socket-handler.js index f76b90991..7c57d358f 100644 --- a/server/socket-handlers/api-key-socket-handler.js +++ b/server/socket-handlers/api-key-socket-handler.js @@ -74,10 +74,7 @@ module.exports.apiKeySocketHandler = (socket) => { log.debug("apikeys", `Deleted API Key: ${keyID} User ID: ${socket.userID}`); - await R.exec("DELETE FROM api_key WHERE id = ? AND user_id = ? ", [ - keyID, - socket.userID, - ]); + await R.exec("DELETE FROM api_key WHERE id = ? ", [ keyID ]); apicache.clear(); diff --git a/server/socket-handlers/maintenance-socket-handler.js b/server/socket-handlers/maintenance-socket-handler.js index 7de13fe57..086811964 100644 --- a/server/socket-handlers/maintenance-socket-handler.js +++ b/server/socket-handlers/maintenance-socket-handler.js @@ -50,10 +50,6 @@ module.exports.maintenanceSocketHandler = (socket) => { let bean = server.getMaintenance(maintenance.id); - if (bean.user_id !== socket.userID) { - throw new Error("Permission denied."); - } - await Maintenance.jsonToBean(bean, maintenance); await R.store(bean); await bean.run(true); @@ -151,10 +147,7 @@ module.exports.maintenanceSocketHandler = (socket) => { log.debug("maintenance", `Get Maintenance: ${maintenanceID} User ID: ${socket.userID}`); - let bean = await R.findOne("maintenance", " id = ? AND user_id = ? ", [ - maintenanceID, - socket.userID, - ]); + let bean = await R.findOne("maintenance", " id = ? ", [ maintenanceID ]); callback({ ok: true, @@ -244,10 +237,7 @@ module.exports.maintenanceSocketHandler = (socket) => { delete server.maintenanceList[maintenanceID]; } - await R.exec("DELETE FROM maintenance WHERE id = ? AND user_id = ? ", [ - maintenanceID, - socket.userID, - ]); + await R.exec("DELETE FROM maintenance WHERE id = ? ", [ maintenanceID ]); apicache.clear(); diff --git a/server/uptime-kuma-server.js b/server/uptime-kuma-server.js index 76bf42565..13f6cb74a 100644 --- a/server/uptime-kuma-server.js +++ b/server/uptime-kuma-server.js @@ -200,7 +200,7 @@ class UptimeKumaServer { * @returns {Promise} List of monitors */ async sendMonitorList(socket) { - let list = await this.getMonitorJSONList(socket.userID); + let list = await this.getMonitorJSONList(); this.io.to(socket.userID).emit("monitorList", list); return list; } @@ -227,25 +227,21 @@ class UptimeKumaServer { } /** - * Get a list of monitors for the given user. - * @param {string} userID - The ID of the user to get monitors for. + * Get a list of monitors. * @param {number} monitorID - The ID of monitor for. * @returns {Promise} A promise that resolves to an object with monitor IDs as keys and monitor objects as values. * * Generated by Trelent */ - async getMonitorJSONList(userID, monitorID = null) { - - let query = " user_id = ? "; - let queryParams = [ userID ]; + async getMonitorJSONList(monitorID = null) { + let monitorList = []; if (monitorID) { - query += "AND id = ? "; - queryParams.push(monitorID); + monitorList = await R.find("monitor", "id = ? ORDER BY weight DESC, name", [ monitorID ]); + } else { + monitorList = await R.findAll("monitor", "ORDER BY weight DESC, name"); } - let monitorList = await R.find("monitor", query + "ORDER BY weight DESC, name", queryParams); - const monitorData = monitorList.map(monitor => ({ id: monitor.id, active: monitor.active, From 51f94d6cf52aeeeba6a1888fc68bbcb1b7280c99 Mon Sep 17 00:00:00 2001 From: M1CK431 Date: Tue, 1 Aug 2023 00:23:55 +0200 Subject: [PATCH 02/11] API: add basic multiple admin users --- server/model/user.js | 1 + server/server.js | 73 ++++++++++++++++++++++++++++++++++------ server/user.js | 78 +++++++++++++++++++++++++++++++++++++++++++ server/util-server.js | 8 ++--- 4 files changed, 145 insertions(+), 15 deletions(-) create mode 100644 server/user.js diff --git a/server/model/user.js b/server/model/user.js index 329402ff5..f3897fd0b 100644 --- a/server/model/user.js +++ b/server/model/user.js @@ -43,6 +43,7 @@ class User extends BeanModel { */ static createJWT(user, jwtSecret) { return jwt.sign({ + userID: user.id, username: user.username, h: shake256(user.password, SHAKE256_LENGTH), }, jwtSecret); diff --git a/server/server.js b/server/server.js index 009dffc38..080d080d2 100644 --- a/server/server.js +++ b/server/server.js @@ -150,6 +150,7 @@ const { resetChrome } = require("./monitor-types/real-browser-monitor-type"); const { EmbeddedMariaDB } = require("./embedded-mariadb"); const { SetupDatabase } = require("./setup-database"); const { chartSocketHandler } = require("./socket-handlers/chart-socket-handler"); +const { sendUserList, getUser, saveUser } = require("./user"); app.use(express.json()); @@ -494,7 +495,7 @@ let needSetup = false; } checkLogin(socket); - await doubleCheckPassword(socket, currentPassword); + await doubleCheckPassword(socket.userID, currentPassword); let user = await R.findOne("user", " id = ? AND active = 1 ", [ socket.userID, @@ -544,7 +545,7 @@ let needSetup = false; } checkLogin(socket); - await doubleCheckPassword(socket, currentPassword); + await doubleCheckPassword(socket.userID, currentPassword); await R.exec("UPDATE `user` SET twofa_status = 1 WHERE id = ? ", [ socket.userID, @@ -577,7 +578,7 @@ let needSetup = false; } checkLogin(socket); - await doubleCheckPassword(socket, currentPassword); + await doubleCheckPassword(socket.userID, currentPassword); await TwoFA.disable2FA(socket.userID); log.info("auth", `Disabled 2FA token. IP=${clientIP}`); @@ -601,7 +602,7 @@ let needSetup = false; socket.on("verifyToken", async (token, currentPassword, callback) => { try { checkLogin(socket); - await doubleCheckPassword(socket, currentPassword); + await doubleCheckPassword(socket.userID, currentPassword); let user = await R.findOne("user", " id = ? AND active = 1 ", [ socket.userID, @@ -668,10 +669,6 @@ let needSetup = false; throw new Error("Password is too weak. It should contain alphabetic and numeric characters. It must be at least 6 characters in length."); } - if ((await R.knex("user").count("id as count").first()).count !== 0) { - throw new Error("Uptime Kuma has been initialized. If you want to run setup again, please delete the database."); - } - let user = R.dispense("user"); user.username = username; user.password = passwordHash.generate(password); @@ -697,6 +694,61 @@ let needSetup = false; // Auth Only API // *************************** + socket.on("getUsers", async callback => { + try { + checkLogin(socket); + + const users = await sendUserList(socket); + + callback({ + ok: true, + users + }); + } catch (e) { + callback({ + ok: false, + msg: e.message, + }); + } + }); + + socket.on("getUser", async (userID, callback) => { + try { + checkLogin(socket); + + const user = await getUser(userID); + + callback({ + ok: true, + user + }); + } catch (e) { + callback({ + ok: false, + msg: e.message, + }); + } + }); + + socket.on("saveUser", async (user, callback) => { + try { + checkLogin(socket); + + await saveUser(socket, user); + await sendUserList(socket); + + callback({ + ok: true, + msg: "Saved Successfully.", + }); + } catch (e) { + callback({ + ok: false, + msg: e.message, + }); + } + }); + // Add a new monitor socket.on("add", async (monitor, callback) => { try { @@ -1282,7 +1334,7 @@ let needSetup = false; } }); - socket.on("changePassword", async (password, callback) => { + socket.on("changePassword", async (userID, password, callback) => { try { checkLogin(socket); @@ -1294,7 +1346,7 @@ let needSetup = false; throw new Error("Password is too weak. It should contain alphabetic and numeric characters. It must be at least 6 characters in length."); } - let user = await doubleCheckPassword(socket, password.currentPassword); + let user = await doubleCheckPassword(userID, password.currentPassword); await user.resetPassword(password.newPassword); server.disconnectAllSocketClients(user.id, socket.id); @@ -1649,6 +1701,7 @@ async function afterLogin(socket, user) { sendAPIKeyList(socket), sendRemoteBrowserList(socket), sendMonitorTypeList(socket), + sendUserList(socket), ]); await StatusPage.sendStatusPageList(io, socket); diff --git a/server/user.js b/server/user.js new file mode 100644 index 000000000..5b8dfa09b --- /dev/null +++ b/server/user.js @@ -0,0 +1,78 @@ +const { TimeLogger } = require("../src/util"); +const { R } = require("redbean-node"); +const { UptimeKumaServer } = require("./uptime-kuma-server"); +const server = UptimeKumaServer.getInstance(); +const io = server.io; + +/** + * Send list of users to client + * @param {Socket} socket Socket.io socket instance + * @returns {Promise} list of users + */ +async function sendUserList(socket) { + const timeLogger = new TimeLogger(); + const userList = await R.getAll("SELECT id, username, active FROM user"); + + io.to(socket.userID).emit("userList", userList); + timeLogger.print("Send User List"); + + return userList; +} + +/** + * Fetch specified user + * @param {number} userID ID of user to retrieve + * @returns {Promise} User + */ +async function getUser(userID) { + const timeLogger = new TimeLogger(); + + const user = await R.getRow( + "SELECT id, username, active FROM user WHERE id = ? ", + [ userID ] + ); + + if (!user) { + throw new Error("User not found"); + } + + timeLogger.print(`Get user ${userID}`); + + return user; +} + +/** + * Saves and updates given user entity + * @param {Socket} socket Socket.io socket instance + * @param {object} user user to update + * @returns {Promise} + */ +async function saveUser(socket, user) { + const timeLogger = new TimeLogger(); + const { id, username, active } = user; + + const bean = await R.findOne("user", " id = ? ", [ id ]); + + if (!bean) { + throw new Error("User not found"); + } + + if (username) { + bean.username = username; + } + if (active !== undefined) { + bean.active = active; + } + + await R.store(bean); + + io.to(socket.userID).emit("saveUser", bean); + + timeLogger.print(`Save user ${user.id}`); +} + +module.exports = { + sendUserList, + getUser, + saveUser +}; diff --git a/server/util-server.js b/server/util-server.js index 5ebc62ac5..6c4111df6 100644 --- a/server/util-server.js +++ b/server/util-server.js @@ -765,20 +765,18 @@ exports.checkLogin = (socket) => { /** * For logged-in users, double-check the password - * @param {Socket} socket Socket.io instance + * @param {number} userID ID of user to check * @param {string} currentPassword Password to validate * @returns {Promise} User * @throws The current password is not a string * @throws The provided password is not correct */ -exports.doubleCheckPassword = async (socket, currentPassword) => { +exports.doubleCheckPassword = async (userID, currentPassword) => { if (typeof currentPassword !== "string") { throw new Error("Wrong data type?"); } - let user = await R.findOne("user", " id = ? AND active = 1 ", [ - socket.userID, - ]); + let user = await R.findOne("user", " id = ? ", [ userID ]); if (!user || !passwordHash.verify(currentPassword, user.password)) { throw new Error("Incorrect current password"); From 9521b8e12260c19eaf3a0f173c1e47ad48620dd0 Mon Sep 17 00:00:00 2001 From: M1CK431 Date: Sat, 12 Aug 2023 22:15:50 +0200 Subject: [PATCH 03/11] API: ensure user is active in checkLogin helper --- server/server.js | 66 +++++++++---------- .../socket-handlers/api-key-socket-handler.js | 10 +-- .../cloudflared-socket-handler.js | 10 +-- .../database-socket-handler.js | 4 +- .../socket-handlers/docker-socket-handler.js | 6 +- .../socket-handlers/general-socket-handler.js | 2 +- .../maintenance-socket-handler.js | 22 +++---- .../socket-handlers/proxy-socket-handler.js | 4 +- .../status-page-socket-handler.js | 12 ++-- server/util-server.js | 6 +- 10 files changed, 72 insertions(+), 70 deletions(-) diff --git a/server/server.js b/server/server.js index 080d080d2..05d1f5b86 100644 --- a/server/server.js +++ b/server/server.js @@ -494,7 +494,7 @@ let needSetup = false; return; } - checkLogin(socket); + await checkLogin(socket); await doubleCheckPassword(socket.userID, currentPassword); let user = await R.findOne("user", " id = ? AND active = 1 ", [ @@ -544,7 +544,7 @@ let needSetup = false; return; } - checkLogin(socket); + await checkLogin(socket); await doubleCheckPassword(socket.userID, currentPassword); await R.exec("UPDATE `user` SET twofa_status = 1 WHERE id = ? ", [ @@ -577,7 +577,7 @@ let needSetup = false; return; } - checkLogin(socket); + await checkLogin(socket); await doubleCheckPassword(socket.userID, currentPassword); await TwoFA.disable2FA(socket.userID); @@ -601,7 +601,7 @@ let needSetup = false; socket.on("verifyToken", async (token, currentPassword, callback) => { try { - checkLogin(socket); + await checkLogin(socket); await doubleCheckPassword(socket.userID, currentPassword); let user = await R.findOne("user", " id = ? AND active = 1 ", [ @@ -634,7 +634,7 @@ let needSetup = false; socket.on("twoFAStatus", async (callback) => { try { - checkLogin(socket); + await checkLogin(socket); let user = await R.findOne("user", " id = ? AND active = 1 ", [ socket.userID, @@ -696,7 +696,7 @@ let needSetup = false; socket.on("getUsers", async callback => { try { - checkLogin(socket); + await checkLogin(socket); const users = await sendUserList(socket); @@ -714,7 +714,7 @@ let needSetup = false; socket.on("getUser", async (userID, callback) => { try { - checkLogin(socket); + await checkLogin(socket); const user = await getUser(userID); @@ -732,7 +732,7 @@ let needSetup = false; socket.on("saveUser", async (user, callback) => { try { - checkLogin(socket); + await checkLogin(socket); await saveUser(socket, user); await sendUserList(socket); @@ -752,7 +752,7 @@ let needSetup = false; // Add a new monitor socket.on("add", async (monitor, callback) => { try { - checkLogin(socket); + await checkLogin(socket); let bean = R.dispense("monitor"); let notificationIDList = monitor.notificationIDList; @@ -809,7 +809,7 @@ let needSetup = false; socket.on("editMonitor", async (monitor, callback) => { try { let removeGroupChildren = false; - checkLogin(socket); + await checkLogin(socket); let bean = await R.findOne("monitor", " id = ? ", [ monitor.id ]); @@ -952,7 +952,7 @@ let needSetup = false; socket.on("getMonitorList", async (callback) => { try { - checkLogin(socket); + await checkLogin(socket); await server.sendMonitorList(socket); callback({ ok: true, @@ -968,7 +968,7 @@ let needSetup = false; socket.on("getMonitor", async (monitorID, callback) => { try { - checkLogin(socket); + await checkLogin(socket); log.info("monitor", `Get Monitor: ${monitorID} User ID: ${socket.userID}`); @@ -992,7 +992,7 @@ let needSetup = false; socket.on("getMonitorBeats", async (monitorID, period, callback) => { try { - checkLogin(socket); + await checkLogin(socket); log.info("monitor", `Get Monitor Beats: ${monitorID} User ID: ${socket.userID}`); @@ -1028,7 +1028,7 @@ let needSetup = false; // Start or Resume the monitor socket.on("resumeMonitor", async (monitorID, callback) => { try { - checkLogin(socket); + await checkLogin(socket); await startMonitor(socket.userID, monitorID); await server.sendUpdateMonitorIntoList(socket, monitorID); @@ -1048,7 +1048,7 @@ let needSetup = false; socket.on("pauseMonitor", async (monitorID, callback) => { try { - checkLogin(socket); + await checkLogin(socket); await pauseMonitor(socket.userID, monitorID); await server.sendUpdateMonitorIntoList(socket, monitorID); @@ -1068,7 +1068,7 @@ let needSetup = false; socket.on("deleteMonitor", async (monitorID, callback) => { try { - checkLogin(socket); + await checkLogin(socket); log.info("manage", `Delete Monitor: ${monitorID} User ID: ${socket.userID}`); @@ -1105,7 +1105,7 @@ let needSetup = false; socket.on("getTags", async (callback) => { try { - checkLogin(socket); + await checkLogin(socket); const list = await R.findAll("tag"); @@ -1124,7 +1124,7 @@ let needSetup = false; socket.on("addTag", async (tag, callback) => { try { - checkLogin(socket); + await checkLogin(socket); let bean = R.dispense("tag"); bean.name = tag.name; @@ -1146,7 +1146,7 @@ let needSetup = false; socket.on("editTag", async (tag, callback) => { try { - checkLogin(socket); + await checkLogin(socket); let bean = await R.findOne("tag", " id = ? ", [ tag.id ]); if (bean == null) { @@ -1178,7 +1178,7 @@ let needSetup = false; socket.on("deleteTag", async (tagID, callback) => { try { - checkLogin(socket); + await checkLogin(socket); await R.exec("DELETE FROM tag WHERE id = ? ", [ tagID ]); @@ -1198,7 +1198,7 @@ let needSetup = false; socket.on("addMonitorTag", async (tagID, monitorID, value, callback) => { try { - checkLogin(socket); + await checkLogin(socket); await R.exec("INSERT INTO monitor_tag (tag_id, monitor_id, value) VALUES (?, ?, ?)", [ tagID, @@ -1222,7 +1222,7 @@ let needSetup = false; socket.on("editMonitorTag", async (tagID, monitorID, value, callback) => { try { - checkLogin(socket); + await checkLogin(socket); await R.exec("UPDATE monitor_tag SET value = ? WHERE tag_id = ? AND monitor_id = ?", [ value, @@ -1246,7 +1246,7 @@ let needSetup = false; socket.on("deleteMonitorTag", async (tagID, monitorID, value, callback) => { try { - checkLogin(socket); + await checkLogin(socket); await R.exec("DELETE FROM monitor_tag WHERE tag_id = ? AND monitor_id = ? AND value = ?", [ tagID, @@ -1336,7 +1336,7 @@ let needSetup = false; socket.on("changePassword", async (userID, password, callback) => { try { - checkLogin(socket); + await checkLogin(socket); if (!password.newPassword) { throw new Error("Invalid new password"); @@ -1368,7 +1368,7 @@ let needSetup = false; socket.on("getSettings", async (callback) => { try { - checkLogin(socket); + await checkLogin(socket); const data = await getSettings("general"); if (!data.serverTimezone) { @@ -1390,7 +1390,7 @@ let needSetup = false; socket.on("setSettings", async (data, currentPassword, callback) => { try { - checkLogin(socket); + await checkLogin(socket); // If currently is disabled auth, don't need to check // Disabled Auth + Want to Disable Auth => No Check @@ -1454,7 +1454,7 @@ let needSetup = false; // Add or Edit socket.on("addNotification", async (notification, notificationID, callback) => { try { - checkLogin(socket); + await checkLogin(socket); let notificationBean = await Notification.save(notification, notificationID, socket.userID); await sendNotificationList(socket); @@ -1476,7 +1476,7 @@ let needSetup = false; socket.on("deleteNotification", async (notificationID, callback) => { try { - checkLogin(socket); + await checkLogin(socket); await Notification.delete(notificationID, socket.userID); await sendNotificationList(socket); @@ -1497,7 +1497,7 @@ let needSetup = false; socket.on("testNotification", async (notification, callback) => { try { - checkLogin(socket); + await checkLogin(socket); let msg = await Notification.send(notification, notification.name + " Testing"); @@ -1518,7 +1518,7 @@ let needSetup = false; socket.on("checkApprise", async (callback) => { try { - checkLogin(socket); + await checkLogin(socket); callback(Notification.checkApprise()); } catch (e) { callback(false); @@ -1527,7 +1527,7 @@ let needSetup = false; socket.on("clearEvents", async (monitorID, callback) => { try { - checkLogin(socket); + await checkLogin(socket); log.info("manage", `Clear Events Monitor: ${monitorID} User ID: ${socket.userID}`); @@ -1551,7 +1551,7 @@ let needSetup = false; socket.on("clearHeartbeats", async (monitorID, callback) => { try { - checkLogin(socket); + await checkLogin(socket); log.info("manage", `Clear Heartbeats Monitor: ${monitorID} User ID: ${socket.userID}`); @@ -1575,7 +1575,7 @@ let needSetup = false; socket.on("clearStatistics", async (callback) => { try { - checkLogin(socket); + await checkLogin(socket); log.info("manage", `Clear Statistics User ID: ${socket.userID}`); diff --git a/server/socket-handlers/api-key-socket-handler.js b/server/socket-handlers/api-key-socket-handler.js index 7c57d358f..efe9984a0 100644 --- a/server/socket-handlers/api-key-socket-handler.js +++ b/server/socket-handlers/api-key-socket-handler.js @@ -17,7 +17,7 @@ module.exports.apiKeySocketHandler = (socket) => { // Add a new api key socket.on("addAPIKey", async (key, callback) => { try { - checkLogin(socket); + await checkLogin(socket); let clearKey = nanoid(40); let hashedKey = passwordHash.generate(clearKey); @@ -54,7 +54,7 @@ module.exports.apiKeySocketHandler = (socket) => { socket.on("getAPIKeyList", async (callback) => { try { - checkLogin(socket); + await checkLogin(socket); await sendAPIKeyList(socket); callback({ ok: true, @@ -70,7 +70,7 @@ module.exports.apiKeySocketHandler = (socket) => { socket.on("deleteAPIKey", async (keyID, callback) => { try { - checkLogin(socket); + await checkLogin(socket); log.debug("apikeys", `Deleted API Key: ${keyID} User ID: ${socket.userID}`); @@ -96,7 +96,7 @@ module.exports.apiKeySocketHandler = (socket) => { socket.on("disableAPIKey", async (keyID, callback) => { try { - checkLogin(socket); + await checkLogin(socket); log.debug("apikeys", `Disabled Key: ${keyID} User ID: ${socket.userID}`); @@ -124,7 +124,7 @@ module.exports.apiKeySocketHandler = (socket) => { socket.on("enableAPIKey", async (keyID, callback) => { try { - checkLogin(socket); + await checkLogin(socket); log.debug("apikeys", `Enabled Key: ${keyID} User ID: ${socket.userID}`); diff --git a/server/socket-handlers/cloudflared-socket-handler.js b/server/socket-handlers/cloudflared-socket-handler.js index 809191fe8..9508aeb6f 100644 --- a/server/socket-handlers/cloudflared-socket-handler.js +++ b/server/socket-handlers/cloudflared-socket-handler.js @@ -36,7 +36,7 @@ module.exports.cloudflaredSocketHandler = (socket) => { socket.on(prefix + "join", async () => { try { - checkLogin(socket); + await checkLogin(socket); socket.join("cloudflared"); io.to(socket.userID).emit(prefix + "installed", cloudflared.checkInstalled()); io.to(socket.userID).emit(prefix + "running", cloudflared.running); @@ -46,14 +46,14 @@ module.exports.cloudflaredSocketHandler = (socket) => { socket.on(prefix + "leave", async () => { try { - checkLogin(socket); + await checkLogin(socket); socket.leave("cloudflared"); } catch (error) { } }); socket.on(prefix + "start", async (token) => { try { - checkLogin(socket); + await checkLogin(socket); if (token && typeof token === "string") { await setSetting("cloudflaredTunnelToken", token); cloudflared.token = token; @@ -66,7 +66,7 @@ module.exports.cloudflaredSocketHandler = (socket) => { socket.on(prefix + "stop", async (currentPassword, callback) => { try { - checkLogin(socket); + await checkLogin(socket); const disabledAuth = await setting("disableAuth"); if (!disabledAuth) { await doubleCheckPassword(socket, currentPassword); @@ -82,7 +82,7 @@ module.exports.cloudflaredSocketHandler = (socket) => { socket.on(prefix + "removeToken", async () => { try { - checkLogin(socket); + await checkLogin(socket); await setSetting("cloudflaredTunnelToken", ""); } catch (error) { } }); diff --git a/server/socket-handlers/database-socket-handler.js b/server/socket-handlers/database-socket-handler.js index ee2394bf6..b440fd5b6 100644 --- a/server/socket-handlers/database-socket-handler.js +++ b/server/socket-handlers/database-socket-handler.js @@ -11,7 +11,7 @@ module.exports.databaseSocketHandler = (socket) => { // Post or edit incident socket.on("getDatabaseSize", async (callback) => { try { - checkLogin(socket); + await checkLogin(socket); callback({ ok: true, size: Database.getSize(), @@ -26,7 +26,7 @@ module.exports.databaseSocketHandler = (socket) => { socket.on("shrinkDatabase", async (callback) => { try { - checkLogin(socket); + await checkLogin(socket); await Database.shrink(); callback({ ok: true, diff --git a/server/socket-handlers/docker-socket-handler.js b/server/socket-handlers/docker-socket-handler.js index 95a60bcd3..c483b253c 100644 --- a/server/socket-handlers/docker-socket-handler.js +++ b/server/socket-handlers/docker-socket-handler.js @@ -11,7 +11,7 @@ const { log } = require("../../src/util"); module.exports.dockerSocketHandler = (socket) => { socket.on("addDockerHost", async (dockerHost, dockerHostID, callback) => { try { - checkLogin(socket); + await checkLogin(socket); let dockerHostBean = await DockerHost.save(dockerHost, dockerHostID, socket.userID); await sendDockerHostList(socket); @@ -33,7 +33,7 @@ module.exports.dockerSocketHandler = (socket) => { socket.on("deleteDockerHost", async (dockerHostID, callback) => { try { - checkLogin(socket); + await checkLogin(socket); await DockerHost.delete(dockerHostID, socket.userID); await sendDockerHostList(socket); @@ -54,7 +54,7 @@ module.exports.dockerSocketHandler = (socket) => { socket.on("testDockerHost", async (dockerHost, callback) => { try { - checkLogin(socket); + await checkLogin(socket); let amount = await DockerHost.testDockerHost(dockerHost); let msg; diff --git a/server/socket-handlers/general-socket-handler.js b/server/socket-handlers/general-socket-handler.js index 50dcd946e..0b8ad99d9 100644 --- a/server/socket-handlers/general-socket-handler.js +++ b/server/socket-handlers/general-socket-handler.js @@ -38,7 +38,7 @@ function getGameList() { module.exports.generalSocketHandler = (socket, server) => { socket.on("initServerTimezone", async (timezone) => { try { - checkLogin(socket); + await checkLogin(socket); log.debug("generalSocketHandler", "Timezone: " + timezone); await Settings.set("initServerTimezone", true); await server.setTimezone(timezone); diff --git a/server/socket-handlers/maintenance-socket-handler.js b/server/socket-handlers/maintenance-socket-handler.js index 086811964..a38943c23 100644 --- a/server/socket-handlers/maintenance-socket-handler.js +++ b/server/socket-handlers/maintenance-socket-handler.js @@ -15,7 +15,7 @@ module.exports.maintenanceSocketHandler = (socket) => { // Add a new maintenance socket.on("addMaintenance", async (maintenance, callback) => { try { - checkLogin(socket); + await checkLogin(socket); log.debug("maintenance", maintenance); @@ -46,7 +46,7 @@ module.exports.maintenanceSocketHandler = (socket) => { // Edit a maintenance socket.on("editMaintenance", async (maintenance, callback) => { try { - checkLogin(socket); + await checkLogin(socket); let bean = server.getMaintenance(maintenance.id); @@ -74,7 +74,7 @@ module.exports.maintenanceSocketHandler = (socket) => { // Add a new monitor_maintenance socket.on("addMonitorMaintenance", async (maintenanceID, monitors, callback) => { try { - checkLogin(socket); + await checkLogin(socket); await R.exec("DELETE FROM monitor_maintenance WHERE maintenance_id = ?", [ maintenanceID @@ -109,7 +109,7 @@ module.exports.maintenanceSocketHandler = (socket) => { // Add a new monitor_maintenance socket.on("addMaintenanceStatusPage", async (maintenanceID, statusPages, callback) => { try { - checkLogin(socket); + await checkLogin(socket); await R.exec("DELETE FROM maintenance_status_page WHERE maintenance_id = ?", [ maintenanceID @@ -143,7 +143,7 @@ module.exports.maintenanceSocketHandler = (socket) => { socket.on("getMaintenance", async (maintenanceID, callback) => { try { - checkLogin(socket); + await checkLogin(socket); log.debug("maintenance", `Get Maintenance: ${maintenanceID} User ID: ${socket.userID}`); @@ -164,7 +164,7 @@ module.exports.maintenanceSocketHandler = (socket) => { socket.on("getMaintenanceList", async (callback) => { try { - checkLogin(socket); + await checkLogin(socket); await server.sendMaintenanceList(socket); callback({ ok: true, @@ -180,7 +180,7 @@ module.exports.maintenanceSocketHandler = (socket) => { socket.on("getMonitorMaintenance", async (maintenanceID, callback) => { try { - checkLogin(socket); + await checkLogin(socket); log.debug("maintenance", `Get Monitors for Maintenance: ${maintenanceID} User ID: ${socket.userID}`); @@ -204,7 +204,7 @@ module.exports.maintenanceSocketHandler = (socket) => { socket.on("getMaintenanceStatusPage", async (maintenanceID, callback) => { try { - checkLogin(socket); + await checkLogin(socket); log.debug("maintenance", `Get Status Pages for Maintenance: ${maintenanceID} User ID: ${socket.userID}`); @@ -228,7 +228,7 @@ module.exports.maintenanceSocketHandler = (socket) => { socket.on("deleteMaintenance", async (maintenanceID, callback) => { try { - checkLogin(socket); + await checkLogin(socket); log.debug("maintenance", `Delete Maintenance: ${maintenanceID} User ID: ${socket.userID}`); @@ -259,7 +259,7 @@ module.exports.maintenanceSocketHandler = (socket) => { socket.on("pauseMaintenance", async (maintenanceID, callback) => { try { - checkLogin(socket); + await checkLogin(socket); log.debug("maintenance", `Pause Maintenance: ${maintenanceID} User ID: ${socket.userID}`); @@ -293,7 +293,7 @@ module.exports.maintenanceSocketHandler = (socket) => { socket.on("resumeMaintenance", async (maintenanceID, callback) => { try { - checkLogin(socket); + await checkLogin(socket); log.debug("maintenance", `Resume Maintenance: ${maintenanceID} User ID: ${socket.userID}`); diff --git a/server/socket-handlers/proxy-socket-handler.js b/server/socket-handlers/proxy-socket-handler.js index 9e80371d7..800977a7f 100644 --- a/server/socket-handlers/proxy-socket-handler.js +++ b/server/socket-handlers/proxy-socket-handler.js @@ -12,7 +12,7 @@ const server = UptimeKumaServer.getInstance(); module.exports.proxySocketHandler = (socket) => { socket.on("addProxy", async (proxy, proxyID, callback) => { try { - checkLogin(socket); + await checkLogin(socket); const proxyBean = await Proxy.save(proxy, proxyID, socket.userID); await sendProxyList(socket); @@ -39,7 +39,7 @@ module.exports.proxySocketHandler = (socket) => { socket.on("deleteProxy", async (proxyID, callback) => { try { - checkLogin(socket); + await checkLogin(socket); await Proxy.delete(proxyID, socket.userID); await sendProxyList(socket); diff --git a/server/socket-handlers/status-page-socket-handler.js b/server/socket-handlers/status-page-socket-handler.js index 0804da15d..c8bf4b698 100644 --- a/server/socket-handlers/status-page-socket-handler.js +++ b/server/socket-handlers/status-page-socket-handler.js @@ -18,7 +18,7 @@ module.exports.statusPageSocketHandler = (socket) => { // Post or edit incident socket.on("postIncident", async (slug, incident, callback) => { try { - checkLogin(socket); + await checkLogin(socket); let statusPageID = await StatusPage.slugToID(slug); @@ -71,7 +71,7 @@ module.exports.statusPageSocketHandler = (socket) => { socket.on("unpinIncident", async (slug, callback) => { try { - checkLogin(socket); + await checkLogin(socket); let statusPageID = await StatusPage.slugToID(slug); @@ -92,7 +92,7 @@ module.exports.statusPageSocketHandler = (socket) => { socket.on("getStatusPage", async (slug, callback) => { try { - checkLogin(socket); + await checkLogin(socket); let statusPage = await R.findOne("status_page", " slug = ? ", [ slug @@ -118,7 +118,7 @@ module.exports.statusPageSocketHandler = (socket) => { // imgDataUrl Only Accept PNG! socket.on("saveStatusPage", async (slug, config, imgDataUrl, publicGroupList, callback) => { try { - checkLogin(socket); + await checkLogin(socket); // Save Config let statusPage = await R.findOne("status_page", " slug = ? ", [ @@ -256,7 +256,7 @@ module.exports.statusPageSocketHandler = (socket) => { // Add a new status page socket.on("addStatusPage", async (title, slug, callback) => { try { - checkLogin(socket); + await checkLogin(socket); title = title?.trim(); slug = slug?.trim(); @@ -304,7 +304,7 @@ module.exports.statusPageSocketHandler = (socket) => { const server = UptimeKumaServer.getInstance(); try { - checkLogin(socket); + await checkLogin(socket); let statusPageID = await StatusPage.slugToID(slug); diff --git a/server/util-server.js b/server/util-server.js index 6c4111df6..954da4444 100644 --- a/server/util-server.js +++ b/server/util-server.js @@ -757,8 +757,10 @@ exports.allowAllOrigin = (res) => { * @returns {void} * @throws The user is not logged in */ -exports.checkLogin = (socket) => { - if (!socket.userID) { +exports.checkLogin = async (socket) => { + const user = await R.findOne("user", " id = ? AND active = 1 ", [ socket.userID ]); + + if (!user) { throw new Error("You are not logged in."); } }; From 27319b6ee14ad6ba47c3f0e67b24a142a2d6a214 Mon Sep 17 00:00:00 2001 From: M1CK431 Date: Sun, 13 Aug 2023 23:06:31 +0200 Subject: [PATCH 04/11] Webapp > Settings > Security: adapt changePassword call --- src/components/settings/Security.vue | 2 +- src/mixins/socket.js | 11 +++++++++-- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/src/components/settings/Security.vue b/src/components/settings/Security.vue index 5d8aed85b..3d900cee4 100644 --- a/src/components/settings/Security.vue +++ b/src/components/settings/Security.vue @@ -171,7 +171,7 @@ export default { } else { this.$root .getSocket() - .emit("changePassword", this.password, (res) => { + .emit("changePassword", this.$root.userID, this.password, (res) => { this.$root.toastRes(res); if (res.ok) { this.password.currentPassword = ""; diff --git a/src/mixins/socket.js b/src/mixins/socket.js index 3272e042c..b3afa7607 100644 --- a/src/mixins/socket.js +++ b/src/mixins/socket.js @@ -33,6 +33,7 @@ export default { connectCount: 0, initedSocketIO: false, }, + userID: null, username: null, remember: (localStorage.remember !== "0"), allowLoginDialog: false, // Allowed to show login dialog, but "loggedIn" have to be true too. This exists because prevent the login dialog show 0.1s in first before the socket server auth-ed. @@ -407,8 +408,11 @@ export default { if (res.ok) { this.storage().token = res.token; this.socket.token = res.token; + + const { userID, username } = this.getJWTPayload() || {}; + this.userID = userID; + this.username = username; this.loggedIn = true; - this.username = this.getJWTPayload()?.username; // Trigger Chrome Save Password history.pushState({}, ""); @@ -430,8 +434,10 @@ export default { if (! res.ok) { this.logout(); } else { + const { userID, username } = this.getJWTPayload() || {}; + this.userID = userID; + this.username = username; this.loggedIn = true; - this.username = this.getJWTPayload()?.username; } }); }, @@ -445,6 +451,7 @@ export default { this.storage().removeItem("token"); this.socket.token = null; this.loggedIn = false; + this.userID = null; this.username = null; this.clearData(); }, From 1e7ec18a2fd98f5e52cf05f4c115dc9ec3983592 Mon Sep 17 00:00:00 2001 From: M1CK431 Date: Fri, 4 Aug 2023 01:40:12 +0200 Subject: [PATCH 05/11] webapp > Settings: add breadcrumbs --- src/pages/Settings.vue | 64 ++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 61 insertions(+), 3 deletions(-) diff --git a/src/pages/Settings.vue b/src/pages/Settings.vue index 96bb1fee1..d0ed076e1 100644 --- a/src/pages/Settings.vue +++ b/src/pages/Settings.vue @@ -35,9 +35,20 @@
-
- {{ subMenus[currentPage].title }} -
+
@@ -54,6 +65,15 @@ diff --git a/src/components/settings/Users/EditUser.vue b/src/components/settings/Users/EditUser.vue new file mode 100644 index 000000000..59a8a1747 --- /dev/null +++ b/src/components/settings/Users/EditUser.vue @@ -0,0 +1,205 @@ + + + diff --git a/src/components/settings/Users/Users.vue b/src/components/settings/Users/Users.vue new file mode 100644 index 000000000..e367fd76d --- /dev/null +++ b/src/components/settings/Users/Users.vue @@ -0,0 +1,153 @@ + + + + + diff --git a/src/components/settings/Users/routes.js b/src/components/settings/Users/routes.js new file mode 100644 index 000000000..6d3517455 --- /dev/null +++ b/src/components/settings/Users/routes.js @@ -0,0 +1,29 @@ +import { h } from "vue"; +import { RouterView } from "vue-router"; + +// Needed for settings enter/leave CSS animation +const AnimatedRouterView = () => h("div", [ h(RouterView) ]); +AnimatedRouterView.displayName = "AnimatedRouterView"; + +export default { + path: "users", + component: AnimatedRouterView, + children: [ + { + path: "", + name: "settings.users", + component: () => import("./Users.vue") + }, + { + path: "add", + name: "settings.users.add", + component: () => import("./AddUser.vue") + }, + { + path: "edit/:id", + name: "settings.users.edit", + props: true, + component: () => import("./EditUser.vue") + }, + ] +}; diff --git a/src/icon.js b/src/icon.js index 7bdfe1ca0..b40482783 100644 --- a/src/icon.js +++ b/src/icon.js @@ -50,6 +50,8 @@ import { faInfoCircle, faClone, faCertificate, + faUserSlash, + faUserCheck, } from "@fortawesome/free-solid-svg-icons"; library.add( @@ -97,6 +99,8 @@ library.add( faInfoCircle, faClone, faCertificate, + faUserSlash, + faUserCheck, ); export { FontAwesomeIcon }; diff --git a/src/lang/en.json b/src/lang/en.json index c07e06fab..81fecfae0 100644 --- a/src/lang/en.json +++ b/src/lang/en.json @@ -1051,5 +1051,12 @@ "From":"From", "Can be found on:": "Can be found on: {0}", "The phone number of the recipient in E.164 format.": "The phone number of the recipient in E.164 format.", - "Either a text sender ID or a phone number in E.164 format if you want to be able to receive replies.":"Either a text sender ID or a phone number in E.164 format if you want to be able to receive replies." -} + "Either a text sender ID or a phone number in E.164 format if you want to be able to receive replies.": "Either a text sender ID or a phone number in E.164 format if you want to be able to receive replies.", + "Users": "Users", + "Add New User": "Add New User", + "confirmDisableUserMsg": "Are you sure you want to disable this user? The user will not be able to login anymore.", + "Create an admin account": "Create an admin account", + "Identity": "Identity", + "Update Username": "Update Username", + "Permissions": "Permissions" +} \ No newline at end of file diff --git a/src/pages/Settings.vue b/src/pages/Settings.vue index d0ed076e1..6dbd88271 100644 --- a/src/pages/Settings.vue +++ b/src/pages/Settings.vue @@ -130,6 +130,13 @@ export default { security: { title: this.$t("Security"), }, + users: { + title: this.$t("Users"), + children: { + add: { title: this.$t("Add") }, + edit: { title: this.$t("Edit") } + }, + }, "api-keys": { title: this.$t("API Keys") }, diff --git a/src/router.js b/src/router.js index bda5078e1..53cf6ac1d 100644 --- a/src/router.js +++ b/src/router.js @@ -27,6 +27,7 @@ import General from "./components/settings/General.vue"; const Notifications = () => import("./components/settings/Notifications.vue"); import ReverseProxy from "./components/settings/ReverseProxy.vue"; import Tags from "./components/settings/Tags.vue"; +import usersSettingsRoutes from "./components/settings/Users/routes.js"; import MonitorHistory from "./components/settings/MonitorHistory.vue"; const Security = () => import("./components/settings/Security.vue"); import Proxies from "./components/settings/Proxies.vue"; @@ -124,6 +125,7 @@ const routes = [ path: "security", component: Security, }, + usersSettingsRoutes, { path: "api-keys", component: APIKeys, diff --git a/src/util-frontend.js b/src/util-frontend.js index d9bf378e5..33fc91a64 100644 --- a/src/util-frontend.js +++ b/src/util-frontend.js @@ -213,3 +213,22 @@ export function getToastErrorTimeout() { return errorTimeout; } + +/** + * Get debounced function + * @returns {Function} debounced function + */ +export function Debounce() { + let timeout = null; + + /** + * exec callback function after delay if no new call to function happens + * @param {Function} callback function to execute after delay + * @param {number} [delay=100] delay before execute the callback if no new call to function happens + * @returns {void} + */ + return function (callback, delay = 100) { + clearTimeout(timeout); + timeout = setTimeout(() => callback(), delay); + }; +} From 5198432cfce48d8b99254e63526be2ed32d9a8f5 Mon Sep 17 00:00:00 2001 From: Ionys <9364594+Ionys320@users.noreply.github.com> Date: Thu, 26 Jun 2025 15:32:23 +0200 Subject: [PATCH 07/11] feat: Add E2E test for multi-admin users --- src/components/settings/Users/AddUser.vue | 8 +-- src/components/settings/Users/EditUser.vue | 1 + src/components/settings/Users/Users.vue | 4 +- test/e2e/specs/multiple-users.spec.js | 63 ++++++++++++++++++++++ test/e2e/util-test.js | 7 +-- 5 files changed, 75 insertions(+), 8 deletions(-) create mode 100644 test/e2e/specs/multiple-users.spec.js diff --git a/src/components/settings/Users/AddUser.vue b/src/components/settings/Users/AddUser.vue index 5f38420b8..3f0eaf14e 100644 --- a/src/components/settings/Users/AddUser.vue +++ b/src/components/settings/Users/AddUser.vue @@ -14,7 +14,7 @@ :placeholder="$t('Username')" required :disabled="creating" - data-cy="username-input" + data-testid="username-input" >
@@ -29,7 +29,7 @@ :placeholder="$t('Password')" required :disabled="creating" - data-cy="password-input" + data-testid="password-input" >
@@ -44,12 +44,12 @@ :placeholder="$t('Repeat Password')" required :disabled="creating" - data-cy="password-repeat-input" + data-testid="password-repeat-input" > - diff --git a/src/components/settings/Users/EditUser.vue b/src/components/settings/Users/EditUser.vue index 59a8a1747..64cb8727f 100644 --- a/src/components/settings/Users/EditUser.vue +++ b/src/components/settings/Users/EditUser.vue @@ -83,6 +83,7 @@ type="checkbox" :disabled="saving" @click="debounceCheckboxClick(() => { active = !active; save({ active }); })" + data-testid="active-checkbox" >
{{ $t("Active") }}
diff --git a/src/components/settings/Users/Users.vue b/src/components/settings/Users/Users.vue index e367fd76d..4f58c2035 100644 --- a/src/components/settings/Users/Users.vue +++ b/src/components/settings/Users/Users.vue @@ -10,12 +10,13 @@ -
+
{{ username }} @@ -31,6 +32,7 @@ :class="active ? 'btn-outline-danger' : 'btn-outline-success'" :disabled="processing" @click.prevent="active ? disableConfirm(usersList[index]) : toggleActiveUser(usersList[index])" + :data-testid="`toggle-active-user-${username}`" > diff --git a/test/e2e/specs/multiple-users.spec.js b/test/e2e/specs/multiple-users.spec.js new file mode 100644 index 000000000..63ff6545e --- /dev/null +++ b/test/e2e/specs/multiple-users.spec.js @@ -0,0 +1,63 @@ +import { expect, test } from "@playwright/test"; +import { login, restoreSqliteSnapshot, screenshot } from "../util-test"; + +test.describe("Multiple Users", () => { + test.beforeEach(async ({ page }) => { + await restoreSqliteSnapshot(page); + }); + + test("test multiple users", async ({ page }, testInfo) => { + // Login as admin + await page.goto("./settings/users"); + await login(page, "admin"); + + // Check if the user list contains only the admin user + await expect(page.getByTestId("users-list")).toHaveCount(1); + + await screenshot(testInfo, page); + + // Add a second user + await page.goto("./settings/users/add"); + + await expect(page.getByTestId("username-input")).toBeVisible(); + await page.getByTestId("username-input").fill("newuser"); + await page.getByTestId("password-input").fill("newuser123"); + await page.getByTestId("password-repeat-input").fill("newuser123"); + await page.getByTestId("submit-create-admin-form").click(); + + // Ensure the new user is created and visible in the user list + await page.waitForURL("./settings/users"); + await expect(page.getByTestId("users-list")).toContainText("newuser"); + await expect(page.getByTestId("users-list").locator("[data-testid='user-item']")).toHaveCount(2); + + await screenshot(testInfo, page); + + // Disable the admin user + await page.getByTestId("toggle-active-user-admin").click(); + // Click on the button containing Yes and having btn-danger class + await page.getByRole("button", { name: "Yes", exact: true}).click(); + await expect(page.getByText("You are not logged in.")).toBeVisible(); + + // Make sure we're back on the login page after refresh + await page.reload(); + await expect(page.getByText("Log in")).toBeVisible(); + + // Login as the new user + await page.goto("./dashboard"); // Assuming the new user has ID 2 + await login(page, "newuser"); + await screenshot(testInfo, page); + + // Disable self-user + await page.goto("./settings/users/edit/2"); + await expect(page.getByTestId("active-checkbox")).toBeVisible(); + await page.getByTestId("active-checkbox").uncheck(); + + await screenshot(testInfo, page); + + // Make sure we're back on the login page after refresh + await page.reload(); + await expect(page.getByText("Log in")).toBeVisible(); + + await screenshot(testInfo, page); + }); +}); diff --git a/test/e2e/util-test.js b/test/e2e/util-test.js index f6af3cbd2..e22c909de 100644 --- a/test/e2e/util-test.js +++ b/test/e2e/util-test.js @@ -19,14 +19,15 @@ export async function screenshot(testInfo, page) { /** * @param {Page} page Page + * @param {string} [user="admin"] Username to log in with * @returns {Promise} */ -export async function login(page) { +export async function login(page, user = "admin") { // Login await page.getByPlaceholder("Username").click(); - await page.getByPlaceholder("Username").fill("admin"); + await page.getByPlaceholder("Username").fill(user); await page.getByPlaceholder("Username").press("Tab"); - await page.getByPlaceholder("Password").fill("admin123"); + await page.getByPlaceholder("Password").fill(user + "123"); await page.getByLabel("Remember me").check(); await page.getByRole("button", { name: "Log in" }).click(); await page.isVisible("text=Add New Monitor"); From 1e4bc5ddb5708a4ce0b6a57dc300351033347252 Mon Sep 17 00:00:00 2001 From: Ionys <9364594+Ionys320@users.noreply.github.com> Date: Thu, 26 Jun 2025 16:14:49 +0200 Subject: [PATCH 08/11] fix(lint): Correct the issues Sorry, my ESLint extension was disabled --- src/components/settings/Users/EditUser.vue | 6 +++--- src/components/settings/Users/Users.vue | 2 +- test/e2e/specs/multiple-users.spec.js | 9 ++++++--- test/e2e/util-test.js | 2 +- 4 files changed, 11 insertions(+), 8 deletions(-) diff --git a/src/components/settings/Users/EditUser.vue b/src/components/settings/Users/EditUser.vue index 64cb8727f..4e36f045b 100644 --- a/src/components/settings/Users/EditUser.vue +++ b/src/components/settings/Users/EditUser.vue @@ -82,8 +82,8 @@ style="scale: 1.4; cursor: pointer;" type="checkbox" :disabled="saving" - @click="debounceCheckboxClick(() => { active = !active; save({ active }); })" data-testid="active-checkbox" + @click="debounceCheckboxClick(() => { active = !active; save({ active }); })" >
{{ $t("Active") }}
@@ -181,8 +181,8 @@ export default { /** * Save user changes * @param {object} user user to save - * @param {string} [user.username] username used as login identifier. - * @param {boolean} [user.active] is the user authorized to login? + * @param {string} user.username username used as login identifier. + * @param {boolean} user.active is the user authorized to login? * @returns {void} */ save(user) { diff --git a/src/components/settings/Users/Users.vue b/src/components/settings/Users/Users.vue index 4f58c2035..02d7e5e9c 100644 --- a/src/components/settings/Users/Users.vue +++ b/src/components/settings/Users/Users.vue @@ -31,8 +31,8 @@ class="btn-ban-user btn ms-2 py-1" :class="active ? 'btn-outline-danger' : 'btn-outline-success'" :disabled="processing" - @click.prevent="active ? disableConfirm(usersList[index]) : toggleActiveUser(usersList[index])" :data-testid="`toggle-active-user-${username}`" + @click.prevent="active ? disableConfirm(usersList[index]) : toggleActiveUser(usersList[index])" > diff --git a/test/e2e/specs/multiple-users.spec.js b/test/e2e/specs/multiple-users.spec.js index 63ff6545e..058de2187 100644 --- a/test/e2e/specs/multiple-users.spec.js +++ b/test/e2e/specs/multiple-users.spec.js @@ -24,7 +24,7 @@ test.describe("Multiple Users", () => { await page.getByTestId("password-input").fill("newuser123"); await page.getByTestId("password-repeat-input").fill("newuser123"); await page.getByTestId("submit-create-admin-form").click(); - + // Ensure the new user is created and visible in the user list await page.waitForURL("./settings/users"); await expect(page.getByTestId("users-list")).toContainText("newuser"); @@ -35,9 +35,12 @@ test.describe("Multiple Users", () => { // Disable the admin user await page.getByTestId("toggle-active-user-admin").click(); // Click on the button containing Yes and having btn-danger class - await page.getByRole("button", { name: "Yes", exact: true}).click(); + await page.getByRole("button", { + name: "Yes", + exact: true + }).click(); await expect(page.getByText("You are not logged in.")).toBeVisible(); - + // Make sure we're back on the login page after refresh await page.reload(); await expect(page.getByText("Log in")).toBeVisible(); diff --git a/test/e2e/util-test.js b/test/e2e/util-test.js index e22c909de..5e85f4ef8 100644 --- a/test/e2e/util-test.js +++ b/test/e2e/util-test.js @@ -19,7 +19,7 @@ export async function screenshot(testInfo, page) { /** * @param {Page} page Page - * @param {string} [user="admin"] Username to log in with + * @param {string} user Username to log in with * @returns {Promise} */ export async function login(page, user = "admin") { From 923d0ede7e6a2116bcbc37938bcd6a88a1c37cef Mon Sep 17 00:00:00 2001 From: Ionys <9364594+Ionys320@users.noreply.github.com> Date: Thu, 26 Jun 2025 16:17:11 +0200 Subject: [PATCH 09/11] fix(lint): Remove another issue Wasn't mine this time! --- src/util-frontend.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/util-frontend.js b/src/util-frontend.js index 33fc91a64..46ff6b736 100644 --- a/src/util-frontend.js +++ b/src/util-frontend.js @@ -224,7 +224,7 @@ export function Debounce() { /** * exec callback function after delay if no new call to function happens * @param {Function} callback function to execute after delay - * @param {number} [delay=100] delay before execute the callback if no new call to function happens + * @param {number} delay before execute the callback if no new call to function happens * @returns {void} */ return function (callback, delay = 100) { From 28dc77f9e3b8da805aed2cc0cfbb1b56f62cb5df Mon Sep 17 00:00:00 2001 From: Ionys <9364594+Ionys320@users.noreply.github.com> Date: Thu, 26 Jun 2025 17:08:21 +0200 Subject: [PATCH 10/11] feat: Disconnect sockets for deactivated users in saveUser function --- server/user.js | 6 ++++++ src/layouts/Layout.vue | 2 +- test/e2e/specs/multiple-users.spec.js | 5 ++--- 3 files changed, 9 insertions(+), 4 deletions(-) diff --git a/server/user.js b/server/user.js index 5b8dfa09b..4f54fa668 100644 --- a/server/user.js +++ b/server/user.js @@ -68,6 +68,12 @@ async function saveUser(socket, user) { io.to(socket.userID).emit("saveUser", bean); + // If user is deactivated, disconnect his sockets + if (!bean.active) { + const roomId = typeof id === "number" ? id : parseInt(id, 10); + io.in(roomId).disconnectSockets(); + } + timeLogger.print(`Save user ${user.id}`); } diff --git a/src/layouts/Layout.vue b/src/layouts/Layout.vue index e93a5159e..82ab098cd 100644 --- a/src/layouts/Layout.vue +++ b/src/layouts/Layout.vue @@ -1,6 +1,6 @@