uptime-kuma/server/socket-handlers/status-page-socket-handler.js
2024-04-12 15:32:50 +02:00

373 lines
11 KiB
JavaScript

const { R } = require("redbean-node");
const { checkLogin, setSetting } = require("../util-server");
const dayjs = require("dayjs");
const { log } = require("../../src/util");
const ImageDataURI = require("../image-data-uri");
const Database = require("../database");
const apicache = require("../modules/apicache");
const StatusPage = require("../model/status_page");
const { UptimeKumaServer } = require("../uptime-kuma-server");
/**
* Socket handlers for status page
* @param {Socket} socket Socket.io instance to add listeners on
* @returns {void}
*/
module.exports.statusPageSocketHandler = (socket) => {
// Post or edit incident
socket.on("postIncident", async (slug, incident, callback) => {
try {
checkLogin(socket);
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
]);
let incidentBean;
if (incident.id) {
incidentBean = await R.findOne("incident", " id = ? AND status_page_id = ? ", [
incident.id,
statusPageID
]);
}
if (incidentBean == null) {
incidentBean = R.dispense("incident");
}
incidentBean.title = incident.title;
incidentBean.content = incident.content;
incidentBean.style = incident.style;
incidentBean.pin = true;
incidentBean.status_page_id = statusPageID;
if (incident.id) {
incidentBean.lastUpdatedDate = R.isoDateTime(dayjs.utc());
} else {
incidentBean.createdDate = R.isoDateTime(dayjs.utc());
}
await R.store(incidentBean);
callback({
ok: true,
incident: incidentBean.toPublicJSON(),
});
} catch (error) {
callback({
ok: false,
msg: error.message,
});
}
});
socket.on("unpinIncident", async (slug, callback) => {
try {
checkLogin(socket);
let statusPageID = await StatusPage.slugToID(slug);
await R.exec("UPDATE incident SET pin = 0 WHERE pin = 1 AND status_page_id = ? ", [
statusPageID
]);
callback({
ok: true,
});
} catch (error) {
callback({
ok: false,
msg: error.message,
});
}
});
socket.on("getStatusPage", async (slug, callback) => {
try {
checkLogin(socket);
let statusPage = await R.findOne("status_page", " slug = ? ", [
slug
]);
if (!statusPage) {
throw new Error("No slug?");
}
callback({
ok: true,
config: await statusPage.toJSON(),
});
} catch (error) {
callback({
ok: false,
msg: error.message,
});
}
});
// Save Status Page
// imgDataUrl Only Accept PNG!
socket.on("saveStatusPage", async (slug, config, imgDataUrl, publicGroupList, callback) => {
try {
checkLogin(socket);
// Save Config
let statusPage = await R.findOne("status_page", " slug = ? ", [
slug
]);
if (!statusPage) {
throw new Error("No slug?");
}
checkSlug(config.slug);
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.");
}
const filename = `logo${statusPage.id}.png`;
// Convert to file
await ImageDataURI.outputFile(imgDataUrl, Database.uploadDir + filename);
config.logo = `/upload/${filename}?t=` + Date.now();
} else {
config.icon = imgDataUrl;
}
statusPage.slug = config.slug;
statusPage.title = config.title;
statusPage.description = config.description;
statusPage.icon = config.logo;
statusPage.theme = config.theme;
//statusPage.published = ;
//statusPage.search_engine_index = ;
statusPage.show_tags = config.showTags;
//statusPage.password = null;
statusPage.footer_text = config.footerText;
statusPage.custom_css = config.customCSS;
statusPage.show_powered_by = config.showPoweredBy;
statusPage.show_last_heartbeat = config.showLastHeartbeat;
statusPage.show_certificate_expiry = config.showCertificateExpiry;
statusPage.modified_date = R.isoDateTime();
statusPage.google_analytics_tag_id = config.googleAnalyticsId;
await R.store(statusPage);
await statusPage.updateDomainNameList(config.domainNameList);
await StatusPage.loadDomainMappingList();
// Save Public Group List
const groupIDList = [];
let groupOrder = 1;
for (let group of publicGroupList) {
let groupBean;
if (group.id) {
groupBean = await R.findOne("group", " id = ? AND public = 1 AND status_page_id = ? ", [
group.id,
statusPage.id
]);
} else {
groupBean = R.dispense("group");
}
groupBean.status_page_id = statusPage.id;
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
]);
let monitorOrder = 1;
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;
if (monitor.sendUrl !== undefined) {
relationBean.send_url = monitor.sendUrl;
}
await R.store(relationBean);
}
groupIDList.push(groupBean.id);
group.id = groupBean.id;
}
// Delete groups that are not in the list
log.debug("socket", "Delete groups that are not in the list");
const slots = groupIDList.map(() => "?").join(",");
const data = [
...groupIDList,
statusPage.id
];
await R.exec(`DELETE FROM \`group\` WHERE id NOT IN (${slots}) AND status_page_id = ?`, data);
const server = UptimeKumaServer.getInstance();
// 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");
}
apicache.clear();
callback({
ok: true,
publicGroupList,
});
} catch (error) {
log.error("socket", error);
callback({
ok: false,
msg: error.message,
});
}
});
// 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();
checkSlug(slug);
let statusPage = R.dispense("status_page");
statusPage.slug = slug;
statusPage.title = title;
statusPage.theme = "auto";
statusPage.icon = "";
await R.store(statusPage);
callback({
ok: true,
msg: "successAdded",
msgi18n: true,
});
} catch (error) {
console.error(error);
callback({
ok: false,
msg: error.message,
});
}
});
// Delete a status page
socket.on("deleteStatusPage", async (slug, callback) => {
const server = UptimeKumaServer.getInstance();
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";
await setSetting("entryPage", server.entryPage, "general");
}
// 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,
});
}
});
};
/**
* Check slug a-z, 0-9, - only
* Regex from: https://stackoverflow.com/questions/22454258/js-regex-string-validation-for-slug
* @param {string} slug Slug to test
* @returns {void}
* @throws Slug is not valid
*/
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");
}
}