mirror of
https://github.com/louislam/uptime-kuma.git
synced 2025-07-18 15:24:03 +02:00
Merge e08f90ed1a
into 2fd4e1cc72
This commit is contained in:
commit
61f8936e03
31 changed files with 930 additions and 196 deletions
|
@ -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());
|
||||
|
|
|
@ -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<void>}
|
||||
*/
|
||||
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");
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -212,10 +212,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");
|
||||
|
@ -241,14 +238,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<void>}
|
||||
*/
|
||||
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");
|
||||
|
@ -272,13 +265,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<void>}
|
||||
*/
|
||||
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 = ? ", [
|
||||
|
|
|
@ -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<void>}
|
||||
*/
|
||||
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");
|
||||
|
@ -181,12 +180,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<void>}
|
||||
*/
|
||||
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) {
|
||||
|
|
179
server/server.js
179
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());
|
||||
|
||||
|
@ -493,8 +494,8 @@ let needSetup = false;
|
|||
return;
|
||||
}
|
||||
|
||||
checkLogin(socket);
|
||||
await doubleCheckPassword(socket, currentPassword);
|
||||
await checkLogin(socket);
|
||||
await doubleCheckPassword(socket.userID, currentPassword);
|
||||
|
||||
let user = await R.findOne("user", " id = ? AND active = 1 ", [
|
||||
socket.userID,
|
||||
|
@ -543,8 +544,8 @@ let needSetup = false;
|
|||
return;
|
||||
}
|
||||
|
||||
checkLogin(socket);
|
||||
await doubleCheckPassword(socket, currentPassword);
|
||||
await checkLogin(socket);
|
||||
await doubleCheckPassword(socket.userID, currentPassword);
|
||||
|
||||
await R.exec("UPDATE `user` SET twofa_status = 1 WHERE id = ? ", [
|
||||
socket.userID,
|
||||
|
@ -576,8 +577,8 @@ let needSetup = false;
|
|||
return;
|
||||
}
|
||||
|
||||
checkLogin(socket);
|
||||
await doubleCheckPassword(socket, currentPassword);
|
||||
await checkLogin(socket);
|
||||
await doubleCheckPassword(socket.userID, currentPassword);
|
||||
await TwoFA.disable2FA(socket.userID);
|
||||
|
||||
log.info("auth", `Disabled 2FA token. IP=${clientIP}`);
|
||||
|
@ -600,8 +601,8 @@ let needSetup = false;
|
|||
|
||||
socket.on("verifyToken", async (token, currentPassword, callback) => {
|
||||
try {
|
||||
checkLogin(socket);
|
||||
await doubleCheckPassword(socket, currentPassword);
|
||||
await checkLogin(socket);
|
||||
await doubleCheckPassword(socket.userID, currentPassword);
|
||||
|
||||
let user = await R.findOne("user", " id = ? AND active = 1 ", [
|
||||
socket.userID,
|
||||
|
@ -633,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,
|
||||
|
@ -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 = await passwordHash.generate(password);
|
||||
|
@ -697,10 +694,65 @@ let needSetup = false;
|
|||
// Auth Only API
|
||||
// ***************************
|
||||
|
||||
socket.on("getUsers", async callback => {
|
||||
try {
|
||||
await 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 {
|
||||
await 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 {
|
||||
await 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 {
|
||||
checkLogin(socket);
|
||||
await checkLogin(socket);
|
||||
let bean = R.dispense("monitor");
|
||||
|
||||
let notificationIDList = monitor.notificationIDList;
|
||||
|
@ -759,14 +811,10 @@ 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 ]);
|
||||
|
||||
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);
|
||||
|
@ -918,7 +966,7 @@ let needSetup = false;
|
|||
|
||||
socket.on("getMonitorList", async (callback) => {
|
||||
try {
|
||||
checkLogin(socket);
|
||||
await checkLogin(socket);
|
||||
await server.sendMonitorList(socket);
|
||||
callback({
|
||||
ok: true,
|
||||
|
@ -934,14 +982,11 @@ 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}`);
|
||||
|
||||
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
|
||||
}];
|
||||
|
@ -961,7 +1006,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}`);
|
||||
|
||||
|
@ -997,7 +1042,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);
|
||||
|
||||
|
@ -1017,7 +1062,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);
|
||||
|
||||
|
@ -1037,7 +1082,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}`);
|
||||
|
||||
|
@ -1048,10 +1093,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();
|
||||
|
@ -1077,7 +1119,7 @@ let needSetup = false;
|
|||
|
||||
socket.on("getTags", async (callback) => {
|
||||
try {
|
||||
checkLogin(socket);
|
||||
await checkLogin(socket);
|
||||
|
||||
const list = await R.findAll("tag");
|
||||
|
||||
|
@ -1096,7 +1138,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;
|
||||
|
@ -1118,7 +1160,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) {
|
||||
|
@ -1150,7 +1192,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 ]);
|
||||
|
||||
|
@ -1170,7 +1212,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,
|
||||
|
@ -1194,7 +1236,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,
|
||||
|
@ -1218,7 +1260,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,
|
||||
|
@ -1306,9 +1348,9 @@ let needSetup = false;
|
|||
}
|
||||
});
|
||||
|
||||
socket.on("changePassword", async (password, callback) => {
|
||||
socket.on("changePassword", async (userID, password, callback) => {
|
||||
try {
|
||||
checkLogin(socket);
|
||||
await checkLogin(socket);
|
||||
|
||||
if (!password.newPassword) {
|
||||
throw new Error("Invalid new password");
|
||||
|
@ -1318,7 +1360,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);
|
||||
|
@ -1340,7 +1382,7 @@ let needSetup = false;
|
|||
|
||||
socket.on("getSettings", async (callback) => {
|
||||
try {
|
||||
checkLogin(socket);
|
||||
await checkLogin(socket);
|
||||
const data = await getSettings("general");
|
||||
|
||||
if (!data.serverTimezone) {
|
||||
|
@ -1362,7 +1404,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
|
||||
|
@ -1426,7 +1468,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);
|
||||
|
@ -1448,7 +1490,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);
|
||||
|
@ -1469,7 +1511,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");
|
||||
|
||||
|
@ -1490,7 +1532,7 @@ let needSetup = false;
|
|||
|
||||
socket.on("checkApprise", async (callback) => {
|
||||
try {
|
||||
checkLogin(socket);
|
||||
await checkLogin(socket);
|
||||
callback(Notification.checkApprise());
|
||||
} catch (e) {
|
||||
callback(false);
|
||||
|
@ -1499,7 +1541,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}`);
|
||||
|
||||
|
@ -1523,7 +1565,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}`);
|
||||
|
||||
|
@ -1547,7 +1589,7 @@ let needSetup = false;
|
|||
|
||||
socket.on("clearStatistics", async (callback) => {
|
||||
try {
|
||||
checkLogin(socket);
|
||||
await checkLogin(socket);
|
||||
|
||||
log.info("manage", `Clear Statistics User ID: ${socket.userID}`);
|
||||
|
||||
|
@ -1654,24 +1696,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<void>}
|
||||
* @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.
|
||||
|
@ -1693,6 +1717,7 @@ async function afterLogin(socket, user) {
|
|||
sendAPIKeyList(socket),
|
||||
sendRemoteBrowserList(socket),
|
||||
sendMonitorTypeList(socket),
|
||||
sendUserList(socket),
|
||||
]);
|
||||
|
||||
await StatusPage.sendStatusPageList(io, socket);
|
||||
|
@ -1755,14 +1780,9 @@ async function initDatabase(testMode = false) {
|
|||
* @returns {Promise<void>}
|
||||
*/
|
||||
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,
|
||||
|
@ -1793,14 +1813,9 @@ async function restartMonitor(userID, monitorID) {
|
|||
* @returns {Promise<void>}
|
||||
*/
|
||||
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();
|
||||
|
|
|
@ -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 = await 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,14 +70,11 @@ 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}`);
|
||||
|
||||
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();
|
||||
|
||||
|
@ -99,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}`);
|
||||
|
||||
|
@ -127,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}`);
|
||||
|
||||
|
|
|
@ -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) { }
|
||||
});
|
||||
|
|
|
@ -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: await 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,
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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,14 +46,10 @@ 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);
|
||||
|
||||
if (bean.user_id !== socket.userID) {
|
||||
throw new Error("Permission denied.");
|
||||
}
|
||||
|
||||
await Maintenance.jsonToBean(bean, maintenance);
|
||||
await R.store(bean);
|
||||
await bean.run(true);
|
||||
|
@ -78,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
|
||||
|
@ -113,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
|
||||
|
@ -147,14 +143,11 @@ 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}`);
|
||||
|
||||
let bean = await R.findOne("maintenance", " id = ? AND user_id = ? ", [
|
||||
maintenanceID,
|
||||
socket.userID,
|
||||
]);
|
||||
let bean = await R.findOne("maintenance", " id = ? ", [ maintenanceID ]);
|
||||
|
||||
callback({
|
||||
ok: true,
|
||||
|
@ -171,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,
|
||||
|
@ -187,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}`);
|
||||
|
||||
|
@ -211,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}`);
|
||||
|
||||
|
@ -235,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}`);
|
||||
|
||||
|
@ -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();
|
||||
|
||||
|
@ -269,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}`);
|
||||
|
||||
|
@ -303,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}`);
|
||||
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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 = ? ", [
|
||||
|
@ -264,7 +264,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();
|
||||
|
@ -313,7 +313,7 @@ module.exports.statusPageSocketHandler = (socket) => {
|
|||
const server = UptimeKumaServer.getInstance();
|
||||
|
||||
try {
|
||||
checkLogin(socket);
|
||||
await checkLogin(socket);
|
||||
|
||||
let statusPageID = await StatusPage.slugToID(slug);
|
||||
|
||||
|
|
|
@ -204,7 +204,7 @@ class UptimeKumaServer {
|
|||
* @returns {Promise<object>} 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;
|
||||
}
|
||||
|
@ -231,25 +231,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<object>} 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,
|
||||
|
|
84
server/user.js
Normal file
84
server/user.js
Normal file
|
@ -0,0 +1,84 @@
|
|||
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);
|
||||
|
||||
// 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}`);
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
sendUserList,
|
||||
getUser,
|
||||
saveUser
|
||||
};
|
|
@ -797,28 +797,28 @@ 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.");
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 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<Bean>} 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");
|
||||
|
|
|
@ -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 = "";
|
||||
|
|
94
src/components/settings/Users/AddUser.vue
Normal file
94
src/components/settings/Users/AddUser.vue
Normal file
|
@ -0,0 +1,94 @@
|
|||
<template>
|
||||
<h4 class="mt-4">
|
||||
{{ $t("Create an admin account") }}
|
||||
</h4>
|
||||
|
||||
<form data-cy="setup-form" @submit.prevent="create">
|
||||
<div class="my-3">
|
||||
<label class="form-label d-block">
|
||||
{{ $t("Username") }}
|
||||
<input
|
||||
v-model="username"
|
||||
type="text"
|
||||
class="form-control mt-2"
|
||||
:placeholder="$t('Username')"
|
||||
required
|
||||
:disabled="creating"
|
||||
data-testid="username-input"
|
||||
>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="form-label d-block">
|
||||
{{ $t("Password") }}
|
||||
<input
|
||||
v-model="password"
|
||||
type="password"
|
||||
class="form-control mt-2"
|
||||
:placeholder="$t('Password')"
|
||||
required
|
||||
:disabled="creating"
|
||||
data-testid="password-input"
|
||||
>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="form-label d-block">
|
||||
{{ $t("Repeat Password") }}
|
||||
<input
|
||||
v-model="repeatPassword"
|
||||
type="password"
|
||||
class="form-control mt-2"
|
||||
:placeholder="$t('Repeat Password')"
|
||||
required
|
||||
:disabled="creating"
|
||||
data-testid="password-repeat-input"
|
||||
>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<button class="btn btn-primary" type="submit" :disabled="creating" data-testid="submit-create-admin-form">
|
||||
<span v-show="creating" class="spinner-border spinner-border-sm" role="status" aria-hidden="true" />
|
||||
{{ $t("Create") }}
|
||||
</button>
|
||||
</form>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { useToast } from "vue-toastification";
|
||||
const toast = useToast();
|
||||
|
||||
export default {
|
||||
data: () => ({
|
||||
creating: false,
|
||||
username: "",
|
||||
password: "",
|
||||
repeatPassword: "",
|
||||
}),
|
||||
|
||||
methods: {
|
||||
/**
|
||||
* Create an admin account
|
||||
* @returns {void}
|
||||
*/
|
||||
create() {
|
||||
this.creating = true;
|
||||
|
||||
if (this.password !== this.repeatPassword) {
|
||||
toast.error(this.$t("PasswordsDoNotMatch"));
|
||||
this.creating = false;
|
||||
return;
|
||||
}
|
||||
|
||||
this.$root.getSocket().emit("setup", this.username, this.password, (res) => {
|
||||
this.creating = false;
|
||||
this.$root.toastRes(res);
|
||||
|
||||
res.ok && this.$router.push({ name: "settings.users" });
|
||||
});
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
206
src/components/settings/Users/EditUser.vue
Normal file
206
src/components/settings/Users/EditUser.vue
Normal file
|
@ -0,0 +1,206 @@
|
|||
<template>
|
||||
<div v-if="loading" class="d-flex align-items-center justify-content-center my-3 spinner">
|
||||
<font-awesome-icon icon="spinner" size="2x" spin />
|
||||
</div>
|
||||
|
||||
<template v-else>
|
||||
<h5 class="my-4 settings-subheading">{{ $t("Identity") }}</h5>
|
||||
<form @submit.prevent="save({ username })">
|
||||
<label class="form-label d-block mb-3">
|
||||
{{ $t("Username") }}
|
||||
<input
|
||||
v-model="username"
|
||||
:placeholder="$t('Username')"
|
||||
class="form-control mt-2"
|
||||
required
|
||||
:disabled="saving"
|
||||
/>
|
||||
</label>
|
||||
|
||||
<button class="btn btn-primary" type="submit" :disabled="saving">
|
||||
<span v-show="saving" class="spinner-border spinner-border-sm" role="status" aria-hidden="true" />
|
||||
{{ $t("Update Username") }}
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<h5 class="mt-5 mb-4 settings-subheading">{{ $t("Change Password") }}</h5>
|
||||
<form class="mb-3" @submit.prevent="savePassword">
|
||||
<div class="mb-3">
|
||||
<label class="form-label d-block">
|
||||
{{ $t("Current Password") }}
|
||||
<input
|
||||
v-model="currentPassword"
|
||||
type="password"
|
||||
:placeholder="$t('Current Password')"
|
||||
class="form-control mt-2"
|
||||
required
|
||||
:disabled="savingPassword"
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="form-label d-block">
|
||||
{{ $t("New Password") }}
|
||||
<input
|
||||
v-model="newPassword"
|
||||
type="password"
|
||||
:placeholder="$t('New Password')"
|
||||
class="form-control mt-2"
|
||||
required
|
||||
:disabled="savingPassword"
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="form-label d-block">
|
||||
{{ $t("Repeat New Password") }}
|
||||
<input
|
||||
v-model="repeatNewPassword"
|
||||
type="password"
|
||||
:placeholder="$t('Repeat New Password')"
|
||||
class="form-control mt-2"
|
||||
required
|
||||
:disabled="savingPassword"
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<button class="btn btn-primary" type="submit" :disabled="savingPassword">
|
||||
<span v-show="savingPassword" class="spinner-border spinner-border-sm" role="status" aria-hidden="true" />
|
||||
{{ $t("Update Password") }}
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<h5 class="mt-5 mb-4 settings-subheading">{{ $t("Permissions") }}</h5>
|
||||
<div class="form-check form-switch">
|
||||
<label class="form-check-label">
|
||||
<input
|
||||
:checked="active"
|
||||
class="form-check-input"
|
||||
style="scale: 1.4; cursor: pointer;"
|
||||
type="checkbox"
|
||||
:disabled="saving"
|
||||
data-testid="active-checkbox"
|
||||
@click="debounceCheckboxClick(() => { active = !active; save({ active }); })"
|
||||
>
|
||||
<div class="ps-2">{{ $t("Active") }}</div>
|
||||
</label>
|
||||
</div>
|
||||
</template>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { Debounce } from "../../../util-frontend.js";
|
||||
import { useToast } from "vue-toastification";
|
||||
const toast = useToast();
|
||||
|
||||
export default {
|
||||
props: {
|
||||
id: {
|
||||
type: String,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
|
||||
data: () => ({
|
||||
loading: false,
|
||||
username: "",
|
||||
saving: false,
|
||||
currentPassword: "",
|
||||
newPassword: "",
|
||||
repeatNewPassword: "",
|
||||
savingPassword: false,
|
||||
active: false
|
||||
}),
|
||||
|
||||
mounted() {
|
||||
this.getUser();
|
||||
},
|
||||
|
||||
methods: {
|
||||
// Used to ignore one of the two "click" events fired when clicking on the checkbox label
|
||||
debounceCheckboxClick: new Debounce(),
|
||||
|
||||
/**
|
||||
* Get user from server
|
||||
* @returns {void}
|
||||
*/
|
||||
getUser() {
|
||||
this.loading = true;
|
||||
this.$root.getSocket().emit("getUser", this.id, (res) => {
|
||||
this.loading = false;
|
||||
if (res.ok) {
|
||||
const { username, active } = res.user;
|
||||
|
||||
this.username = username;
|
||||
this.active = active;
|
||||
} else {
|
||||
toast.error(res.msg);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Check new passwords match before saving it
|
||||
* @returns {void}
|
||||
*/
|
||||
savePassword() {
|
||||
this.savingPassword = true;
|
||||
const { currentPassword, newPassword, repeatNewPassword } = this;
|
||||
|
||||
if (newPassword !== repeatNewPassword) {
|
||||
toast.error(this.$t("PasswordsDoNotMatch"));
|
||||
this.savingPassword = false;
|
||||
return;
|
||||
}
|
||||
|
||||
this.$root
|
||||
.getSocket()
|
||||
.emit(
|
||||
"changePassword",
|
||||
this.id,
|
||||
{
|
||||
currentPassword,
|
||||
newPassword
|
||||
},
|
||||
(res) => {
|
||||
this.savingPassword = false;
|
||||
this.$root.toastRes(res);
|
||||
|
||||
if (res.ok) {
|
||||
this.currentPassword = "";
|
||||
this.newPassword = "";
|
||||
this.repeatNewPassword = "";
|
||||
}
|
||||
});
|
||||
|
||||
},
|
||||
|
||||
/**
|
||||
* 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?
|
||||
* @returns {void}
|
||||
*/
|
||||
save(user) {
|
||||
this.saving = true;
|
||||
|
||||
this.$root
|
||||
.getSocket()
|
||||
.emit(
|
||||
"saveUser",
|
||||
{
|
||||
id: this.id,
|
||||
...user
|
||||
},
|
||||
(res) => {
|
||||
this.saving = false;
|
||||
this.$root.toastRes(res);
|
||||
});
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
155
src/components/settings/Users/Users.vue
Normal file
155
src/components/settings/Users/Users.vue
Normal file
|
@ -0,0 +1,155 @@
|
|||
<template>
|
||||
<div class="my-4">
|
||||
<div class="mx-0 mx-lg-4 pt-1 mb-4">
|
||||
<button class="btn btn-primary" @click="$router.push({ name: 'settings.users.add' })">
|
||||
<font-awesome-icon icon="plus" /> {{ $t("Add New User") }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div v-if="loading" class="d-flex align-items-center justify-content-center my-3 spinner">
|
||||
<font-awesome-icon icon="spinner" size="2x" spin />
|
||||
</div>
|
||||
|
||||
<div v-else class="my-3" data-testid="users-list">
|
||||
<RouterLink
|
||||
v-for="({ id, username, active }, index) in usersList"
|
||||
:key="id"
|
||||
class="d-flex align-items-center mx-0 mx-lg-4 py-1 text-decoration-none users-list-row"
|
||||
:to="{ name: 'settings.users.edit', params: { id } }"
|
||||
data-testid="user-item"
|
||||
>
|
||||
<div class="col-10 col-sm-5 m-2 flex-shrink-1 fw-bold">
|
||||
{{ username }}
|
||||
</div>
|
||||
<div class="col-5 px-1 flex-shrink-1 d-none d-sm-flex gap-2 align-items-center">
|
||||
<font-awesome-icon :class="active ? 'text-success' : 'text-muted'" :icon="active ? 'check-circle' : 'times-circle'" />
|
||||
<div>{{ $t(active ? "Active" : "Inactive") }}</div>
|
||||
</div>
|
||||
<div class="col-2 pe-2 pe-lg-3 d-flex justify-content-end">
|
||||
<button
|
||||
type="button"
|
||||
class="btn-ban-user btn ms-2 py-1"
|
||||
:class="active ? 'btn-outline-danger' : 'btn-outline-success'"
|
||||
:disabled="processing"
|
||||
:data-testid="`toggle-active-user-${username}`"
|
||||
@click.prevent="active ? disableConfirm(usersList[index]) : toggleActiveUser(usersList[index])"
|
||||
>
|
||||
<font-awesome-icon class="" :icon="active ? 'user-slash' : 'user-check'" />
|
||||
</button>
|
||||
</div>
|
||||
</RouterLink>
|
||||
</div>
|
||||
|
||||
<Confirm
|
||||
ref="confirmDisable"
|
||||
btn-style="btn-danger"
|
||||
:yes-text="$t('Yes')"
|
||||
:no-text="$t('No')"
|
||||
@yes="toggleActiveUser(disablingUser)"
|
||||
@no="disablingUser = null"
|
||||
>
|
||||
{{ $t("confirmDisableUserMsg") }}
|
||||
</Confirm>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { useToast } from "vue-toastification";
|
||||
import Confirm from "../../Confirm.vue";
|
||||
const toast = useToast();
|
||||
|
||||
export default {
|
||||
components: { Confirm },
|
||||
|
||||
data: () => ({
|
||||
loading: false,
|
||||
processing: false,
|
||||
usersList: null,
|
||||
disablingUser: null,
|
||||
}),
|
||||
|
||||
mounted() {
|
||||
this.getUsers();
|
||||
},
|
||||
|
||||
methods: {
|
||||
/**
|
||||
* Get list of users from server
|
||||
* @returns {void}
|
||||
*/
|
||||
getUsers() {
|
||||
this.loading = true;
|
||||
this.$root.getSocket().emit("getUsers", (res) => {
|
||||
this.loading = false;
|
||||
if (res.ok) {
|
||||
this.usersList = res.users;
|
||||
} else {
|
||||
toast.error(res.msg);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Show confirmation for disabling a user
|
||||
* @param {object} user the user to confirm disable in the local usersList
|
||||
* @returns {void}
|
||||
*/
|
||||
disableConfirm(user) {
|
||||
this.disablingUser = user;
|
||||
this.$refs.confirmDisable.show();
|
||||
},
|
||||
|
||||
/**
|
||||
* Disable a user from server
|
||||
* @param {object} user the user to disable in the local usersList
|
||||
* @param {boolean} user.active is the user authorized to login?
|
||||
* @returns {void}
|
||||
*/
|
||||
toggleActiveUser({ active, ...rest }) {
|
||||
this.processing = true;
|
||||
this.$root.getSocket().emit(
|
||||
"saveUser",
|
||||
{
|
||||
...rest,
|
||||
active: !active
|
||||
},
|
||||
(res) => {
|
||||
this.$root.toastRes(res);
|
||||
this.processing = false;
|
||||
this.disablingUser &&= null;
|
||||
|
||||
if (res.ok) {
|
||||
this.getUsers();
|
||||
}
|
||||
});
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import "../../../assets/vars.scss";
|
||||
|
||||
.btn-ban-user {
|
||||
padding-left: 7px;
|
||||
padding-right: 7px;
|
||||
}
|
||||
|
||||
.users-list-row {
|
||||
cursor: pointer;
|
||||
border-top: 1px solid rgba(0, 0, 0, 0.125);
|
||||
|
||||
.dark & {
|
||||
border-top: 1px solid $dark-border-color;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background-color: $highlight-white;
|
||||
}
|
||||
|
||||
.dark &:hover {
|
||||
background-color: $dark-bg2;
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
29
src/components/settings/Users/routes.js
Normal file
29
src/components/settings/Users/routes.js
Normal file
|
@ -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")
|
||||
},
|
||||
]
|
||||
};
|
|
@ -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 };
|
||||
|
|
|
@ -1075,6 +1075,13 @@
|
|||
"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.",
|
||||
"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",
|
||||
"RabbitMQ Nodes": "RabbitMQ Management Nodes",
|
||||
"rabbitmqNodesDescription": "Enter the URL for the RabbitMQ management nodes including protocol and port. Example: {0}",
|
||||
"rabbitmqNodesRequired": "Please set the nodes for this monitor.",
|
||||
|
@ -1126,4 +1133,4 @@
|
|||
"Clear Form": "Clear Form",
|
||||
"pause": "Pause",
|
||||
"Manual": "Manual"
|
||||
}
|
||||
}
|
|
@ -1,6 +1,6 @@
|
|||
<template>
|
||||
<div :class="classes">
|
||||
<div v-if="! $root.socket.connected && ! $root.socket.firstConnect" class="lost-connection">
|
||||
<div v-if="! $root.socket.connected && ! $root.socket.firstConnect" class="lost-connection" data-testid="lost-connection">
|
||||
<div class="container-fluid">
|
||||
{{ $root.connectionErrorMsg }}
|
||||
<div v-if="$root.showReverseProxyGuide">
|
||||
|
|
|
@ -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();
|
||||
},
|
||||
|
|
|
@ -35,9 +35,20 @@
|
|||
</a>
|
||||
</div>
|
||||
<div class="settings-content col-lg-9 col-md-7">
|
||||
<div v-if="currentPage" class="settings-content-header">
|
||||
{{ subMenus[currentPage].title }}
|
||||
</div>
|
||||
<nav aria-label="breadcrumb">
|
||||
<ol class="breadcrumb settings-content-header">
|
||||
<li
|
||||
v-for="{ path, title, active } in breadcrumbs"
|
||||
:key="path"
|
||||
class="breadcrumb-item"
|
||||
v-bind="active ? { 'aria-current': 'page' } : {}"
|
||||
aria-current="page"
|
||||
>
|
||||
<template v-if="active">{{ title }}</template>
|
||||
<RouterLink v-else :to="path">{{ title }}</RouterLink>
|
||||
</li>
|
||||
</ol>
|
||||
</nav>
|
||||
<div class="mx-3">
|
||||
<router-view v-slot="{ Component }">
|
||||
<transition name="slide-fade" appear>
|
||||
|
@ -54,6 +65,15 @@
|
|||
<script>
|
||||
import { useRoute } from "vue-router";
|
||||
|
||||
/**
|
||||
* Deduplicate an array of objects using the provided key
|
||||
* @param {object[]} arr array of objects to deduplicate
|
||||
* @param {string} key key used for uniqness
|
||||
* @returns {object[]} the deduplicated array
|
||||
*/
|
||||
const uniqBy = (arr, key) =>
|
||||
[ ...(new Map(arr.map(item => [ item[key], item ]))).values() ];
|
||||
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
|
@ -110,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")
|
||||
},
|
||||
|
@ -121,6 +148,44 @@ export default {
|
|||
},
|
||||
};
|
||||
},
|
||||
|
||||
breadcrumbs: ({ $route, subMenus }) => {
|
||||
// List of setting routes matching the current path
|
||||
// for ex, with path "/settings/users/edit/1", we got:
|
||||
// [
|
||||
// { path: "/settings/users", ...otherRoutesProps },
|
||||
// { path: "/settings/users/edit/:id", ...otherRoutesProps }
|
||||
// ]
|
||||
const settingRoutes = uniqBy(
|
||||
$route.matched.filter(({ path }) => /^\/settings\//.test(path)),
|
||||
"path"
|
||||
);
|
||||
|
||||
/**
|
||||
* Get leaf submenu for a given setting path
|
||||
* @param {string} path setting path, ex: "/settings/users/edit/1"
|
||||
* @returns {object} the leaf subMenu corresponding to the given path
|
||||
*/
|
||||
const getLeafSubMenu = path => {
|
||||
const pathParts = path.split("/").slice(2);
|
||||
|
||||
// walk through submenus and there children until reaching the corresponding leaf subMenu
|
||||
// ex with "/settings/users/edit/1" we got { title: this.$t("Edit") }
|
||||
// because this path match "users" > "children" > "edit" in subMenus
|
||||
return pathParts.reduce(
|
||||
(acc, pathPart) =>
|
||||
acc[pathPart] || acc.children?.[pathPart] || acc,
|
||||
subMenus
|
||||
);
|
||||
};
|
||||
|
||||
// construct an array of setting routes path joined with submenus
|
||||
return settingRoutes.map(({ path }, idx) => ({
|
||||
path,
|
||||
title: getLeafSubMenu(path).title,
|
||||
active: !settingRoutes[idx + 1],
|
||||
}));
|
||||
}
|
||||
},
|
||||
|
||||
watch: {
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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 before execute the callback if no new call to function happens
|
||||
* @returns {void}
|
||||
*/
|
||||
return function (callback, delay = 100) {
|
||||
clearTimeout(timeout);
|
||||
timeout = setTimeout(() => callback(), delay);
|
||||
};
|
||||
}
|
||||
|
|
71
test/e2e/specs/multiple-users.spec.js
Normal file
71
test/e2e/specs/multiple-users.spec.js
Normal file
|
@ -0,0 +1,71 @@
|
|||
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();
|
||||
await page.getByRole("button", {
|
||||
name: "Yes",
|
||||
exact: true
|
||||
}).click();
|
||||
await expect(page.getByTestId("lost-connection")).toBeVisible();
|
||||
|
||||
// Make sure we're back on the login page after refresh
|
||||
await page.reload();
|
||||
await expect(page.getByText("Log in")).toBeVisible();
|
||||
|
||||
// Try to log in as the admin user
|
||||
await login(page, "admin", true); // Expect failure
|
||||
await screenshot(testInfo, page);
|
||||
|
||||
// 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 page.getByTestId("active-checkbox").uncheck();
|
||||
await expect(page.getByTestId("lost-connection")).toBeVisible();
|
||||
|
||||
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();
|
||||
|
||||
// Try to log in as the new user
|
||||
await login(page, "newuser", true); // Expect failure
|
||||
await screenshot(testInfo, page);
|
||||
});
|
||||
});
|
|
@ -1,3 +1,5 @@
|
|||
import { expect } from "@playwright/test";
|
||||
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
const serverUrl = require("../../config/playwright.config.js").url;
|
||||
|
@ -19,17 +21,24 @@ export async function screenshot(testInfo, page) {
|
|||
|
||||
/**
|
||||
* @param {Page} page Page
|
||||
* @param {string} user Username to log in with
|
||||
* @param {boolean} expectFail Whether to expect a failure (true) or success (false)
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
export async function login(page) {
|
||||
export async function login(page, user = "admin", expectFail = false) {
|
||||
// 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");
|
||||
|
||||
if (expectFail) {
|
||||
await expect(page.getByRole("alert")).toBeVisible();
|
||||
} else {
|
||||
await page.isVisible("text=Add New Monitor");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
Loading…
Add table
Reference in a new issue