From 193a273557ea8f5d713029d500500cc3b8df496e Mon Sep 17 00:00:00 2001 From: Nelson Chan Date: Fri, 3 Mar 2023 08:25:41 +0800 Subject: [PATCH 01/17] Feat: Add status page countdown to refresh --- src/pages/StatusPage.vue | 42 ++++++++++++++++++++++++++++++++++++++-- 1 file changed, 40 insertions(+), 2 deletions(-) diff --git a/src/pages/StatusPage.vue b/src/pages/StatusPage.vue index edf32561b..b02bf8a7b 100644 --- a/src/pages/StatusPage.vue +++ b/src/pages/StatusPage.vue @@ -306,6 +306,11 @@

{{ $t("Powered by") }} {{ $t("Uptime Kuma" ) }}

+ +
+
{{ $t("last update") }}:
+
{{ $t("refresh in") }}: {{ updateCountdownText }}
+
@@ -322,6 +327,7 @@ From d9558833fcf12a89061602b537e9c3641dd6a95b Mon Sep 17 00:00:00 2001 From: "niclas.koegl" Date: Tue, 21 Mar 2023 19:45:44 +0100 Subject: [PATCH 06/17] Fix linting --- server/notification-providers/opsgenie.js | 38 +++++++++++------------ server/notification.js | 2 +- src/components/notifications/Opsgenie.vue | 12 +++---- 3 files changed, 26 insertions(+), 26 deletions(-) diff --git a/server/notification-providers/opsgenie.js b/server/notification-providers/opsgenie.js index 3ccbb756c..16bf9fc60 100644 --- a/server/notification-providers/opsgenie.js +++ b/server/notification-providers/opsgenie.js @@ -2,14 +2,14 @@ const NotificationProvider = require("./notification-provider"); const axios = require("axios"); const { UP, DOWN } = require("../../src/util"); -const opsgenieAlertsUrlEU = "https://api.eu.opsgenie.com/v2/alerts" -const opsgenieAlertsUrlUS = "https://api.opsgenie.com/v2/alerts" +const opsgenieAlertsUrlEU = "https://api.eu.opsgenie.com/v2/alerts"; +const opsgenieAlertsUrlUS = "https://api.opsgenie.com/v2/alerts"; let okMsg = "Sent Successfully."; class Opsgenie extends NotificationProvider { - + name = "Opsgenie"; - + /** * @inheritdoc */ @@ -17,7 +17,7 @@ class Opsgenie extends NotificationProvider { let opsgenieAlertsUrl; let priority = (notification.opsgeniePriority == "") ? 3 : notification.opsgeniePriority; const textMsg = "Uptime Kuma Alert"; - + try { switch (notification.opsgenieRegion) { case "US": @@ -28,7 +28,7 @@ class Opsgenie extends NotificationProvider { break; default: opsgenieAlertsUrl = opsgenieAlertsUrlUS; - }; + } if (heartbeatJSON == null) { let notificationTestAlias = "uptime-kuma-notification-test"; @@ -38,12 +38,12 @@ class Opsgenie extends NotificationProvider { "source": "Uptime Kuma", "priority": "P5" }; - - return this.post(notification, opsgenieAlertsUrl, data) - }; + + return this.post(notification, opsgenieAlertsUrl, data); + } if (heartbeatJSON.status === DOWN) { - let data = { + let data = { "message": monitorJSON ? textMsg + `: ${monitorJSON.name}` : textMsg, "alias": monitorJSON.name, "description": msg, @@ -51,24 +51,24 @@ class Opsgenie extends NotificationProvider { "priority": `P${priority}` }; - return this.post(notification, opsgenieAlertsUrl, data) - }; + return this.post(notification, opsgenieAlertsUrl, data); + } if (heartbeatJSON.status === UP) { let opsgenieAlertsCloseUrl = `${opsgenieAlertsUrl}/${encodeURIComponent(monitorJSON.name)}/close?identifierType=alias`; - let data = { + let data = { "source": "Uptime Kuma", }; - return this.post(notification, opsgenieAlertsCloseUrl, data) - }; + return this.post(notification, opsgenieAlertsCloseUrl, data); + } } catch (error) { this.throwGeneralAxiosError(error); } } /** - * + * * @param {BeanModel} notification * @param {string} url Request url * @param {Object} data Request body @@ -85,12 +85,12 @@ class Opsgenie extends NotificationProvider { let res = await axios.post(url, data, config); if (res.status == null) { return "Opsgenie notification failed with invalid response!"; - }; + } if (res.status < 200 || res.status >= 300) { return `Opsgenie notification failed with status code ${res.status}`; - }; + } - return okMsg + return okMsg; } } diff --git a/server/notification.js b/server/notification.js index 5d110f215..f69e0a384 100644 --- a/server/notification.js +++ b/server/notification.js @@ -84,7 +84,7 @@ class Notification { new Ntfy(), new Octopush(), new OneBot(), - new Opsgenie(), + new Opsgenie(), new PagerDuty(), new PagerTree(), new PromoSMS(), diff --git a/src/components/notifications/Opsgenie.vue b/src/components/notifications/Opsgenie.vue index 3dc96bc87..3f07d0528 100644 --- a/src/components/notifications/Opsgenie.vue +++ b/src/components/notifications/Opsgenie.vue @@ -18,12 +18,12 @@ -
- *{{ $t("Required") }} - - https://docs.opsgenie.com/docs/alert-api - -
+
+ *{{ $t("Required") }} + + https://docs.opsgenie.com/docs/alert-api + +
From 35f300c8ebd56b916aadb23c6882113842a43029 Mon Sep 17 00:00:00 2001 From: Louis Lam Date: Thu, 23 Mar 2023 17:17:53 +0800 Subject: [PATCH 08/17] Improve and reuse language keys --- src/lang/en.json | 1 + src/pages/StatusPage.vue | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/lang/en.json b/src/lang/en.json index 8f28c8e3a..2da2e4442 100644 --- a/src/lang/en.json +++ b/src/lang/en.json @@ -173,6 +173,7 @@ "Avg. Response": "Avg. Response", "Entry Page": "Entry Page", "statusPageNothing": "Nothing here, please add a group or a monitor.", + "statusPageRefreshIn": "Refresh in: {0}", "No Services": "No Services", "All Systems Operational": "All Systems Operational", "Partially Degraded Service": "Partially Degraded Service", diff --git a/src/pages/StatusPage.vue b/src/pages/StatusPage.vue index b02bf8a7b..b202be305 100644 --- a/src/pages/StatusPage.vue +++ b/src/pages/StatusPage.vue @@ -308,8 +308,8 @@

-
{{ $t("last update") }}:
-
{{ $t("refresh in") }}: {{ updateCountdownText }}
+
{{ $t("Last Updated") }}:
+
{{ $tc("statusPageRefreshIn", [ updateCountdownText]) }}
From 7330db35631a5510af438f2793ce71df00c0f42f Mon Sep 17 00:00:00 2001 From: Louis Lam Date: Fri, 24 Mar 2023 16:08:30 +0800 Subject: [PATCH 09/17] Improve error handling of mysqlQuery and return row count as result --- server/model/monitor.js | 4 +--- server/util-server.js | 29 +++++++++++++++++++---------- 2 files changed, 20 insertions(+), 13 deletions(-) diff --git a/server/model/monitor.js b/server/model/monitor.js index 1e011c5a0..1ffae58e5 100644 --- a/server/model/monitor.js +++ b/server/model/monitor.js @@ -637,9 +637,7 @@ class Monitor extends BeanModel { } else if (this.type === "mysql") { let startTime = dayjs().valueOf(); - await mysqlQuery(this.databaseConnectionString, this.databaseQuery); - - bean.msg = ""; + bean.msg = await mysqlQuery(this.databaseConnectionString, this.databaseQuery); bean.status = UP; bean.ping = dayjs().valueOf() - startTime; } else if (this.type === "mongodb") { diff --git a/server/util-server.js b/server/util-server.js index 01e66ee26..1cdf33de5 100644 --- a/server/util-server.js +++ b/server/util-server.js @@ -322,21 +322,30 @@ exports.postgresQuery = function (connectionString, query) { * Run a query on MySQL/MariaDB * @param {string} connectionString The database connection string * @param {string} query The query to validate the database with - * @returns {Promise<(string[]|Object[]|Object)>} + * @returns {Promise<(string)>} */ exports.mysqlQuery = function (connectionString, query) { return new Promise((resolve, reject) => { const connection = mysql.createConnection(connectionString); - connection.promise().query(query) - .then(res => { - resolve(res); - }) - .catch(err => { + + connection.on("error", (err) => { + reject(err); + }); + + connection.query(query, (err, res) => { + if (err) { reject(err); - }) - .finally(() => { - connection.destroy(); - }); + } + + // Check if res is an array + if (Array.isArray(res)) { + resolve("Rows: " + res.length); + } else { + resolve("No Error, but the result is not an array. Type: " + typeof res); + } + + connection.destroy(); + }); }); }; From f4ee5271afeb44cac945d0f07e9bc0413956ee19 Mon Sep 17 00:00:00 2001 From: Louis Lam Date: Fri, 24 Mar 2023 16:24:00 +0800 Subject: [PATCH 10/17] Improve error handling of mysqlQuery and return row count as result --- server/util-server.js | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/server/util-server.js b/server/util-server.js index 1cdf33de5..47f6bb48c 100644 --- a/server/util-server.js +++ b/server/util-server.js @@ -335,15 +335,13 @@ exports.mysqlQuery = function (connectionString, query) { connection.query(query, (err, res) => { if (err) { reject(err); - } - - // Check if res is an array - if (Array.isArray(res)) { - resolve("Rows: " + res.length); } else { - resolve("No Error, but the result is not an array. Type: " + typeof res); + if (Array.isArray(res)) { + resolve("Rows: " + res.length); + } else { + resolve("No Error, but the result is not an array. Type: " + typeof res); + } } - connection.destroy(); }); }); From 70572af1affd1cc7e6d203560d73a8b9a0479334 Mon Sep 17 00:00:00 2001 From: Louis Lam Date: Fri, 24 Mar 2023 18:39:52 +0800 Subject: [PATCH 11/17] Fix #2969 --- src/pages/EditMonitor.vue | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/pages/EditMonitor.vue b/src/pages/EditMonitor.vue index 60af4933f..24b35ca20 100644 --- a/src/pages/EditMonitor.vue +++ b/src/pages/EditMonitor.vue @@ -944,6 +944,14 @@ message HealthCheckResponse { } else if (this.isEdit || this.isClone) { this.$root.getSocket().emit("getMonitor", this.$route.params.id, (res) => { if (res.ok) { + + if (this.isClone) { + // Reset push token for cloned monitors + if (res.monitor.type === "push") { + res.monitor.pushToken = undefined; + } + } + this.monitor = res.monitor; if (this.isClone) { From a2014278b89a4e8178588d475ea3bb9180eee68f Mon Sep 17 00:00:00 2001 From: Louis Lam Date: Fri, 24 Mar 2023 19:16:12 +0800 Subject: [PATCH 12/17] Fix #2969 --- src/components/CopyableInput.vue | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/src/components/CopyableInput.vue b/src/components/CopyableInput.vue index 2e1dee766..943193f4d 100644 --- a/src/components/CopyableInput.vue +++ b/src/components/CopyableInput.vue @@ -13,6 +13,9 @@ :disabled="disabled" > + + + @@ -111,24 +114,19 @@ export default { }, 3000); // navigator clipboard api needs a secure context (https) + // For http, use the text area method (else part) if (navigator.clipboard && window.isSecureContext) { // navigator clipboard api method' return navigator.clipboard.writeText(textToCopy); } else { // text area method - let textArea = document.createElement("textarea"); + let textArea = this.$refs.hiddenTextarea; textArea.value = textToCopy; - // make the textarea out of viewport - textArea.style.position = "fixed"; - textArea.style.left = "-999999px"; - textArea.style.top = "-999999px"; - document.body.appendChild(textArea); textArea.focus(); textArea.select(); return new Promise((res, rej) => { // here the magic happens document.execCommand("copy") ? res() : rej(); - textArea.remove(); }); } } From bf525371d9669bf62faf3ec2521bc69f52129902 Mon Sep 17 00:00:00 2001 From: Nelson Chan Date: Fri, 24 Mar 2023 22:42:50 +0800 Subject: [PATCH 13/17] Fix: Apply toPrecision as last step --- server/routers/api-router.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/server/routers/api-router.js b/server/routers/api-router.js index a36159cae..d2b32600b 100644 --- a/server/routers/api-router.js +++ b/server/routers/api-router.js @@ -224,7 +224,7 @@ router.get("/api/badge/:id/uptime/:duration?", cache("5 minutes"), async (reques ); // limit the displayed uptime percentage to four (two, when displayed as percent) decimal digits - const cleanUptime = parseFloat(uptime.toPrecision(4)); + const cleanUptime = (uptime * 100).toPrecision(4); // use a given, custom color or calculate one based on the uptime value badgeValues.color = color ?? percentageToColor(uptime); @@ -235,7 +235,7 @@ router.get("/api/badge/:id/uptime/:duration?", cache("5 minutes"), async (reques labelPrefix, label ?? `Uptime (${requestedDuration}${labelSuffix})`, ]); - badgeValues.message = filterAndJoin([ prefix, `${cleanUptime * 100}`, suffix ]); + badgeValues.message = filterAndJoin([ prefix, cleanUptime, suffix ]); } // build the SVG based on given values From 4f05912276e106cc9e708e230a8313b51c3cd1ee Mon Sep 17 00:00:00 2001 From: Nelson Chan Date: Sat, 25 Mar 2023 02:44:15 +0800 Subject: [PATCH 14/17] Fix: Allow status badge with empty label --- server/routers/api-router.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/server/routers/api-router.js b/server/routers/api-router.js index a36159cae..84c3d3f38 100644 --- a/server/routers/api-router.js +++ b/server/routers/api-router.js @@ -147,7 +147,11 @@ router.get("/api/badge/:id/status", cache("5 minutes"), async (request, response const heartbeat = await Monitor.getPreviousHeartbeat(requestedMonitorId); const state = overrideValue !== undefined ? overrideValue : heartbeat.status; - badgeValues.label = label ?? "Status"; + if (label === undefined) { + badgeValues.label = "Status"; + } else { + badgeValues.label = label; + } switch (state) { case DOWN: badgeValues.color = downColor; From 86579d245f169c2b0ddc6ce254d048666b95ca2f Mon Sep 17 00:00:00 2001 From: Louis Lam Date: Sat, 25 Mar 2023 23:06:23 +0800 Subject: [PATCH 15/17] Update ask-for-help.yaml --- .github/ISSUE_TEMPLATE/ask-for-help.yaml | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/.github/ISSUE_TEMPLATE/ask-for-help.yaml b/.github/ISSUE_TEMPLATE/ask-for-help.yaml index 3442e8b73..fe9339af8 100644 --- a/.github/ISSUE_TEMPLATE/ask-for-help.yaml +++ b/.github/ISSUE_TEMPLATE/ask-for-help.yaml @@ -26,6 +26,13 @@ body: label: "📝 Describe your problem" description: "Please walk us through it step by step." placeholder: "Describe what are you asking for..." + - type: textarea + id: error-msg + validations: + required: true + attributes: + label: "📝 Error Message(s) or Log" + placeholder: "" - type: input id: uptime-kuma-version attributes: From 5266e713e61bfd67c2c46a8f34fc8165562d581b Mon Sep 17 00:00:00 2001 From: Louis Lam Date: Sat, 25 Mar 2023 23:06:51 +0800 Subject: [PATCH 16/17] Update ask-for-help.yaml --- .github/ISSUE_TEMPLATE/ask-for-help.yaml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/ask-for-help.yaml b/.github/ISSUE_TEMPLATE/ask-for-help.yaml index fe9339af8..9c30b2dc8 100644 --- a/.github/ISSUE_TEMPLATE/ask-for-help.yaml +++ b/.github/ISSUE_TEMPLATE/ask-for-help.yaml @@ -29,10 +29,9 @@ body: - type: textarea id: error-msg validations: - required: true + required: false attributes: label: "📝 Error Message(s) or Log" - placeholder: "" - type: input id: uptime-kuma-version attributes: From b64c835ceed723960b90c81b33141164bd1405bf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Faruk=20Gen=C3=A7?= Date: Sat, 25 Mar 2023 19:56:01 +0300 Subject: [PATCH 17/17] Add Twilio Sms Notification Provider --- server/notification-providers/twilio.js | 41 +++++++++++++++++++++++++ server/notification.js | 2 ++ src/components/NotificationDialog.vue | 1 + src/components/notifications/Twilio.vue | 27 ++++++++++++++++ src/components/notifications/index.js | 2 ++ src/lang/en.json | 6 +++- 6 files changed, 78 insertions(+), 1 deletion(-) create mode 100644 server/notification-providers/twilio.js create mode 100644 src/components/notifications/Twilio.vue diff --git a/server/notification-providers/twilio.js b/server/notification-providers/twilio.js new file mode 100644 index 000000000..8f4db0404 --- /dev/null +++ b/server/notification-providers/twilio.js @@ -0,0 +1,41 @@ +const NotificationProvider = require("./notification-provider"); +const axios = require("axios"); + +class Twilio extends NotificationProvider { + + name = "twilio"; + + async send(notification, msg, monitorJSON = null, heartbeatJSON = null) { + + let okMsg = "Sent Successfully."; + + let accountSID = notification.twilioAccountSID; + let authToken = notification.twilioAuthToken; + + try { + + let config = { + headers: { + "Content-Type": "application/x-www-form-urlencoded;charset=utf-8", + "Authorization": "Basic " + Buffer.from(accountSID + ":" + authToken).toString("base64"), + } + }; + + let data = new URLSearchParams(); + data.append("To", notification.twilioToNumber); + data.append("From", notification.twilioFromNumber); + data.append("Body", msg); + + let url = "https://api.twilio.com/2010-04-01/Accounts/" + accountSID + "/Messages.json"; + + await axios.post(url, data, config); + + return okMsg; + } catch (error) { + this.throwGeneralAxiosError(error); + } + } + +} + +module.exports = Twilio; diff --git a/server/notification.js b/server/notification.js index 1897f5cc0..68f675e94 100644 --- a/server/notification.js +++ b/server/notification.js @@ -41,6 +41,7 @@ const Stackfield = require("./notification-providers/stackfield"); const Teams = require("./notification-providers/teams"); const TechulusPush = require("./notification-providers/techulus-push"); const Telegram = require("./notification-providers/telegram"); +const Twilio = require("./notification-providers/twilio"); const Splunk = require("./notification-providers/splunk"); const Webhook = require("./notification-providers/webhook"); const WeCom = require("./notification-providers/wecom"); @@ -103,6 +104,7 @@ class Notification { new Teams(), new TechulusPush(), new Telegram(), + new Twilio(), new Splunk(), new Webhook(), new WeCom(), diff --git a/src/components/NotificationDialog.vue b/src/components/NotificationDialog.vue index c3851b568..557e75fc4 100644 --- a/src/components/NotificationDialog.vue +++ b/src/components/NotificationDialog.vue @@ -143,6 +143,7 @@ export default { "stackfield": "Stackfield", "teams": "Microsoft Teams", "telegram": "Telegram", + "twilio": "Twilio", "Splunk": "Splunk", "webhook": "Webhook", "GoAlert": "GoAlert", diff --git a/src/components/notifications/Twilio.vue b/src/components/notifications/Twilio.vue new file mode 100644 index 000000000..3edf1e3df --- /dev/null +++ b/src/components/notifications/Twilio.vue @@ -0,0 +1,27 @@ + diff --git a/src/components/notifications/index.js b/src/components/notifications/index.js index ed9dde0f1..8017b622e 100644 --- a/src/components/notifications/index.js +++ b/src/components/notifications/index.js @@ -41,6 +41,7 @@ import STMP from "./SMTP.vue"; import Teams from "./Teams.vue"; import TechulusPush from "./TechulusPush.vue"; import Telegram from "./Telegram.vue"; +import Twilio from "./Twilio.vue"; import Webhook from "./Webhook.vue"; import WeCom from "./WeCom.vue"; import GoAlert from "./GoAlert.vue"; @@ -95,6 +96,7 @@ const NotificationFormList = { "stackfield": Stackfield, "teams": Teams, "telegram": Telegram, + "twilio": Twilio, "Splunk": Splunk, "webhook": Webhook, "WeCom": WeCom, diff --git a/src/lang/en.json b/src/lang/en.json index 0c9fc3840..e7656c474 100644 --- a/src/lang/en.json +++ b/src/lang/en.json @@ -707,5 +707,9 @@ "wayToGetPagerTreeIntegrationURL": "After creating the Uptime Kuma integration in PagerTree, copy the Endpoint. See full details {0}", "lunaseaTarget": "Target", "lunaseaDeviceID": "Device ID", - "lunaseaUserID": "User ID" + "lunaseaUserID": "User ID", + "twilioAccountSID": "Account SID", + "twilioAuthToken": "Auth Token", + "twilioFromNumber": "From Number", + "twilioToNumber": "To Number" }