diff --git a/.devcontainer/README.md b/.devcontainer/README.md new file mode 100644 index 00000000..4a34b211 --- /dev/null +++ b/.devcontainer/README.md @@ -0,0 +1,28 @@ +# Codespaces + +You can modifiy Uptime Kuma in your browser without setting up a local development. + +![image](https://github.com/louislam/uptime-kuma/assets/1336778/31d9f06d-dd0b-4405-8e0d-a96586ee4595) + +1. Click `Code` -> `Create codespace on master` +2. Wait a few minutes until you see there are two exposed ports +3. Go to the `3000` url, see if it is working + +![image](https://github.com/louislam/uptime-kuma/assets/1336778/909b2eb4-4c5e-44e4-ac26-6d20ed856e7f) + +## Frontend + +Since the frontend is using [Vite.js](https://vitejs.dev/), all changes in this area will be hot-reloaded. +You don't need to restart the frontend, unless you try to add a new frontend dependency. + +## Backend + +The backend does not automatically hot-reload. +You will need to restart the backend after changing something using these steps: + +1. Click `Terminal` +2. Click `Codespaces: server-dev` in the right panel +3. Press `Ctrl + C` to stop the server +4. Press `Up` to run `npm run start-server-dev` + +![image](https://github.com/louislam/uptime-kuma/assets/1336778/e0c0a350-fe46-4588-9f37-e053c85834d1) diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json new file mode 100644 index 00000000..5b3ceabc --- /dev/null +++ b/.devcontainer/devcontainer.json @@ -0,0 +1,22 @@ +{ + "image": "mcr.microsoft.com/devcontainers/javascript-node:dev-18-bookworm", + "features": { + "ghcr.io/devcontainers/features/github-cli:1": {} + }, + "updateContentCommand": "npm ci", + "postCreateCommand": "", + "postAttachCommand": { + "frontend-dev": "npm run start-frontend-devcontainer", + "server-dev": "npm run start-server-dev", + "open-port": "gh codespace ports visibility 3001:public -c $CODESPACE_NAME" + }, + "customizations": { + "vscode": { + "extensions": [ + "streetsidesoftware.code-spell-checker", + "dbaeumer.vscode-eslint" + ] + } + }, + "forwardPorts": [3000, 3001] +} diff --git a/.github/ISSUE_TEMPLATE/ask-for-help.yaml b/.github/ISSUE_TEMPLATE/ask-for-help.yaml index 9c30b2dc..c082b2e3 100644 --- a/.github/ISSUE_TEMPLATE/ask-for-help.yaml +++ b/.github/ISSUE_TEMPLATE/ask-for-help.yaml @@ -44,7 +44,7 @@ body: id: operating-system attributes: label: "💻 Operating System and Arch" - description: "Which OS is your server/device running on?" + description: "Which OS is your server/device running on? (For Replit, please do not report this bug)" placeholder: "Ex. Ubuntu 20.04 x86" validations: required: true @@ -52,7 +52,7 @@ body: id: browser-vendor attributes: label: "🌐 Browser" - description: "Which browser are you running on?" + description: "Which browser are you running on? (For Replit, please do not report this bug)" placeholder: "Ex. Google Chrome 95.0.4638.69" validations: required: true diff --git a/README.md b/README.md index c7aa4150..0e41652d 100644 --- a/README.md +++ b/README.md @@ -54,7 +54,7 @@ Requirements: - ✅ Major Linux distros such as Debian, Ubuntu, CentOS, Fedora and ArchLinux etc. - ✅ Windows 10 (x64), Windows Server 2012 R2 (x64) or higher - ❌ Replit / Heroku -- [Node.js](https://nodejs.org/en/download/) 14 / 16 / 18 (20 is not supported) +- [Node.js](https://nodejs.org/en/download/) 14 / 16 / 18 / 20.4 - [npm](https://docs.npmjs.com/cli/) >= 7 - [Git](https://git-scm.com/downloads) - [pm2](https://pm2.keymetrics.io/) - For running Uptime Kuma in the background diff --git a/config/vite.config.js b/config/vite.config.js index 6e9ebbde..11c61006 100644 --- a/config/vite.config.js +++ b/config/vite.config.js @@ -3,6 +3,7 @@ import vue from "@vitejs/plugin-vue"; import { defineConfig } from "vite"; import visualizer from "rollup-plugin-visualizer"; import viteCompression from "vite-plugin-compression"; +import commonjs from "vite-plugin-commonjs"; const postCssScss = require("postcss-scss"); const postcssRTLCSS = require("postcss-rtlcss"); @@ -16,8 +17,12 @@ export default defineConfig({ }, define: { "FRONTEND_VERSION": JSON.stringify(process.env.npm_package_version), + "DEVCONTAINER": JSON.stringify(process.env.DEVCONTAINER), + "GITHUB_CODESPACES_PORT_FORWARDING_DOMAIN": JSON.stringify(process.env.GITHUB_CODESPACES_PORT_FORWARDING_DOMAIN), + "CODESPACE_NAME": JSON.stringify(process.env.CODESPACE_NAME), }, plugins: [ + commonjs(), vue(), legacy({ targets: [ "since 2015" ], @@ -42,6 +47,9 @@ export default defineConfig({ } }, build: { + commonjsOptions: { + include: [ /.js$/ ], + }, rollupOptions: { output: { manualChunks(id, { getModuleInfo, getModuleIds }) { diff --git a/db/patch-added-kafka-producer.sql b/db/patch-added-kafka-producer.sql new file mode 100644 index 00000000..933d30b8 --- /dev/null +++ b/db/patch-added-kafka-producer.sql @@ -0,0 +1,22 @@ +-- You should not modify if this have pushed to Github, unless it does serious wrong with the db. +BEGIN TRANSACTION; + +ALTER TABLE monitor + ADD kafka_producer_topic VARCHAR(255); + +ALTER TABLE monitor + ADD kafka_producer_brokers TEXT; + +ALTER TABLE monitor + ADD kafka_producer_ssl INTEGER; + +ALTER TABLE monitor + ADD kafka_producer_allow_auto_topic_creation VARCHAR(255); + +ALTER TABLE monitor + ADD kafka_producer_sasl_options TEXT; + +ALTER TABLE monitor + ADD kafka_producer_message TEXT; + +COMMIT; diff --git a/docker/dockerfile b/docker/dockerfile index 239a0c95..1bc90f92 100644 --- a/docker/dockerfile +++ b/docker/dockerfile @@ -72,7 +72,6 @@ RUN git clone https://github.com/louislam/uptime-kuma.git . RUN npm ci EXPOSE 3000 3001 -VOLUME ["/app/data"] HEALTHCHECK --interval=60s --timeout=30s --start-period=180s --retries=5 CMD extra/healthcheck CMD ["npm", "run", "start-pr-test"] diff --git a/package-lock.json b/package-lock.json index ad26942e..62c5adb0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,7 +10,7 @@ "license": "MIT", "dependencies": { "@grpc/grpc-js": "~1.7.3", - "@louislam/ping": "~0.4.4-mod.0", + "@louislam/ping": "~0.4.4-mod.1", "@louislam/sqlite3": "15.1.6", "args-parser": "~1.3.0", "axios": "~0.27.0", @@ -41,6 +41,7 @@ "jsonata": "^2.0.3", "jsonwebtoken": "~9.0.0", "jwt-decode": "~3.1.2", + "kafkajs": "^2.2.4", "limiter": "~2.1.0", "liquidjs": "^10.7.0", "mongodb": "~4.14.0", @@ -62,6 +63,7 @@ "qs": "~6.10.4", "redbean-node": "~0.3.0", "redis": "~4.5.1", + "semver": "~7.5.4", "socket.io": "~4.6.1", "socket.io-client": "~4.6.1", "socks-proxy-agent": "6.1.1", @@ -116,6 +118,7 @@ "typescript": "~4.4.4", "v-pagination-3": "~0.1.7", "vite": "~4.4.1", + "vite-plugin-commonjs": "^0.8.0", "vite-plugin-compression": "^0.5.1", "vue": "~3.3.4", "vue-chartjs": "~5.2.0", @@ -133,7 +136,7 @@ "whatwg-url": "~12.0.1" }, "engines": { - "node": "14.* || 16.* || 18.*" + "node": "14 || 16 || 18 || >= 20.4.0" } }, "node_modules/@aashutoshrathi/word-wrap": { @@ -4395,11 +4398,12 @@ "dev": true }, "node_modules/@louislam/ping": { - "version": "0.4.4-mod.0", - "resolved": "https://registry.npmjs.org/@louislam/ping/-/ping-0.4.4-mod.0.tgz", - "integrity": "sha512-U2ZXcgFRPmZYd/ooA8KILG4aCMBsDrGP9NDWseHriZSsKlu5Y1lf/LbenN6tnqQ9JjAsbJjqwSi3xtAcWqU+1w==", + "version": "0.4.4-mod.1", + "resolved": "https://registry.npmjs.org/@louislam/ping/-/ping-0.4.4-mod.1.tgz", + "integrity": "sha512-uMq6qwL9/VYh2YBbDEhM7ZzJ8YON+juw/3k+28P3s9ue3uDMQ56MNPfywXoRpsxkU8RgjN0TDzEhQDzO1WisMw==", "dependencies": { - "command-exists": "~1.2.9" + "command-exists": "~1.2.9", + "underscore": "~1.13.6" }, "engines": { "node": ">=4.0.0" @@ -8751,6 +8755,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/es-module-lexer": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.3.0.tgz", + "integrity": "sha512-vZK7T0N2CBmBOixhmjdqx2gWVbFZ4DXZ/NyRMZVlJXPa7CyFS+/a4QQsDGDQy9ZfEzxFuNEsMLeQJnKP2p5/JA==", + "dev": true + }, "node_modules/es-set-tostringtag": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.0.1.tgz", @@ -13001,6 +13011,14 @@ "resolved": "https://registry.npmjs.org/jwt-decode/-/jwt-decode-3.1.2.tgz", "integrity": "sha512-UfpWE/VZn0iP50d8cz9NrZLM9lSWhcJ+0Gt/nm4by88UL+J1SiKN8/5dkjMmbEzwL2CAe+67GsegCbIKtbp75A==" }, + "node_modules/kafkajs": { + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/kafkajs/-/kafkajs-2.2.4.tgz", + "integrity": "sha512-j/YeapB1vfPT2iOIUn/vxdyKEuhuY2PxMBvf5JWux6iSaukAccrMtXEY/Lb7OvavDhOWME589bpLrEdnVHjfjA==", + "engines": { + "node": ">=14.0.0" + } + }, "node_modules/keyv": { "version": "4.5.2", "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.2.tgz", @@ -17741,6 +17759,11 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/underscore": { + "version": "1.13.6", + "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.13.6.tgz", + "integrity": "sha512-+A5Sja4HP1M08MaXya7p5LvjuM7K6q/2EaC0+iovj/wOcMsTzMvDFbasi/oSapiwOlt252IqsKqPjCl7huKS0A==" + }, "node_modules/unicode-canonical-property-names-ecmascript": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.0.tgz", @@ -18020,6 +18043,18 @@ } } }, + "node_modules/vite-plugin-commonjs": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/vite-plugin-commonjs/-/vite-plugin-commonjs-0.8.1.tgz", + "integrity": "sha512-hL2wwqgSiLBcrmCH7z+H468Z9uyBnKXX5OAwoYmWd/i03PBGCqkOBR3rjeojyWOoGmWgDVB7lj6Xn5pVw3Fwyg==", + "dev": true, + "dependencies": { + "acorn": "^8.8.2", + "fast-glob": "^3.2.12", + "magic-string": "^0.30.1", + "vite-plugin-dynamic-import": "^1.5.0" + } + }, "node_modules/vite-plugin-compression": { "version": "0.5.1", "resolved": "https://registry.npmjs.org/vite-plugin-compression/-/vite-plugin-compression-0.5.1.tgz", @@ -18118,6 +18153,18 @@ "node": ">=8" } }, + "node_modules/vite-plugin-dynamic-import": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/vite-plugin-dynamic-import/-/vite-plugin-dynamic-import-1.5.0.tgz", + "integrity": "sha512-Qp85c+AVJmLa8MLni74U4BDiWpUeFNx7NJqbGZyR2XJOU7mgW0cb7nwlAMucFyM4arEd92Nfxp4j44xPi6Fu7g==", + "dev": true, + "dependencies": { + "acorn": "^8.8.2", + "es-module-lexer": "^1.2.1", + "fast-glob": "^3.2.12", + "magic-string": "^0.30.1" + } + }, "node_modules/vue": { "version": "3.3.4", "resolved": "https://registry.npmjs.org/vue/-/vue-3.3.4.tgz", diff --git a/package.json b/package.json index fadd0f0d..d19f01f1 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,7 @@ "url": "https://github.com/louislam/uptime-kuma.git" }, "engines": { - "node": "14.* || 16.* || 18.*" + "node": "14 || 16 || 18 || >= 20.4.0" }, "scripts": { "install-legacy": "npm install", @@ -19,6 +19,7 @@ "lint": "npm run lint:js && npm run lint:style", "dev": "concurrently -k -r \"wait-on tcp:3000 && npm run start-server-dev \" \"npm run start-frontend-dev\"", "start-frontend-dev": "cross-env NODE_ENV=development vite --host --config ./config/vite.config.js", + "start-frontend-devcontainer": "cross-env NODE_ENV=development DEVCONTAINER=1 vite --host --config ./config/vite.config.js", "start": "npm run start-server", "start-server": "node server/server.js", "start-server-dev": "cross-env NODE_ENV=development node server/server.js", @@ -69,7 +70,7 @@ }, "dependencies": { "@grpc/grpc-js": "~1.7.3", - "@louislam/ping": "~0.4.4-mod.0", + "@louislam/ping": "~0.4.4-mod.1", "@louislam/sqlite3": "15.1.6", "args-parser": "~1.3.0", "axios": "~0.27.0", @@ -100,6 +101,7 @@ "jsonata": "^2.0.3", "jsonwebtoken": "~9.0.0", "jwt-decode": "~3.1.2", + "kafkajs": "^2.2.4", "limiter": "~2.1.0", "liquidjs": "^10.7.0", "mongodb": "~4.14.0", @@ -121,6 +123,7 @@ "qs": "~6.10.4", "redbean-node": "~0.3.0", "redis": "~4.5.1", + "semver": "~7.5.4", "socket.io": "~4.6.1", "socket.io-client": "~4.6.1", "socks-proxy-agent": "6.1.1", @@ -175,6 +178,7 @@ "typescript": "~4.4.4", "v-pagination-3": "~0.1.7", "vite": "~4.4.1", + "vite-plugin-commonjs": "^0.8.0", "vite-plugin-compression": "^0.5.1", "vue": "~3.3.4", "vue-chartjs": "~5.2.0", diff --git a/server/client.js b/server/client.js index 3efbe8fd..2e3bd43b 100644 --- a/server/client.js +++ b/server/client.js @@ -141,12 +141,21 @@ async function sendAPIKeyList(socket) { /** * Emits the version information to the client. * @param {Socket} socket Socket.io socket instance + * @param {boolean} hideVersion * @returns {Promise} */ -async function sendInfo(socket) { +async function sendInfo(socket, hideVersion = false) { + let version; + let latestVersion; + + if (!hideVersion) { + version = checkVersion.version; + latestVersion = checkVersion.latestVersion; + } + socket.emit("info", { - version: checkVersion.version, - latestVersion: checkVersion.latestVersion, + version, + latestVersion, primaryBaseURL: await setting("primaryBaseURL"), serverTimezone: await server.getTimezone(), serverTimezoneOffset: server.getTimezoneOffset(), diff --git a/server/config.js b/server/config.js index 43a40f67..77f9e74b 100644 --- a/server/config.js +++ b/server/config.js @@ -1,4 +1,5 @@ -const args = require("args-parser")(process.argv); +// Interop with browser +const args = (typeof process !== "undefined") ? require("args-parser")(process.argv) : {}; const demoMode = args["demo"] || false; const badgeConstants = { diff --git a/server/database.js b/server/database.js index c283a55b..7b1d9f93 100644 --- a/server/database.js +++ b/server/database.js @@ -73,6 +73,7 @@ class Database { "patch-add-parent-monitor.sql": true, "patch-add-invert-keyword.sql": true, "patch-added-json-query.sql": true, + "patch-added-kafka-producer.sql": true, "patch-add-certificate-expiry-status-page.sql": true, }; diff --git a/server/model/monitor.js b/server/model/monitor.js index 4d760d36..f28b4fe2 100644 --- a/server/model/monitor.js +++ b/server/model/monitor.js @@ -6,7 +6,7 @@ const { log, UP, DOWN, PENDING, MAINTENANCE, flipStatus, TimeLogger, MAX_INTERVA SQL_DATETIME_FORMAT } = require("../../src/util"); const { tcping, ping, dnsResolve, checkCertificate, checkStatusCode, getTotalClientInRoom, setting, mssqlQuery, postgresQuery, mysqlQuery, mqttAsync, setSetting, httpNtlm, radius, grpcQuery, - redisPingAsync, mongodbPing, + redisPingAsync, mongodbPing, kafkaProducerAsync } = require("../util-server"); const { R } = require("redbean-node"); const { BeanModel } = require("redbean-node/dist/bean-model"); @@ -137,6 +137,11 @@ class Monitor extends BeanModel { httpBodyEncoding: this.httpBodyEncoding, jsonPath: this.jsonPath, expectedValue: this.expectedValue, + kafkaProducerTopic: this.kafkaProducerTopic, + kafkaProducerBrokers: JSON.parse(this.kafkaProducerBrokers), + kafkaProducerSsl: this.kafkaProducerSsl === "1" && true || false, + kafkaProducerAllowAutoTopicCreation: this.kafkaProducerAllowAutoTopicCreation === "1" && true || false, + kafkaProducerMessage: this.kafkaProducerMessage, screenshot, }; @@ -161,6 +166,7 @@ class Monitor extends BeanModel { tlsCa: this.tlsCa, tlsCert: this.tlsCert, tlsKey: this.tlsKey, + kafkaProducerSaslOptions: JSON.parse(this.kafkaProducerSaslOptions), }; } @@ -175,7 +181,7 @@ class Monitor extends BeanModel { async isActive() { const parentActive = await Monitor.isParentActive(this.id); - return this.active && parentActive; + return (this.active === 1) && parentActive; } /** @@ -825,6 +831,24 @@ class Monitor extends BeanModel { bean.ping = dayjs().valueOf() - startTime; } + } else if (this.type === "kafka-producer") { + let startTime = dayjs().valueOf(); + + bean.msg = await kafkaProducerAsync( + JSON.parse(this.kafkaProducerBrokers), + this.kafkaProducerTopic, + this.kafkaProducerMessage, + { + allowAutoTopicCreation: this.kafkaProducerAllowAutoTopicCreation, + ssl: this.kafkaProducerSsl, + clientId: `Uptime-Kuma/${version}`, + interval: this.interval, + }, + JSON.parse(this.kafkaProducerSaslOptions), + ); + bean.status = UP; + bean.ping = dayjs().valueOf() - startTime; + } else { throw new Error("Unknown Monitor Type"); } diff --git a/server/notification-providers/slack.js b/server/notification-providers/slack.js index 12207bd4..41c2bd02 100644 --- a/server/notification-providers/slack.js +++ b/server/notification-providers/slack.js @@ -27,6 +27,11 @@ class Slack extends NotificationProvider { async send(notification, msg, monitorJSON = null, heartbeatJSON = null) { let okMsg = "Sent Successfully."; + + if (notification.slackchannelnotify) { + msg += " "; + } + try { if (heartbeatJSON == null) { let data = { @@ -53,7 +58,7 @@ class Slack extends NotificationProvider { "type": "header", "text": { "type": "plain_text", - "text": "Uptime Kuma Alert", + "text": textMsg, }, }, { diff --git a/server/notification-providers/smsc.js b/server/notification-providers/smsc.js new file mode 100644 index 00000000..251bc455 --- /dev/null +++ b/server/notification-providers/smsc.js @@ -0,0 +1,42 @@ +const NotificationProvider = require("./notification-provider"); +const axios = require("axios"); + +class SMSC extends NotificationProvider { + name = "smsc"; + + async send(notification, msg, monitorJSON = null, heartbeatJSON = null) { + let okMsg = "Sent Successfully."; + try { + let config = { + headers: { + "Content-Type": "application/json", + "Accept": "text/json", + } + }; + + let getArray = [ + "fmt=3", + "translit=" + notification.smscTranslit, + "login=" + notification.smscLogin, + "psw=" + notification.smscPassword, + "phones=" + notification.smscToNumber, + "mes=" + encodeURIComponent(msg.replace(/[^\x00-\x7F]/g, "")), + ]; + if (notification.smscSenderName !== "") { + getArray.push("sender=" + notification.smscSenderName); + } + + let resp = await axios.get("https://smsc.kz/sys/send.php?" + getArray.join("&"), config); + if (resp.data.id === undefined) { + let error = `Something gone wrong. Api returned code ${resp.data.error_code}: ${resp.data.error}`; + this.throwGeneralAxiosError(error); + } + + return okMsg; + } catch (error) { + this.throwGeneralAxiosError(error); + } + } +} + +module.exports = SMSC; diff --git a/server/notification.js b/server/notification.js index 9bfa371d..ea5c8ee0 100644 --- a/server/notification.js +++ b/server/notification.js @@ -6,6 +6,7 @@ const AliyunSms = require("./notification-providers/aliyun-sms"); const Apprise = require("./notification-providers/apprise"); const Bark = require("./notification-providers/bark"); const ClickSendSMS = require("./notification-providers/clicksendsms"); +const SMSC = require("./notification-providers/smsc"); const DingDing = require("./notification-providers/dingding"); const Discord = require("./notification-providers/discord"); const Feishu = require("./notification-providers/feishu"); @@ -68,6 +69,7 @@ class Notification { new Apprise(), new Bark(), new ClickSendSMS(), + new SMSC(), new DingDing(), new Discord(), new Feishu(), diff --git a/server/routers/api-router.js b/server/routers/api-router.js index 8b5d36f2..f51f046d 100644 --- a/server/routers/api-router.js +++ b/server/routers/api-router.js @@ -442,7 +442,7 @@ router.get("/api/badge/:id/cert-exp", cache("5 minutes"), async (request, respon if (!tlsInfo.valid) { // return a "Bad Cert" badge in naColor (grey), when cert is not valid badgeValues.message = "Bad Cert"; - badgeValues.color = badgeConstants.downColor; + badgeValues.color = downColor; } else { const daysRemaining = parseInt(overrideValue ?? tlsInfo.certInfo.daysRemaining); diff --git a/server/server.js b/server/server.js index b9d618f5..5f4ccc46 100644 --- a/server/server.js +++ b/server/server.js @@ -15,20 +15,27 @@ dayjs.extend(require("dayjs/plugin/customParseFormat")); require("dotenv").config(); // Check Node.js Version -const nodeVersion = parseInt(process.versions.node.split(".")[0]); -const requiredVersion = 14; +const nodeVersion = process.versions.node; + +// Get the required Node.js version from package.json +const requiredNodeVersions = require("../package.json").engines.node; +const bannedNodeVersions = " < 14 || 20.0.* || 20.1.* || 20.2.* || 20.3.* "; console.log(`Your Node.js version: ${nodeVersion}`); -// See more: https://github.com/louislam/uptime-kuma/issues/3138 -if (nodeVersion >= 20) { - console.warn("\x1b[31m%s\x1b[0m", "Warning: Uptime Kuma is currently not stable on Node.js >= 20, please use Node.js 18."); -} +const semver = require("semver"); +const requiredNodeVersionsComma = requiredNodeVersions.split("||").map((version) => version.trim()).join(", "); -if (nodeVersion < requiredVersion) { - console.error(`Error: Your Node.js version is not supported, please upgrade to Node.js >= ${requiredVersion}.`); +// Exit Uptime Kuma immediately if the Node.js version is banned +if (semver.satisfies(nodeVersion, bannedNodeVersions)) { + console.error("\x1b[31m%s\x1b[0m", `Error: Your Node.js version: ${nodeVersion} is not supported, please upgrade your Node.js to ${requiredNodeVersionsComma}.`); process.exit(-1); } +// Warning if the Node.js version is not in the support list, but it maybe still works +if (!semver.satisfies(nodeVersion, requiredNodeVersions)) { + console.warn("\x1b[31m%s\x1b[0m", `Warning: Your Node.js version: ${nodeVersion} is not officially supported, please upgrade your Node.js to ${requiredNodeVersionsComma}.`); +} + const args = require("args-parser")(process.argv); const { sleep, log, getRandomInt, genSecret, isDev } = require("../src/util"); const config = require("./config"); @@ -263,7 +270,7 @@ let needSetup = false; log.info("server", "Adding socket handler"); io.on("connection", async (socket) => { - sendInfo(socket); + sendInfo(socket, true); if (needSetup) { log.info("server", "Redirect to setup page"); @@ -636,6 +643,9 @@ let needSetup = false; monitor.accepted_statuscodes_json = JSON.stringify(monitor.accepted_statuscodes); delete monitor.accepted_statuscodes; + monitor.kafkaProducerBrokers = JSON.stringify(monitor.kafkaProducerBrokers); + monitor.kafkaProducerSaslOptions = JSON.stringify(monitor.kafkaProducerSaslOptions); + bean.import(monitor); bean.user_id = socket.userID; @@ -750,6 +760,11 @@ let needSetup = false; bean.httpBodyEncoding = monitor.httpBodyEncoding; bean.expectedValue = monitor.expectedValue; bean.jsonPath = monitor.jsonPath; + bean.kafkaProducerTopic = monitor.kafkaProducerTopic; + bean.kafkaProducerBrokers = JSON.stringify(monitor.kafkaProducerBrokers); + bean.kafkaProducerAllowAutoTopicCreation = monitor.kafkaProducerAllowAutoTopicCreation; + bean.kafkaProducerSaslOptions = JSON.stringify(monitor.kafkaProducerSaslOptions); + bean.kafkaProducerMessage = monitor.kafkaProducerMessage; bean.validate(); @@ -1651,6 +1666,7 @@ async function afterLogin(socket, user) { socket.join(user.id); let monitorList = await server.sendMonitorList(socket); + sendInfo(socket); server.sendMaintenanceList(socket); sendNotificationList(socket); sendProxyList(socket); diff --git a/server/uptime-kuma-server.js b/server/uptime-kuma-server.js index b45a749b..da86f3b9 100644 --- a/server/uptime-kuma-server.js +++ b/server/uptime-kuma-server.js @@ -10,7 +10,7 @@ const util = require("util"); const { CacheableDnsHttpAgent } = require("./cacheable-dns-http-agent"); const { Settings } = require("./settings"); const dayjs = require("dayjs"); -// DO NOT IMPORT HERE IF THE MODULES USED `UptimeKumaServer.getInstance()` +// DO NOT IMPORT HERE IF THE MODULES USED `UptimeKumaServer.getInstance()`, put at the bottom of this file instead. /** * `module.exports` (alias: `server`) should be inside this class, in order to avoid circular dependency issue. @@ -249,9 +249,9 @@ class UptimeKumaServer { return (typeof forwardedFor === "string" ? forwardedFor.split(",")[0].trim() : null) || socket.client.conn.request.headers["x-real-ip"] - || clientIP.replace(/^.*:/, ""); + || clientIP.replace(/^::ffff:/, ""); } else { - return clientIP.replace(/^.*:/, ""); + return clientIP.replace(/^::ffff:/, ""); } } @@ -262,13 +262,43 @@ class UptimeKumaServer { * @returns {Promise} */ async getTimezone() { + // From process.env.TZ + try { + if (process.env.TZ) { + this.checkTimezone(process.env.TZ); + return process.env.TZ; + } + } catch (e) { + log.warn("timezone", e.message + " in process.env.TZ"); + } + let timezone = await Settings.get("serverTimezone"); - if (timezone) { - return timezone; - } else if (process.env.TZ) { - return process.env.TZ; - } else { - return dayjs.tz.guess(); + + // From Settings + try { + log.debug("timezone", "Using timezone from settings: " + timezone); + if (timezone) { + this.checkTimezone(timezone); + return timezone; + } + } catch (e) { + log.warn("timezone", e.message + " in settings"); + } + + // Guess + try { + let guess = dayjs.tz.guess(); + log.debug("timezone", "Guessing timezone: " + guess); + if (guess) { + this.checkTimezone(guess); + return guess; + } else { + return "UTC"; + } + } catch (e) { + // Guess failed, fall back to UTC + log.debug("timezone", "Guessed an invalid timezone. Use UTC as fallback"); + return "UTC"; } } @@ -280,11 +310,24 @@ class UptimeKumaServer { return dayjs().format("Z"); } + /** + * Throw an error if the timezone is invalid + * @param timezone + */ + checkTimezone(timezone) { + try { + dayjs.utc("2013-11-18 11:55").tz(timezone).format(); + } catch (e) { + throw new Error("Invalid timezone:" + timezone); + } + } + /** * Set the current server timezone and environment variables * @param {string} timezone */ async setTimezone(timezone) { + this.checkTimezone(timezone); await Settings.set("serverTimezone", timezone, "general"); process.env.TZ = timezone; dayjs.tz.setDefault(timezone); @@ -300,6 +343,5 @@ module.exports = { UptimeKumaServer }; -// Must be at the end -const { MonitorType } = require("./monitor-types/monitor-type"); +// Must be at the end to avoid circular dependencies const { RealBrowserMonitorType } = require("./monitor-types/real-browser-monitor-type"); diff --git a/server/util-server.js b/server/util-server.js index 4ddb6ce3..031d8b67 100644 --- a/server/util-server.js +++ b/server/util-server.js @@ -28,8 +28,11 @@ const { } = require("node-radius-utils"); const dayjs = require("dayjs"); -const isWindows = process.platform === /^win/.test(process.platform); +// SASLOptions used in JSDoc +// eslint-disable-next-line no-unused-vars +const { Kafka, SASLOptions } = require("kafkajs"); +const isWindows = process.platform === /^win/.test(process.platform); /** * Init or reset JWT secret * @returns {Promise} @@ -196,6 +199,94 @@ exports.mqttAsync = function (hostname, topic, okMessage, options = {}) { }); }; +/** + * Monitor Kafka using Producer + * @param {string} topic Topic name to produce into + * @param {string} message Message to produce + * @param {Object} [options={interval = 20, allowAutoTopicCreation = false, ssl = false, clientId = "Uptime-Kuma"}] + * Kafka client options. Contains ssl, clientId, allowAutoTopicCreation and + * interval (interval defaults to 20, allowAutoTopicCreation defaults to false, clientId defaults to "Uptime-Kuma" + * and ssl defaults to false) + * @param {string[]} brokers List of kafka brokers to connect, host and port joined by ':' + * @param {SASLOptions} [saslOptions={}] Options for kafka client Authentication (SASL) (defaults to + * {}) + * @returns {Promise} + */ +exports.kafkaProducerAsync = function (brokers, topic, message, options = {}, saslOptions = {}) { + return new Promise((resolve, reject) => { + const { interval = 20, allowAutoTopicCreation = false, ssl = false, clientId = "Uptime-Kuma" } = options; + + let connectedToKafka = false; + + const timeoutID = setTimeout(() => { + log.debug("kafkaProducer", "KafkaProducer timeout triggered"); + connectedToKafka = true; + reject(new Error("Timeout")); + }, interval * 1000 * 0.8); + + if (saslOptions.mechanism === "None") { + saslOptions = undefined; + } + + let client = new Kafka({ + brokers: brokers, + clientId: clientId, + sasl: saslOptions, + retry: { + retries: 0, + }, + ssl: ssl, + }); + + let producer = client.producer({ + allowAutoTopicCreation: allowAutoTopicCreation, + retry: { + retries: 0, + } + }); + + producer.connect().then( + () => { + try { + producer.send({ + topic: topic, + messages: [{ + value: message, + }], + }); + connectedToKafka = true; + clearTimeout(timeoutID); + resolve("Message sent successfully"); + } catch (e) { + connectedToKafka = true; + producer.disconnect(); + clearTimeout(timeoutID); + reject(new Error("Error sending message: " + e.message)); + } + } + ).catch( + (e) => { + connectedToKafka = true; + producer.disconnect(); + clearTimeout(timeoutID); + reject(new Error("Error in producer connection: " + e.message)); + } + ); + + producer.on("producer.network.request_timeout", (_) => { + clearTimeout(timeoutID); + reject(new Error("producer.network.request_timeout")); + }); + + producer.on("producer.disconnect", (_) => { + if (!connectedToKafka) { + clearTimeout(timeoutID); + reject(new Error("producer.disconnect")); + } + }); + }); +}; + /** * Use NTLM Auth for a http request. * @param {Object} options The http request options diff --git a/src/assets/app.scss b/src/assets/app.scss index b648cbcd..0eff9a06 100644 --- a/src/assets/app.scss +++ b/src/assets/app.scss @@ -436,12 +436,12 @@ optgroup { .monitor-list { &.scrollbar { overflow-y: auto; - height: calc(100% - 65px); + height: calc(100% - 107px); } @media (max-width: 770px) { &.scrollbar { - height: calc(100% - 40px); + height: calc(100% - 97px); } } diff --git a/src/components/BadgeGeneratorDialog.vue b/src/components/BadgeGeneratorDialog.vue index 9e073e39..aa6fa6e8 100644 --- a/src/components/BadgeGeneratorDialog.vue +++ b/src/components/BadgeGeneratorDialog.vue @@ -22,78 +22,78 @@
- - + +
- +
- +
- +
- +
- +
- +
- +
- +
- +
- +
- +
- +
- +
- +
@@ -109,12 +109,16 @@
- + +
+ +
+
- - + +
@@ -131,6 +135,7 @@ @@ -159,8 +205,6 @@ export default { margin: -10px; margin-bottom: 10px; padding: 10px; - display: flex; - justify-content: space-between; .dark & { background-color: $dark-header-bg; @@ -168,6 +212,17 @@ export default { } } +.header-top { + display: flex; + justify-content: space-between; + align-items: center; +} + +.header-filter { + display: flex; + align-items: center; +} + @media (max-width: 770px) { .list-header { margin: -20px; @@ -216,5 +271,4 @@ export default { padding-left: 67px; margin-top: 5px; } - diff --git a/src/components/MonitorListFilter.vue b/src/components/MonitorListFilter.vue new file mode 100644 index 00000000..dbb1eb94 --- /dev/null +++ b/src/components/MonitorListFilter.vue @@ -0,0 +1,284 @@ + + + + + diff --git a/src/components/MonitorListFilterDropdown.vue b/src/components/MonitorListFilterDropdown.vue new file mode 100644 index 00000000..01b9678f --- /dev/null +++ b/src/components/MonitorListFilterDropdown.vue @@ -0,0 +1,131 @@ + + + + + diff --git a/src/components/NotificationDialog.vue b/src/components/NotificationDialog.vue index 856d6f53..f977225f 100644 --- a/src/components/NotificationDialog.vue +++ b/src/components/NotificationDialog.vue @@ -164,6 +164,7 @@ export default { "SMSManager": "SmsManager (smsmanager.cz)", "WeCom": "WeCom (企业微信群机器人)", "ServerChan": "ServerChan (Server酱)", + "smsc": "SMSC", }; // Sort by notification name diff --git a/src/components/TagEditDialog.vue b/src/components/TagEditDialog.vue index bdfbe132..e601aa42 100644 --- a/src/components/TagEditDialog.vue +++ b/src/components/TagEditDialog.vue @@ -99,7 +99,7 @@ + +
+ + +
+
+ {{ $t("aboutNotifyChannel") }} +
diff --git a/src/components/notifications/index.js b/src/components/notifications/index.js index 7b5e6b6c..673a84a9 100644 --- a/src/components/notifications/index.js +++ b/src/components/notifications/index.js @@ -4,6 +4,7 @@ import AliyunSMS from "./AliyunSms.vue"; import Apprise from "./Apprise.vue"; import Bark from "./Bark.vue"; import ClickSendSMS from "./ClickSendSMS.vue"; +import SMSC from "./SMSC.vue"; import DingDing from "./DingDing.vue"; import Discord from "./Discord.vue"; import Feishu from "./Feishu.vue"; @@ -61,6 +62,7 @@ const NotificationFormList = { "apprise": Apprise, "Bark": Bark, "clicksendsms": ClickSendSMS, + "smsc": SMSC, "DingDing": DingDing, "discord": Discord, "Feishu": Feishu, diff --git a/src/lang/en.json b/src/lang/en.json index 504e1a3c..5cdbc171 100644 --- a/src/lang/en.json +++ b/src/lang/en.json @@ -155,6 +155,8 @@ "Disable 2FA": "Disable 2FA", "2FA Settings": "2FA Settings", "Two Factor Authentication": "Two Factor Authentication", + "filterActive": "Active", + "filterActivePaused": "Paused", "Active": "Active", "Inactive": "Inactive", "Token": "Token", @@ -640,6 +642,8 @@ "matrixDesc1": "You can find the internal room ID by looking in the advanced section of the room settings in your Matrix client. It should look like !QMdRCpUIfLwsfjxye6:home.server.", "matrixDesc2": "It is highly recommended you create a new user and do not use your own Matrix user's access token as it will allow full access to your account and all the rooms you joined. Instead, create a new user and only invite it to the room that you want to receive the notification in. You can get the access token by running {0}", "Channel Name": "Channel Name", + "Notify Channel": "Notify Channel", + "aboutNotifyChannel": "Notify channel will trigger a desktop or mobile notification for all members of the channel, whether their availability is set to active or away.", "Uptime Kuma URL": "Uptime Kuma URL", "Icon Emoji": "Icon Emoji", "signalImportant": "IMPORTANT: You cannot mix groups and numbers in recipients!", @@ -743,13 +747,14 @@ "Open Badge Generator": "Open Badge Generator", "Badge Generator": "{0}'s Badge Generator", "Badge Type": "Badge Type", - "Badge Duration": "Badge Duration", + "Badge Duration (in hours)": "Badge Duration (in hours)", "Badge Label": "Badge Label", - "Badge Prefix": "Badge Prefix", - "Badge Suffix": "Badge Suffix", + "Badge Prefix": "Badge Value Prefix", + "Badge Suffix": "Badge Value Suffix", "Badge Label Color": "Badge Label Color", "Badge Color": "Badge Color", "Badge Label Prefix": "Badge Label Prefix", + "Badge Preview": "Badge Preview", "Badge Label Suffix": "Badge Label Suffix", "Badge Up Color": "Badge Up Color", "Badge Down Color": "Badge Down Color", @@ -763,6 +768,20 @@ "Badge URL": "Badge URL", "Group": "Group", "Monitor Group": "Monitor Group", + "Kafka Brokers": "Kafka Brokers", + "Enter the list of brokers": "Enter the list of brokers", + "Press Enter to add broker": "Press Enter to add broker", + "Kafka Topic Name": "Kafka Topic Name", + "Kafka Producer Message": "Kafka Producer Message", + "Enable Kafka SSL": "Enable Kafka SSL", + "Enable Kafka Producer Auto Topic Creation": "Enable Kafka Producer Auto Topic Creation", + "Kafka SASL Options": "Kafka SASL Options", + "Mechanism": "Mechanism", + "Pick a SASL Mechanism...": "Pick a SASL Mechanism...", + "Authorization Identity": "Authorization Identity", + "AccessKey Id": "AccessKey Id", + "Secret AccessKey": "Secret AccessKey", + "Session Token": "Session Token", "noGroupMonitorMsg": "Not Available. Create a Group Monitor First.", "Close": "Close", "Request Body": "Request Body", diff --git a/src/lang/zh-HK.json b/src/lang/zh-HK.json index fd5d35e3..aa43caa5 100644 --- a/src/lang/zh-HK.json +++ b/src/lang/zh-HK.json @@ -139,6 +139,8 @@ "Disable 2FA": "關閉 2FA", "2FA Settings": "2FA 設定", "Two Factor Authentication": "雙重認證", + "filterActive": "執行狀態", + "filterActivePaused": "已暫停", "Active": "生效", "Inactive": "未生效", "Token": "Token", diff --git a/src/mixins/public.js b/src/mixins/public.js index a3e12f46..c87bfb35 100644 --- a/src/mixins/public.js +++ b/src/mixins/public.js @@ -1,9 +1,12 @@ import axios from "axios"; +import { getDevContainerServerHostname, isDevContainer } from "../util-frontend"; const env = process.env.NODE_ENV || "production"; // change the axios base url for development -if (env === "development" || localStorage.dev === "dev") { +if (env === "development" && isDevContainer()) { + axios.defaults.baseURL = location.protocol + "//" + getDevContainerServerHostname(); +} else if (env === "development" || localStorage.dev === "dev") { axios.defaults.baseURL = location.protocol + "//" + location.hostname + ":3001"; } diff --git a/src/mixins/socket.js b/src/mixins/socket.js index e2834251..2d27d109 100644 --- a/src/mixins/socket.js +++ b/src/mixins/socket.js @@ -4,6 +4,7 @@ import jwtDecode from "jwt-decode"; import Favico from "favico.js"; import dayjs from "dayjs"; import { DOWN, MAINTENANCE, PENDING, UP } from "../util.ts"; +import { getDevContainerServerHostname, isDevContainer } from "../util-frontend.js"; const toast = useToast(); let socket; @@ -93,7 +94,9 @@ export default { let wsHost; const env = process.env.NODE_ENV || "production"; - if (env === "development" || localStorage.dev === "dev") { + if (env === "development" && isDevContainer()) { + wsHost = protocol + getDevContainerServerHostname(); + } else if (env === "development" || localStorage.dev === "dev") { wsHost = protocol + location.hostname + ":3001"; } else { wsHost = protocol + location.host; @@ -693,9 +696,11 @@ export default { stats() { let result = { + active: 0, up: 0, down: 0, maintenance: 0, + pending: 0, unknown: 0, pause: 0, }; @@ -707,12 +712,13 @@ export default { if (monitor && ! monitor.active) { result.pause++; } else if (beat) { + result.active++; if (beat.status === UP) { result.up++; } else if (beat.status === DOWN) { result.down++; } else if (beat.status === PENDING) { - result.up++; + result.pending++; } else if (beat.status === MAINTENANCE) { result.maintenance++; } else { diff --git a/src/pages/EditMonitor.vue b/src/pages/EditMonitor.vue index 1ce62279..0ffef8fe 100644 --- a/src/pages/EditMonitor.vue +++ b/src/pages/EditMonitor.vue @@ -61,6 +61,9 @@ + @@ -166,6 +169,57 @@ + +
@@ -512,6 +566,56 @@
+ + + +