2021-09-16 16:48:28 +02:00
|
|
|
const { R } = require("redbean-node");
|
2022-04-13 18:30:32 +02:00
|
|
|
const { checkLogin, setSetting } = require("../util-server");
|
2021-09-16 16:48:28 +02:00
|
|
|
const dayjs = require("dayjs");
|
2022-04-13 17:33:37 +02:00
|
|
|
const { log } = require("../../src/util");
|
2021-09-21 15:22:35 +02:00
|
|
|
const ImageDataURI = require("../image-data-uri");
|
|
|
|
const Database = require("../database");
|
2021-09-21 18:58:22 +02:00
|
|
|
const apicache = require("../modules/apicache");
|
2022-03-16 09:38:10 +02:00
|
|
|
const StatusPage = require("../model/status_page");
|
2022-04-19 09:38:59 +02:00
|
|
|
const { UptimeKumaServer } = require("../uptime-kuma-server");
|
2021-09-16 16:48:28 +02:00
|
|
|
|
2022-04-21 18:10:23 +02:00
|
|
|
/**
|
|
|
|
* Socket handlers for status page
|
|
|
|
* @param {Socket} socket Socket.io instance to add listeners on
|
2023-08-11 09:46:41 +02:00
|
|
|
* @returns {void}
|
2022-04-21 18:10:23 +02:00
|
|
|
*/
|
2021-09-16 16:48:28 +02:00
|
|
|
module.exports.statusPageSocketHandler = (socket) => {
|
|
|
|
|
|
|
|
// Post or edit incident
|
2022-03-16 09:38:10 +02:00
|
|
|
socket.on("postIncident", async (slug, incident, callback) => {
|
2021-09-16 16:48:28 +02:00
|
|
|
try {
|
|
|
|
checkLogin(socket);
|
|
|
|
|
2022-03-16 09:38:10 +02:00
|
|
|
let statusPageID = await StatusPage.slugToID(slug);
|
|
|
|
|
|
|
|
if (!statusPageID) {
|
|
|
|
throw new Error("slug is not found");
|
|
|
|
}
|
|
|
|
|
|
|
|
await R.exec("UPDATE incident SET pin = 0 WHERE status_page_id = ? ", [
|
|
|
|
statusPageID
|
|
|
|
]);
|
2021-09-16 16:48:28 +02:00
|
|
|
|
|
|
|
let incidentBean;
|
|
|
|
|
|
|
|
if (incident.id) {
|
2022-03-16 09:38:10 +02:00
|
|
|
incidentBean = await R.findOne("incident", " id = ? AND status_page_id = ? ", [
|
|
|
|
incident.id,
|
|
|
|
statusPageID
|
2021-09-16 16:48:28 +02:00
|
|
|
]);
|
|
|
|
}
|
|
|
|
|
|
|
|
if (incidentBean == null) {
|
|
|
|
incidentBean = R.dispense("incident");
|
|
|
|
}
|
|
|
|
|
|
|
|
incidentBean.title = incident.title;
|
|
|
|
incidentBean.content = incident.content;
|
|
|
|
incidentBean.style = incident.style;
|
|
|
|
incidentBean.pin = true;
|
2022-03-16 09:38:10 +02:00
|
|
|
incidentBean.status_page_id = statusPageID;
|
2021-09-19 13:04:51 +02:00
|
|
|
|
|
|
|
if (incident.id) {
|
|
|
|
incidentBean.lastUpdatedDate = R.isoDateTime(dayjs.utc());
|
|
|
|
} else {
|
|
|
|
incidentBean.createdDate = R.isoDateTime(dayjs.utc());
|
|
|
|
}
|
|
|
|
|
2021-09-16 16:48:28 +02:00
|
|
|
await R.store(incidentBean);
|
|
|
|
|
|
|
|
callback({
|
|
|
|
ok: true,
|
|
|
|
incident: incidentBean.toPublicJSON(),
|
2021-09-16 16:57:34 +02:00
|
|
|
});
|
2021-09-16 16:48:28 +02:00
|
|
|
} catch (error) {
|
|
|
|
callback({
|
|
|
|
ok: false,
|
|
|
|
msg: error.message,
|
2021-09-16 16:57:34 +02:00
|
|
|
});
|
2021-09-16 16:48:28 +02:00
|
|
|
}
|
|
|
|
});
|
|
|
|
|
2022-03-16 09:38:10 +02:00
|
|
|
socket.on("unpinIncident", async (slug, callback) => {
|
2021-09-16 16:48:28 +02:00
|
|
|
try {
|
|
|
|
checkLogin(socket);
|
|
|
|
|
2022-03-16 09:38:10 +02:00
|
|
|
let statusPageID = await StatusPage.slugToID(slug);
|
|
|
|
|
|
|
|
await R.exec("UPDATE incident SET pin = 0 WHERE pin = 1 AND status_page_id = ? ", [
|
|
|
|
statusPageID
|
|
|
|
]);
|
2021-09-16 16:48:28 +02:00
|
|
|
|
|
|
|
callback({
|
|
|
|
ok: true,
|
2021-09-16 16:57:34 +02:00
|
|
|
});
|
2021-09-16 16:48:28 +02:00
|
|
|
} catch (error) {
|
|
|
|
callback({
|
|
|
|
ok: false,
|
|
|
|
msg: error.message,
|
2021-09-16 16:57:34 +02:00
|
|
|
});
|
2021-09-16 16:48:28 +02:00
|
|
|
}
|
|
|
|
});
|
2021-09-19 13:04:51 +02:00
|
|
|
|
2022-04-09 18:25:27 +02:00
|
|
|
socket.on("getStatusPage", async (slug, callback) => {
|
2021-09-16 16:48:28 +02:00
|
|
|
try {
|
|
|
|
checkLogin(socket);
|
|
|
|
|
2022-04-09 18:25:27 +02:00
|
|
|
let statusPage = await R.findOne("status_page", " slug = ? ", [
|
|
|
|
slug
|
|
|
|
]);
|
|
|
|
|
|
|
|
if (!statusPage) {
|
|
|
|
throw new Error("No slug?");
|
|
|
|
}
|
2021-09-16 16:48:28 +02:00
|
|
|
|
|
|
|
callback({
|
|
|
|
ok: true,
|
2022-04-09 18:25:27 +02:00
|
|
|
config: await statusPage.toJSON(),
|
2021-09-16 16:57:34 +02:00
|
|
|
});
|
2021-09-16 16:48:28 +02:00
|
|
|
} catch (error) {
|
|
|
|
callback({
|
|
|
|
ok: false,
|
|
|
|
msg: error.message,
|
2021-09-16 16:57:34 +02:00
|
|
|
});
|
2021-09-16 16:48:28 +02:00
|
|
|
}
|
|
|
|
});
|
2021-09-19 13:04:51 +02:00
|
|
|
|
|
|
|
// Save Status Page
|
2021-09-21 15:22:35 +02:00
|
|
|
// imgDataUrl Only Accept PNG!
|
2022-03-15 06:00:29 +02:00
|
|
|
socket.on("saveStatusPage", async (slug, config, imgDataUrl, publicGroupList, callback) => {
|
2021-09-19 13:04:51 +02:00
|
|
|
try {
|
|
|
|
checkLogin(socket);
|
|
|
|
|
2022-03-17 10:42:26 +02:00
|
|
|
// Save Config
|
|
|
|
let statusPage = await R.findOne("status_page", " slug = ? ", [
|
|
|
|
slug
|
|
|
|
]);
|
|
|
|
|
|
|
|
if (!statusPage) {
|
|
|
|
throw new Error("No slug?");
|
|
|
|
}
|
|
|
|
|
2022-04-09 18:25:27 +02:00
|
|
|
checkSlug(config.slug);
|
2021-09-21 18:58:22 +02:00
|
|
|
|
2021-09-21 15:22:35 +02:00
|
|
|
const header = "data:image/png;base64,";
|
|
|
|
|
|
|
|
// Check logo format
|
|
|
|
// If is image data url, convert to png file
|
|
|
|
// Else assume it is a url, nothing to do
|
|
|
|
if (imgDataUrl.startsWith("data:")) {
|
|
|
|
if (! imgDataUrl.startsWith(header)) {
|
|
|
|
throw new Error("Only allowed PNG logo.");
|
|
|
|
}
|
|
|
|
|
2022-03-17 10:42:26 +02:00
|
|
|
const filename = `logo${statusPage.id}.png`;
|
|
|
|
|
2021-09-21 15:22:35 +02:00
|
|
|
// Convert to file
|
2022-03-17 10:42:26 +02:00
|
|
|
await ImageDataURI.outputFile(imgDataUrl, Database.uploadDir + filename);
|
|
|
|
config.logo = `/upload/${filename}?t=` + Date.now();
|
2021-09-21 15:22:35 +02:00
|
|
|
|
|
|
|
} else {
|
2024-05-10 20:35:36 +02:00
|
|
|
config.logo = imgDataUrl;
|
2021-09-21 15:22:35 +02:00
|
|
|
}
|
|
|
|
|
2022-03-15 06:00:29 +02:00
|
|
|
statusPage.slug = config.slug;
|
|
|
|
statusPage.title = config.title;
|
2022-03-16 08:14:47 +02:00
|
|
|
statusPage.description = config.description;
|
2022-03-15 06:00:29 +02:00
|
|
|
statusPage.icon = config.logo;
|
2024-05-19 21:56:55 +02:00
|
|
|
statusPage.autoRefreshInterval = config.autoRefreshInterval,
|
2022-03-15 06:00:29 +02:00
|
|
|
statusPage.theme = config.theme;
|
|
|
|
//statusPage.published = ;
|
|
|
|
//statusPage.search_engine_index = ;
|
|
|
|
statusPage.show_tags = config.showTags;
|
|
|
|
//statusPage.password = null;
|
2022-04-17 08:53:13 +02:00
|
|
|
statusPage.footer_text = config.footerText;
|
|
|
|
statusPage.custom_css = config.customCSS;
|
|
|
|
statusPage.show_powered_by = config.showPoweredBy;
|
2023-07-05 01:37:45 +02:00
|
|
|
statusPage.show_certificate_expiry = config.showCertificateExpiry;
|
2022-03-16 08:14:47 +02:00
|
|
|
statusPage.modified_date = R.isoDateTime();
|
2023-01-31 15:18:02 +02:00
|
|
|
statusPage.google_analytics_tag_id = config.googleAnalyticsId;
|
2022-03-15 06:00:29 +02:00
|
|
|
|
|
|
|
await R.store(statusPage);
|
2021-09-20 14:44:07 +02:00
|
|
|
|
2022-04-09 18:25:27 +02:00
|
|
|
await statusPage.updateDomainNameList(config.domainNameList);
|
|
|
|
await StatusPage.loadDomainMappingList();
|
2021-09-20 14:44:07 +02:00
|
|
|
|
2021-09-22 09:10:08 +02:00
|
|
|
// Save Public Group List
|
|
|
|
const groupIDList = [];
|
|
|
|
let groupOrder = 1;
|
|
|
|
|
|
|
|
for (let group of publicGroupList) {
|
|
|
|
let groupBean;
|
|
|
|
if (group.id) {
|
2022-03-16 09:38:10 +02:00
|
|
|
groupBean = await R.findOne("group", " id = ? AND public = 1 AND status_page_id = ? ", [
|
|
|
|
group.id,
|
|
|
|
statusPage.id
|
2021-09-19 13:04:51 +02:00
|
|
|
]);
|
2021-09-22 09:10:08 +02:00
|
|
|
} else {
|
|
|
|
groupBean = R.dispense("group");
|
|
|
|
}
|
|
|
|
|
2022-03-16 09:38:10 +02:00
|
|
|
groupBean.status_page_id = statusPage.id;
|
2021-09-22 09:10:08 +02:00
|
|
|
groupBean.name = group.name;
|
|
|
|
groupBean.public = true;
|
|
|
|
groupBean.weight = groupOrder++;
|
|
|
|
|
|
|
|
await R.store(groupBean);
|
|
|
|
|
|
|
|
await R.exec("DELETE FROM monitor_group WHERE group_id = ? ", [
|
|
|
|
groupBean.id
|
|
|
|
]);
|
2021-09-19 13:04:51 +02:00
|
|
|
|
2021-09-22 09:10:08 +02:00
|
|
|
let monitorOrder = 1;
|
2021-09-22 09:23:58 +02:00
|
|
|
|
2021-09-22 09:10:08 +02:00
|
|
|
for (let monitor of group.monitorList) {
|
|
|
|
let relationBean = R.dispense("monitor_group");
|
|
|
|
relationBean.weight = monitorOrder++;
|
|
|
|
relationBean.group_id = groupBean.id;
|
|
|
|
relationBean.monitor_id = monitor.id;
|
2022-07-22 17:27:02 +02:00
|
|
|
|
|
|
|
if (monitor.sendUrl !== undefined) {
|
|
|
|
relationBean.send_url = monitor.sendUrl;
|
|
|
|
}
|
|
|
|
|
2021-09-22 09:10:08 +02:00
|
|
|
await R.store(relationBean);
|
2021-09-19 13:04:51 +02:00
|
|
|
}
|
|
|
|
|
2021-09-22 09:10:08 +02:00
|
|
|
groupIDList.push(groupBean.id);
|
|
|
|
group.id = groupBean.id;
|
|
|
|
}
|
|
|
|
|
2021-11-11 13:31:28 +02:00
|
|
|
// Delete groups that are not in the list
|
2022-04-13 17:33:37 +02:00
|
|
|
log.debug("socket", "Delete groups that are not in the list");
|
2021-09-22 09:10:08 +02:00
|
|
|
const slots = groupIDList.map(() => "?").join(",");
|
2022-03-21 18:06:29 +02:00
|
|
|
|
|
|
|
const data = [
|
|
|
|
...groupIDList,
|
|
|
|
statusPage.id
|
|
|
|
];
|
|
|
|
await R.exec(`DELETE FROM \`group\` WHERE id NOT IN (${slots}) AND status_page_id = ?`, data);
|
2021-09-19 13:04:51 +02:00
|
|
|
|
2022-04-19 09:38:59 +02:00
|
|
|
const server = UptimeKumaServer.getInstance();
|
|
|
|
|
2022-03-18 11:57:08 +02:00
|
|
|
// Also change entry page to new slug if it is the default one, and slug is changed.
|
|
|
|
if (server.entryPage === "statusPage-" + slug && statusPage.slug !== slug) {
|
|
|
|
server.entryPage = "statusPage-" + statusPage.slug;
|
|
|
|
await setSetting("entryPage", server.entryPage, "general");
|
|
|
|
}
|
|
|
|
|
2022-04-09 18:25:27 +02:00
|
|
|
apicache.clear();
|
2021-09-19 13:04:51 +02:00
|
|
|
|
2021-09-22 09:10:08 +02:00
|
|
|
callback({
|
|
|
|
ok: true,
|
|
|
|
publicGroupList,
|
2021-09-19 13:04:51 +02:00
|
|
|
});
|
2021-09-22 09:10:08 +02:00
|
|
|
|
2021-09-19 13:04:51 +02:00
|
|
|
} catch (error) {
|
2022-04-13 17:33:37 +02:00
|
|
|
log.error("socket", error);
|
2021-09-19 13:04:51 +02:00
|
|
|
|
|
|
|
callback({
|
|
|
|
ok: false,
|
|
|
|
msg: error.message,
|
|
|
|
});
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
2022-03-17 17:38:43 +02:00
|
|
|
// Add a new status page
|
|
|
|
socket.on("addStatusPage", async (title, slug, callback) => {
|
|
|
|
try {
|
|
|
|
checkLogin(socket);
|
|
|
|
|
|
|
|
title = title?.trim();
|
|
|
|
slug = slug?.trim();
|
|
|
|
|
|
|
|
// Check empty
|
|
|
|
if (!title || !slug) {
|
|
|
|
throw new Error("Please input all fields");
|
|
|
|
}
|
|
|
|
|
|
|
|
// Make sure slug is string
|
|
|
|
if (typeof slug !== "string") {
|
|
|
|
throw new Error("Slug -Accept string only");
|
|
|
|
}
|
|
|
|
|
|
|
|
// lower case only
|
|
|
|
slug = slug.toLowerCase();
|
|
|
|
|
2022-03-24 17:43:07 +02:00
|
|
|
checkSlug(slug);
|
2022-03-17 17:38:43 +02:00
|
|
|
|
|
|
|
let statusPage = R.dispense("status_page");
|
|
|
|
statusPage.slug = slug;
|
|
|
|
statusPage.title = title;
|
2023-03-02 01:26:26 +02:00
|
|
|
statusPage.theme = "auto";
|
2022-03-17 17:38:43 +02:00
|
|
|
statusPage.icon = "";
|
2024-05-19 21:56:55 +02:00
|
|
|
statusPage.autoRefreshInterval = 300;
|
2022-03-17 17:38:43 +02:00
|
|
|
await R.store(statusPage);
|
|
|
|
|
|
|
|
callback({
|
|
|
|
ok: true,
|
2023-09-26 22:53:14 +02:00
|
|
|
msg: "successAdded",
|
|
|
|
msgi18n: true,
|
2022-03-17 17:38:43 +02:00
|
|
|
});
|
|
|
|
|
|
|
|
} catch (error) {
|
|
|
|
console.error(error);
|
|
|
|
callback({
|
|
|
|
ok: false,
|
|
|
|
msg: error.message,
|
|
|
|
});
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
2022-03-18 09:19:52 +02:00
|
|
|
// Delete a status page
|
|
|
|
socket.on("deleteStatusPage", async (slug, callback) => {
|
2022-04-19 09:38:59 +02:00
|
|
|
const server = UptimeKumaServer.getInstance();
|
|
|
|
|
2022-03-18 09:19:52 +02:00
|
|
|
try {
|
|
|
|
checkLogin(socket);
|
|
|
|
|
|
|
|
let statusPageID = await StatusPage.slugToID(slug);
|
|
|
|
|
|
|
|
if (statusPageID) {
|
|
|
|
|
|
|
|
// Reset entry page if it is the default one.
|
|
|
|
if (server.entryPage === "statusPage-" + slug) {
|
|
|
|
server.entryPage = "dashboard";
|
2022-03-18 11:57:08 +02:00
|
|
|
await setSetting("entryPage", server.entryPage, "general");
|
2022-03-18 09:19:52 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
// No need to delete records from `status_page_cname`, because it has cascade foreign key.
|
|
|
|
// But for incident & group, it is hard to add cascade foreign key during migration, so they have to be deleted manually.
|
|
|
|
|
|
|
|
// Delete incident
|
|
|
|
await R.exec("DELETE FROM incident WHERE status_page_id = ? ", [
|
|
|
|
statusPageID
|
|
|
|
]);
|
|
|
|
|
|
|
|
// Delete group
|
|
|
|
await R.exec("DELETE FROM `group` WHERE status_page_id = ? ", [
|
|
|
|
statusPageID
|
|
|
|
]);
|
|
|
|
|
|
|
|
// Delete status_page
|
|
|
|
await R.exec("DELETE FROM status_page WHERE id = ? ", [
|
|
|
|
statusPageID
|
|
|
|
]);
|
|
|
|
|
|
|
|
} else {
|
|
|
|
throw new Error("Status Page is not found");
|
|
|
|
}
|
|
|
|
|
|
|
|
callback({
|
|
|
|
ok: true,
|
|
|
|
});
|
|
|
|
} catch (error) {
|
|
|
|
callback({
|
|
|
|
ok: false,
|
|
|
|
msg: error.message,
|
|
|
|
});
|
|
|
|
}
|
|
|
|
});
|
2021-09-16 16:57:34 +02:00
|
|
|
};
|
2022-03-24 17:43:07 +02:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Check slug a-z, 0-9, - only
|
|
|
|
* Regex from: https://stackoverflow.com/questions/22454258/js-regex-string-validation-for-slug
|
2022-04-21 18:10:23 +02:00
|
|
|
* @param {string} slug Slug to test
|
2023-08-11 09:46:41 +02:00
|
|
|
* @returns {void}
|
|
|
|
* @throws Slug is not valid
|
2022-03-24 17:43:07 +02:00
|
|
|
*/
|
|
|
|
function checkSlug(slug) {
|
|
|
|
if (typeof slug !== "string") {
|
|
|
|
throw new Error("Slug must be string");
|
|
|
|
}
|
|
|
|
|
|
|
|
slug = slug.trim();
|
|
|
|
|
|
|
|
if (!slug) {
|
|
|
|
throw new Error("Slug cannot be empty");
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!slug.match(/^[A-Za-z0-9]+(?:-[A-Za-z0-9]+)*$/)) {
|
|
|
|
throw new Error("Invalid Slug");
|
|
|
|
}
|
|
|
|
}
|