API: add basic multiple admin users

pull/3571/head
M1CK431 1 year ago
parent d5db40f40d
commit 51f94d6cf5

@ -43,6 +43,7 @@ class User extends BeanModel {
*/ */
static createJWT(user, jwtSecret) { static createJWT(user, jwtSecret) {
return jwt.sign({ return jwt.sign({
userID: user.id,
username: user.username, username: user.username,
h: shake256(user.password, SHAKE256_LENGTH), h: shake256(user.password, SHAKE256_LENGTH),
}, jwtSecret); }, jwtSecret);

@ -150,6 +150,7 @@ const { resetChrome } = require("./monitor-types/real-browser-monitor-type");
const { EmbeddedMariaDB } = require("./embedded-mariadb"); const { EmbeddedMariaDB } = require("./embedded-mariadb");
const { SetupDatabase } = require("./setup-database"); const { SetupDatabase } = require("./setup-database");
const { chartSocketHandler } = require("./socket-handlers/chart-socket-handler"); const { chartSocketHandler } = require("./socket-handlers/chart-socket-handler");
const { sendUserList, getUser, saveUser } = require("./user");
app.use(express.json()); app.use(express.json());
@ -494,7 +495,7 @@ let needSetup = false;
} }
checkLogin(socket); checkLogin(socket);
await doubleCheckPassword(socket, currentPassword); await doubleCheckPassword(socket.userID, currentPassword);
let user = await R.findOne("user", " id = ? AND active = 1 ", [ let user = await R.findOne("user", " id = ? AND active = 1 ", [
socket.userID, socket.userID,
@ -544,7 +545,7 @@ let needSetup = false;
} }
checkLogin(socket); checkLogin(socket);
await doubleCheckPassword(socket, currentPassword); await doubleCheckPassword(socket.userID, currentPassword);
await R.exec("UPDATE `user` SET twofa_status = 1 WHERE id = ? ", [ await R.exec("UPDATE `user` SET twofa_status = 1 WHERE id = ? ", [
socket.userID, socket.userID,
@ -577,7 +578,7 @@ let needSetup = false;
} }
checkLogin(socket); checkLogin(socket);
await doubleCheckPassword(socket, currentPassword); await doubleCheckPassword(socket.userID, currentPassword);
await TwoFA.disable2FA(socket.userID); await TwoFA.disable2FA(socket.userID);
log.info("auth", `Disabled 2FA token. IP=${clientIP}`); log.info("auth", `Disabled 2FA token. IP=${clientIP}`);
@ -601,7 +602,7 @@ let needSetup = false;
socket.on("verifyToken", async (token, currentPassword, callback) => { socket.on("verifyToken", async (token, currentPassword, callback) => {
try { try {
checkLogin(socket); checkLogin(socket);
await doubleCheckPassword(socket, currentPassword); await doubleCheckPassword(socket.userID, currentPassword);
let user = await R.findOne("user", " id = ? AND active = 1 ", [ let user = await R.findOne("user", " id = ? AND active = 1 ", [
socket.userID, 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."); 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"); let user = R.dispense("user");
user.username = username; user.username = username;
user.password = passwordHash.generate(password); user.password = passwordHash.generate(password);
@ -697,6 +694,61 @@ let needSetup = false;
// Auth Only API // 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 // Add a new monitor
socket.on("add", async (monitor, callback) => { socket.on("add", async (monitor, callback) => {
try { try {
@ -1282,7 +1334,7 @@ let needSetup = false;
} }
}); });
socket.on("changePassword", async (password, callback) => { socket.on("changePassword", async (userID, password, callback) => {
try { try {
checkLogin(socket); 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."); 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); await user.resetPassword(password.newPassword);
server.disconnectAllSocketClients(user.id, socket.id); server.disconnectAllSocketClients(user.id, socket.id);
@ -1649,6 +1701,7 @@ async function afterLogin(socket, user) {
sendAPIKeyList(socket), sendAPIKeyList(socket),
sendRemoteBrowserList(socket), sendRemoteBrowserList(socket),
sendMonitorTypeList(socket), sendMonitorTypeList(socket),
sendUserList(socket),
]); ]);
await StatusPage.sendStatusPageList(io, socket); await StatusPage.sendStatusPageList(io, socket);

@ -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<Bean[]>} 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<Bean[]>} 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<void>}
*/
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
};

@ -765,20 +765,18 @@ exports.checkLogin = (socket) => {
/** /**
* For logged-in users, double-check the password * 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 * @param {string} currentPassword Password to validate
* @returns {Promise<Bean>} User * @returns {Promise<Bean>} User
* @throws The current password is not a string * @throws The current password is not a string
* @throws The provided password is not correct * @throws The provided password is not correct
*/ */
exports.doubleCheckPassword = async (socket, currentPassword) => { exports.doubleCheckPassword = async (userID, currentPassword) => {
if (typeof currentPassword !== "string") { if (typeof currentPassword !== "string") {
throw new Error("Wrong data type?"); throw new Error("Wrong data type?");
} }
let user = await R.findOne("user", " id = ? AND active = 1 ", [ let user = await R.findOne("user", " id = ? ", [ userID ]);
socket.userID,
]);
if (!user || !passwordHash.verify(currentPassword, user.password)) { if (!user || !passwordHash.verify(currentPassword, user.password)) {
throw new Error("Incorrect current password"); throw new Error("Incorrect current password");

Loading…
Cancel
Save