diff --git a/.editorconfig b/.editorconfig index a882f5c2d..3b2721931 100644 --- a/.editorconfig +++ b/.editorconfig @@ -16,3 +16,6 @@ indent_size = 2 [*.yml] indent_size = 2 + +[*.vue] +trim_trailing_whitespace = false diff --git a/.eslintrc.js b/.eslintrc.js index 3a699223a..41ad54b81 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -2,48 +2,43 @@ module.exports = { env: { browser: true, commonjs: true, - es2017: true, + es2020: true, node: true, }, extends: [ "eslint:recommended", "plugin:vue/vue3-recommended", ], + parser: "vue-eslint-parser", parserOptions: { - ecmaVersion: 2018, + parser: "@babel/eslint-parser", sourceType: "module", + requireConfigFile: false, }, rules: { // override/add rules settings here, such as: // 'vue/no-unused-vars': 'error' "no-unused-vars": "warn", - indent: ["error", 4], + indent: [ + "error", + 4, + { + ignoredNodes: ["TemplateLiteral"], + SwitchCase: 1, + }, + ], quotes: ["warn", "double"], //semi: ['off', 'never'], "vue/html-indent": ["warn", 4], // default: 2 "vue/max-attributes-per-line": "off", "vue/singleline-html-element-content-newline": "off", + "vue/html-self-closing": "off", "no-multi-spaces": ["error", { ignoreEOLComments: true, }], "curly": "error", "object-curly-spacing": ["error", "always"], - "object-curly-newline": ["error", { - "ObjectExpression": { - "minProperties": 1, - }, - "ObjectPattern": { - "multiline": true, - "minProperties": 2, - }, - "ImportDeclaration": { - "multiline": true, - }, - "ExportDeclaration": { - "multiline": true, - //'minProperties': 2, - }, - }], + "object-curly-newline": "off", "object-property-newline": "error", "comma-spacing": "error", "brace-style": "error", @@ -53,6 +48,9 @@ module.exports = { "space-infix-ops": "warn", "arrow-spacing": "warn", "no-trailing-spaces": "warn", + "no-constant-condition": ["error", { + "checkLoops": false, + }], "space-before-blocks": "warn", //'no-console': 'warn', "no-extra-boolean-cast": "off", @@ -70,6 +68,6 @@ module.exports = { "array-bracket-newline": ["error", "consistent"], "eol-last": ["error", "always"], //'prefer-template': 'error', - "comma-dangle": ["warn", "always-multiline"], + "comma-dangle": ["warn", "only-multiline"], }, } diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 000000000..4cbcc7bdb --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,128 @@ +# Contributor Covenant Code of Conduct + +## Our Pledge + +We as members, contributors, and leaders pledge to make participation in our +community a harassment-free experience for everyone, regardless of age, body +size, visible or invisible disability, ethnicity, sex characteristics, gender +identity and expression, level of experience, education, socio-economic status, +nationality, personal appearance, race, religion, or sexual identity +and orientation. + +We pledge to act and interact in ways that contribute to an open, welcoming, +diverse, inclusive, and healthy community. + +## Our Standards + +Examples of behavior that contributes to a positive environment for our +community include: + +* Demonstrating empathy and kindness toward other people +* Being respectful of differing opinions, viewpoints, and experiences +* Giving and gracefully accepting constructive feedback +* Accepting responsibility and apologizing to those affected by our mistakes, + and learning from the experience +* Focusing on what is best not just for us as individuals, but for the + overall community + +Examples of unacceptable behavior include: + +* The use of sexualized language or imagery, and sexual attention or + advances of any kind +* Trolling, insulting or derogatory comments, and personal or political attacks +* Public or private harassment +* Publishing others' private information, such as a physical or email + address, without their explicit permission +* Other conduct which could reasonably be considered inappropriate in a + professional setting + +## Enforcement Responsibilities + +Community leaders are responsible for clarifying and enforcing our standards of +acceptable behavior and will take appropriate and fair corrective action in +response to any behavior that they deem inappropriate, threatening, offensive, +or harmful. + +Community leaders have the right and responsibility to remove, edit, or reject +comments, commits, code, wiki edits, issues, and other contributions that are +not aligned to this Code of Conduct, and will communicate reasons for moderation +decisions when appropriate. + +## Scope + +This Code of Conduct applies within all community spaces, and also applies when +an individual is officially representing the community in public spaces. +Examples of representing our community include using an official e-mail address, +posting via an official social media account, or acting as an appointed +representative at an online or offline event. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be +reported to the community leaders responsible for enforcement at +louis@uptimekuma.louislam.net. +All complaints will be reviewed and investigated promptly and fairly. + +All community leaders are obligated to respect the privacy and security of the +reporter of any incident. + +## Enforcement Guidelines + +Community leaders will follow these Community Impact Guidelines in determining +the consequences for any action they deem in violation of this Code of Conduct: + +### 1. Correction + +**Community Impact**: Use of inappropriate language or other behavior deemed +unprofessional or unwelcome in the community. + +**Consequence**: A private, written warning from community leaders, providing +clarity around the nature of the violation and an explanation of why the +behavior was inappropriate. A public apology may be requested. + +### 2. Warning + +**Community Impact**: A violation through a single incident or series +of actions. + +**Consequence**: A warning with consequences for continued behavior. No +interaction with the people involved, including unsolicited interaction with +those enforcing the Code of Conduct, for a specified period of time. This +includes avoiding interactions in community spaces as well as external channels +like social media. Violating these terms may lead to a temporary or +permanent ban. + +### 3. Temporary Ban + +**Community Impact**: A serious violation of community standards, including +sustained inappropriate behavior. + +**Consequence**: A temporary ban from any sort of interaction or public +communication with the community for a specified period of time. No public or +private interaction with the people involved, including unsolicited interaction +with those enforcing the Code of Conduct, is allowed during this period. +Violating these terms may lead to a permanent ban. + +### 4. Permanent Ban + +**Community Impact**: Demonstrating a pattern of violation of community +standards, including sustained inappropriate behavior, harassment of an +individual, or aggression toward or disparagement of classes of individuals. + +**Consequence**: A permanent ban from any sort of public interaction within +the community. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant][homepage], +version 2.0, available at +https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. + +Community Impact Guidelines were inspired by [Mozilla's code of conduct +enforcement ladder](https://github.com/mozilla/diversity). + +[homepage]: https://www.contributor-covenant.org + +For answers to common questions about this code of conduct, see the FAQ at +https://www.contributor-covenant.org/faq. Translations are available at +https://www.contributor-covenant.org/translations. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 000000000..b3308df09 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,104 @@ +# Project Info + +First of all, thank you everyone who made pull requests for Uptime Kuma, I never thought GitHub Community can be that nice! And also because of this, I also never thought other people actually read my code and edit my code. It is not structed and commented so well, lol. Sorry about that. + +The project was created with vite.js (vue3). Then I created a sub-directory called "server" for server part. Both frontend and backend share the same package.json. + +The frontend code build into "dist" directory. The server uses "dist" as root. This is how production is working. + +Your IDE should follow the config in ".editorconfig". The most special thing is I set it to 4 spaces indentation. I know 2 spaces indentation became a kind of standard nowadays for js, but my eyes is not so comfortable for this. In my opinion, there is no callback-hell nowadays, it is good to go back 4 spaces world again. + +# Project Styles + +I personally do not like something need to learn so much and need to config so much before you can finally start the app. + +For example, recently, because I am not a python expert, I spent a 2 hours to resolve all problems in order to install and use the Apprise cli. Apprise requires so many hidden requirements, I have to figure out myself how to solve the problems by Google search for my OS. That is painful. I do not want Uptime Kuma to be like this way, so: + +- Easy to install for non-Docker users, no native build dependency is needed (at least for x86_64), no extra config, no extra effort to get it run +- Single container for Docker users, no very complex docker-composer file. Just map the volume and expose the port, then good to go +- All settings in frontend. +- Easy to use + +# Tools +- Node.js >= 14 +- Git +- IDE that supports .editorconfig (I am using Intellji Idea) +- A SQLite tool (I am using SQLite Expert Personal) + +# Prepare the dev + +```bash +npm install +``` + +# Backend Dev + +```bash +npm run start-server + +# Or + +node server/server.js + +``` + +It binds to 0.0.0.0:3001 by default. + + +## Backend Details + +It is mainly a socket.io app + express.js. + +express.js is just used for serving the frontend built files (index.html, .js and .css etc.) + +# Frontend Dev + +Start frontend dev server. Hot-reload enabled in this way. It binds to 0.0.0.0:3000. + +```bash +npm run dev +``` + +PS: You can ignore those scss warnings, those warnings are from Bootstrap that I cannot fix. + +You can use Vue Devtool Chrome extension for debugging. + +After the frontend server started. It cannot connect to the websocket server even you have started the server. You need to tell the frontend that is a dev env by running this in DevTool console and refresh: + +```javascript +localStorage.dev = "dev"; +``` + +So that the frontend will try to connect websocket server in 3001. + +Alternately, you can specific NODE_ENV to "development". + + +## Build the frontend + +```bash +npm run build +``` + +## Frontend Details + +Uptime Kuma Frontend is a single page application (SPA). Most paths are handled by Vue Router. + +The router in "src/main.js" + +As you can see, most data in frontend is stored in root level, even though you changed the current router to any other pages. + +The data and socket logic in "src/mixins/socket.js" + +# Database Migration + +TODO + +# Unit Test + +Yes, no unit test for now. I know it is very important, but at the same time my spare time is very limited. I want to implement my ideas first. I will go back to this in some points. + + + + + diff --git a/README.md b/README.md index ef8211368..30a5b3c4f 100644 --- a/README.md +++ b/README.md @@ -15,12 +15,12 @@ It is a self-hosted monitoring tool like "Uptime Robot". * Monitoring uptime for HTTP(s) / TCP / Ping. * Fancy, Reactive, Fast UI/UX. -* Notifications via Webhook, Telegram, Discord, Gotify, Slack, Pushover, Email (SMTP) and more by Apprise. +* Notifications via Webhook, Telegram, Discord, Gotify, Slack, Pushover, Email (SMTP) and more by Apprise. * 20 seconds interval. # How to Use -### Docker +## Docker ```bash # Create a volume @@ -38,9 +38,9 @@ Change Port and Volume docker run -d --restart=always -p :3001 -v :/app/data --name uptime-kuma louislam/uptime-kuma:1 ``` -### Without Docker +## Without Docker -Required Tools: Node.js >= 14, git and pm2. +Required Tools: Node.js >= 14, git and pm2. ```bash git clone https://github.com/louislam/uptime-kuma.git @@ -62,12 +62,25 @@ pm2 start npm --name uptime-kuma -- run start-server -- --port=80 --hostname=0.0 Browse to http://localhost:3001 after started. -### One-click Deploy to DigitalOcean + +## (Optional) One more step for Reverse Proxy + +This is optional for someone who want to do reverse proxy. + +Unlikely other web apps, Uptime Kuma is based on WebSocket. You need two more headers **"Upgrade"** and **"Connection"** in order to reverse proxy WebSocket. + +Please read wiki for more info: +https://github.com/louislam/uptime-kuma/wiki/Reverse-Proxy + +## One-click Deploy + + [![Deploy to DO](https://www.deploytodo.com/do-btn-blue.svg)](https://cloud.digitalocean.com/apps/new?repo=https://github.com/louislam/uptime-kuma/tree/master&refcode=e2c7eb658434) -Choose Cheapest Plan is enough. (US$ 5) - # How to Update ### Docker @@ -88,7 +101,7 @@ pm2 restart uptime-kuma # What's Next? -I will mark requests/issues to the next milestone. +I will mark requests/issues to the next milestone. https://github.com/louislam/uptime-kuma/milestones # More Screenshots @@ -104,10 +117,10 @@ Telegram Notification Sample: # Motivation -* I was looking for a self-hosted monitoring tool like "Uptime Robot", but it is hard to find a suitable one. One of the close one is statping. Unfortunately, it is not stable and unmaintained. +* I was looking for a self-hosted monitoring tool like "Uptime Robot", but it is hard to find a suitable one. One of the close one is statping. Unfortunately, it is not stable and unmaintained. * Want to build a fancy UI. * Learn Vue 3 and vite.js. -* Show the power of Bootstrap 5. +* Show the power of Bootstrap 5. * Try to use WebSocket with SPA instead of REST API. * Deploy my first Docker image to Docker Hub. @@ -119,6 +132,6 @@ If you love this project, please consider giving me a ⭐. If you want to report a bug or request a new feature. Free feel to open a new issue. -If you want to modify Uptime Kuma, this guideline maybe useful for you: https://github.com/louislam/uptime-kuma/wiki/%5BDev%5D-Setup-Development-Environment +If you want to modify Uptime Kuma, this guideline maybe useful for you: https://github.com/louislam/uptime-kuma/blob/master/CONTRIBUTING.md English proofreading is needed too, because my grammar is not that great sadly. Feel free to correct my grammar in this Readme, source code or wiki. diff --git a/db/patch4.sql b/db/patch4.sql index 66f93596e..ff40da2e2 100644 --- a/db/patch4.sql +++ b/db/patch4.sql @@ -1,4 +1,6 @@ -- You should not modify if this have pushed to Github, unless it does serious wrong with the db. +-- OK.... serious wrong, missing maxretries column +-- Developers should patch it manually if you have missing the maxretries column PRAGMA foreign_keys=off; BEGIN TRANSACTION; @@ -20,11 +22,12 @@ create table monitor_dg_tmp port INTEGER, created_date DATETIME, keyword VARCHAR(255), + maxretries INTEGER NOT NULL DEFAULT 0, ignore_tls BOOLEAN default 0 not null, upside_down BOOLEAN default 0 not null ); -insert into monitor_dg_tmp(id, name, active, user_id, interval, url, type, weight, hostname, port, created_date, keyword) select id, name, active, user_id, interval, url, type, weight, hostname, port, created_date, keyword from monitor; +insert into monitor_dg_tmp(id, name, active, user_id, interval, url, type, weight, hostname, port, created_date, keyword, maxretries) select id, name, active, user_id, interval, url, type, weight, hostname, port, created_date, keyword, maxretries from monitor; drop table monitor; diff --git a/dockerfile b/dockerfile index 1a5c5998c..bad4a1ad8 100644 --- a/dockerfile +++ b/dockerfile @@ -11,21 +11,9 @@ RUN apk add --no-cache --virtual .build-deps make g++ python3 python3-dev && \ # Touching above code may causes sqlite3 re-compile again, painful slow. # Install apprise -# Hate pip!!! I never run pip install successfully in first run for anything in my life without Google :/ -# Compilation Fail 1 => Google Search "alpine ffi.h" => Add libffi-dev -# Compilation Fail 2 => Google Search "alpine cargo" => Add cargo -# Compilation Fail 3 => Google Search "alpine opensslv.h" => Add openssl-dev -# Compilation Fail 4 => Google Search "alpine opensslv.h" again => Change to libressl-dev musl-dev -# Compilation Fail 5 => Google Search "ERROR: libressl3.3-libtls-3.3.3-r0: trying to overwrite usr/lib/libtls.so.20 owned by libretls-3.3.3-r0." again => Change back to openssl-dev with musl-dev -# Runtime Error => ModuleNotFoundError: No module named 'six' => pip3 install six -# Runtime Error 2 => ModuleNotFoundError: No module named 'six' => apk add py3-six -ENV CRYPTOGRAPHY_DONT_BUILD_RUST=1 -RUN apk add --no-cache py3-six cargo -RUN apk add --no-cache --virtual .build-deps python3 py3-pip libffi-dev musl-dev openssl-dev python3-dev && \ - pip3 install apprise && \ - pip3 cache purge && \ - rm -rf /root/.cache && \ - apk del .build-deps +RUN apk add --no-cache python3 py3-cryptography py3-pip py3-six py3-yaml py3-click py3-markdown py3-requests py3-requests-oauthlib +RUN pip3 --no-cache-dir install apprise && \ + rm -rf /root/.cache RUN apprise --version # New things add here @@ -33,8 +21,9 @@ RUN apprise --version FROM release-base AS build COPY . . -RUN npm install -RUN npm run build +RUN npm install && \ + npm run build && \ + npm prune FROM release-base AS release-final diff --git a/package-lock.json b/package-lock.json index 98d445e50..c62dc8eed 100644 --- a/package-lock.json +++ b/package-lock.json @@ -96,6 +96,25 @@ } } }, + "@babel/eslint-parser": { + "version": "7.14.7", + "resolved": "https://registry.npmjs.org/@babel/eslint-parser/-/eslint-parser-7.14.7.tgz", + "integrity": "sha512-6WPwZqO5priAGIwV6msJcdc9TsEPzYeYdS/Xuoap+/ihkgN6dzHp2bcAAwyWZ5bLzk0vvjDmKvRwkqNaiJ8BiQ==", + "dev": true, + "requires": { + "eslint-scope": "^5.1.1", + "eslint-visitor-keys": "^2.1.0", + "semver": "^6.3.0" + }, + "dependencies": { + "semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "dev": true + } + } + }, "@babel/generator": { "version": "7.14.8", "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.14.8.tgz", @@ -600,6 +619,16 @@ "@types/node": "*" } }, + "@types/bootstrap": { + "version": "5.0.17", + "resolved": "https://registry.npmjs.org/@types/bootstrap/-/bootstrap-5.0.17.tgz", + "integrity": "sha512-uQQQ3p+zw10VjZLvtCuKWI6QgVCYEnK/yHnno3gyEhikfQdiZexS2XPxjWRboGmX135o470GkmCta9eAgQMVLQ==", + "dev": true, + "requires": { + "@popperjs/core": "^2.9.2", + "@types/jquery": "*" + } + }, "@types/component-emitter": { "version": "1.2.10", "resolved": "https://registry.npmjs.org/@types/component-emitter/-/component-emitter-1.2.10.tgz", @@ -676,6 +705,15 @@ "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-1.8.1.tgz", "integrity": "sha512-e+2rjEwK6KDaNOm5Aa9wNGgyS9oSZU/4pfSMMPYNOfjvFI0WVXm29+ITRFr6aKDvvKo7uU1jV68MW4ScsfDi7Q==" }, + "@types/jquery": { + "version": "3.5.6", + "resolved": "https://registry.npmjs.org/@types/jquery/-/jquery-3.5.6.tgz", + "integrity": "sha512-SmgCQRzGPId4MZQKDj9Hqc6kSXFNWZFHpELkyK8AQhf8Zr6HKfCzFv9ZC1Fv3FyQttJZOlap3qYb12h61iZAIg==", + "dev": true, + "requires": { + "@types/sizzle": "*" + } + }, "@types/keygrip": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/@types/keygrip/-/keygrip-1.0.2.tgz", @@ -760,6 +798,12 @@ "@types/node": "*" } }, + "@types/sizzle": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/@types/sizzle/-/sizzle-2.3.3.tgz", + "integrity": "sha512-JYM8x9EGF163bEyhdJBpR2QX1R5naCJHC8ucJylJ3w9/CVBaskdQ8WqBf8MmQrd1kRvp/a4TS8HJ+bxzR7ZJYQ==", + "dev": true + }, "@types/unist": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.6.tgz", @@ -6673,6 +6717,12 @@ "is-typedarray": "^1.0.0" } }, + "typescript": { + "version": "4.3.5", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.3.5.tgz", + "integrity": "sha512-DqQgihaQ9cUrskJo9kIyW/+g0Vxsk8cDtZ52a3NGh0YNTfpUSArXSohyUGnvbPazEPLu398C0UxmKSOrPumUzA==", + "dev": true + }, "unc-path-regex": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/unc-path-regex/-/unc-path-regex-0.1.2.tgz", diff --git a/package.json b/package.json index df39d0cf8..7b80ae39b 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,7 @@ "url": "https://github.com/louislam/uptime-kuma.git" }, "engines": { - "node": ">=14" + "node": "14.*" }, "scripts": { "dev": "vite --host", @@ -56,6 +56,8 @@ "vue-toastification": "^2.0.0-rc.1" }, "devDependencies": { + "@babel/eslint-parser": "^7.13.10", + "@types/bootstrap": "^5.0.17", "@vitejs/plugin-legacy": "^1.5.0", "@vitejs/plugin-vue": "^1.3.0", "@vue/compiler-sfc": "^3.1.5", @@ -66,6 +68,7 @@ "stylelint": "^13.13.1", "stylelint-config-recommended": "^5.0.0", "stylelint-config-standard": "^22.0.0", + "typescript": "^4.3.5", "vite": "^2.4.4" } } diff --git a/server/database.js b/server/database.js index e386543a8..571313bc4 100644 --- a/server/database.js +++ b/server/database.js @@ -1,8 +1,9 @@ const fs = require("fs"); -const {sleep} = require("./util"); -const {R} = require("redbean-node"); -const {setSetting, setting} = require("./util-server"); - +const { sleep } = require("../src/util"); +const { R } = require("redbean-node"); +const { + setSetting, setting, +} = require("./util-server"); class Database { @@ -95,7 +96,7 @@ class Database { const listener = (reason, p) => { Database.noReject = false; }; - process.addListener('unhandledRejection', listener); + process.addListener("unhandledRejection", listener); console.log("Closing DB") @@ -112,7 +113,7 @@ class Database { } console.log("SQLite closed") - process.removeListener('unhandledRejection', listener); + process.removeListener("unhandledRejection", listener); } } diff --git a/server/model/monitor.js b/server/model/monitor.js index b725f43c6..49fcfb303 100644 --- a/server/model/monitor.js +++ b/server/model/monitor.js @@ -1,22 +1,16 @@ -const https = require('https'); +const https = require("https"); const dayjs = require("dayjs"); -const utc = require('dayjs/plugin/utc') -var timezone = require('dayjs/plugin/timezone') +const utc = require("dayjs/plugin/utc") +let timezone = require("dayjs/plugin/timezone") dayjs.extend(utc) dayjs.extend(timezone) const axios = require("axios"); -const {Prometheus} = require("../prometheus"); -const {debug, UP, DOWN, PENDING} = require("../util"); -const {tcping, ping, checkCertificate} = require("../util-server"); -const {R} = require("redbean-node"); -const {BeanModel} = require("redbean-node/dist/bean-model"); -const {Notification} = require("../notification") - -// Use Custom agent to disable session reuse -// https://github.com/nodejs/node/issues/3940 -const customAgent = new https.Agent({ - maxCachedSessions: 0 -}); +const { Prometheus } = require("../prometheus"); +const { debug, UP, DOWN, PENDING, flipStatus } = require("../../src/util"); +const { tcping, ping, checkCertificate } = require("../util-server"); +const { R } = require("redbean-node"); +const { BeanModel } = require("redbean-node/dist/bean-model"); +const { Notification } = require("../notification") /** * status: @@ -30,7 +24,7 @@ class Monitor extends BeanModel { let notificationIDList = {}; let list = await R.find("monitor_notification", " monitor_id = ? ", [ - this.id + this.id, ]) for (let bean of list) { @@ -49,10 +43,28 @@ class Monitor extends BeanModel { type: this.type, interval: this.interval, keyword: this.keyword, - notificationIDList + ignoreTls: this.getIgnoreTls(), + upsideDown: this.isUpsideDown(), + notificationIDList, }; } + /** + * Parse to boolean + * @returns {boolean} + */ + getIgnoreTls() { + return Boolean(this.ignoreTls) + } + + /** + * Parse to boolean + * @returns {boolean} + */ + isUpsideDown() { + return Boolean(this.upsideDown); + } + start(io) { let previousBeat = null; let retries = 0; @@ -63,7 +75,7 @@ class Monitor extends BeanModel { if (! previousBeat) { previousBeat = await R.findOne("heartbeat", " monitor_id = ? ORDER BY time DESC", [ - this.id + this.id, ]) } @@ -74,9 +86,13 @@ class Monitor extends BeanModel { bean.time = R.isoDateTime(dayjs.utc()); bean.status = DOWN; + if (this.isUpsideDown()) { + bean.status = flipStatus(bean.status); + } + // Duration if (! isFirstBeat) { - bean.duration = dayjs(bean.time).diff(dayjs(previousBeat.time), 'second'); + bean.duration = dayjs(bean.time).diff(dayjs(previousBeat.time), "second"); } else { bean.duration = 0; } @@ -84,9 +100,17 @@ class Monitor extends BeanModel { try { if (this.type === "http" || this.type === "keyword") { let startTime = dayjs().valueOf(); + + // Use Custom agent to disable session reuse + // https://github.com/nodejs/node/issues/3940 let res = await axios.get(this.url, { - headers: { "User-Agent": "Uptime-Kuma" }, - httpsAgent: customAgent, + headers: { + "User-Agent": "Uptime-Kuma", + }, + httpsAgent: new https.Agent({ + maxCachedSessions: 0, + rejectUnauthorized: ! this.getIgnoreTls(), + }), }); bean.msg = `${res.status} - ${res.statusText}` bean.ping = dayjs().valueOf() - startTime; @@ -124,7 +148,6 @@ class Monitor extends BeanModel { } - } else if (this.type === "port") { bean.ping = await tcping(this.hostname, this.port); bean.msg = "" @@ -136,14 +159,29 @@ class Monitor extends BeanModel { bean.status = UP; } + if (this.isUpsideDown()) { + bean.status = flipStatus(bean.status); + + if (bean.status === DOWN) { + throw new Error("Flip UP to DOWN"); + } + } + retries = 0; } catch (error) { - if ((this.maxretries > 0) && (retries < this.maxretries)) { + + bean.msg = error.message; + + // If UP come in here, it must be upside down mode + // Just reset the retries + if (this.isUpsideDown() && bean.status === UP) { + retries = 0; + + } else if ((this.maxretries > 0) && (retries < this.maxretries)) { retries++; bean.status = PENDING; } - bean.msg = error.message; } // * ? -> ANY STATUS = important [isFirstBeat] @@ -168,8 +206,8 @@ class Monitor extends BeanModel { // Send only if the first beat is DOWN if (!isFirstBeat || bean.status === DOWN) { - let notificationList = await R.getAll(`SELECT notification.* FROM notification, monitor_notification WHERE monitor_id = ? AND monitor_notification.notification_id = notification.id `, [ - this.id + let notificationList = await R.getAll("SELECT notification.* FROM notification, monitor_notification WHERE monitor_id = ? AND monitor_notification.notification_id = notification.id ", [ + this.id, ]) let text; @@ -181,7 +219,7 @@ class Monitor extends BeanModel { let msg = `[${this.name}] [${text}] ${bean.msg}`; - for(let notification of notificationList) { + for (let notification of notificationList) { try { await Notification.send(JSON.parse(notification.config), msg, await this.toJSON(), bean.toJSON()) } catch (e) { @@ -194,7 +232,6 @@ class Monitor extends BeanModel { bean.important = false; } - if (bean.status === UP) { console.info(`Monitor #${this.id} '${this.name}': Successful Response: ${bean.ping} ms | Interval: ${this.interval} seconds | Type: ${this.type}`) } else if (bean.status === PENDING) { @@ -221,9 +258,12 @@ class Monitor extends BeanModel { clearInterval(this.heartbeatInterval) } - // Helper Method: - // returns URL object for further usage - // returns null if url is invalid + /** + * Helper Method: + * returns URL object for further usage + * returns null if url is invalid + * @returns {null|URL} + */ getUrl() { try { return new URL(this.url); @@ -232,10 +272,14 @@ class Monitor extends BeanModel { } } - // Store TLS info to database + /** + * Store TLS info to database + * @param checkCertificateResult + * @returns {Promise} + */ async updateTlsInfo(checkCertificateResult) { let tls_info_bean = await R.findOne("monitor_tls_info", "monitor_id = ?", [ - this.id + this.id, ]); if (tls_info_bean == null) { tls_info_bean = R.dispense("monitor_tls_info"); @@ -264,15 +308,15 @@ class Monitor extends BeanModel { AND ping IS NOT NULL AND monitor_id = ? `, [ -duration, - monitorID + monitorID, ])); io.to(userID).emit("avgPing", monitorID, avgPing); } static async sendCertInfo(io, monitorID, userID) { - let tls_info = await R.findOne("monitor_tls_info", "monitor_id = ?", [ - monitorID + let tls_info = await R.findOne("monitor_tls_info", "monitor_id = ?", [ + monitorID, ]); if (tls_info != null) { io.to(userID).emit("certInfo", monitorID, tls_info.info_json); @@ -294,7 +338,7 @@ class Monitor extends BeanModel { WHERE time > DATETIME('now', ? || ' hours') AND monitor_id = ? `, [ -duration, - monitorID + monitorID, ]); let downtime = 0; @@ -318,7 +362,7 @@ class Monitor extends BeanModel { // Handle if heartbeat duration longer than the target duration // e.g. Heartbeat duration = 28hrs, but target duration = 24hrs if (value > sec) { - let trim = dayjs.utc().diff(dayjs(time), 'second'); + let trim = dayjs.utc().diff(dayjs(time), "second"); value = sec - trim; if (value < 0) { @@ -339,8 +383,6 @@ class Monitor extends BeanModel { } } - - io.to(userID).emit("uptime", monitorID, duration, uptime); } } diff --git a/server/notification.js b/server/notification.js index f25ca80a7..0e963a128 100644 --- a/server/notification.js +++ b/server/notification.js @@ -235,6 +235,41 @@ class Notification { return Notification.apprise(notification, msg) + } else if (notification.type === "lunasea") { + let lunaseadevice = "https://notify.lunasea.app/v1/custom/device/" + notification.lunaseaDevice + + try { + if (heartbeatJSON == null) { + let testdata = { + "title": "Uptime Kuma Alert", + "body": "Testing Successful.", + } + await axios.post(lunaseadevice, testdata) + return okMsg; + } + + if (heartbeatJSON["status"] == 0) { + let downdata = { + "title": "UptimeKuma Alert:" + monitorJSON["name"], + "body": "[🔴 Down]" + heartbeatJSON["msg"] + "\nTime (UTC):" + heartbeatJSON["time"], + } + await axios.post(lunaseadevice, downdata) + return okMsg; + } + + if (heartbeatJSON["status"] == 1) { + let updata = { + "title": "UptimeKuma Alert:" + monitorJSON["name"], + "body": "[✅ Up]" + heartbeatJSON["msg"] + "\nTime (UTC):" + heartbeatJSON["time"], + } + await axios.post(lunaseadevice, updata) + return okMsg; + } + + } catch (error) { + throwGeneralAxiosError(error) + } + } else { throw new Error("Notification type is not supported") } diff --git a/server/server.js b/server/server.js index 70198015b..25c44ebf9 100644 --- a/server/server.js +++ b/server/server.js @@ -1,26 +1,46 @@ -console.log("Welcome to Uptime Kuma ") -console.log("Importing libraries") -const express = require("express"); -const http = require("http"); -const { Server } = require("socket.io"); -const dayjs = require("dayjs"); -const { R } = require("redbean-node"); -const jwt = require("jsonwebtoken"); -const Monitor = require("./model/monitor"); +console.log("Welcome to Uptime Kuma") + +const { sleep, debug } = require("../src/util"); + +console.log("Importing Node libraries") const fs = require("fs"); -const { getSettings } = require("./util-server"); -const { Notification } = require("./notification") +const http = require("http"); + +console.log("Importing 3rd-party libraries") +debug("Importing express"); +const express = require("express"); +debug("Importing socket.io"); +const { Server } = require("socket.io"); +debug("Importing dayjs"); +const dayjs = require("dayjs"); +debug("Importing redbean-node"); +const { R } = require("redbean-node"); +debug("Importing jsonwebtoken"); +const jwt = require("jsonwebtoken"); +debug("Importing http-graceful-shutdown"); const gracefulShutdown = require("http-graceful-shutdown"); -const Database = require("./database"); -const { sleep } = require("./util"); -const args = require("args-parser")(process.argv); +debug("Importing prometheus-api-metrics"); const prometheusAPIMetrics = require("prometheus-api-metrics"); + +console.log("Importing this project modules"); +debug("Importing Monitor"); +const Monitor = require("./model/monitor"); +debug("Importing Settings"); +const { getSettings, setSettings, setting } = require("./util-server"); +debug("Importing Notification"); +const { Notification } = require("./notification"); +debug("Importing Database"); +const Database = require("./database"); + const { basicAuth } = require("./auth"); const { login } = require("./auth"); const passwordHash = require("./password-hash"); + +const args = require("args-parser")(process.argv); + const version = require("../package.json").version; -const hostname = args.host || "0.0.0.0" -const port = process.env.PORT || args.port || 3001 +const hostname = process.env.HOST || args.host || "0.0.0.0" +const port = parseInt(process.env.PORT || args.port || 3001); console.info("Version: " + version) @@ -94,11 +114,18 @@ let indexHTML = fs.readFileSync("./dist/index.html").toString(); socket.emit("setup") } + if (await setting("disableAuth")) { + console.log("Disabled Auth: auto login to admin") + await afterLogin(socket, await R.findOne("user", " username = 'admin' ")) + } + socket.on("disconnect", () => { totalClient--; }); + // *************************** // Public API + // *************************** socket.on("loginByToken", async (token, callback) => { @@ -191,8 +218,11 @@ let indexHTML = fs.readFileSync("./dist/index.html").toString(); } }); + // *************************** // Auth Only API + // *************************** + // Add a new monitor socket.on("add", async (monitor, callback) => { try { checkLogin(socket) @@ -224,6 +254,7 @@ let indexHTML = fs.readFileSync("./dist/index.html").toString(); } }); + // Edit a monitor socket.on("editMonitor", async (monitor, callback) => { try { checkLogin(socket) @@ -242,6 +273,8 @@ let indexHTML = fs.readFileSync("./dist/index.html").toString(); bean.maxretries = monitor.maxretries; bean.port = monitor.port; bean.keyword = monitor.keyword; + bean.ignoreTls = monitor.ignoreTls; + bean.upsideDown = monitor.upsideDown; await R.store(bean) @@ -397,13 +430,32 @@ let indexHTML = fs.readFileSync("./dist/index.html").toString(); } }); - socket.on("getSettings", async (type, callback) => { + socket.on("getSettings", async (callback) => { try { checkLogin(socket) callback({ ok: true, - data: await getSettings(type), + data: await getSettings("general"), + }); + + } catch (e) { + callback({ + ok: false, + msg: e.message, + }); + } + }); + + socket.on("setSettings", async (data, callback) => { + try { + checkLogin(socket) + + await setSettings("general", data) + + callback({ + ok: true, + msg: "Saved" }); } catch (e) { @@ -553,6 +605,8 @@ async function afterLogin(socket, user) { } sendNotificationList(socket) + + socket.emit("autoLogin") } async function getMonitorJSONList(userID) { diff --git a/server/util-server.js b/server/util-server.js index 43aa5ccfb..c329d0ab6 100644 --- a/server/util-server.js +++ b/server/util-server.js @@ -40,9 +40,15 @@ exports.ping = function (hostname) { } exports.setting = async function (key) { - return await R.getCell("SELECT `value` FROM setting WHERE `key` = ? ", [ + let value = await R.getCell("SELECT `value` FROM setting WHERE `key` = ? ", [ key, - ]) + ]); + + try { + return JSON.parse(value); + } catch (e) { + return value; + } } exports.setSetting = async function (key, value) { @@ -53,24 +59,53 @@ exports.setSetting = async function (key, value) { bean = R.dispense("setting") bean.key = key; } - bean.value = value; + bean.value = JSON.stringify(value); await R.store(bean) } exports.getSettings = async function (type) { - let list = await R.getAll("SELECT * FROM setting WHERE `type` = ? ", [ + let list = await R.getAll("SELECT `key`, `value` FROM setting WHERE `type` = ? ", [ type, ]) let result = {}; for (let row of list) { - result[row.key] = row.value; + try { + result[row.key] = JSON.parse(row.value); + } catch (e) { + result[row.key] = row.value; + } } return result; } +exports.setSettings = async function (type, data) { + let keyList = Object.keys(data); + + let promiseList = []; + + for (let key of keyList) { + let bean = await R.findOne("setting", " `key` = ? ", [ + key + ]); + + if (bean == null) { + bean = R.dispense("setting"); + bean.type = type; + bean.key = key; + } + + if (bean.type === type) { + bean.value = JSON.stringify(data[key]); + promiseList.push(R.store(bean)) + } + } + + await Promise.all(promiseList); +} + // ssl-checker by @dyaa // param: res - response object from axios // return an object containing the certificate information diff --git a/server/util.js b/server/util.js deleted file mode 100644 index 6e90dc42b..000000000 --- a/server/util.js +++ /dev/null @@ -1,25 +0,0 @@ -// Common JS cannot be used in frontend sadly -// sleep, ucfirst is duplicated in ../src/util-frontend.js - -exports.DOWN = 0; -exports.UP = 1; -exports.PENDING = 2; - -exports.sleep = function (ms) { - return new Promise(resolve => setTimeout(resolve, ms)); -} - -exports.ucfirst = function (str) { - if (! str) { - return str; - } - - const firstLetter = str.substr(0, 1); - return firstLetter.toUpperCase() + str.substr(1); -} - -exports.debug = (msg) => { - if (process.env.NODE_ENV === "development") { - console.log(msg) - } -} diff --git a/src/components/Confirm.vue b/src/components/Confirm.vue index 36d3781a6..b235824be 100644 --- a/src/components/Confirm.vue +++ b/src/components/Confirm.vue @@ -13,10 +13,10 @@ @@ -33,6 +33,14 @@ export default { type: String, default: "btn-primary", }, + yesText: { + type: String, + default: "Yes", + }, + noText: { + type: String, + default: "No", + }, }, data: () => ({ modal: null, diff --git a/src/components/CountUp.vue b/src/components/CountUp.vue index c63aea14b..b321fde19 100644 --- a/src/components/CountUp.vue +++ b/src/components/CountUp.vue @@ -3,9 +3,9 @@ {{ value }} - diff --git a/src/util-frontend.js b/src/util-frontend.js index d80d4385e..07b1914e1 100644 --- a/src/util-frontend.js +++ b/src/util-frontend.js @@ -5,19 +5,6 @@ import utc from "dayjs/plugin/utc"; dayjs.extend(utc) dayjs.extend(timezone) -export function sleep(ms) { - return new Promise(resolve => setTimeout(resolve, ms)); -} - -export function ucfirst(str) { - if (! str) { - return str; - } - - const firstLetter = str.substr(0, 1); - return firstLetter.toUpperCase() + str.substr(1); -} - function getTimezoneOffset(timeZone) { const now = new Date(); const tzString = now.toLocaleString("en-US", { diff --git a/src/util.js b/src/util.js new file mode 100644 index 000000000..9b4c4dab7 --- /dev/null +++ b/src/util.js @@ -0,0 +1,34 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.debug = exports.ucfirst = exports.sleep = exports.flipStatus = exports.PENDING = exports.UP = exports.DOWN = void 0; +exports.DOWN = 0; +exports.UP = 1; +exports.PENDING = 2; +function flipStatus(s) { + if (s === exports.UP) { + return exports.DOWN; + } + if (s === exports.DOWN) { + return exports.UP; + } + return s; +} +exports.flipStatus = flipStatus; +function sleep(ms) { + return new Promise(resolve => setTimeout(resolve, ms)); +} +exports.sleep = sleep; +function ucfirst(str) { + if (!str) { + return str; + } + const firstLetter = str.substr(0, 1); + return firstLetter.toUpperCase() + str.substr(1); +} +exports.ucfirst = ucfirst; +function debug(msg) { + if (process.env.NODE_ENV === "development") { + console.log(msg); + } +} +exports.debug = debug; diff --git a/src/util.ts b/src/util.ts new file mode 100644 index 000000000..23446c830 --- /dev/null +++ b/src/util.ts @@ -0,0 +1,43 @@ +// Common Util for frontend and backend +// Backend uses the compiled file util.js +// Frontend uses util.ts +// Need to run "tsc" to compile if there are any changes. + +export const DOWN = 0; +export const UP = 1; +export const PENDING = 2; + +export function flipStatus(s) { + if (s === UP) { + return DOWN; + } + + if (s === DOWN) { + return UP; + } + + return s; +} + +export function sleep(ms) { + return new Promise(resolve => setTimeout(resolve, ms)); +} + +/** + * PHP's ucfirst + * @param str + */ +export function ucfirst(str) { + if (! str) { + return str; + } + + const firstLetter = str.substr(0, 1); + return firstLetter.toUpperCase() + str.substr(1); +} + +export function debug(msg) { + if (process.env.NODE_ENV === "development") { + console.log(msg) + } +} diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 000000000..d47fb63eb --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,14 @@ +{ + "compileOnSave": true, + "compilerOptions": { + "target": "ES2018", + "module": "commonjs", + "removeComments": true, + "preserveConstEnums": true, + "sourceMap": false, + "files.insertFinalNewline": true + }, + "files": [ + "./server/util.ts" + ] +}