diff --git a/.github/ISSUE_TEMPLATE/ask-for-help.yaml b/.github/ISSUE_TEMPLATE/ask-for-help.yaml index 3442e8b7..9c30b2dc 100644 --- a/.github/ISSUE_TEMPLATE/ask-for-help.yaml +++ b/.github/ISSUE_TEMPLATE/ask-for-help.yaml @@ -26,6 +26,12 @@ 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: false + attributes: + label: "📝 Error Message(s) or Log" - type: input id: uptime-kuma-version attributes: diff --git a/server/model/monitor.js b/server/model/monitor.js index 1e011c5a..44460819 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") { @@ -1247,7 +1245,7 @@ class Monitor extends BeanModel { if (notificationList.length > 0) { - let row = await R.getRow("SELECT * FROM notification_sent_history WHERE type = ? AND monitor_id = ? AND days = ?", [ + let row = await R.getRow("SELECT * FROM notification_sent_history WHERE type = ? AND monitor_id = ? AND days <= ?", [ "certificate", this.id, targetDays, diff --git a/server/notification-providers/opsgenie.js b/server/notification-providers/opsgenie.js new file mode 100644 index 00000000..16bf9fc6 --- /dev/null +++ b/server/notification-providers/opsgenie.js @@ -0,0 +1,97 @@ +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"; +let okMsg = "Sent Successfully."; + +class Opsgenie extends NotificationProvider { + + name = "Opsgenie"; + + /** + * @inheritdoc + */ + async send(notification, msg, monitorJSON = null, heartbeatJSON = null) { + let opsgenieAlertsUrl; + let priority = (notification.opsgeniePriority == "") ? 3 : notification.opsgeniePriority; + const textMsg = "Uptime Kuma Alert"; + + try { + switch (notification.opsgenieRegion) { + case "US": + opsgenieAlertsUrl = opsgenieAlertsUrlUS; + break; + case "EU": + opsgenieAlertsUrl = opsgenieAlertsUrlEU; + break; + default: + opsgenieAlertsUrl = opsgenieAlertsUrlUS; + } + + if (heartbeatJSON == null) { + let notificationTestAlias = "uptime-kuma-notification-test"; + let data = { + "message": msg, + "alias": notificationTestAlias, + "source": "Uptime Kuma", + "priority": "P5" + }; + + return this.post(notification, opsgenieAlertsUrl, data); + } + + if (heartbeatJSON.status === DOWN) { + let data = { + "message": monitorJSON ? textMsg + `: ${monitorJSON.name}` : textMsg, + "alias": monitorJSON.name, + "description": msg, + "source": "Uptime Kuma", + "priority": `P${priority}` + }; + + return this.post(notification, opsgenieAlertsUrl, data); + } + + if (heartbeatJSON.status === UP) { + let opsgenieAlertsCloseUrl = `${opsgenieAlertsUrl}/${encodeURIComponent(monitorJSON.name)}/close?identifierType=alias`; + let data = { + "source": "Uptime Kuma", + }; + + return this.post(notification, opsgenieAlertsCloseUrl, data); + } + } catch (error) { + this.throwGeneralAxiosError(error); + } + } + + /** + * + * @param {BeanModel} notification + * @param {string} url Request url + * @param {Object} data Request body + * @returns {Promise} + */ + async post(notification, url, data) { + let config = { + headers: { + "Content-Type": "application/json", + "Authorization": `GenieKey ${notification.opsgenieApiKey}`, + } + }; + + 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; + } +} + +module.exports = Opsgenie; diff --git a/server/notification-providers/twilio.js b/server/notification-providers/twilio.js new file mode 100644 index 00000000..8f4db040 --- /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 1897f5cc..9bfa371d 100644 --- a/server/notification.js +++ b/server/notification.js @@ -23,6 +23,7 @@ const Mattermost = require("./notification-providers/mattermost"); const Ntfy = require("./notification-providers/ntfy"); const Octopush = require("./notification-providers/octopush"); const OneBot = require("./notification-providers/onebot"); +const Opsgenie = require("./notification-providers/opsgenie"); const PagerDuty = require("./notification-providers/pagerduty"); const PagerTree = require("./notification-providers/pagertree"); const PromoSMS = require("./notification-providers/promosms"); @@ -41,6 +42,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"); @@ -83,6 +85,7 @@ class Notification { new Ntfy(), new Octopush(), new OneBot(), + new Opsgenie(), new PagerDuty(), new PagerTree(), new PromoSMS(), @@ -103,6 +106,7 @@ class Notification { new Teams(), new TechulusPush(), new Telegram(), + new Twilio(), new Splunk(), new Webhook(), new WeCom(), diff --git a/server/routers/api-router.js b/server/routers/api-router.js index a36159ca..0b4982db 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; @@ -224,7 +228,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 +239,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 diff --git a/server/uptime-kuma-server.js b/server/uptime-kuma-server.js index 3dd7ba93..d28f00a9 100644 --- a/server/uptime-kuma-server.js +++ b/server/uptime-kuma-server.js @@ -74,6 +74,7 @@ class UptimeKumaServer { // SSL const sslKey = args["ssl-key"] || process.env.UPTIME_KUMA_SSL_KEY || process.env.SSL_KEY || undefined; const sslCert = args["ssl-cert"] || process.env.UPTIME_KUMA_SSL_CERT || process.env.SSL_CERT || undefined; + const sslKeyPassphrase = args["ssl-key-passphrase"] || process.env.UPTIME_KUMA_SSL_KEY_PASSPHRASE || process.env.SSL_KEY_PASSPHRASE || undefined; log.info("server", "Creating express and socket.io instance"); this.app = express(); @@ -81,7 +82,8 @@ class UptimeKumaServer { log.info("server", "Server Type: HTTPS"); this.httpServer = https.createServer({ key: fs.readFileSync(sslKey), - cert: fs.readFileSync(sslCert) + cert: fs.readFileSync(sslCert), + passphrase: sslKeyPassphrase, }, this.app); } else { log.info("server", "Server Type: HTTP"); diff --git a/server/util-server.js b/server/util-server.js index 01e66ee2..47f6bb48 100644 --- a/server/util-server.js +++ b/server/util-server.js @@ -322,21 +322,28 @@ 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(); - }); + } else { + if (Array.isArray(res)) { + resolve("Rows: " + res.length); + } else { + resolve("No Error, but the result is not an array. Type: " + typeof res); + } + } + connection.destroy(); + }); }); }; diff --git a/src/components/APIKeyDialog.vue b/src/components/APIKeyDialog.vue index 745efd4a..47120376 100644 --- a/src/components/APIKeyDialog.vue +++ b/src/components/APIKeyDialog.vue @@ -159,6 +159,16 @@ export default { } }); }, + + /** Clear Form inputs */ + clearForm() { + this.key = { + name: "", + expires: this.minDate, + active: 1, + }; + this.noExpire = false; + }, } }; diff --git a/src/components/CopyableInput.vue b/src/components/CopyableInput.vue index 2e1dee76..943193f4 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(); }); } } diff --git a/src/components/NotificationDialog.vue b/src/components/NotificationDialog.vue index c3851b56..7454931e 100644 --- a/src/components/NotificationDialog.vue +++ b/src/components/NotificationDialog.vue @@ -129,6 +129,7 @@ export default { "ntfy": "Ntfy", "octopush": "Octopush", "OneBot": "OneBot", + "Opsgenie": "Opsgenie", "PagerDuty": "PagerDuty", "pushbullet": "Pushbullet", "PushByTechulus": "Push by Techulus", @@ -143,6 +144,7 @@ export default { "stackfield": "Stackfield", "teams": "Microsoft Teams", "telegram": "Telegram", + "twilio": "Twilio", "Splunk": "Splunk", "webhook": "Webhook", "GoAlert": "GoAlert", diff --git a/src/components/notifications/Opsgenie.vue b/src/components/notifications/Opsgenie.vue new file mode 100644 index 00000000..3f07d052 --- /dev/null +++ b/src/components/notifications/Opsgenie.vue @@ -0,0 +1,36 @@ + + + diff --git a/src/components/notifications/Twilio.vue b/src/components/notifications/Twilio.vue new file mode 100644 index 00000000..3edf1e3d --- /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 ed9dde0f..7b5e6b6c 100644 --- a/src/components/notifications/index.js +++ b/src/components/notifications/index.js @@ -21,6 +21,7 @@ import Mattermost from "./Mattermost.vue"; import Ntfy from "./Ntfy.vue"; import Octopush from "./Octopush.vue"; import OneBot from "./OneBot.vue"; +import Opsgenie from "./Opsgenie.vue"; import PagerDuty from "./PagerDuty.vue"; import PagerTree from "./PagerTree.vue"; import PromoSMS from "./PromoSMS.vue"; @@ -41,6 +42,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"; @@ -76,6 +78,7 @@ const NotificationFormList = { "ntfy": Ntfy, "octopush": Octopush, "OneBot": OneBot, + "Opsgenie": Opsgenie, "PagerDuty": PagerDuty, "PagerTree": PagerTree, "promosms": PromoSMS, @@ -95,6 +98,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 7db92e49..365c6598 100644 --- a/src/lang/en.json +++ b/src/lang/en.json @@ -174,6 +174,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", @@ -708,5 +709,9 @@ "lunaseaDeviceID": "Device ID", "lunaseaUserID": "User ID", "AuthenticationMethod": "Authentication Method", - "UsernameAndPassword": "Username and Password" + "UsernameAndPassword": "Username and Password", + "twilioAccountSID": "Account SID", + "twilioAuthToken": "Auth Token", + "twilioFromNumber": "From Number", + "twilioToNumber": "To Number" } diff --git a/src/pages/EditMonitor.vue b/src/pages/EditMonitor.vue index 60af4933..24b35ca2 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) { diff --git a/src/pages/StatusPage.vue b/src/pages/StatusPage.vue index edf32561..b202be30 100644 --- a/src/pages/StatusPage.vue +++ b/src/pages/StatusPage.vue @@ -306,6 +306,11 @@

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

+ +
+
{{ $t("Last Updated") }}:
+
{{ $tc("statusPageRefreshIn", [ updateCountdownText]) }}
+
@@ -322,6 +327,7 @@