From b4bb715a6f1cce486aef22b7d1b95590819cb18b Mon Sep 17 00:00:00 2001 From: M1CK431 Date: Tue, 1 Aug 2023 00:23:55 +0200 Subject: [PATCH] API: add basic multiple admin users --- server/server.js | 74 ++++++++++++++++++++++++++++++++++------ server/user.js | 78 +++++++++++++++++++++++++++++++++++++++++++ server/util-server.js | 8 ++--- 3 files changed, 145 insertions(+), 15 deletions(-) create mode 100644 server/user.js diff --git a/server/server.js b/server/server.js index 4b0dec1589..440ac8d127 100644 --- a/server/server.js +++ b/server/server.js @@ -157,6 +157,7 @@ const { Settings } = require("./settings"); const { CacheableDnsHttpAgent } = require("./cacheable-dns-http-agent"); const apicache = require("./modules/apicache"); const { resetChrome } = require("./monitor-types/real-browser-monitor-type"); +const { sendUserList, getUser, saveUser } = require("./user"); app.use(express.json()); @@ -358,6 +359,7 @@ let needSetup = false; callback({ ok: true, token: jwt.sign({ + userID: user.id, username: data.username, }, server.jwtSecret), }); @@ -434,7 +436,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, @@ -483,7 +485,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, @@ -515,7 +517,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}`); @@ -538,7 +540,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, @@ -604,10 +606,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.count("user")) !== 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); @@ -632,6 +630,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 { @@ -1122,7 +1175,7 @@ let needSetup = false; } }); - socket.on("changePassword", async (password, callback) => { + socket.on("changePassword", async (userID, password, callback) => { try { checkLogin(socket); @@ -1134,7 +1187,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); callback({ @@ -1668,6 +1721,7 @@ async function afterLogin(socket, user) { sendProxyList(socket); sendDockerHostList(socket); sendAPIKeyList(socket); + sendUserList(socket); await sleep(500); diff --git a/server/user.js b/server/user.js new file mode 100644 index 0000000000..5b8dfa09b3 --- /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 8354b56094..d6366a379c 100644 --- a/server/util-server.js +++ b/server/util-server.js @@ -810,18 +810,16 @@ 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 * @returns {Promise} */ -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");