mirror of
https://github.com/louislam/uptime-kuma.git
synced 2025-04-20 07:36:05 -04:00
Merge branch 'master' into reduce_docker_image_size
This commit is contained in:
commit
713bbe0014
@ -16,3 +16,6 @@ indent_size = 2
|
||||
|
||||
[*.yml]
|
||||
indent_size = 2
|
||||
|
||||
[*.vue]
|
||||
trim_trailing_whitespace = false
|
||||
|
38
.eslintrc.js
38
.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"],
|
||||
},
|
||||
}
|
||||
|
128
CODE_OF_CONDUCT.md
Normal file
128
CODE_OF_CONDUCT.md
Normal file
@ -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.
|
104
CONTRIBUTING.md
Normal file
104
CONTRIBUTING.md
Normal file
@ -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.
|
||||
|
||||
|
||||
|
||||
|
||||
|
35
README.md
35
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 <YOUR_PORT>:3001 -v <YOUR_DIR OR VOLUME>:/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
|
||||
|
||||
<!---
|
||||
Abort. Heroku instance killed the server.js if idle, stupid.
|
||||
[](https://heroku.com/deploy?template=https://github.com/louislam/uptime-kuma/tree/1.0.8)
|
||||
-->
|
||||
|
||||
[](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.
|
||||
|
@ -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;
|
||||
|
||||
|
23
dockerfile
23
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
|
||||
|
||||
|
50
package-lock.json
generated
50
package-lock.json
generated
@ -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",
|
||||
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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<void>}
|
||||
*/
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
@ -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")
|
||||
}
|
||||
|
@ -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) {
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
@ -13,10 +13,10 @@
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn" :class="btnStyle" data-bs-dismiss="modal" @click="yes">
|
||||
Yes
|
||||
{{ yesText }}
|
||||
</button>
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">
|
||||
No
|
||||
{{ noText }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@ -33,6 +33,14 @@ export default {
|
||||
type: String,
|
||||
default: "btn-primary",
|
||||
},
|
||||
yesText: {
|
||||
type: String,
|
||||
default: "Yes",
|
||||
},
|
||||
noText: {
|
||||
type: String,
|
||||
default: "No",
|
||||
},
|
||||
},
|
||||
data: () => ({
|
||||
modal: null,
|
||||
|
@ -3,9 +3,9 @@
|
||||
<span v-else>{{ value }}</span>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
<script lang="ts">
|
||||
|
||||
import { sleep } from "../util-frontend"
|
||||
import { sleep } from "../util.ts"
|
||||
|
||||
export default {
|
||||
|
||||
|
@ -13,33 +13,16 @@
|
||||
<div class="mb-3">
|
||||
<label for="type" class="form-label">Notification Type</label>
|
||||
<select id="type" v-model="notification.type" class="form-select">
|
||||
<option value="telegram">
|
||||
Telegram
|
||||
</option>
|
||||
<option value="webhook">
|
||||
Webhook
|
||||
</option>
|
||||
<option value="smtp">
|
||||
Email (SMTP)
|
||||
</option>
|
||||
<option value="discord">
|
||||
Discord
|
||||
</option>
|
||||
<option value="signal">
|
||||
Signal
|
||||
</option>
|
||||
<option value="gotify">
|
||||
Gotify
|
||||
</option>
|
||||
<option value="slack">
|
||||
Slack
|
||||
</option>
|
||||
<option value="pushover">
|
||||
Pushover
|
||||
</option>
|
||||
<option value="apprise">
|
||||
Apprise (Support 50+ Notification services)
|
||||
</option>
|
||||
<option value="telegram">Telegram</option>
|
||||
<option value="webhook">Webhook</option>
|
||||
<option value="smtp">Email (SMTP)</option>
|
||||
<option value="discord">Discord</option>
|
||||
<option value="signal">Signal</option>
|
||||
<option value="gotify">Gotify</option>
|
||||
<option value="slack">Slack</option>
|
||||
<option value="pushover">Pushover</option>
|
||||
<option value="lunasea">LunaSea</option>
|
||||
<option value="apprise">Apprise (Support 50+ Notification services)</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
@ -323,6 +306,17 @@
|
||||
</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template v-if="notification.type === 'lunasea'">
|
||||
<div class="mb-3">
|
||||
<label for="lunasea-device" class="form-label">LunaSea Device ID<span style="color:red;"><sup>*</sup></span></label>
|
||||
<input id="lunasea-device" v-model="notification.lunaseaDevice" type="text" class="form-control" required>
|
||||
<div class="form-text">
|
||||
<p><span style="color:red;"><sup>*</sup></span>Required</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button v-if="id" type="button" class="btn btn-danger" :disabled="processing" @click="deleteConfirm">
|
||||
@ -345,9 +339,9 @@
|
||||
</Confirm>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
<script lang="ts">
|
||||
import { Modal } from "bootstrap"
|
||||
import { ucfirst } from "../util-frontend"
|
||||
import { ucfirst } from "../util.ts"
|
||||
import axios from "axios";
|
||||
import { useToast } from "vue-toastification"
|
||||
import Confirm from "./Confirm.vue";
|
||||
|
@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<div v-if="! $root.socket.connected && ! $root.socket.firstConnect" class="lost-connection">
|
||||
<div class="container-fluid">
|
||||
Lost connection to the socket server. Reconnecting...
|
||||
{{ $root.connectionErrorMsg }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
@ -29,6 +29,7 @@ export default {
|
||||
notificationList: [],
|
||||
windowWidth: window.innerWidth,
|
||||
showListMobile: false,
|
||||
connectionErrorMsg: "Cannot connect to the socket server. Reconnecting..."
|
||||
}
|
||||
},
|
||||
|
||||
@ -47,10 +48,6 @@ export default {
|
||||
transports: ["websocket"],
|
||||
});
|
||||
|
||||
socket.on("connect_error", (err) => {
|
||||
console.error(`Failed to connect to the backend. Socket.io connect_error: ${err.message}`);
|
||||
});
|
||||
|
||||
socket.on("info", (info) => {
|
||||
this.info = info;
|
||||
});
|
||||
@ -59,6 +56,11 @@ export default {
|
||||
this.$router.push("/setup")
|
||||
});
|
||||
|
||||
socket.on("autoLogin", (monitorID, data) => {
|
||||
this.loggedIn = true;
|
||||
this.storage().token = "autoLogin"
|
||||
});
|
||||
|
||||
socket.on("monitorList", (data) => {
|
||||
// Add Helper function
|
||||
Object.entries(data).forEach(([monitorID, monitor]) => {
|
||||
@ -136,8 +138,16 @@ export default {
|
||||
}
|
||||
});
|
||||
|
||||
socket.on("connect_error", (err) => {
|
||||
console.error(`Failed to connect to the backend. Socket.io connect_error: ${err.message}`);
|
||||
this.connectionErrorMsg = `Cannot connect to the socket server. [${err}] Reconnecting...`;
|
||||
this.socket.connected = false;
|
||||
this.socket.firstConnect = false;
|
||||
});
|
||||
|
||||
socket.on("disconnect", () => {
|
||||
console.log("disconnect")
|
||||
this.connectionErrorMsg = "Lost connection to the socket server. Reconnecting...";
|
||||
this.socket.connected = false;
|
||||
});
|
||||
|
||||
@ -151,8 +161,12 @@ export default {
|
||||
this.clearData()
|
||||
}
|
||||
|
||||
if (this.storage().token) {
|
||||
this.loginByToken(this.storage().token)
|
||||
let token = this.storage().token;
|
||||
|
||||
if (token) {
|
||||
if (token !== "autoLogin") {
|
||||
this.loginByToken(token)
|
||||
}
|
||||
} else {
|
||||
this.allowLoginDialog = true;
|
||||
}
|
||||
|
@ -15,7 +15,7 @@
|
||||
<font-awesome-icon icon="pause" /> Pause
|
||||
</button>
|
||||
<button v-if="! monitor.active" class="btn btn-primary" @click="resumeMonitor">
|
||||
<font-awesome-icon icon="pause" /> Resume
|
||||
<font-awesome-icon icon="play" /> Resume
|
||||
</button>
|
||||
<router-link :to=" '/edit/' + monitor.id " class="btn btn-secondary">
|
||||
<font-awesome-icon icon="edit" /> Edit
|
||||
|
@ -67,6 +67,25 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h2>Advanced</h2>
|
||||
|
||||
<div v-if="monitor.type === 'http' || monitor.type === 'keyword' " class="mb-3 form-check">
|
||||
<input id="ignore-tls" v-model="monitor.ignoreTls" class="form-check-input" type="checkbox" value="">
|
||||
<label class="form-check-label" for="ignore-tls">
|
||||
Ignore TLS/SSL error for HTTPS websites
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="mb-3 form-check">
|
||||
<input id="upside-down" v-model="monitor.upsideDown" class="form-check-input" type="checkbox">
|
||||
<label class="form-check-label" for="upside-down">
|
||||
Upside Down Mode
|
||||
</label>
|
||||
<div class="form-text">
|
||||
Flip the status upside down. If the service is reachable, it is DOWN.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<button class="btn btn-primary" type="submit" :disabled="processing">
|
||||
Save
|
||||
@ -149,6 +168,8 @@ export default {
|
||||
interval: 60,
|
||||
maxretries: 0,
|
||||
notificationIDList: {},
|
||||
ignoreTls: false,
|
||||
upsideDown: false,
|
||||
}
|
||||
} else if (this.isEdit) {
|
||||
this.$root.getSocket().emit("getMonitor", this.$route.params.id, (res) => {
|
||||
|
@ -27,38 +27,44 @@
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<h2>Change Password</h2>
|
||||
<form class="mb-3" @submit.prevent="savePassword">
|
||||
<div class="mb-3">
|
||||
<label for="current-password" class="form-label">Current Password</label>
|
||||
<input id="current-password" v-model="password.currentPassword" type="password" class="form-control" required>
|
||||
</div>
|
||||
<template v-if="loaded">
|
||||
<template v-if="! settings.disableAuth">
|
||||
<h2>Change Password</h2>
|
||||
<form class="mb-3" @submit.prevent="savePassword">
|
||||
<div class="mb-3">
|
||||
<label for="current-password" class="form-label">Current Password</label>
|
||||
<input id="current-password" v-model="password.currentPassword" type="password" class="form-control" required>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="new-password" class="form-label">New Password</label>
|
||||
<input id="new-password" v-model="password.newPassword" type="password" class="form-control" required>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="repeat-new-password" class="form-label">Repeat New Password</label>
|
||||
<input id="repeat-new-password" v-model="password.repeatNewPassword" type="password" class="form-control" :class="{ 'is-invalid' : invalidPassword }" required>
|
||||
<div class="invalid-feedback">
|
||||
The repeat password does not match.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<button class="btn btn-primary" type="submit">
|
||||
Update Password
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</template>
|
||||
|
||||
<h2>Advanced</h2>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="new-password" class="form-label">New Password</label>
|
||||
<input id="new-password" v-model="password.newPassword" type="password" class="form-control" required>
|
||||
<button v-if="settings.disableAuth" class="btn btn-outline-primary me-1" @click="enableAuth">Enable Auth</button>
|
||||
<button v-if="! settings.disableAuth" class="btn btn-primary me-1" @click="confirmDisableAuth">Disable Auth</button>
|
||||
<button v-if="! settings.disableAuth" class="btn btn-danger me-1" @click="$root.logout">Logout</button>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="repeat-new-password" class="form-label">Repeat New Password</label>
|
||||
<input id="repeat-new-password" v-model="password.repeatNewPassword" type="password" class="form-control" :class="{ 'is-invalid' : invalidPassword }" required>
|
||||
<div class="invalid-feedback">
|
||||
The repeat password does not match.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<button class="btn btn-primary" type="submit">
|
||||
Update Password
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<div>
|
||||
<button class="btn btn-danger" @click="$root.logout">
|
||||
Logout
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6">
|
||||
@ -87,15 +93,23 @@
|
||||
</div>
|
||||
|
||||
<NotificationDialog ref="notificationDialog" />
|
||||
|
||||
<Confirm ref="confirmDisableAuth" btn-style="btn-danger" yes-text="I understand, please disable" no-text="Leave" @yes="disableAuth">
|
||||
<p>Are you sure want to <strong>disable auth</strong>?</p>
|
||||
<p>It is for <strong>someone who have 3rd-party auth</strong> in front of Uptime Kuma such as Cloudflare Access.</p>
|
||||
<p>Please use it carefully.</p>
|
||||
</Confirm>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Confirm from "../components/Confirm.vue";
|
||||
import dayjs from "dayjs";
|
||||
import utc from "dayjs/plugin/utc"
|
||||
import timezone from "dayjs/plugin/timezone"
|
||||
import NotificationDialog from "../components/NotificationDialog.vue";
|
||||
dayjs.extend(utc)
|
||||
dayjs.extend(timezone)
|
||||
|
||||
import { timezoneList } from "../util-frontend";
|
||||
import { useToast } from "vue-toastification"
|
||||
const toast = useToast()
|
||||
@ -103,6 +117,7 @@ const toast = useToast()
|
||||
export default {
|
||||
components: {
|
||||
NotificationDialog,
|
||||
Confirm,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
@ -115,6 +130,10 @@ export default {
|
||||
newPassword: "",
|
||||
repeatNewPassword: "",
|
||||
},
|
||||
settings: {
|
||||
|
||||
},
|
||||
loaded: false,
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
@ -124,7 +143,7 @@ export default {
|
||||
},
|
||||
|
||||
mounted() {
|
||||
|
||||
this.loadSettings();
|
||||
},
|
||||
|
||||
methods: {
|
||||
@ -148,6 +167,36 @@ export default {
|
||||
})
|
||||
}
|
||||
},
|
||||
|
||||
loadSettings() {
|
||||
this.$root.getSocket().emit("getSettings", (res) => {
|
||||
this.settings = res.data;
|
||||
this.loaded = true;
|
||||
})
|
||||
},
|
||||
|
||||
saveSettings() {
|
||||
this.$root.getSocket().emit("setSettings", this.settings, (res) => {
|
||||
this.$root.toastRes(res);
|
||||
this.loadSettings();
|
||||
})
|
||||
},
|
||||
|
||||
confirmDisableAuth() {
|
||||
this.$refs.confirmDisableAuth.show();
|
||||
},
|
||||
|
||||
disableAuth() {
|
||||
this.settings.disableAuth = true;
|
||||
this.saveSettings();
|
||||
},
|
||||
|
||||
enableAuth() {
|
||||
this.settings.disableAuth = false;
|
||||
this.saveSettings();
|
||||
this.$root.storage().token = null;
|
||||
},
|
||||
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
@ -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", {
|
||||
|
34
src/util.js
Normal file
34
src/util.js
Normal file
@ -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;
|
43
src/util.ts
Normal file
43
src/util.ts
Normal file
@ -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)
|
||||
}
|
||||
}
|
14
tsconfig.json
Normal file
14
tsconfig.json
Normal file
@ -0,0 +1,14 @@
|
||||
{
|
||||
"compileOnSave": true,
|
||||
"compilerOptions": {
|
||||
"target": "ES2018",
|
||||
"module": "commonjs",
|
||||
"removeComments": true,
|
||||
"preserveConstEnums": true,
|
||||
"sourceMap": false,
|
||||
"files.insertFinalNewline": true
|
||||
},
|
||||
"files": [
|
||||
"./server/util.ts"
|
||||
]
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user