diff --git a/server/model/user.js b/server/model/user.js index 329402ff..f3897fd0 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 009dffc3..080d080d 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 00000000..5b8dfa09 --- /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 5ebc62ac..6c4111df 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");