Merge branch 'master' into introduce-resend-interval

This commit is contained in:
OidaTiftla
2022-03-27 21:19:57 +02:00
committed by GitHub
88 changed files with 9565 additions and 4432 deletions

View File

@@ -31,31 +31,34 @@ exports.login = async function (username, password) {
};
function myAuthorizer(username, password, callback) {
setting("disableAuth").then((result) => {
if (result) {
callback(null, true);
} else {
// Login Rate Limit
loginRateLimiter.pass(null, 0).then((pass) => {
if (pass) {
exports.login(username, password).then((user) => {
callback(null, user != null);
// Login Rate Limit
loginRateLimiter.pass(null, 0).then((pass) => {
if (pass) {
exports.login(username, password).then((user) => {
callback(null, user != null);
if (user == null) {
loginRateLimiter.removeTokens(1);
}
});
} else {
callback(null, false);
if (user == null) {
loginRateLimiter.removeTokens(1);
}
});
} else {
callback(null, false);
}
});
}
exports.basicAuth = basicAuth({
authorizer: myAuthorizer,
authorizeAsync: true,
challenge: true,
});
exports.basicAuth = async function (req, res, next) {
const middleware = basicAuth({
authorizer: myAuthorizer,
authorizeAsync: true,
challenge: true,
});
const disabledAuth = await setting("disableAuth");
if (!disabledAuth) {
middleware(req, res, next);
} else {
next();
}
};

View File

@@ -1,5 +1,6 @@
const { setSetting } = require("./util-server");
const { setSetting, setting } = require("./util-server");
const axios = require("axios");
const compareVersions = require("compare-versions");
exports.version = require("../package.json").version;
exports.latestVersion = null;
@@ -16,6 +17,19 @@ exports.startInterval = () => {
res.data.slow = "1000.0.0";
}
if (!await setting("checkUpdate")) {
return;
}
let checkBeta = await setting("checkBeta");
if (checkBeta && res.data.beta) {
if (compareVersions.compare(res.data.beta, res.data.beta, ">")) {
exports.latestVersion = res.data.beta;
return;
}
}
if (res.data.slow) {
exports.latestVersion = res.data.slow;
}

View File

@@ -53,6 +53,7 @@ class Database {
"patch-2fa-invalidate-used-token.sql": true,
"patch-notification_sent_history.sql": true,
"patch-monitor-basic-auth.sql": true,
"patch-status-page.sql": true,
"patch-monitor-add-resend-interval.sql": true,
"patch-heartbeat-add-last-notified-time.sql": true,
}
@@ -172,6 +173,7 @@ class Database {
}
await this.patch2();
await this.migrateNewStatusPage();
}
/**
@@ -213,6 +215,74 @@ class Database {
await setSetting("databasePatchedFiles", databasePatchedFiles);
}
/**
* Migrate status page value in setting to "status_page" table
* @returns {Promise<void>}
*/
static async migrateNewStatusPage() {
// Fix 1.13.0 empty slug bug
await R.exec("UPDATE status_page SET slug = 'empty-slug-recover' WHERE TRIM(slug) = ''");
let title = await setting("title");
if (title) {
console.log("Migrating Status Page");
let statusPageCheck = await R.findOne("status_page", " slug = 'default' ");
if (statusPageCheck !== null) {
console.log("Migrating Status Page - Skip, default slug record is already existing");
return;
}
let statusPage = R.dispense("status_page");
statusPage.slug = "default";
statusPage.title = title;
statusPage.description = await setting("description");
statusPage.icon = await setting("icon");
statusPage.theme = await setting("statusPageTheme");
statusPage.published = !!await setting("statusPagePublished");
statusPage.search_engine_index = !!await setting("searchEngineIndex");
statusPage.show_tags = !!await setting("statusPageTags");
statusPage.password = null;
if (!statusPage.title) {
statusPage.title = "My Status Page";
}
if (!statusPage.icon) {
statusPage.icon = "";
}
if (!statusPage.theme) {
statusPage.theme = "light";
}
let id = await R.store(statusPage);
await R.exec("UPDATE incident SET status_page_id = ? WHERE status_page_id IS NULL", [
id
]);
await R.exec("UPDATE [group] SET status_page_id = ? WHERE status_page_id IS NULL", [
id
]);
await R.exec("DELETE FROM setting WHERE type = 'statusPage'");
// Migrate Entry Page if it is status page
let entryPage = await setting("entryPage");
if (entryPage === "statusPage") {
await setSetting("entryPage", "statusPage-default", "general");
}
console.log("Migrating Status Page - Done");
}
}
/**
* Used it patch2() only
* @param sqlFilename

View File

@@ -3,12 +3,12 @@ const { R } = require("redbean-node");
class Group extends BeanModel {
async toPublicJSON() {
async toPublicJSON(showTags = false) {
let monitorBeanList = await this.getMonitorList();
let monitorList = [];
for (let bean of monitorBeanList) {
monitorList.push(await bean.toPublicJSON());
monitorList.push(await bean.toPublicJSON(showTags));
}
return {

View File

@@ -24,18 +24,22 @@ const apicache = require("../modules/apicache");
class Monitor extends BeanModel {
/**
* Return a object that ready to parse to JSON for public
* Return an object that ready to parse to JSON for public
* Only show necessary data to public
*/
async toPublicJSON() {
return {
async toPublicJSON(showTags = false) {
let obj = {
id: this.id,
name: this.name,
};
if (showTags) {
obj.tags = await this.getTags();
}
return obj;
}
/**
* Return a object that ready to parse to JSON
* Return an object that ready to parse to JSON
*/
async toJSON() {
@@ -49,7 +53,7 @@ class Monitor extends BeanModel {
notificationIDList[bean.notification_id] = true;
}
const tags = await R.getAll("SELECT mt.*, tag.name, tag.color FROM monitor_tag mt JOIN tag ON mt.tag_id = tag.id WHERE mt.monitor_id = ?", [this.id]);
const tags = await this.getTags();
return {
id: this.id,
@@ -83,6 +87,10 @@ class Monitor extends BeanModel {
};
}
async getTags() {
return await R.getAll("SELECT mt.*, tag.name, tag.color FROM monitor_tag mt JOIN tag ON mt.tag_id = tag.id WHERE mt.monitor_id = ?", [this.id]);
}
/**
* Encode user and password to Base64 encoding
* for HTTP "basic" auth, as per RFC-7617
@@ -120,6 +128,19 @@ class Monitor extends BeanModel {
const beat = async () => {
let beatInterval = this.interval;
if (! beatInterval) {
beatInterval = 1;
}
if (demoMode) {
if (beatInterval < 20) {
console.log("beat interval too low, reset to 20s");
beatInterval = 20;
}
}
// Expose here for prometheus update
// undefined if not https
let tlsInfo = undefined;
@@ -305,7 +326,7 @@ class Monitor extends BeanModel {
} else {
// No need to insert successful heartbeat for push type, so end here
retries = 0;
this.heartbeatInterval = setTimeout(beat, this.interval * 1000);
this.heartbeatInterval = setTimeout(beat, beatInterval * 1000);
return;
}
@@ -379,8 +400,6 @@ class Monitor extends BeanModel {
}
}
let beatInterval = this.interval;
debug(`[${this.name}] Check isImportant`);
let isImportant = Monitor.isImportantBeat(isFirstBeat, previousBeat?.status, bean.status);
@@ -439,14 +458,6 @@ class Monitor extends BeanModel {
previousBeat = bean;
if (! this.isStop) {
if (demoMode) {
if (beatInterval < 20) {
console.log("beat interval too low, reset to 20s");
beatInterval = 20;
}
}
debug(`[${this.name}] SetTimeout for next check.`);
this.heartbeatInterval = setTimeout(safeBeat, beatInterval * 1000);
} else {
@@ -483,6 +494,12 @@ class Monitor extends BeanModel {
stop() {
clearTimeout(this.heartbeatInterval);
this.isStop = true;
this.prometheus().remove();
}
prometheus() {
return new Prometheus(this);
}
/**

View File

@@ -0,0 +1,60 @@
const { BeanModel } = require("redbean-node/dist/bean-model");
const { R } = require("redbean-node");
class StatusPage extends BeanModel {
static async sendStatusPageList(io, socket) {
let result = {};
let list = await R.findAll("status_page", " ORDER BY title ");
for (let item of list) {
result[item.id] = await item.toJSON();
}
io.to(socket.userID).emit("statusPageList", result);
return list;
}
async toJSON() {
return {
id: this.id,
slug: this.slug,
title: this.title,
description: this.description,
icon: this.getIcon(),
theme: this.theme,
published: !!this.published,
showTags: !!this.show_tags,
};
}
async toPublicJSON() {
return {
slug: this.slug,
title: this.title,
description: this.description,
icon: this.getIcon(),
theme: this.theme,
published: !!this.published,
showTags: !!this.show_tags,
};
}
static async slugToID(slug) {
return await R.getCell("SELECT id FROM status_page WHERE slug = ? ", [
slug
]);
}
getIcon() {
if (!this.icon) {
return "/icon.svg";
} else {
return this.icon;
}
}
}
module.exports = StatusPage;

View File

@@ -0,0 +1,67 @@
const NotificationProvider = require("./notification-provider");
const { DOWN, UP } = require("../../src/util");
const axios = require("axios");
class Alerta extends NotificationProvider {
name = "alerta";
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
let okMsg = "Sent Successfully.";
try {
let alertaUrl = `${notification.alertaApiEndpoint}`;
let config = {
headers: {
"Content-Type": "application/json;charset=UTF-8",
"Authorization": "Key " + notification.alertaapiKey,
}
};
let data = {
environment: notification.alertaEnvironment,
severity: "critical",
correlate: [],
service: [ "UptimeKuma" ],
value: "Timeout",
tags: [ "uptimekuma" ],
attributes: {},
origin: "uptimekuma",
type: "exceptionAlert",
};
if (heartbeatJSON == null) {
let postData = Object.assign({
event: "msg",
text: msg,
group: "uptimekuma-msg",
resource: "Message",
}, data);
await axios.post(alertaUrl, postData, config);
} else {
let datadup = Object.assign( {
correlate: ["service_up", "service_down"],
event: monitorJSON["type"],
group: "uptimekuma-" + monitorJSON["type"],
resource: monitorJSON["name"],
}, data );
if (heartbeatJSON["status"] == DOWN) {
datadup.severity = notification.alertaAlertState; // critical
datadup.text = "Service " + monitorJSON["type"] + " is down.";
await axios.post(alertaUrl, datadup, config);
} else if (heartbeatJSON["status"] == UP) {
datadup.severity = notification.alertaRecoverState; // cleaned
datadup.text = "Service " + monitorJSON["type"] + " is up.";
await axios.post(alertaUrl, datadup, config);
}
}
return okMsg;
} catch (error) {
this.throwGeneralAxiosError(error);
}
}
}
module.exports = Alerta;

View File

@@ -0,0 +1,42 @@
const NotificationProvider = require("./notification-provider");
const axios = require("axios");
class Gorush extends NotificationProvider {
name = "gorush";
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
let okMsg = "Sent Successfully.";
let platformMapping = {
"ios": 1,
"android": 2,
"huawei": 3,
};
try {
let data = {
"notifications": [
{
"tokens": [notification.gorushDeviceToken],
"platform": platformMapping[notification.gorushPlatform],
"message": msg,
// Optional
"title": notification.gorushTitle,
"priority": notification.gorushPriority,
"retry": parseInt(notification.gorushRetry) || 0,
"topic": notification.gorushTopic,
}
]
};
let config = {};
await axios.post(`${notification.gorushServerURL}/api/push`, data, config);
return okMsg;
} catch (error) {
this.throwGeneralAxiosError(error);
}
}
}
module.exports = Gorush;

View File

@@ -20,7 +20,7 @@ class Mattermost extends NotificationProvider {
return okMsg;
}
const mattermostChannel = notification.mattermostchannel;
const mattermostChannel = notification.mattermostchannel.toLowerCase();
const mattermostIconEmoji = notification.mattermosticonemo;
const mattermostIconUrl = notification.mattermosticonurl;

View File

@@ -9,36 +9,31 @@ class Pushover extends NotificationProvider {
let okMsg = "Sent Successfully.";
let pushoverlink = "https://api.pushover.net/1/messages.json";
let data = {
"message": "<b>Uptime Kuma Alert</b>\n\n<b>Message</b>:" + msg,
"user": notification.pushoveruserkey,
"token": notification.pushoverapptoken,
"sound": notification.pushoversounds,
"priority": notification.pushoverpriority,
"title": notification.pushovertitle,
"retry": "30",
"expire": "3600",
"html": 1,
};
if (notification.pushoverdevice) {
data.device = notification.pushoverdevice;
}
try {
if (heartbeatJSON == null) {
let data = {
"message": msg,
"user": notification.pushoveruserkey,
"token": notification.pushoverapptoken,
"sound": notification.pushoversounds,
"priority": notification.pushoverpriority,
"title": notification.pushovertitle,
"retry": "30",
"expire": "3600",
"html": 1,
};
await axios.post(pushoverlink, data);
return okMsg;
} else {
data.message += "\n<b>Time (UTC)</b>:" + heartbeatJSON["time"];
await axios.post(pushoverlink, data);
return okMsg;
}
let data = {
"message": "<b>Uptime Kuma Alert</b>\n\n<b>Message</b>:" + msg + "\n<b>Time (UTC)</b>:" + heartbeatJSON["time"],
"user": notification.pushoveruserkey,
"token": notification.pushoverapptoken,
"sound": notification.pushoversounds,
"priority": notification.pushoverpriority,
"title": notification.pushovertitle,
"retry": "30",
"expire": "3600",
"html": 1,
};
await axios.post(pushoverlink, data);
return okMsg;
} catch (error) {
this.throwGeneralAxiosError(error);
}

View File

@@ -0,0 +1,23 @@
const NotificationProvider = require("./notification-provider");
const axios = require("axios");
class TechulusPush extends NotificationProvider {
name = "PushByTechulus";
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
let okMsg = "Sent Successfully.";
try {
await axios.post(`https://push.techulus.com/api/v1/notify/${notification.pushAPIKey}`, {
"title": "Uptime-Kuma",
"body": msg,
})
return okMsg;
} catch (error) {
this.throwGeneralAxiosError(error)
}
}
}
module.exports = TechulusPush;

View File

@@ -12,6 +12,7 @@ const ClickSendSMS = require("./notification-providers/clicksendsms");
const Pushbullet = require("./notification-providers/pushbullet");
const Pushover = require("./notification-providers/pushover");
const Pushy = require("./notification-providers/pushy");
const TechulusPush = require("./notification-providers/techulus-push");
const RocketChat = require("./notification-providers/rocket-chat");
const Signal = require("./notification-providers/signal");
const Slack = require("./notification-providers/slack");
@@ -27,6 +28,8 @@ const SerwerSMS = require("./notification-providers/serwersms");
const Stackfield = require("./notification-providers/stackfield");
const WeCom = require("./notification-providers/wecom");
const GoogleChat = require("./notification-providers/google-chat");
const Gorush = require("./notification-providers/gorush");
const Alerta = require("./notification-providers/alerta");
class Notification {
@@ -55,6 +58,7 @@ class Notification {
new Pushbullet(),
new Pushover(),
new Pushy(),
new TechulusPush(),
new RocketChat(),
new Signal(),
new Slack(),
@@ -65,7 +69,9 @@ class Notification {
new SerwerSMS(),
new Stackfield(),
new WeCom(),
new GoogleChat()
new GoogleChat(),
new Gorush(),
new Alerta(),
];
for (let item of list) {

View File

@@ -86,6 +86,16 @@ class Prometheus {
}
}
remove() {
try {
monitor_cert_days_remaining.remove(this.monitorLabelValues);
monitor_cert_is_valid.remove(this.monitorLabelValues);
monitor_response_time.remove(this.monitorLabelValues);
monitor_status.remove(this.monitorLabelValues);
} catch (e) {
console.error(e);
}
}
}
module.exports = {

View File

@@ -6,6 +6,7 @@ const apicache = require("../modules/apicache");
const Monitor = require("../model/monitor");
const dayjs = require("dayjs");
const { UP, flipStatus, debug } = require("../../src/util");
const StatusPage = require("../model/status_page");
let router = express.Router();
let cache = apicache.middleware;
@@ -82,110 +83,80 @@ router.get("/api/push/:pushToken", async (request, response) => {
}
});
// Status Page Config
router.get("/api/status-page/config", async (_request, response) => {
// Status page config, incident, monitor list
router.get("/api/status-page/:slug", cache("5 minutes"), async (request, response) => {
allowDevAllOrigin(response);
let slug = request.params.slug;
let config = await getSettings("statusPage");
// Get Status Page
let statusPage = await R.findOne("status_page", " slug = ? ", [
slug
]);
if (! config.statusPageTheme) {
config.statusPageTheme = "light";
if (!statusPage) {
response.statusCode = 404;
response.json({
msg: "Not Found"
});
return;
}
if (! config.statusPagePublished) {
config.statusPagePublished = true;
}
if (! config.statusPageTags) {
config.statusPageTags = false;
}
if (! config.title) {
config.title = "Uptime Kuma";
}
response.json(config);
});
// Status Page - Get the current Incident
// Can fetch only if published
router.get("/api/status-page/incident", async (_, response) => {
allowDevAllOrigin(response);
try {
await checkPublished();
let incident = await R.findOne("incident", " pin = 1 AND active = 1");
// Incident
let incident = await R.findOne("incident", " pin = 1 AND active = 1 AND status_page_id = ? ", [
statusPage.id,
]);
if (incident) {
incident = incident.toPublicJSON();
}
// Public Group List
const publicGroupList = [];
const showTags = !!statusPage.show_tags;
debug("Show Tags???" + showTags);
const list = await R.find("group", " public = 1 AND status_page_id = ? ORDER BY weight ", [
statusPage.id
]);
for (let groupBean of list) {
let monitorGroup = await groupBean.toPublicJSON(showTags);
publicGroupList.push(monitorGroup);
}
// Response
response.json({
ok: true,
config: await statusPage.toPublicJSON(),
incident,
publicGroupList
});
} catch (error) {
send403(response, error.message);
}
});
// Status Page - Monitor List
// Can fetch only if published
router.get("/api/status-page/monitor-list", cache("5 minutes"), async (_request, response) => {
allowDevAllOrigin(response);
try {
await checkPublished();
const publicGroupList = [];
const tagsVisible = (await getSettings("statusPage")).statusPageTags;
const list = await R.find("group", " public = 1 ORDER BY weight ");
for (let groupBean of list) {
let monitorGroup = await groupBean.toPublicJSON();
if (tagsVisible) {
monitorGroup.monitorList = await Promise.all(monitorGroup.monitorList.map(async (monitor) => {
// Includes tags as an array in response, allows for tags to be displayed on public status page
const tags = await R.getAll(
`SELECT monitor_tag.monitor_id, monitor_tag.value, tag.name, tag.color
FROM monitor_tag
JOIN tag
ON monitor_tag.tag_id = tag.id
WHERE monitor_tag.monitor_id = ?`, [monitor.id]
);
return {
...monitor,
tags: tags
};
}));
}
publicGroupList.push(monitorGroup);
}
response.json(publicGroupList);
} catch (error) {
send403(response, error.message);
}
});
// Status Page Polling Data
// Can fetch only if published
router.get("/api/status-page/heartbeat", cache("5 minutes"), async (_request, response) => {
router.get("/api/status-page/heartbeat/:slug", cache("1 minutes"), async (request, response) => {
allowDevAllOrigin(response);
try {
await checkPublished();
let heartbeatList = {};
let uptimeList = {};
let slug = request.params.slug;
let statusPageID = await StatusPage.slugToID(slug);
let monitorIDList = await R.getCol(`
SELECT monitor_group.monitor_id FROM monitor_group, \`group\`
WHERE monitor_group.group_id = \`group\`.id
AND public = 1
`);
AND \`group\`.status_page_id = ?
`, [
statusPageID
]);
for (let monitorID of monitorIDList) {
let list = await R.getAll(`
@@ -214,22 +185,12 @@ router.get("/api/status-page/heartbeat", cache("5 minutes"), async (_request, re
}
});
async function checkPublished() {
if (! await isPublished()) {
throw new Error("The status page is not published");
}
}
/**
* Default is published
* @returns {Promise<boolean>}
*/
async function isPublished() {
const value = await setting("statusPagePublished");
if (value === null) {
return true;
}
return value;
return true;
}
function send403(res, msg = "") {

View File

@@ -132,6 +132,7 @@ const { sendNotificationList, sendHeartbeatList, sendImportantHeartbeatList, sen
const { statusPageSocketHandler } = require("./socket-handlers/status-page-socket-handler");
const databaseSocketHandler = require("./socket-handlers/database-socket-handler");
const TwoFA = require("./2fa");
const StatusPage = require("./model/status_page");
app.use(express.json());
@@ -200,8 +201,8 @@ exports.entryPage = "dashboard";
// Entry Page
app.get("/", async (_request, response) => {
if (exports.entryPage === "statusPage") {
response.redirect("/status");
if (exports.entryPage && exports.entryPage.startsWith("statusPage-")) {
response.redirect("/status/" + exports.entryPage.replace("statusPage-", ""));
} else {
response.redirect("/dashboard");
}
@@ -578,6 +579,9 @@ exports.entryPage = "dashboard";
throw new Error("Permission denied.");
}
// Reset Prometheus labels
monitorList[monitor.id]?.prometheus()?.remove();
bean.name = monitor.name;
bean.type = monitor.type;
bean.url = monitor.url;
@@ -1411,6 +1415,8 @@ async function afterLogin(socket, user) {
await sleep(500);
await StatusPage.sendStatusPageList(io, socket);
for (let monitorID in monitorList) {
await sendHeartbeatList(socket, monitorID);
}

View File

@@ -1,25 +1,36 @@
const { R } = require("redbean-node");
const { checkLogin, setSettings } = require("../util-server");
const { checkLogin, setSettings, setSetting } = require("../util-server");
const dayjs = require("dayjs");
const { debug } = 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 server = require("../server");
module.exports.statusPageSocketHandler = (socket) => {
// Post or edit incident
socket.on("postIncident", async (incident, callback) => {
socket.on("postIncident", async (slug, incident, callback) => {
try {
checkLogin(socket);
await R.exec("UPDATE incident SET pin = 0 ");
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 = ?", [
incident.id
incidentBean = await R.findOne("incident", " id = ? AND status_page_id = ? ", [
incident.id,
statusPageID
]);
}
@@ -31,6 +42,7 @@ module.exports.statusPageSocketHandler = (socket) => {
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());
@@ -52,11 +64,15 @@ module.exports.statusPageSocketHandler = (socket) => {
}
});
socket.on("unpinIncident", async (callback) => {
socket.on("unpinIncident", async (slug, callback) => {
try {
checkLogin(socket);
await R.exec("UPDATE incident SET pin = 0 WHERE pin = 1");
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,
@@ -71,13 +87,23 @@ module.exports.statusPageSocketHandler = (socket) => {
// Save Status Page
// imgDataUrl Only Accept PNG!
socket.on("saveStatusPage", async (config, imgDataUrl, publicGroupList, callback) => {
socket.on("saveStatusPage", async (slug, config, imgDataUrl, publicGroupList, callback) => {
try {
checkLogin(socket);
checkSlug(config.slug);
checkLogin(socket);
apicache.clear();
// Save Config
let statusPage = await R.findOne("status_page", " slug = ? ", [
slug
]);
if (!statusPage) {
throw new Error("No slug?");
}
const header = "data:image/png;base64,";
// Check logo format
@@ -88,16 +114,28 @@ module.exports.statusPageSocketHandler = (socket) => {
throw new Error("Only allowed PNG logo.");
}
const filename = `logo${statusPage.id}.png`;
// Convert to file
await ImageDataURI.outputFile(imgDataUrl, Database.uploadDir + "logo.png");
config.logo = "/upload/logo.png?t=" + Date.now();
await ImageDataURI.outputFile(imgDataUrl, Database.uploadDir + filename);
config.logo = `/upload/${filename}?t=` + Date.now();
} else {
config.icon = imgDataUrl;
}
// Save Config
await setSettings("statusPage", config);
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.modified_date = R.isoDateTime();
await R.store(statusPage);
// Save Public Group List
const groupIDList = [];
@@ -106,13 +144,15 @@ module.exports.statusPageSocketHandler = (socket) => {
for (let group of publicGroupList) {
let groupBean;
if (group.id) {
groupBean = await R.findOne("group", " id = ? AND public = 1 ", [
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++;
@@ -124,7 +164,6 @@ module.exports.statusPageSocketHandler = (socket) => {
]);
let monitorOrder = 1;
console.log(group.monitorList);
for (let monitor of group.monitorList) {
let relationBean = R.dispense("monitor_group");
@@ -141,7 +180,18 @@ module.exports.statusPageSocketHandler = (socket) => {
// Delete groups that not in the list
debug("Delete groups that not in the list");
const slots = groupIDList.map(() => "?").join(",");
await R.exec(`DELETE FROM \`group\` WHERE id NOT IN (${slots})`, groupIDList);
const data = [
...groupIDList,
statusPage.id
];
await R.exec(`DELETE FROM \`group\` WHERE id NOT IN (${slots}) AND status_page_id = ?`, data);
// 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");
}
callback({
ok: true,
@@ -149,7 +199,7 @@ module.exports.statusPageSocketHandler = (socket) => {
});
} catch (error) {
console.log(error);
console.error(error);
callback({
ok: false,
@@ -158,4 +208,115 @@ module.exports.statusPageSocketHandler = (socket) => {
}
});
// 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 = "light";
statusPage.icon = "";
await R.store(statusPage);
callback({
ok: true,
msg: "OK!"
});
} catch (error) {
console.error(error);
callback({
ok: false,
msg: error.message,
});
}
});
// Delete a status page
socket.on("deleteStatusPage", async (slug, callback) => {
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
*/
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");
}
}