summaryrefslogtreecommitdiffstats
path: root/server/socket-handlers/status-page-socket-handler.js
diff options
context:
space:
mode:
authorDaniel Baumann <daniel@debian.org>2024-11-26 09:28:28 +0100
committerDaniel Baumann <daniel@debian.org>2024-11-26 12:25:58 +0100
commita1882b67c41fe9901a0cd8059b5cc78a5beadec0 (patch)
tree2a24507c67aa99a15416707b2f7e645142230ed8 /server/socket-handlers/status-page-socket-handler.js
parentInitial commit. (diff)
downloaduptime-kuma-a1882b67c41fe9901a0cd8059b5cc78a5beadec0.tar.xz
uptime-kuma-a1882b67c41fe9901a0cd8059b5cc78a5beadec0.zip
Adding upstream version 2.0.0~beta.0+dfsg.upstream/2.0.0_beta.0+dfsgupstream
Signed-off-by: Daniel Baumann <daniel@debian.org>
Diffstat (limited to 'server/socket-handlers/status-page-socket-handler.js')
-rw-r--r--server/socket-handlers/status-page-socket-handler.js374
1 files changed, 374 insertions, 0 deletions
diff --git a/server/socket-handlers/status-page-socket-handler.js b/server/socket-handlers/status-page-socket-handler.js
new file mode 100644
index 0000000..0804da1
--- /dev/null
+++ b/server/socket-handlers/status-page-socket-handler.js
@@ -0,0 +1,374 @@
+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.logo = imgDataUrl;
+ }
+
+ statusPage.slug = config.slug;
+ statusPage.title = config.title;
+ statusPage.description = config.description;
+ statusPage.icon = config.logo;
+ statusPage.autoRefreshInterval = config.autoRefreshInterval,
+ 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_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 = "";
+ statusPage.autoRefreshInterval = 300;
+ 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");
+ }
+}