mirror of
https://github.com/louislam/uptime-kuma.git
synced 2025-02-14 05:31:28 -05:00
Merge branch 'louislam:master' into master
This commit is contained in:
commit
f05651d235
@ -1,5 +1,4 @@
|
|||||||
/.idea
|
/.idea
|
||||||
/dist
|
|
||||||
/node_modules
|
/node_modules
|
||||||
/data
|
/data
|
||||||
/out
|
/out
|
||||||
|
22
.github/ISSUE_TEMPLATE/ask-for-help.md
vendored
22
.github/ISSUE_TEMPLATE/ask-for-help.md
vendored
@ -1,22 +0,0 @@
|
|||||||
---
|
|
||||||
name: Ask for help
|
|
||||||
about: You can ask any question related to Uptime Kuma.
|
|
||||||
title: ''
|
|
||||||
labels: help
|
|
||||||
assignees: ''
|
|
||||||
|
|
||||||
---
|
|
||||||
**Is it a duplicate question?**
|
|
||||||
Please search in Issues without filters: https://github.com/louislam/uptime-kuma/issues?q=
|
|
||||||
|
|
||||||
|
|
||||||
**Describe your problem**
|
|
||||||
|
|
||||||
|
|
||||||
**Info**
|
|
||||||
Uptime Kuma Version:
|
|
||||||
Using Docker?: Yes/No
|
|
||||||
Docker Version:
|
|
||||||
Node.js Version (Without Docker only):
|
|
||||||
OS:
|
|
||||||
Browser:
|
|
68
.github/ISSUE_TEMPLATE/ask-for-help.yaml
vendored
Normal file
68
.github/ISSUE_TEMPLATE/ask-for-help.yaml
vendored
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
name: "❓ Ask for help"
|
||||||
|
description: "Submit any question related to Uptime Kuma"
|
||||||
|
#title: "[Help] "
|
||||||
|
labels: [help]
|
||||||
|
body:
|
||||||
|
- type: checkboxes
|
||||||
|
id: no-duplicate-issues
|
||||||
|
attributes:
|
||||||
|
label: "⚠️ Please verify that this bug has NOT been raised before."
|
||||||
|
description: "Search in the issues sections by clicking [HERE](https://github.com/louislam/uptime-kuma/issues?q=)"
|
||||||
|
options:
|
||||||
|
- label: "I checked and didn't find similar issue"
|
||||||
|
required: true
|
||||||
|
- type: checkboxes
|
||||||
|
attributes:
|
||||||
|
label: "🛡️ Security Policy"
|
||||||
|
description: Please review the security policy before reporting security related issues/bugs.
|
||||||
|
options:
|
||||||
|
- label: I agree to have read this project [Security Policy](https://github.com/louislam/uptime-kuma/security/policy)
|
||||||
|
required: true
|
||||||
|
- type: textarea
|
||||||
|
id: steps-to-reproduce
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
attributes:
|
||||||
|
label: "📝 Describe your problem"
|
||||||
|
description: "Please walk us through it step by step."
|
||||||
|
placeholder: "Describe what are you asking for..."
|
||||||
|
- type: input
|
||||||
|
id: uptime-kuma-version
|
||||||
|
attributes:
|
||||||
|
label: "🐻 Uptime-Kuma Version"
|
||||||
|
description: "Which version of Uptime-Kuma are you running? Please do NOT provide the docker tag such as latest or 1"
|
||||||
|
placeholder: "Ex. 1.10.0"
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
- type: input
|
||||||
|
id: operating-system
|
||||||
|
attributes:
|
||||||
|
label: "💻 Operating System and Arch"
|
||||||
|
description: "Which OS is your server/device running on?"
|
||||||
|
placeholder: "Ex. Ubuntu 20.04 x86"
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
- type: input
|
||||||
|
id: browser-vendor
|
||||||
|
attributes:
|
||||||
|
label: "🌐 Browser"
|
||||||
|
description: "Which browser are you running on?"
|
||||||
|
placeholder: "Ex. Google Chrome 95.0.4638.69"
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
- type: input
|
||||||
|
id: docker-version
|
||||||
|
attributes:
|
||||||
|
label: "🐋 Docker Version"
|
||||||
|
description: "If running with Docker, which version are you running?"
|
||||||
|
placeholder: "Ex. Docker 20.10.9 / K8S / Podman"
|
||||||
|
validations:
|
||||||
|
required: false
|
||||||
|
- type: input
|
||||||
|
id: nodejs-version
|
||||||
|
attributes:
|
||||||
|
label: "🟩 NodeJS Version"
|
||||||
|
description: "If running with Node.js? which version are you running?"
|
||||||
|
placeholder: "Ex. 14.18.0"
|
||||||
|
validations:
|
||||||
|
required: false
|
42
.github/ISSUE_TEMPLATE/bug_report.md
vendored
42
.github/ISSUE_TEMPLATE/bug_report.md
vendored
@ -1,42 +0,0 @@
|
|||||||
---
|
|
||||||
name: Bug report
|
|
||||||
about: Create a report to help us improve
|
|
||||||
title: ''
|
|
||||||
labels: bug
|
|
||||||
assignees: ''
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
**Is it a duplicate question?**
|
|
||||||
Please search in Issues without filters: https://github.com/louislam/uptime-kuma/issues?q=
|
|
||||||
|
|
||||||
**Describe the bug**
|
|
||||||
A clear and concise description of what the bug is.
|
|
||||||
|
|
||||||
**To Reproduce**
|
|
||||||
Steps to reproduce the behavior:
|
|
||||||
|
|
||||||
1. Go to '...'
|
|
||||||
2. Click on '....'
|
|
||||||
3. Scroll down to '....'
|
|
||||||
4. See error
|
|
||||||
|
|
||||||
**Expected behavior**
|
|
||||||
A clear and concise description of what you expected to happen.
|
|
||||||
|
|
||||||
**Info**
|
|
||||||
Uptime Kuma Version:
|
|
||||||
Using Docker?: Yes/No
|
|
||||||
Docker Version:
|
|
||||||
Node.js Version (Without Docker only):
|
|
||||||
OS:
|
|
||||||
Browser:
|
|
||||||
|
|
||||||
**Screenshots**
|
|
||||||
If applicable, add screenshots to help explain your problem.
|
|
||||||
|
|
||||||
**Error Log**
|
|
||||||
It is easier for us to find out the problem.
|
|
||||||
|
|
||||||
Docker: `docker logs <container id>`
|
|
||||||
PM2: `~/.pm2/logs/` (e.g. `/home/ubuntu/.pm2/logs`)
|
|
99
.github/ISSUE_TEMPLATE/bug_report.yaml
vendored
Normal file
99
.github/ISSUE_TEMPLATE/bug_report.yaml
vendored
Normal file
@ -0,0 +1,99 @@
|
|||||||
|
name: "🐛 Bug Report"
|
||||||
|
description: "Submit a bug report to help us improve"
|
||||||
|
#title: "[Bug] "
|
||||||
|
labels: [bug]
|
||||||
|
body:
|
||||||
|
- type: checkboxes
|
||||||
|
id: no-duplicate-issues
|
||||||
|
attributes:
|
||||||
|
label: "⚠️ Please verify that this bug has NOT been raised before."
|
||||||
|
description: "Search in the issues sections by clicking [HERE](https://github.com/louislam/uptime-kuma/issues?q=)"
|
||||||
|
options:
|
||||||
|
- label: "I checked and didn't find similar issue"
|
||||||
|
required: true
|
||||||
|
- type: checkboxes
|
||||||
|
attributes:
|
||||||
|
label: "🛡️ Security Policy"
|
||||||
|
description: Please review the security policy before reporting security related issues/bugs.
|
||||||
|
options:
|
||||||
|
- label: I agree to have read this project [Security Policy](https://github.com/louislam/uptime-kuma/security/policy)
|
||||||
|
required: true
|
||||||
|
- type: textarea
|
||||||
|
id: description
|
||||||
|
validations:
|
||||||
|
required: false
|
||||||
|
attributes:
|
||||||
|
label: "Description"
|
||||||
|
description: "You could also upload screenshots"
|
||||||
|
- type: textarea
|
||||||
|
id: steps-to-reproduce
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
attributes:
|
||||||
|
label: "👟 Reproduction steps"
|
||||||
|
description: "How do you trigger this bug? Please walk us through it step by step."
|
||||||
|
placeholder: "..."
|
||||||
|
- type: textarea
|
||||||
|
id: expected-behavior
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
attributes:
|
||||||
|
label: "👀 Expected behavior"
|
||||||
|
description: "What did you think would happen?"
|
||||||
|
placeholder: "..."
|
||||||
|
- type: textarea
|
||||||
|
id: actual-behavior
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
attributes:
|
||||||
|
label: "😓 Actual Behavior"
|
||||||
|
description: "What actually happen?"
|
||||||
|
placeholder: "..."
|
||||||
|
- type: input
|
||||||
|
id: uptime-kuma-version
|
||||||
|
attributes:
|
||||||
|
label: "🐻 Uptime-Kuma Version"
|
||||||
|
description: "Which version of Uptime-Kuma are you running? Please do NOT provide the docker tag such as latest or 1"
|
||||||
|
placeholder: "Ex. 1.10.0"
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
- type: input
|
||||||
|
id: operating-system
|
||||||
|
attributes:
|
||||||
|
label: "💻 Operating System and Arch"
|
||||||
|
description: "Which OS is your server/device running on?"
|
||||||
|
placeholder: "Ex. Ubuntu 20.04 x86"
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
- type: input
|
||||||
|
id: browser-vendor
|
||||||
|
attributes:
|
||||||
|
label: "🌐 Browser"
|
||||||
|
description: "Which browser are you running on?"
|
||||||
|
placeholder: "Ex. Google Chrome 95.0.4638.69"
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
- type: input
|
||||||
|
id: docker-version
|
||||||
|
attributes:
|
||||||
|
label: "🐋 Docker Version"
|
||||||
|
description: "If running with Docker, which version are you running?"
|
||||||
|
placeholder: "Ex. Docker 20.10.9 / K8S / Podman"
|
||||||
|
validations:
|
||||||
|
required: false
|
||||||
|
- type: input
|
||||||
|
id: nodejs-version
|
||||||
|
attributes:
|
||||||
|
label: "🟩 NodeJS Version"
|
||||||
|
description: "If running with Node.js? which version are you running?"
|
||||||
|
placeholder: "Ex. 14.18.0"
|
||||||
|
validations:
|
||||||
|
required: false
|
||||||
|
- type: textarea
|
||||||
|
id: logs
|
||||||
|
attributes:
|
||||||
|
label: "📝 Relevant log output"
|
||||||
|
description: Please copy and paste any relevant log output. This will be automatically formatted into code, so no need for backticks.
|
||||||
|
render: shell
|
||||||
|
validations:
|
||||||
|
required: false
|
22
.github/ISSUE_TEMPLATE/feature_request.md
vendored
22
.github/ISSUE_TEMPLATE/feature_request.md
vendored
@ -1,22 +0,0 @@
|
|||||||
---
|
|
||||||
name: Feature request
|
|
||||||
about: Suggest an idea for this project
|
|
||||||
title: ''
|
|
||||||
labels: enhancement
|
|
||||||
assignees: ''
|
|
||||||
|
|
||||||
---
|
|
||||||
**Is it a duplicate question?**
|
|
||||||
Please search in Issues without filters: https://github.com/louislam/uptime-kuma/issues?q=
|
|
||||||
|
|
||||||
**Is your feature request related to a problem? Please describe.**
|
|
||||||
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
|
|
||||||
|
|
||||||
**Describe the solution you'd like**
|
|
||||||
A clear and concise description of what you want to happen.
|
|
||||||
|
|
||||||
**Describe alternatives you've considered**
|
|
||||||
A clear and concise description of any alternative solutions or features you've considered.
|
|
||||||
|
|
||||||
**Additional context**
|
|
||||||
Add any other context or screenshots about the feature request here.
|
|
59
.github/ISSUE_TEMPLATE/feature_request.yaml
vendored
Normal file
59
.github/ISSUE_TEMPLATE/feature_request.yaml
vendored
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
name: 🚀 Feature Request
|
||||||
|
description: "Submit a proposal for a new feature"
|
||||||
|
#title: "[Feature] "
|
||||||
|
labels: [feature-request]
|
||||||
|
body:
|
||||||
|
- type: checkboxes
|
||||||
|
id: no-duplicate-issues
|
||||||
|
attributes:
|
||||||
|
label: "⚠️ Please verify that this feature request has NOT been suggested before."
|
||||||
|
description: "Search in the issues sections by clicking [HERE](https://github.com/louislam/uptime-kuma/issues?q=)"
|
||||||
|
options:
|
||||||
|
- label: "I checked and didn't find similar feature request"
|
||||||
|
required: true
|
||||||
|
- type: dropdown
|
||||||
|
id: feature-area
|
||||||
|
attributes:
|
||||||
|
label: "🏷️ Feature Request Type"
|
||||||
|
description: "What kind of feature request is this?"
|
||||||
|
multiple: true
|
||||||
|
options:
|
||||||
|
- API
|
||||||
|
- New Notification
|
||||||
|
- New Monitor
|
||||||
|
- UI Feature
|
||||||
|
- Other
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
- type: textarea
|
||||||
|
id: feature-description
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
attributes:
|
||||||
|
label: "🔖 Feature description"
|
||||||
|
description: "A clear and concise description of what the feature request is."
|
||||||
|
placeholder: "You should add ..."
|
||||||
|
- type: textarea
|
||||||
|
id: solution
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
attributes:
|
||||||
|
label: "✔️ Solution"
|
||||||
|
description: "A clear and concise description of what you want to happen."
|
||||||
|
placeholder: "In my use-case, ..."
|
||||||
|
- type: textarea
|
||||||
|
id: alternatives
|
||||||
|
validations:
|
||||||
|
required: false
|
||||||
|
attributes:
|
||||||
|
label: "❓ Alternatives"
|
||||||
|
description: "A clear and concise description of any alternative solutions or features you've considered."
|
||||||
|
placeholder: "I have considered ..."
|
||||||
|
- type: textarea
|
||||||
|
id: additional-context
|
||||||
|
validations:
|
||||||
|
required: false
|
||||||
|
attributes:
|
||||||
|
label: "📝 Additional Context"
|
||||||
|
description: "Add any other context or screenshots about the feature request here."
|
||||||
|
placeholder: "..."
|
28
.github/PULL_REQUEST_TEMPLATE.md
vendored
Normal file
28
.github/PULL_REQUEST_TEMPLATE.md
vendored
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
# Description
|
||||||
|
|
||||||
|
Fixes #(issue)
|
||||||
|
|
||||||
|
## Type of change
|
||||||
|
|
||||||
|
Please delete options that are not relevant.
|
||||||
|
|
||||||
|
- Bug fix (non-breaking change which fixes an issue)
|
||||||
|
- User Interface
|
||||||
|
- New feature (non-breaking change which adds functionality)
|
||||||
|
- Breaking change (fix or feature that would cause existing functionality to not work as expected)
|
||||||
|
- Translation update
|
||||||
|
- Other
|
||||||
|
- This change requires a documentation update
|
||||||
|
|
||||||
|
## Checklist
|
||||||
|
|
||||||
|
- [ ] My code follows the style guidelines of this project
|
||||||
|
- [ ] I ran ESLint and other linters for modified files
|
||||||
|
- [ ] I have performed a self-review of my own code and test it
|
||||||
|
- [ ] I have commented my code, particularly in hard-to-understand areas
|
||||||
|
- [ ] My changes generate no new warnings
|
||||||
|
- [ ] My code needed automated testing. I have added them (this is optional task)
|
||||||
|
|
||||||
|
## Screenshots (if any)
|
||||||
|
|
||||||
|
Please do not use any external image service. Instead, just paste in or drag and drop the image here, and it will be uploaded automatically.
|
2
.github/workflows/auto-test.yml
vendored
2
.github/workflows/auto-test.yml
vendored
@ -16,7 +16,7 @@ jobs:
|
|||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
os: [macos-latest, ubuntu-latest, windows-latest]
|
os: [macos-latest, ubuntu-latest, windows-latest]
|
||||||
node-version: [14.x, 16.x]
|
node-version: [14.x, 16.x, 17.x]
|
||||||
# See supported Node.js release schedule at https://nodejs.org/en/about/releases/
|
# See supported Node.js release schedule at https://nodejs.org/en/about/releases/
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
|
26
.github/workflows/close-incorrect-issue.yml
vendored
Normal file
26
.github/workflows/close-incorrect-issue.yml
vendored
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
|
||||||
|
name: Close Incorrect Issue
|
||||||
|
|
||||||
|
on:
|
||||||
|
issues:
|
||||||
|
types: [opened]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
close-incorrect-issue:
|
||||||
|
runs-on: ${{ matrix.os }}
|
||||||
|
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
os: [ubuntu-latest]
|
||||||
|
node-version: [16.x]
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v2
|
||||||
|
|
||||||
|
- name: Use Node.js ${{ matrix.node-version }}
|
||||||
|
uses: actions/setup-node@v2
|
||||||
|
with:
|
||||||
|
node-version: ${{ matrix.node-version }}
|
||||||
|
cache: 'npm'
|
||||||
|
- run: npm ci
|
||||||
|
- run: node extra/close-incorrect-issue.js ${{ secrets.GITHUB_TOKEN }} ${{ github.event.issue.number }} ${{ github.event.issue.user.login }}
|
22
.github/workflows/stale-bot.yml
vendored
Normal file
22
.github/workflows/stale-bot.yml
vendored
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
name: 'Automatically close stale issues and PRs'
|
||||||
|
on:
|
||||||
|
schedule:
|
||||||
|
- cron: '0 0 * * *'
|
||||||
|
#Run once a day at midnight
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
stale:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/stale@v4
|
||||||
|
with:
|
||||||
|
stale-issue-message: 'We are clearing up our old issues and your ticket has been open for 6 months with no activity. Remove stale label or comment or this will be closed in 7 days.'
|
||||||
|
stale-pr-message: 'We are clearing up our old Pull Requests and yours has been open for 6 months with no activity. Remove stale label or comment or this will be closed in 7 days.'
|
||||||
|
close-issue-message: 'This issue was closed because it has been stalled for 7 days with no activity.'
|
||||||
|
close-pr-message: 'This PR was closed because it has been stalled for 7 days with no activity.'
|
||||||
|
days-before-stale: 180
|
||||||
|
days-before-close: 7
|
||||||
|
exempt-issue-labels: 'News,Medium,High,discussion,bug,doc,'
|
||||||
|
exempt-pr-labels: 'awaiting-approval,work-in-progress,enhancement,'
|
||||||
|
exempt-issue-assignees: 'louislam'
|
||||||
|
exempt-pr-assignees: 'louislam'
|
@ -60,7 +60,7 @@ representative at an online or offline event.
|
|||||||
|
|
||||||
Instances of abusive, harassing, or otherwise unacceptable behavior may be
|
Instances of abusive, harassing, or otherwise unacceptable behavior may be
|
||||||
reported to the community leaders responsible for enforcement at
|
reported to the community leaders responsible for enforcement at
|
||||||
louis@uptimekuma.louislam.net.
|
uptime@kuma.pet.
|
||||||
All complaints will be reviewed and investigated promptly and fairly.
|
All complaints will be reviewed and investigated promptly and fairly.
|
||||||
|
|
||||||
All community leaders are obligated to respect the privacy and security of the
|
All community leaders are obligated to respect the privacy and security of the
|
||||||
|
141
CONTRIBUTING.md
141
CONTRIBUTING.md
@ -1,12 +1,12 @@
|
|||||||
# Project Info
|
# 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.
|
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 structured 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 project was created with vite.js (vue3). Then I created a subdirectory called "server" for server part. Both frontend and backend share the same package.json.
|
||||||
|
|
||||||
The frontend code build into "dist" directory. The server (express.js) exposes the "dist" directory as root of the endpoint. This is how production is working.
|
The frontend code build into "dist" directory. The server (express.js) exposes the "dist" directory as root of the endpoint. This is how production is working.
|
||||||
|
|
||||||
# Key Technical Skills
|
## Key Technical Skills
|
||||||
|
|
||||||
- Node.js (You should know what are promise, async/await and arrow function etc.)
|
- Node.js (You should know what are promise, async/await and arrow function etc.)
|
||||||
- Socket.io
|
- Socket.io
|
||||||
@ -15,7 +15,7 @@ The frontend code build into "dist" directory. The server (express.js) exposes t
|
|||||||
- Bootstrap
|
- Bootstrap
|
||||||
- SQLite
|
- SQLite
|
||||||
|
|
||||||
# Directories
|
## Directories
|
||||||
|
|
||||||
- data (App data)
|
- data (App data)
|
||||||
- dist (Frontend build)
|
- dist (Frontend build)
|
||||||
@ -25,41 +25,69 @@ The frontend code build into "dist" directory. The server (express.js) exposes t
|
|||||||
- src (Frontend source code)
|
- src (Frontend source code)
|
||||||
- test (unit test)
|
- test (unit test)
|
||||||
|
|
||||||
# Can I create a pull request for Uptime Kuma?
|
## Can I create a pull request for Uptime Kuma?
|
||||||
|
|
||||||
Generally, if the pull request is working fine and it do not affect any existing logic, workflow and perfomance, I will merge into the master branch once it is tested.
|
Generally, if the pull request is working fine, and it does not affect any existing logic, workflow and performance, I will merge into the master branch once it is tested.
|
||||||
|
|
||||||
If you are not sure, feel free to create an empty pull request draft first.
|
If you are not sure whether I will accept your pull request, feel free to create an empty pull request draft first.
|
||||||
|
|
||||||
## Pull Request Examples
|
### Recommended Pull Request Guideline
|
||||||
|
|
||||||
### ✅ High - Medium Priority
|
1. Fork the project
|
||||||
|
1. Clone your fork repo to local
|
||||||
|
1. Create a new branch
|
||||||
|
1. Create an empty commit
|
||||||
|
`git commit -m "[empty commit] pull request for <YOUR TASK NAME>" --allow-empty`
|
||||||
|
1. Push to your fork repo
|
||||||
|
1. Create a pull request: https://github.com/louislam/uptime-kuma/compare
|
||||||
|
1. Write a proper description
|
||||||
|
1. Click "Change to draft"
|
||||||
|
|
||||||
|
### Pull Request Examples
|
||||||
|
|
||||||
|
Here are some example situations in the past.
|
||||||
|
|
||||||
|
#### ✅ High - Medium Priority
|
||||||
|
|
||||||
|
Easy to review, no breaking change and not touching the existing code
|
||||||
|
|
||||||
- Add a new notification
|
- Add a new notification
|
||||||
- Add a chart
|
- Add a chart
|
||||||
- Fix a bug
|
- Fix a bug
|
||||||
- Translations
|
- Translations
|
||||||
|
- Add a independent new feature
|
||||||
|
|
||||||
### *️⃣ Requires one more reviewer
|
#### *️⃣ Requires one more reviewer
|
||||||
|
|
||||||
I do not have such knowledge to test it.
|
I do not have such knowledge to test it.
|
||||||
|
|
||||||
- Add k8s supports
|
- Add k8s supports
|
||||||
|
|
||||||
### *️⃣ Low Priority
|
#### ⚠ Low Priority - Harsh Mode
|
||||||
|
|
||||||
|
Some pull requests are required to modify the core. To be honest, I do not want anyone to try to do that, because it would spend a lot of your time. I will review your pull request harshly. Also, you may need to write a lot of unit tests to ensure that there is no breaking change.
|
||||||
|
|
||||||
|
- Touch large parts of code of any very important features
|
||||||
|
- Touch monitoring logic
|
||||||
|
- Drop a table or drop a column for any reason
|
||||||
|
- Touch the entry point of Docker or Node.js
|
||||||
|
- Modify auth
|
||||||
|
|
||||||
|
#### *️⃣ Low Priority
|
||||||
|
|
||||||
It changed my current workflow and require further studies.
|
It changed my current workflow and require further studies.
|
||||||
|
|
||||||
- Change my release approach
|
- Change my release approach
|
||||||
|
|
||||||
### ❌ Won't Merge
|
#### ❌ Won't Merge
|
||||||
|
|
||||||
|
- Any breaking changes
|
||||||
- Duplicated pull request
|
- Duplicated pull request
|
||||||
- Buggy
|
- Buggy
|
||||||
- Existing logic is completely modified or deleted
|
- Existing logic is completely modified or deleted
|
||||||
- A function that is completely out of scope
|
- A function that is completely out of scope
|
||||||
|
|
||||||
# Project Styles
|
## 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.
|
I personally do not like something need to learn so much and need to config so much before you can finally start the app.
|
||||||
|
|
||||||
@ -68,32 +96,32 @@ I personally do not like something need to learn so much and need to config so m
|
|||||||
- Settings should be configurable in the frontend. Env var is not encouraged.
|
- Settings should be configurable in the frontend. Env var is not encouraged.
|
||||||
- Easy to use
|
- Easy to use
|
||||||
|
|
||||||
# Coding Styles
|
## Coding Styles
|
||||||
|
|
||||||
- 4 spaces indentation
|
- 4 spaces indentation
|
||||||
- Follow `.editorconfig`
|
- Follow `.editorconfig`
|
||||||
- Follow ESLint
|
- Follow ESLint
|
||||||
|
|
||||||
# Name convention
|
## Name convention
|
||||||
|
|
||||||
- Javascript/Typescript: camelCaseType
|
- Javascript/Typescript: camelCaseType
|
||||||
- SQLite: underscore_type
|
- SQLite: underscore_type
|
||||||
- CSS/SCSS: dash-type
|
- CSS/SCSS: dash-type
|
||||||
|
|
||||||
# Tools
|
## Tools
|
||||||
|
|
||||||
- Node.js >= 14
|
- Node.js >= 14
|
||||||
- Git
|
- Git
|
||||||
- IDE that supports ESLint and EditorConfig (I am using Intellji Idea)
|
- IDE that supports ESLint and EditorConfig (I am using IntelliJ IDEA)
|
||||||
- A SQLite tool (SQLite Expert Personal is suggested)
|
- A SQLite tool (SQLite Expert Personal is suggested)
|
||||||
|
|
||||||
# Install dependencies
|
## Install dependencies
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
npm ci
|
npm ci
|
||||||
```
|
```
|
||||||
|
|
||||||
# How to start the Backend Dev Server
|
## How to start the Backend Dev Server
|
||||||
|
|
||||||
(2021-09-23 Update)
|
(2021-09-23 Update)
|
||||||
|
|
||||||
@ -103,7 +131,7 @@ npm run start-server-dev
|
|||||||
|
|
||||||
It binds to `0.0.0.0:3001` by default.
|
It binds to `0.0.0.0:3001` by default.
|
||||||
|
|
||||||
## Backend Details
|
### Backend Details
|
||||||
|
|
||||||
It is mainly a socket.io app + express.js.
|
It is mainly a socket.io app + express.js.
|
||||||
|
|
||||||
@ -111,29 +139,31 @@ express.js is just used for serving the frontend built files (index.html, .js an
|
|||||||
|
|
||||||
- model/ (Object model, auto mapping to the database table name)
|
- model/ (Object model, auto mapping to the database table name)
|
||||||
- modules/ (Modified 3rd-party modules)
|
- modules/ (Modified 3rd-party modules)
|
||||||
- notification-providers/ (indivdual notification logic)
|
- notification-providers/ (individual notification logic)
|
||||||
- routers/ (Express Routers)
|
- routers/ (Express Routers)
|
||||||
- scoket-handler (Socket.io Handlers)
|
- socket-handler (Socket.io Handlers)
|
||||||
- server.js (Server main logic)
|
- server.js (Server main logic)
|
||||||
|
|
||||||
# How to start the Frontend Dev Server
|
## How to start the Frontend Dev Server
|
||||||
|
|
||||||
1. Set the env var `NODE_ENV` to "development".
|
1. Set the env var `NODE_ENV` to "development".
|
||||||
2. Start the frontend dev server by the following command.
|
2. Start the frontend dev server by the following command.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
npm run dev
|
npm run dev
|
||||||
```
|
```
|
||||||
|
|
||||||
It binds to `0.0.0.0:3000` by default.
|
It binds to `0.0.0.0:3000` by default.
|
||||||
|
|
||||||
You can use Vue.js devtools Chrome extension for debugging.
|
You can use Vue.js devtools Chrome extension for debugging.
|
||||||
|
|
||||||
## Build the frontend
|
### Build the frontend
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
npm run build
|
npm run build
|
||||||
```
|
```
|
||||||
|
|
||||||
## Frontend Details
|
### Frontend Details
|
||||||
|
|
||||||
Uptime Kuma Frontend is a single page application (SPA). Most paths are handled by Vue Router.
|
Uptime Kuma Frontend is a single page application (SPA). Most paths are handled by Vue Router.
|
||||||
|
|
||||||
@ -143,24 +173,23 @@ As you can see, most data in frontend is stored in root level, even though you c
|
|||||||
|
|
||||||
The data and socket logic are in `src/mixins/socket.js`.
|
The data and socket logic are in `src/mixins/socket.js`.
|
||||||
|
|
||||||
|
## Database Migration
|
||||||
# Database Migration
|
|
||||||
|
|
||||||
1. Create `patch-{name}.sql` in `./db/`
|
1. Create `patch-{name}.sql` in `./db/`
|
||||||
2. Add your patch filename in the `patchList` list in `./server/database.js`
|
2. Add your patch filename in the `patchList` list in `./server/database.js`
|
||||||
|
|
||||||
# Unit Test
|
## Unit Test
|
||||||
|
|
||||||
It is an end-to-end testing. It is using Jest and Puppeteer.
|
It is an end-to-end testing. It is using Jest and Puppeteer.
|
||||||
|
|
||||||
```
|
```bash
|
||||||
npm run build
|
npm run build
|
||||||
npm test
|
npm test
|
||||||
```
|
```
|
||||||
|
|
||||||
By default, the Chromium window will be shown up during the test. Specifying `HEADLESS_TEST=1` for terminal environments.
|
By default, the Chromium window will be shown up during the test. Specifying `HEADLESS_TEST=1` for terminal environments.
|
||||||
|
|
||||||
# Update Dependencies
|
## Update Dependencies
|
||||||
|
|
||||||
Install `ncu`
|
Install `ncu`
|
||||||
https://github.com/raineorshine/npm-check-updates
|
https://github.com/raineorshine/npm-check-updates
|
||||||
@ -170,10 +199,56 @@ ncu -u -t patch
|
|||||||
npm install
|
npm install
|
||||||
```
|
```
|
||||||
|
|
||||||
Since previously updating vite 2.5.10 to 2.6.0 broke the application completely, from now on, it should update patch release version only.
|
Since previously updating Vite 2.5.10 to 2.6.0 broke the application completely, from now on, it should update patch release version only.
|
||||||
|
|
||||||
Patch release = the third digit
|
Patch release = the third digit ([Semantic Versioning](https://semver.org/))
|
||||||
|
|
||||||
# Translations
|
## Translations
|
||||||
|
|
||||||
Please read: https://github.com/louislam/uptime-kuma/tree/master/src/languages
|
Please read: https://github.com/louislam/uptime-kuma/tree/master/src/languages
|
||||||
|
|
||||||
|
## Wiki
|
||||||
|
|
||||||
|
Since there is no way to make a pull request to wiki's repo, I have set up another repo to do that.
|
||||||
|
|
||||||
|
https://github.com/louislam/uptime-kuma-wiki
|
||||||
|
|
||||||
|
## Maintainer
|
||||||
|
|
||||||
|
Check the latest issues and pull requests:
|
||||||
|
https://github.com/louislam/uptime-kuma/issues?q=sort%3Aupdated-desc
|
||||||
|
|
||||||
|
### Release Procedures
|
||||||
|
|
||||||
|
1. Draft a release note
|
||||||
|
1. Make sure the repo is cleared
|
||||||
|
1. `npm run update-version 1.X.X`
|
||||||
|
1. `npm run build`
|
||||||
|
1. `npm run build-docker`
|
||||||
|
1. `git push`
|
||||||
|
1. Publish the release note as 1.X.X
|
||||||
|
1. `npm run upload-artifacts`
|
||||||
|
1. SSH to demo site server and update to 1.X.X
|
||||||
|
|
||||||
|
Checking:
|
||||||
|
|
||||||
|
- Check all tags is fine on https://hub.docker.com/r/louislam/uptime-kuma/tags
|
||||||
|
- Try the Docker image with tag 1.X.X (Clean install / amd64 / arm64 / armv7)
|
||||||
|
- Try clean installation with Node.js
|
||||||
|
|
||||||
|
### Release Wiki
|
||||||
|
|
||||||
|
#### Setup Repo
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git clone https://github.com/louislam/uptime-kuma-wiki.git
|
||||||
|
cd uptime-kuma-wiki
|
||||||
|
git remote add production https://github.com/louislam/uptime-kuma.wiki.git
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Push to Production Wiki
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git pull
|
||||||
|
git push production master
|
||||||
|
```
|
||||||
|
34
README.md
34
README.md
@ -1,7 +1,7 @@
|
|||||||
# Uptime Kuma
|
# Uptime Kuma
|
||||||
|
|
||||||
<a target="_blank" href="https://github.com/louislam/uptime-kuma"><img src="https://img.shields.io/github/stars/louislam/uptime-kuma" /></a> <a target="_blank" href="https://hub.docker.com/r/louislam/uptime-kuma"><img src="https://img.shields.io/docker/pulls/louislam/uptime-kuma" /></a> <a target="_blank" href="https://hub.docker.com/r/louislam/uptime-kuma"><img src="https://img.shields.io/docker/v/louislam/uptime-kuma/latest?label=docker%20image%20ver." /></a> <a target="_blank" href="https://github.com/louislam/uptime-kuma"><img src="https://img.shields.io/github/last-commit/louislam/uptime-kuma" /></a> <a target="_blank" href="https://opencollective.com/uptime-kuma"><img src="https://opencollective.com/uptime-kuma/total/badge.svg?label=Backers&color=brightgreen" /></a>
|
<a target="_blank" href="https://github.com/louislam/uptime-kuma"><img src="https://img.shields.io/github/stars/louislam/uptime-kuma" /></a> <a target="_blank" href="https://hub.docker.com/r/louislam/uptime-kuma"><img src="https://img.shields.io/docker/pulls/louislam/uptime-kuma" /></a> <a target="_blank" href="https://hub.docker.com/r/louislam/uptime-kuma"><img src="https://img.shields.io/docker/v/louislam/uptime-kuma/latest?label=docker%20image%20ver." /></a> <a target="_blank" href="https://github.com/louislam/uptime-kuma"><img src="https://img.shields.io/github/last-commit/louislam/uptime-kuma" /></a> <a target="_blank" href="https://opencollective.com/uptime-kuma"><img src="https://opencollective.com/uptime-kuma/total/badge.svg?label=Open%20Collective%20Backers&color=brightgreen" /></a>
|
||||||
|
[![GitHub Sponsors](https://img.shields.io/github/sponsors/louislam?label=GitHub%20Sponsors)](https://github.com/sponsors/louislam)
|
||||||
|
|
||||||
<div align="center" width="100%">
|
<div align="center" width="100%">
|
||||||
<img src="./public/icon.svg" width="128" alt="" />
|
<img src="./public/icon.svg" width="128" alt="" />
|
||||||
@ -17,20 +17,20 @@ Try it!
|
|||||||
|
|
||||||
https://demo.uptime.kuma.pet
|
https://demo.uptime.kuma.pet
|
||||||
|
|
||||||
It is a 5 minutes live demo, all data will be deleted after that. The server is located at Tokyo, if you live far away from here, it may affact your experience. I suggest that you should install to try it.
|
It is a temporary live demo, all data will be deleted after 10 minutes. The server is located in Tokyo, so if you live far from there, it may affect your experience. I suggest that you should install and try it out for the best demo experience.
|
||||||
|
|
||||||
VPS is sponsored by Uptime Kuma sponsors on [Open Collective](https://opencollective.com/uptime-kuma)! Thank you so much!
|
VPS is sponsored by Uptime Kuma sponsors on [Open Collective](https://opencollective.com/uptime-kuma)! Thank you so much!
|
||||||
|
|
||||||
## ⭐ Features
|
## ⭐ Features
|
||||||
|
|
||||||
* Monitoring uptime for HTTP(s) / TCP / Ping / DNS Record / Push.
|
* Monitoring uptime for HTTP(s) / TCP / HTTP(s) Keyword / Ping / DNS Record / Push / Steam Game Server.
|
||||||
* Fancy, Reactive, Fast UI/UX.
|
* Fancy, Reactive, Fast UI/UX.
|
||||||
* Notifications via Telegram, Discord, Gotify, Slack, Pushover, Email (SMTP), and [70+ notification services, click here for the full list](https://github.com/louislam/uptime-kuma/tree/master/src/components/notifications).
|
* Notifications via Telegram, Discord, Gotify, Slack, Pushover, Email (SMTP), and [70+ notification services, click here for the full list](https://github.com/louislam/uptime-kuma/tree/master/src/components/notifications).
|
||||||
* 20 seconds interval.
|
* 20 second intervals.
|
||||||
* [Multi Languages](https://github.com/louislam/uptime-kuma/tree/master/src/languages)
|
* [Multi Languages](https://github.com/louislam/uptime-kuma/tree/master/src/languages)
|
||||||
* Simple Status Page
|
* Simple Status Page
|
||||||
* Ping Chart
|
* Ping Chart
|
||||||
* Certicate Info
|
* Certificate Info
|
||||||
|
|
||||||
## 🔧 How to Install
|
## 🔧 How to Install
|
||||||
|
|
||||||
@ -41,9 +41,11 @@ docker volume create uptime-kuma
|
|||||||
docker run -d --restart=always -p 3001:3001 -v uptime-kuma:/app/data --name uptime-kuma louislam/uptime-kuma:1
|
docker run -d --restart=always -p 3001:3001 -v uptime-kuma:/app/data --name uptime-kuma louislam/uptime-kuma:1
|
||||||
```
|
```
|
||||||
|
|
||||||
Browse to http://localhost:3001 after started.
|
⚠️ Please use a **local volume** only. Other types such as NFS are not supported.
|
||||||
|
|
||||||
### 💪🏻 Without Docker
|
Browse to http://localhost:3001 after starting.
|
||||||
|
|
||||||
|
### 💪🏻 Non-Docker
|
||||||
|
|
||||||
Required Tools: Node.js >= 14, git and pm2.
|
Required Tools: Node.js >= 14, git and pm2.
|
||||||
|
|
||||||
@ -59,15 +61,15 @@ npm run setup
|
|||||||
node server/server.js
|
node server/server.js
|
||||||
|
|
||||||
# (Recommended) Option 2. Run in background using PM2
|
# (Recommended) Option 2. Run in background using PM2
|
||||||
# Install PM2 if you don't have: npm install pm2 -g
|
# Install PM2 if you don't have it: npm install pm2 -g
|
||||||
pm2 start server/server.js --name uptime-kuma
|
pm2 start server/server.js --name uptime-kuma
|
||||||
```
|
```
|
||||||
|
|
||||||
Browse to http://localhost:3001 after started.
|
Browse to http://localhost:3001 after starting.
|
||||||
|
|
||||||
### Advanced Installation
|
### Advanced Installation
|
||||||
|
|
||||||
If you need more options or need to browse via a reserve proxy, please read:
|
If you need more options or need to browse via a reverse proxy, please read:
|
||||||
|
|
||||||
https://github.com/louislam/uptime-kuma/wiki/%F0%9F%94%A7-How-to-Install
|
https://github.com/louislam/uptime-kuma/wiki/%F0%9F%94%A7-How-to-Install
|
||||||
|
|
||||||
@ -119,19 +121,21 @@ If you love this project, please consider giving me a ⭐.
|
|||||||
## 🗣️ Discussion
|
## 🗣️ Discussion
|
||||||
|
|
||||||
### Issues Page
|
### Issues Page
|
||||||
You can discuss or ask for help in [Issues](https://github.com/louislam/uptime-kuma/issues).
|
|
||||||
|
You can discuss or ask for help in [issues](https://github.com/louislam/uptime-kuma/issues).
|
||||||
|
|
||||||
### Subreddit
|
### Subreddit
|
||||||
|
|
||||||
My Reddit account: louislamlam
|
My Reddit account: louislamlam
|
||||||
You can mention me if you ask question on Reddit.
|
You can mention me if you ask a question on Reddit.
|
||||||
https://www.reddit.com/r/UptimeKuma/
|
https://www.reddit.com/r/UptimeKuma/
|
||||||
|
|
||||||
## Contribute
|
## Contribute
|
||||||
|
|
||||||
If you want to report a bug or request a new feature. Free feel to open a [new issue](https://github.com/louislam/uptime-kuma/issues).
|
If you want to report a bug or request a new feature. Free feel to open a [new issue](https://github.com/louislam/uptime-kuma/issues).
|
||||||
|
|
||||||
If you want to translate Uptime Kuma into your langauge, please read: https://github.com/louislam/uptime-kuma/tree/master/src/languages
|
If you want to translate Uptime Kuma into your language, please read: https://github.com/louislam/uptime-kuma/tree/master/src/languages
|
||||||
|
|
||||||
If you want to modify Uptime Kuma, this guideline may be useful for you: https://github.com/louislam/uptime-kuma/blob/master/CONTRIBUTING.md
|
If you want to modify Uptime Kuma, this guideline may be 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.
|
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.
|
||||||
|
21
SECURITY.md
21
SECURITY.md
@ -1,17 +1,25 @@
|
|||||||
# Security Policy
|
# Security Policy
|
||||||
|
|
||||||
|
## Reporting a Vulnerability
|
||||||
|
|
||||||
|
Please report security issues to uptime@kuma.pet.
|
||||||
|
|
||||||
|
Do not use the issue tracker or discuss it in the public as it will cause more damage.
|
||||||
|
|
||||||
## Supported Versions
|
## Supported Versions
|
||||||
|
|
||||||
Use this section to tell people about which versions of your project are
|
Use this section to tell people about which versions of your project are
|
||||||
currently being supported with security updates.
|
currently being supported with security updates.
|
||||||
|
|
||||||
#### Uptime Kuma Versions:
|
### Uptime Kuma Versions
|
||||||
|
|
||||||
| Version | Supported |
|
| Version | Supported |
|
||||||
| ------- | ------------------ |
|
| ------- | ------------------ |
|
||||||
| 1.7.X | :white_check_mark: |
|
| 1.9.X | :white_check_mark: |
|
||||||
| < 1.7 | ❌ |
|
| <= 1.8.X | ❌ |
|
||||||
|
|
||||||
|
### Upgradable Docker Tags
|
||||||
|
|
||||||
#### Upgradable Docker Tags:
|
|
||||||
| Tag | Supported |
|
| Tag | Supported |
|
||||||
| ------- | ------------------ |
|
| ------- | ------------------ |
|
||||||
| 1 | :white_check_mark: |
|
| 1 | :white_check_mark: |
|
||||||
@ -21,8 +29,3 @@ currently being supported with security updates.
|
|||||||
| debian | :white_check_mark: |
|
| debian | :white_check_mark: |
|
||||||
| alpine | :white_check_mark: |
|
| alpine | :white_check_mark: |
|
||||||
| All other tags | ❌ |
|
| All other tags | ❌ |
|
||||||
|
|
||||||
## Reporting a Vulnerability
|
|
||||||
Please report security issues to uptime@kuma.pet.
|
|
||||||
|
|
||||||
Do not use the issue tracker or discuss it in the public as it will cause more damage.
|
|
||||||
|
@ -4,4 +4,8 @@ if (process.env.TEST_FRONTEND) {
|
|||||||
config.presets = ["@babel/preset-env"];
|
config.presets = ["@babel/preset-env"];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (process.env.TEST_BACKEND) {
|
||||||
|
config.plugins = ["babel-plugin-rewire"];
|
||||||
|
}
|
||||||
|
|
||||||
module.exports = config;
|
module.exports = config;
|
||||||
|
5
config/jest-backend.config.js
Normal file
5
config/jest-backend.config.js
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
module.exports = {
|
||||||
|
"rootDir": "..",
|
||||||
|
"testRegex": "./test/backend.spec.js",
|
||||||
|
};
|
||||||
|
|
33
config/jest-debug-env.js
Normal file
33
config/jest-debug-env.js
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
const PuppeteerEnvironment = require("jest-environment-puppeteer");
|
||||||
|
const util = require("util");
|
||||||
|
|
||||||
|
class DebugEnv extends PuppeteerEnvironment {
|
||||||
|
async handleTestEvent(event, state) {
|
||||||
|
const ignoredEvents = [
|
||||||
|
"setup",
|
||||||
|
"add_hook",
|
||||||
|
"start_describe_definition",
|
||||||
|
"add_test",
|
||||||
|
"finish_describe_definition",
|
||||||
|
"run_start",
|
||||||
|
"run_describe_start",
|
||||||
|
"test_start",
|
||||||
|
"hook_start",
|
||||||
|
"hook_success",
|
||||||
|
"test_fn_start",
|
||||||
|
"test_fn_success",
|
||||||
|
"test_done",
|
||||||
|
"run_describe_finish",
|
||||||
|
"run_finish",
|
||||||
|
"teardown",
|
||||||
|
"test_fn_failure",
|
||||||
|
];
|
||||||
|
if (!ignoredEvents.includes(event.name)) {
|
||||||
|
console.log(
|
||||||
|
new Date().toString() + ` Unhandled event [${event.name}] ` + util.inspect(event)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = DebugEnv;
|
@ -1,5 +1,5 @@
|
|||||||
module.exports = {
|
module.exports = {
|
||||||
"rootDir": ".",
|
"rootDir": "..",
|
||||||
"testRegex": "./test/frontend.spec.js",
|
"testRegex": "./test/frontend.spec.js",
|
||||||
};
|
};
|
||||||
|
|
20
config/jest-puppeteer.config.js
Normal file
20
config/jest-puppeteer.config.js
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
module.exports = {
|
||||||
|
"launch": {
|
||||||
|
"dumpio": true,
|
||||||
|
"slowMo": 500,
|
||||||
|
"headless": process.env.HEADLESS_TEST || false,
|
||||||
|
"userDataDir": "./data/test-chrome-profile",
|
||||||
|
args: [
|
||||||
|
"--disable-setuid-sandbox",
|
||||||
|
"--disable-gpu",
|
||||||
|
"--disable-dev-shm-usage",
|
||||||
|
"--no-default-browser-check",
|
||||||
|
"--no-experiments",
|
||||||
|
"--no-first-run",
|
||||||
|
"--no-pings",
|
||||||
|
"--no-sandbox",
|
||||||
|
"--no-zygote",
|
||||||
|
"--single-process",
|
||||||
|
],
|
||||||
|
}
|
||||||
|
};
|
@ -5,7 +5,8 @@ module.exports = {
|
|||||||
"__DEV__": true
|
"__DEV__": true
|
||||||
},
|
},
|
||||||
"testRegex": "./test/e2e.spec.js",
|
"testRegex": "./test/e2e.spec.js",
|
||||||
"rootDir": ".",
|
"testEnvironment": "./config/jest-debug-env.js",
|
||||||
|
"rootDir": "..",
|
||||||
"testTimeout": 30000,
|
"testTimeout": 30000,
|
||||||
};
|
};
|
||||||
|
|
@ -1,9 +1,9 @@
|
|||||||
import legacy from "@vitejs/plugin-legacy"
|
import legacy from "@vitejs/plugin-legacy";
|
||||||
import vue from "@vitejs/plugin-vue"
|
import vue from "@vitejs/plugin-vue";
|
||||||
import { defineConfig } from "vite"
|
import { defineConfig } from "vite";
|
||||||
|
|
||||||
const postCssScss = require("postcss-scss")
|
const postCssScss = require("postcss-scss");
|
||||||
const postcssRTLCSS = require('postcss-rtlcss');
|
const postcssRTLCSS = require("postcss-rtlcss");
|
||||||
|
|
||||||
// https://vitejs.dev/config/
|
// https://vitejs.dev/config/
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
@ -20,5 +20,5 @@ export default defineConfig({
|
|||||||
"map": false,
|
"map": false,
|
||||||
"plugins": [postcssRTLCSS]
|
"plugins": [postcssRTLCSS]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
})
|
});
|
7
db/patch-2fa-invalidate-used-token.sql
Normal file
7
db/patch-2fa-invalidate-used-token.sql
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
-- You should not modify if this have pushed to Github, unless it does serious wrong with the db.
|
||||||
|
BEGIN TRANSACTION;
|
||||||
|
|
||||||
|
ALTER TABLE user
|
||||||
|
ADD twofa_last_token VARCHAR(6);
|
||||||
|
|
||||||
|
COMMIT;
|
13
db/patch-http-monitor-method-body-and-headers.sql
Normal file
13
db/patch-http-monitor-method-body-and-headers.sql
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
-- You should not modify if this have pushed to Github, unless it does serious wrong with the db.
|
||||||
|
BEGIN TRANSACTION;
|
||||||
|
|
||||||
|
ALTER TABLE monitor
|
||||||
|
ADD method TEXT default 'GET' not null;
|
||||||
|
|
||||||
|
ALTER TABLE monitor
|
||||||
|
ADD body TEXT default null;
|
||||||
|
|
||||||
|
ALTER TABLE monitor
|
||||||
|
ADD headers TEXT default null;
|
||||||
|
|
||||||
|
COMMIT;
|
10
db/patch-monitor-basic-auth.sql
Normal file
10
db/patch-monitor-basic-auth.sql
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
-- You should not modify if this have pushed to Github, unless it does serious wrong with the db.
|
||||||
|
BEGIN TRANSACTION;
|
||||||
|
|
||||||
|
ALTER TABLE monitor
|
||||||
|
ADD basic_auth_user TEXT default null;
|
||||||
|
|
||||||
|
ALTER TABLE monitor
|
||||||
|
ADD basic_auth_pass TEXT default null;
|
||||||
|
|
||||||
|
COMMIT;
|
18
db/patch-notification_sent_history.sql
Normal file
18
db/patch-notification_sent_history.sql
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
-- You should not modify if this have pushed to Github, unless it does serious wrong with the db.
|
||||||
|
BEGIN TRANSACTION;
|
||||||
|
|
||||||
|
CREATE TABLE [notification_sent_history] (
|
||||||
|
[id] INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
|
||||||
|
[type] VARCHAR(50) NOT NULL,
|
||||||
|
[monitor_id] INTEGER NOT NULL,
|
||||||
|
[days] INTEGER NOT NULL,
|
||||||
|
UNIQUE([type], [monitor_id], [days])
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX [good_index] ON [notification_sent_history] (
|
||||||
|
[type],
|
||||||
|
[monitor_id],
|
||||||
|
[days]
|
||||||
|
);
|
||||||
|
|
||||||
|
COMMIT;
|
@ -4,5 +4,5 @@ WORKDIR /app
|
|||||||
|
|
||||||
# Install apprise, iputils for non-root ping, setpriv
|
# Install apprise, iputils for non-root ping, setpriv
|
||||||
RUN apk add --no-cache iputils setpriv dumb-init python3 py3-cryptography py3-pip py3-six py3-yaml py3-click py3-markdown py3-requests py3-requests-oauthlib && \
|
RUN apk add --no-cache iputils setpriv dumb-init python3 py3-cryptography py3-pip py3-six py3-yaml py3-click py3-markdown py3-requests py3-requests-oauthlib && \
|
||||||
pip3 --no-cache-dir install apprise && \
|
pip3 --no-cache-dir install apprise==0.9.6 && \
|
||||||
rm -rf /root/.cache
|
rm -rf /root/.cache
|
||||||
|
@ -4,9 +4,9 @@ FROM node:14-buster-slim
|
|||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
# Install Apprise, add sqlite3 cli for debugging in the future, iputils-ping for ping, util-linux for setpriv
|
# Install Apprise, add sqlite3 cli for debugging in the future, iputils-ping for ping, util-linux for setpriv
|
||||||
# Stupid python3 and python3-pip actually install a lot of useless things into Debian, specific --no-install-recommends to skip them, make the base even smaller than alpine!
|
# Stupid python3 and python3-pip actually install a lot of useless things into Debian, specify --no-install-recommends to skip them, make the base even smaller than alpine!
|
||||||
RUN apt update && \
|
RUN apt update && \
|
||||||
apt --yes --no-install-recommends install python3 python3-pip python3-cryptography python3-six python3-yaml python3-click python3-markdown python3-requests python3-requests-oauthlib \
|
apt --yes --no-install-recommends install python3 python3-pip python3-cryptography python3-six python3-yaml python3-click python3-markdown python3-requests python3-requests-oauthlib \
|
||||||
sqlite3 iputils-ping util-linux dumb-init && \
|
sqlite3 iputils-ping util-linux dumb-init && \
|
||||||
pip3 --no-cache-dir install apprise && \
|
pip3 --no-cache-dir install apprise==0.9.6 && \
|
||||||
rm -rf /var/lib/apt/lists/*
|
rm -rf /var/lib/apt/lists/*
|
||||||
|
@ -4,9 +4,7 @@ WORKDIR /app
|
|||||||
ENV PUPPETEER_SKIP_CHROMIUM_DOWNLOAD=1
|
ENV PUPPETEER_SKIP_CHROMIUM_DOWNLOAD=1
|
||||||
|
|
||||||
COPY . .
|
COPY . .
|
||||||
RUN npm ci && \
|
RUN npm ci --production && \
|
||||||
npm run build && \
|
|
||||||
npm ci --production && \
|
|
||||||
chmod +x /app/extra/entrypoint.sh
|
chmod +x /app/extra/entrypoint.sh
|
||||||
|
|
||||||
|
|
||||||
@ -22,23 +20,26 @@ HEALTHCHECK --interval=60s --timeout=30s --start-period=180s --retries=5 CMD nod
|
|||||||
ENTRYPOINT ["/usr/bin/dumb-init", "--", "extra/entrypoint.sh"]
|
ENTRYPOINT ["/usr/bin/dumb-init", "--", "extra/entrypoint.sh"]
|
||||||
CMD ["node", "server/server.js"]
|
CMD ["node", "server/server.js"]
|
||||||
|
|
||||||
|
|
||||||
FROM release AS nightly
|
FROM release AS nightly
|
||||||
RUN npm run mark-as-nightly
|
RUN npm run mark-as-nightly
|
||||||
|
|
||||||
|
|
||||||
# Upload the artifact to Github
|
# Upload the artifact to Github
|
||||||
FROM louislam/uptime-kuma:base-debian AS upload-artifact
|
FROM louislam/uptime-kuma:base-debian AS upload-artifact
|
||||||
WORKDIR /
|
WORKDIR /
|
||||||
RUN apt update && \
|
RUN apt update && \
|
||||||
apt --yes install curl file
|
apt --yes install curl file
|
||||||
|
|
||||||
|
COPY --from=build /app /app
|
||||||
|
|
||||||
|
ARG VERSION
|
||||||
ARG GITHUB_TOKEN
|
ARG GITHUB_TOKEN
|
||||||
ARG TARGETARCH
|
ARG TARGETARCH
|
||||||
ARG PLATFORM=debian
|
ARG PLATFORM=debian
|
||||||
ARG VERSION
|
|
||||||
ARG FILE=$PLATFORM-$TARGETARCH-$VERSION.tar.gz
|
ARG FILE=$PLATFORM-$TARGETARCH-$VERSION.tar.gz
|
||||||
ARG DIST=dist.tar.gz
|
ARG DIST=dist.tar.gz
|
||||||
|
|
||||||
COPY --from=build /app /app
|
|
||||||
RUN chmod +x /app/extra/upload-github-release-asset.sh
|
RUN chmod +x /app/extra/upload-github-release-asset.sh
|
||||||
|
|
||||||
# Full Build
|
# Full Build
|
||||||
@ -47,5 +48,5 @@ RUN chmod +x /app/extra/upload-github-release-asset.sh
|
|||||||
|
|
||||||
# Dist only
|
# Dist only
|
||||||
RUN cd /app && tar -zcvf $DIST dist
|
RUN cd /app && tar -zcvf $DIST dist
|
||||||
RUN /app/extra/upload-github-release-asset.sh github_api_token=$GITHUB_TOKEN owner=louislam repo=uptime-kuma tag=$VERSION filename=$DIST
|
RUN /app/extra/upload-github-release-asset.sh github_api_token=$GITHUB_TOKEN owner=louislam repo=uptime-kuma tag=$VERSION filename=/app/$DIST
|
||||||
|
|
@ -4,9 +4,7 @@ WORKDIR /app
|
|||||||
ENV PUPPETEER_SKIP_CHROMIUM_DOWNLOAD=1
|
ENV PUPPETEER_SKIP_CHROMIUM_DOWNLOAD=1
|
||||||
|
|
||||||
COPY . .
|
COPY . .
|
||||||
RUN npm ci && \
|
RUN npm ci --production && \
|
||||||
npm run build && \
|
|
||||||
npm ci --production && \
|
|
||||||
chmod +x /app/extra/entrypoint.sh
|
chmod +x /app/extra/entrypoint.sh
|
||||||
|
|
||||||
|
|
||||||
@ -22,5 +20,6 @@ HEALTHCHECK --interval=60s --timeout=30s --start-period=180s --retries=5 CMD nod
|
|||||||
ENTRYPOINT ["/usr/bin/dumb-init", "--", "extra/entrypoint.sh"]
|
ENTRYPOINT ["/usr/bin/dumb-init", "--", "extra/entrypoint.sh"]
|
||||||
CMD ["node", "server/server.js"]
|
CMD ["node", "server/server.js"]
|
||||||
|
|
||||||
|
|
||||||
FROM release AS nightly
|
FROM release AS nightly
|
||||||
RUN npm run mark-as-nightly
|
RUN npm run mark-as-nightly
|
57
extra/close-incorrect-issue.js
Normal file
57
extra/close-incorrect-issue.js
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
const github = require("@actions/github");
|
||||||
|
|
||||||
|
(async () => {
|
||||||
|
try {
|
||||||
|
const token = process.argv[2];
|
||||||
|
const issueNumber = process.argv[3];
|
||||||
|
const username = process.argv[4];
|
||||||
|
|
||||||
|
const client = github.getOctokit(token).rest;
|
||||||
|
|
||||||
|
const issue = {
|
||||||
|
owner: "louislam",
|
||||||
|
repo: "uptime-kuma",
|
||||||
|
number: issueNumber,
|
||||||
|
};
|
||||||
|
|
||||||
|
const labels = (
|
||||||
|
await client.issues.listLabelsOnIssue({
|
||||||
|
owner: issue.owner,
|
||||||
|
repo: issue.repo,
|
||||||
|
issue_number: issue.number
|
||||||
|
})
|
||||||
|
).data.map(({ name }) => name);
|
||||||
|
|
||||||
|
if (labels.length === 0) {
|
||||||
|
console.log("Bad format here");
|
||||||
|
|
||||||
|
await client.issues.addLabels({
|
||||||
|
owner: issue.owner,
|
||||||
|
repo: issue.repo,
|
||||||
|
issue_number: issue.number,
|
||||||
|
labels: ["invalid-format"]
|
||||||
|
});
|
||||||
|
|
||||||
|
// Add the issue closing comment
|
||||||
|
await client.issues.createComment({
|
||||||
|
owner: issue.owner,
|
||||||
|
repo: issue.repo,
|
||||||
|
issue_number: issue.number,
|
||||||
|
body: `@${username}: Hello! :wave:\n\nThis issue is being automatically closed because it does not follow the issue template. Please DO NOT open a blank issue`
|
||||||
|
});
|
||||||
|
|
||||||
|
// Close the issue
|
||||||
|
await client.issues.update({
|
||||||
|
owner: issue.owner,
|
||||||
|
repo: issue.repo,
|
||||||
|
issue_number: issue.number,
|
||||||
|
state: "closed"
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
console.log("Pass!");
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.log(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
})();
|
@ -34,9 +34,11 @@ function download(url) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
tarStream.on("close", () => {
|
tarStream.on("close", () => {
|
||||||
fs.rmdirSync("./dist-backup", {
|
if (fs.existsSync("./dist-backup")) {
|
||||||
recursive: true
|
fs.rmdirSync("./dist-backup", {
|
||||||
});
|
recursive: true
|
||||||
|
});
|
||||||
|
}
|
||||||
console.log("Done");
|
console.log("Done");
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -44,7 +46,7 @@ function download(url) {
|
|||||||
if (fs.existsSync("./dist-backup")) {
|
if (fs.existsSync("./dist-backup")) {
|
||||||
fs.renameSync("./dist-backup", "./dist");
|
fs.renameSync("./dist-backup", "./dist");
|
||||||
}
|
}
|
||||||
console.log("Done");
|
console.error("Error from tarStream");
|
||||||
});
|
});
|
||||||
|
|
||||||
response.pipe(tarStream);
|
response.pipe(tarStream);
|
||||||
|
@ -1,25 +1,41 @@
|
|||||||
/*
|
/*
|
||||||
* This script should be run after a period of time (180s), because the server may need some time to prepare.
|
* This script should be run after a period of time (180s), because the server may need some time to prepare.
|
||||||
*/
|
*/
|
||||||
|
const { FBSD } = require("../server/util-server");
|
||||||
|
|
||||||
process.env.NODE_TLS_REJECT_UNAUTHORIZED = "0";
|
process.env.NODE_TLS_REJECT_UNAUTHORIZED = "0";
|
||||||
|
|
||||||
let client;
|
let client;
|
||||||
|
|
||||||
if (process.env.SSL_KEY && process.env.SSL_CERT) {
|
const sslKey = process.env.UPTIME_KUMA_SSL_KEY || process.env.SSL_KEY || undefined;
|
||||||
|
const sslCert = process.env.UPTIME_KUMA_SSL_CERT || process.env.SSL_CERT || undefined;
|
||||||
|
|
||||||
|
if (sslKey && sslCert) {
|
||||||
client = require("https");
|
client = require("https");
|
||||||
} else {
|
} else {
|
||||||
client = require("http");
|
client = require("http");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If host is omitted, the server will accept connections on the unspecified IPv6 address (::) when IPv6 is available and the unspecified IPv4 address (0.0.0.0) otherwise.
|
||||||
|
// Dual-stack support for (::)
|
||||||
|
let hostname = process.env.UPTIME_KUMA_HOST;
|
||||||
|
|
||||||
|
// Also read HOST if not FreeBSD, as HOST is a system environment variable in FreeBSD
|
||||||
|
if (!hostname && !FBSD) {
|
||||||
|
hostname = process.env.HOST;
|
||||||
|
}
|
||||||
|
|
||||||
|
const port = parseInt(process.env.UPTIME_KUMA_PORT || process.env.PORT || 3001);
|
||||||
|
|
||||||
let options = {
|
let options = {
|
||||||
host: process.env.HOST || "127.0.0.1",
|
host: hostname || "127.0.0.1",
|
||||||
port: parseInt(process.env.PORT) || 3001,
|
port: port,
|
||||||
timeout: 28 * 1000,
|
timeout: 28 * 1000,
|
||||||
};
|
};
|
||||||
|
|
||||||
let request = client.request(options, (res) => {
|
let request = client.request(options, (res) => {
|
||||||
console.log(`Health Check OK [Res Code: ${res.statusCode}]`);
|
console.log(`Health Check OK [Res Code: ${res.statusCode}]`);
|
||||||
if (res.statusCode === 200) {
|
if (res.statusCode === 302) {
|
||||||
process.exit(0);
|
process.exit(0);
|
||||||
} else {
|
} else {
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
|
60
extra/remove-2fa.js
Normal file
60
extra/remove-2fa.js
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
console.log("== Uptime Kuma Remove 2FA Tool ==");
|
||||||
|
console.log("Loading the database");
|
||||||
|
|
||||||
|
const Database = require("../server/database");
|
||||||
|
const { R } = require("redbean-node");
|
||||||
|
const readline = require("readline");
|
||||||
|
const TwoFA = require("../server/2fa");
|
||||||
|
const args = require("args-parser")(process.argv);
|
||||||
|
const rl = readline.createInterface({
|
||||||
|
input: process.stdin,
|
||||||
|
output: process.stdout
|
||||||
|
});
|
||||||
|
|
||||||
|
const main = async () => {
|
||||||
|
Database.init(args);
|
||||||
|
await Database.connect();
|
||||||
|
|
||||||
|
try {
|
||||||
|
// No need to actually reset the password for testing, just make sure no connection problem. It is ok for now.
|
||||||
|
if (!process.env.TEST_BACKEND) {
|
||||||
|
const user = await R.findOne("user");
|
||||||
|
if (! user) {
|
||||||
|
throw new Error("user not found, have you installed?");
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("Found user: " + user.username);
|
||||||
|
|
||||||
|
let ans = await question("Are you sure want to remove 2FA? [y/N]");
|
||||||
|
|
||||||
|
if (ans.toLowerCase() === "y") {
|
||||||
|
await TwoFA.disable2FA(user.id);
|
||||||
|
console.log("2FA has been removed successfully.");
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error("Error: " + e.message);
|
||||||
|
}
|
||||||
|
|
||||||
|
await Database.close();
|
||||||
|
rl.close();
|
||||||
|
|
||||||
|
console.log("Finished.");
|
||||||
|
};
|
||||||
|
|
||||||
|
function question(question) {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
rl.question(question, (answer) => {
|
||||||
|
resolve(answer);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!process.env.TEST_BACKEND) {
|
||||||
|
main();
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
main,
|
||||||
|
};
|
@ -12,50 +12,59 @@ const rl = readline.createInterface({
|
|||||||
output: process.stdout
|
output: process.stdout
|
||||||
});
|
});
|
||||||
|
|
||||||
(async () => {
|
const main = async () => {
|
||||||
Database.init(args);
|
Database.init(args);
|
||||||
await Database.connect();
|
await Database.connect();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const user = await R.findOne("user");
|
// No need to actually reset the password for testing, just make sure no connection problem. It is ok for now.
|
||||||
|
if (!process.env.TEST_BACKEND) {
|
||||||
if (! user) {
|
const user = await R.findOne("user");
|
||||||
throw new Error("user not found, have you installed?");
|
if (! user) {
|
||||||
}
|
throw new Error("user not found, have you installed?");
|
||||||
|
|
||||||
console.log("Found user: " + user.username);
|
|
||||||
|
|
||||||
while (true) {
|
|
||||||
let password = await question("New Password: ");
|
|
||||||
let confirmPassword = await question("Confirm New Password: ");
|
|
||||||
|
|
||||||
if (password === confirmPassword) {
|
|
||||||
await user.resetPassword(password);
|
|
||||||
|
|
||||||
// Reset all sessions by reset jwt secret
|
|
||||||
await initJWTSecret();
|
|
||||||
|
|
||||||
rl.close();
|
|
||||||
break;
|
|
||||||
} else {
|
|
||||||
console.log("Passwords do not match, please try again.");
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
console.log("Password reset successfully.");
|
console.log("Found user: " + user.username);
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
let password = await question("New Password: ");
|
||||||
|
let confirmPassword = await question("Confirm New Password: ");
|
||||||
|
|
||||||
|
if (password === confirmPassword) {
|
||||||
|
await user.resetPassword(password);
|
||||||
|
|
||||||
|
// Reset all sessions by reset jwt secret
|
||||||
|
await initJWTSecret();
|
||||||
|
|
||||||
|
break;
|
||||||
|
} else {
|
||||||
|
console.log("Passwords do not match, please try again.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
console.log("Password reset successfully.");
|
||||||
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error("Error: " + e.message);
|
console.error("Error: " + e.message);
|
||||||
}
|
}
|
||||||
|
|
||||||
await Database.close();
|
await Database.close();
|
||||||
|
rl.close();
|
||||||
|
|
||||||
console.log("Finished. You should restart the Uptime Kuma server.")
|
console.log("Finished.");
|
||||||
})();
|
};
|
||||||
|
|
||||||
function question(question) {
|
function question(question) {
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
rl.question(question, (answer) => {
|
rl.question(question, (answer) => {
|
||||||
resolve(answer);
|
resolve(answer);
|
||||||
})
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!process.env.TEST_BACKEND) {
|
||||||
|
main();
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
main,
|
||||||
|
};
|
||||||
|
@ -26,10 +26,12 @@ const copyRecursiveSync = function (src, dest) {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
console.log("Arguments:", process.argv)
|
console.log("Arguments:", process.argv);
|
||||||
const baseLangCode = process.argv[2] || "en";
|
const baseLangCode = process.argv[2] || "en";
|
||||||
console.log("Base Lang: " + baseLangCode);
|
console.log("Base Lang: " + baseLangCode);
|
||||||
fs.rmdirSync("./languages", { recursive: true });
|
if (fs.existsSync("./languages")) {
|
||||||
|
fs.rmdirSync("./languages", { recursive: true });
|
||||||
|
}
|
||||||
copyRecursiveSync("../../src/languages", "./languages");
|
copyRecursiveSync("../../src/languages", "./languages");
|
||||||
|
|
||||||
const en = (await import("./languages/en.js")).default;
|
const en = (await import("./languages/en.js")).default;
|
||||||
@ -39,7 +41,7 @@ console.log("Files:", files);
|
|||||||
|
|
||||||
for (const file of files) {
|
for (const file of files) {
|
||||||
if (!file.endsWith(".js")) {
|
if (!file.endsWith(".js")) {
|
||||||
console.log("Skipping " + file)
|
console.log("Skipping " + file);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -5,7 +5,7 @@
|
|||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png">
|
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png">
|
||||||
<link rel="icon" type="image/svg+xml" href="/icon.svg" />
|
<link rel="icon" type="image/svg+xml" href="/icon.svg" />
|
||||||
<link rel="manifest" href="manifest.json" />
|
<link rel="manifest" href="/manifest.json" />
|
||||||
<meta name="theme-color" id="theme-color" content="" />
|
<meta name="theme-color" id="theme-color" content="" />
|
||||||
<meta name="description" content="Uptime Kuma monitoring tool" />
|
<meta name="description" content="Uptime Kuma monitoring tool" />
|
||||||
<title>Uptime Kuma</title>
|
<title>Uptime Kuma</title>
|
||||||
|
@ -1,6 +0,0 @@
|
|||||||
module.exports = {
|
|
||||||
"launch": {
|
|
||||||
"headless": process.env.HEADLESS_TEST || false,
|
|
||||||
"userDataDir": "./data/test-chrome-profile",
|
|
||||||
}
|
|
||||||
};
|
|
@ -1,32 +0,0 @@
|
|||||||
# Uptime-Kuma K8s Deployment
|
|
||||||
|
|
||||||
⚠ Warning: K8s deployment is provided by contributors. I have no experience with K8s and I can't fix error in the future. I only test Docker and Node.js. Use at your own risk.
|
|
||||||
|
|
||||||
## How does it work?
|
|
||||||
|
|
||||||
Kustomize is a tool which builds a complete deployment file for all config elements.
|
|
||||||
You can edit the files in the ```uptime-kuma``` folder except the ```kustomization.yml``` until you know what you're doing.
|
|
||||||
If you want to choose another namespace you can edit the ```kustomization.yml``` in the ```kubernetes```-Folder and change the ```namespace: uptime-kuma``` to something you like.
|
|
||||||
|
|
||||||
It creates a certificate with the specified Issuer and creates the Ingress for the Uptime-Kuma ClusterIP-Service.
|
|
||||||
|
|
||||||
## What do I have to edit?
|
|
||||||
|
|
||||||
You have to edit the ```ingressroute.yml``` to your needs.
|
|
||||||
This ingressroute.yml is for the [nginx-ingress-controller](https://kubernetes.github.io/ingress-nginx/) in combination with the [cert-manager](https://cert-manager.io/).
|
|
||||||
|
|
||||||
- Host
|
|
||||||
- Secrets and secret names
|
|
||||||
- (Cluster)Issuer (optional)
|
|
||||||
- The Version in the Deployment-File
|
|
||||||
- Update:
|
|
||||||
- Change to newer version and run the above commands, it will update the pods one after another
|
|
||||||
|
|
||||||
## How To use
|
|
||||||
|
|
||||||
- Install [kustomize](https://kubectl.docs.kubernetes.io/installation/kustomize/)
|
|
||||||
- Edit files mentioned above to your needs
|
|
||||||
- Run ```kustomize build > apply.yml```
|
|
||||||
- Run ```kubectl apply -f apply.yml```
|
|
||||||
|
|
||||||
Now you should see some k8s magic and Uptime-Kuma should be available at the specified address.
|
|
@ -1,10 +0,0 @@
|
|||||||
namespace: uptime-kuma
|
|
||||||
namePrefix: uptime-kuma-
|
|
||||||
|
|
||||||
commonLabels:
|
|
||||||
app: uptime-kuma
|
|
||||||
|
|
||||||
bases:
|
|
||||||
- uptime-kuma
|
|
||||||
|
|
||||||
|
|
@ -1,45 +0,0 @@
|
|||||||
apiVersion: apps/v1
|
|
||||||
kind: Deployment
|
|
||||||
metadata:
|
|
||||||
labels:
|
|
||||||
component: uptime-kuma
|
|
||||||
name: deployment
|
|
||||||
spec:
|
|
||||||
selector:
|
|
||||||
matchLabels:
|
|
||||||
component: uptime-kuma
|
|
||||||
replicas: 1
|
|
||||||
strategy:
|
|
||||||
type: Recreate
|
|
||||||
|
|
||||||
template:
|
|
||||||
metadata:
|
|
||||||
labels:
|
|
||||||
component: uptime-kuma
|
|
||||||
spec:
|
|
||||||
containers:
|
|
||||||
- name: app
|
|
||||||
image: louislam/uptime-kuma:1
|
|
||||||
ports:
|
|
||||||
- containerPort: 3001
|
|
||||||
volumeMounts:
|
|
||||||
- mountPath: /app/data
|
|
||||||
name: storage
|
|
||||||
livenessProbe:
|
|
||||||
exec:
|
|
||||||
command:
|
|
||||||
- node
|
|
||||||
- extra/healthcheck.js
|
|
||||||
initialDelaySeconds: 180
|
|
||||||
periodSeconds: 60
|
|
||||||
timeoutSeconds: 30
|
|
||||||
readinessProbe:
|
|
||||||
httpGet:
|
|
||||||
path: /
|
|
||||||
port: 3001
|
|
||||||
scheme: HTTP
|
|
||||||
|
|
||||||
volumes:
|
|
||||||
- name: storage
|
|
||||||
persistentVolumeClaim:
|
|
||||||
claimName: pvc
|
|
@ -1,39 +0,0 @@
|
|||||||
apiVersion: networking.k8s.io/v1
|
|
||||||
kind: Ingress
|
|
||||||
metadata:
|
|
||||||
annotations:
|
|
||||||
kubernetes.io/ingress.class: nginx
|
|
||||||
cert-manager.io/cluster-issuer: letsencrypt-prod
|
|
||||||
nginx.ingress.kubernetes.io/proxy-read-timeout: "3600"
|
|
||||||
nginx.ingress.kubernetes.io/proxy-send-timeout: "3600"
|
|
||||||
nginx.ingress.kubernetes.io/server-snippets: |
|
|
||||||
location / {
|
|
||||||
proxy_set_header Upgrade $http_upgrade;
|
|
||||||
proxy_http_version 1.1;
|
|
||||||
proxy_set_header X-Forwarded-Host $http_host;
|
|
||||||
proxy_set_header X-Forwarded-Proto $scheme;
|
|
||||||
proxy_set_header X-Forwarded-For $remote_addr;
|
|
||||||
proxy_set_header Host $host;
|
|
||||||
proxy_set_header Connection "upgrade";
|
|
||||||
proxy_set_header X-Real-IP $remote_addr;
|
|
||||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
|
||||||
proxy_set_header Upgrade $http_upgrade;
|
|
||||||
proxy_cache_bypass $http_upgrade;
|
|
||||||
}
|
|
||||||
name: ingress
|
|
||||||
spec:
|
|
||||||
tls:
|
|
||||||
- hosts:
|
|
||||||
- example.com
|
|
||||||
secretName: example-com-tls
|
|
||||||
rules:
|
|
||||||
- host: example.com
|
|
||||||
http:
|
|
||||||
paths:
|
|
||||||
- path: /
|
|
||||||
pathType: Prefix
|
|
||||||
backend:
|
|
||||||
service:
|
|
||||||
name: service
|
|
||||||
port:
|
|
||||||
number: 3001
|
|
@ -1,5 +0,0 @@
|
|||||||
resources:
|
|
||||||
- deployment.yml
|
|
||||||
- service.yml
|
|
||||||
- ingressroute.yml
|
|
||||||
- pvc.yml
|
|
@ -1,10 +0,0 @@
|
|||||||
apiVersion: v1
|
|
||||||
kind: PersistentVolumeClaim
|
|
||||||
metadata:
|
|
||||||
name: pvc
|
|
||||||
spec:
|
|
||||||
accessModes:
|
|
||||||
- ReadWriteOnce
|
|
||||||
resources:
|
|
||||||
requests:
|
|
||||||
storage: 4Gi
|
|
@ -1,13 +0,0 @@
|
|||||||
apiVersion: v1
|
|
||||||
kind: Service
|
|
||||||
metadata:
|
|
||||||
name: service
|
|
||||||
spec:
|
|
||||||
selector:
|
|
||||||
component: uptime-kuma
|
|
||||||
type: ClusterIP
|
|
||||||
ports:
|
|
||||||
- name: http
|
|
||||||
port: 3001
|
|
||||||
targetPort: 3001
|
|
||||||
protocol: TCP
|
|
12373
package-lock.json
generated
12373
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
81
package.json
81
package.json
@ -1,13 +1,13 @@
|
|||||||
{
|
{
|
||||||
"name": "uptime-kuma",
|
"name": "uptime-kuma",
|
||||||
"version": "1.8.0",
|
"version": "1.11.1",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://github.com/louislam/uptime-kuma.git"
|
"url": "https://github.com/louislam/uptime-kuma.git"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": "14.*"
|
"node": "14.* || >=16.*"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"install-legacy": "npm install --legacy-peer-deps",
|
"install-legacy": "npm install --legacy-peer-deps",
|
||||||
@ -15,31 +15,33 @@
|
|||||||
"lint:js": "eslint --ext \".js,.vue\" --ignore-path .gitignore .",
|
"lint:js": "eslint --ext \".js,.vue\" --ignore-path .gitignore .",
|
||||||
"lint:style": "stylelint \"**/*.{vue,css,scss}\" --ignore-path .gitignore",
|
"lint:style": "stylelint \"**/*.{vue,css,scss}\" --ignore-path .gitignore",
|
||||||
"lint": "npm run lint:js && npm run lint:style",
|
"lint": "npm run lint:js && npm run lint:style",
|
||||||
"dev": "vite --host",
|
"dev": "vite --host --config ./config/vite.config.js",
|
||||||
"start": "npm run start-server",
|
"start": "npm run start-server",
|
||||||
"start-server": "node server/server.js",
|
"start-server": "node server/server.js",
|
||||||
"start-server-dev": "cross-env NODE_ENV=development node server/server.js",
|
"start-server-dev": "cross-env NODE_ENV=development node server/server.js",
|
||||||
"build": "vite build",
|
"build": "vite build --config ./config/vite.config.js",
|
||||||
"test": "node test/prepare-test-server.js && node server/server.js --port=3002 --data-dir=./data/test/ --test",
|
"test": "node test/prepare-test-server.js && node server/server.js --port=3002 --data-dir=./data/test/ --test",
|
||||||
"test-with-build": "npm run build && npm test",
|
"test-with-build": "npm run build && npm test",
|
||||||
"jest": "node test/prepare-jest.js && npm run jest-frontend && jest ",
|
"jest": "node test/prepare-jest.js && npm run jest-frontend && npm run jest-backend",
|
||||||
"jest-frontend": "cross-env TEST_FRONTEND=1 jest --config=./jest-frontend.config.js",
|
"jest-frontend": "cross-env TEST_FRONTEND=1 jest --config=./config/jest-frontend.config.js",
|
||||||
|
"jest-backend": "cross-env TEST_BACKEND=1 jest --config=./config/jest-backend.config.js",
|
||||||
"tsc": "tsc",
|
"tsc": "tsc",
|
||||||
"vite-preview-dist": "vite preview --host",
|
"vite-preview-dist": "vite preview --host --config ./config/vite.config.js",
|
||||||
"build-docker": "npm run build-docker-debian && npm run build-docker-alpine",
|
"build-docker": "npm run build-docker-debian && npm run build-docker-alpine",
|
||||||
"build-docker-alpine-base": "docker buildx build -f docker/alpine-base.dockerfile --platform linux/amd64,linux/arm64,linux/arm/v7 -t louislam/uptime-kuma:base-alpine . --push",
|
"build-docker-alpine-base": "docker buildx build -f docker/alpine-base.dockerfile --platform linux/amd64,linux/arm64,linux/arm/v7 -t louislam/uptime-kuma:base-alpine . --push",
|
||||||
"build-docker-debian-base": "docker buildx build -f docker/debian-base.dockerfile --platform linux/amd64,linux/arm64,linux/arm/v7 -t louislam/uptime-kuma:base-debian . --push",
|
"build-docker-debian-base": "docker buildx build -f docker/debian-base.dockerfile --platform linux/amd64,linux/arm64,linux/arm/v7 -t louislam/uptime-kuma:base-debian . --push",
|
||||||
"build-docker-alpine": "docker buildx build -f dockerfile-alpine --platform linux/amd64,linux/arm64,linux/arm/v7 -t louislam/uptime-kuma:alpine -t louislam/uptime-kuma:1-alpine -t louislam/uptime-kuma:1.8.0-alpine --target release . --push",
|
"build-docker-alpine": "docker buildx build -f docker/dockerfile-alpine --platform linux/amd64,linux/arm64,linux/arm/v7 -t louislam/uptime-kuma:alpine -t louislam/uptime-kuma:1-alpine -t louislam/uptime-kuma:1.11.1-alpine --target release . --push",
|
||||||
"build-docker-debian": "docker buildx build --platform linux/amd64,linux/arm64,linux/arm/v7 -t louislam/uptime-kuma -t louislam/uptime-kuma:1 -t louislam/uptime-kuma:1.8.0 -t louislam/uptime-kuma:debian -t louislam/uptime-kuma:1-debian -t louislam/uptime-kuma:1.8.0-debian --target release . --push",
|
"build-docker-debian": "docker buildx build -f docker/dockerfile --platform linux/amd64,linux/arm64,linux/arm/v7 -t louislam/uptime-kuma -t louislam/uptime-kuma:1 -t louislam/uptime-kuma:1.11.1 -t louislam/uptime-kuma:debian -t louislam/uptime-kuma:1-debian -t louislam/uptime-kuma:1.11.1-debian --target release . --push",
|
||||||
"build-docker-nightly": "docker buildx build --platform linux/amd64,linux/arm64,linux/arm/v7 -t louislam/uptime-kuma:nightly --target nightly . --push",
|
"build-docker-nightly": "docker buildx build -f docker/dockerfile --platform linux/amd64,linux/arm64,linux/arm/v7 -t louislam/uptime-kuma:nightly --target nightly . --push",
|
||||||
"build-docker-nightly-alpine": "docker buildx build -f dockerfile-alpine --platform linux/amd64,linux/arm64,linux/arm/v7 -t louislam/uptime-kuma:nightly-alpine --target nightly . --push",
|
"build-docker-nightly-alpine": "docker buildx build -f docker/dockerfile-alpine --platform linux/amd64,linux/arm64,linux/arm/v7 -t louislam/uptime-kuma:nightly-alpine --target nightly . --push",
|
||||||
"build-docker-nightly-amd64": "docker buildx build --platform linux/amd64 -t louislam/uptime-kuma:nightly-amd64 --target nightly . --push --progress plain",
|
"build-docker-nightly-amd64": "docker buildx build -f docker/dockerfile --platform linux/amd64 -t louislam/uptime-kuma:nightly-amd64 --target nightly . --push --progress plain",
|
||||||
"upload-artifacts": "docker buildx build --platform linux/amd64 -t louislam/uptime-kuma:upload-artifact --build-arg GITHUB_TOKEN --target upload-artifact . --progress plain",
|
"upload-artifacts": "docker buildx build -f docker/dockerfile --platform linux/amd64 -t louislam/uptime-kuma:upload-artifact --build-arg VERSION --build-arg GITHUB_TOKEN --target upload-artifact . --progress plain",
|
||||||
"setup": "git checkout 1.8.0 && npm ci --production && npm run download-dist",
|
"setup": "git checkout 1.11.1 && npm ci --production && npm run download-dist",
|
||||||
"download-dist": "node extra/download-dist.js",
|
"download-dist": "node extra/download-dist.js",
|
||||||
"update-version": "node extra/update-version.js",
|
"update-version": "node extra/update-version.js",
|
||||||
"mark-as-nightly": "node extra/mark-as-nightly.js",
|
"mark-as-nightly": "node extra/mark-as-nightly.js",
|
||||||
"reset-password": "node extra/reset-password.js",
|
"reset-password": "node extra/reset-password.js",
|
||||||
|
"remove-2fa": "node extra/remove-2fa.js",
|
||||||
"compile-install-script": "@powershell -NoProfile -ExecutionPolicy Unrestricted -Command ./extra/compile-install-script.ps1",
|
"compile-install-script": "@powershell -NoProfile -ExecutionPolicy Unrestricted -Command ./extra/compile-install-script.ps1",
|
||||||
"test-install-script-centos7": "npm run compile-install-script && docker build --progress plain -f test/test_install_script/centos7.dockerfile .",
|
"test-install-script-centos7": "npm run compile-install-script && docker build --progress plain -f test/test_install_script/centos7.dockerfile .",
|
||||||
"test-install-script-alpine3": "npm run compile-install-script && docker build --progress plain -f test/test_install_script/alpine3.dockerfile .",
|
"test-install-script-alpine3": "npm run compile-install-script && docker build --progress plain -f test/test_install_script/alpine3.dockerfile .",
|
||||||
@ -48,76 +50,85 @@
|
|||||||
"test-nodejs16": "docker build --progress plain -f test/ubuntu-nodejs16.dockerfile .",
|
"test-nodejs16": "docker build --progress plain -f test/ubuntu-nodejs16.dockerfile .",
|
||||||
"simple-dns-server": "node extra/simple-dns-server.js",
|
"simple-dns-server": "node extra/simple-dns-server.js",
|
||||||
"update-language-files-with-base-lang": "cd extra/update-language-files && node index.js %npm_config_base_lang% && eslint ../../src/languages/**.js --fix",
|
"update-language-files-with-base-lang": "cd extra/update-language-files && node index.js %npm_config_base_lang% && eslint ../../src/languages/**.js --fix",
|
||||||
"update-language-files": "cd extra/update-language-files && node index.js && eslint ../../src/languages/**.js --fix"
|
"update-language-files": "cd extra/update-language-files && node index.js && eslint ../../src/languages/**.js --fix",
|
||||||
|
"ncu-patch": "ncu -u -t patch"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@fortawesome/fontawesome-svg-core": "~1.2.36",
|
"@fortawesome/fontawesome-svg-core": "~1.2.36",
|
||||||
"@fortawesome/free-regular-svg-icons": "~5.15.4",
|
"@fortawesome/free-regular-svg-icons": "~5.15.4",
|
||||||
"@fortawesome/free-solid-svg-icons": "~5.15.4",
|
"@fortawesome/free-solid-svg-icons": "~5.15.4",
|
||||||
"@fortawesome/vue-fontawesome": "~3.0.0-4",
|
"@fortawesome/vue-fontawesome": "~3.0.0-5",
|
||||||
"@louislam/sqlite3": "~6.0.0",
|
"@louislam/sqlite3": "~6.0.1",
|
||||||
"@popperjs/core": "~2.10.2",
|
"@popperjs/core": "~2.10.2",
|
||||||
"args-parser": "~1.3.0",
|
"args-parser": "~1.3.0",
|
||||||
"axios": "~0.21.4",
|
"axios": "~0.21.4",
|
||||||
"bcryptjs": "~2.4.3",
|
"bcryptjs": "~2.4.3",
|
||||||
"bootstrap": "~5.1.1",
|
"bootstrap": "5.1.3",
|
||||||
"chart.js": "~3.5.1",
|
"bree": "~7.1.0",
|
||||||
|
"chardet": "^1.3.0",
|
||||||
|
"chart.js": "~3.6.0",
|
||||||
"chartjs-adapter-dayjs": "~1.0.0",
|
"chartjs-adapter-dayjs": "~1.0.0",
|
||||||
|
"check-password-strength": "^2.0.3",
|
||||||
"command-exists": "~1.2.9",
|
"command-exists": "~1.2.9",
|
||||||
"compare-versions": "~3.6.0",
|
"compare-versions": "~3.6.0",
|
||||||
"dayjs": "~1.10.7",
|
"dayjs": "~1.10.7",
|
||||||
"express": "~4.17.1",
|
"express": "~4.17.1",
|
||||||
"express-basic-auth": "~1.2.0",
|
"express-basic-auth": "~1.2.0",
|
||||||
"form-data": "~4.0.0",
|
"form-data": "~4.0.0",
|
||||||
"http-graceful-shutdown": "~3.1.4",
|
"http-graceful-shutdown": "~3.1.5",
|
||||||
|
"iconv-lite": "^0.6.3",
|
||||||
"jsonwebtoken": "~8.5.1",
|
"jsonwebtoken": "~8.5.1",
|
||||||
|
"jwt-decode": "^3.1.2",
|
||||||
|
"limiter": "^2.1.0",
|
||||||
"nodemailer": "~6.6.5",
|
"nodemailer": "~6.6.5",
|
||||||
"notp": "~2.0.3",
|
"notp": "~2.0.3",
|
||||||
"password-hash": "~1.2.2",
|
"password-hash": "~1.2.2",
|
||||||
"postcss-rtlcss": "~3.4.1",
|
"postcss-rtlcss": "~3.4.1",
|
||||||
"postcss-scss": "~4.0.1",
|
"postcss-scss": "~4.0.2",
|
||||||
"prom-client": "~13.2.0",
|
"prom-client": "~13.2.0",
|
||||||
"prometheus-api-metrics": "~3.2.0",
|
"prometheus-api-metrics": "~3.2.0",
|
||||||
"qrcode": "~1.4.4",
|
"qrcode": "~1.5.0",
|
||||||
"redbean-node": "0.1.2",
|
"redbean-node": "0.1.3",
|
||||||
"socket.io": "~4.2.0",
|
"socket.io": "~4.2.0",
|
||||||
"socket.io-client": "~4.2.0",
|
"socket.io-client": "~4.2.0",
|
||||||
"tar": "^6.1.11",
|
"tar": "^6.1.11",
|
||||||
"tcp-ping": "~0.1.1",
|
"tcp-ping": "~0.1.1",
|
||||||
"thirty-two": "~1.0.2",
|
"thirty-two": "~1.0.2",
|
||||||
"timezones-list": "~3.0.1",
|
"timezones-list": "~3.0.1",
|
||||||
"v-pagination-3": "~0.1.6",
|
"v-pagination-3": "~0.1.7",
|
||||||
"vue": "next",
|
"vue": "next",
|
||||||
"vue-chart-3": "~0.5.8",
|
"vue-chart-3": "~0.5.11",
|
||||||
"vue-confirm-dialog": "~1.0.2",
|
"vue-confirm-dialog": "~1.0.2",
|
||||||
"vue-contenteditable": "~3.0.4",
|
"vue-contenteditable": "~3.0.4",
|
||||||
"vue-i18n": "~9.1.9",
|
"vue-i18n": "~9.1.9",
|
||||||
"vue-image-crop-upload": "~3.0.3",
|
"vue-image-crop-upload": "~3.0.3",
|
||||||
"vue-multiselect": "~3.0.0-alpha.2",
|
"vue-multiselect": "~3.0.0-alpha.2",
|
||||||
"vue-qrcode": "~1.0.0",
|
"vue-qrcode": "~1.0.0",
|
||||||
"vue-router": "~4.0.11",
|
"vue-router": "~4.0.12",
|
||||||
"vue-toastification": "~2.0.0-rc.1",
|
"vue-toastification": "~2.0.0-rc.5",
|
||||||
"vuedraggable": "~4.1.0"
|
"vuedraggable": "~4.1.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/eslint-parser": "~7.15.7",
|
"@actions/github": "~5.0.0",
|
||||||
|
"@babel/eslint-parser": "~7.15.8",
|
||||||
"@babel/preset-env": "^7.15.8",
|
"@babel/preset-env": "^7.15.8",
|
||||||
"@types/bootstrap": "~5.1.6",
|
"@types/bootstrap": "~5.1.6",
|
||||||
"@vitejs/plugin-legacy": "~1.6.1",
|
"@vitejs/plugin-legacy": "~1.6.3",
|
||||||
"@vitejs/plugin-vue": "~1.9.2",
|
"@vitejs/plugin-vue": "~1.9.4",
|
||||||
"@vue/compiler-sfc": "~3.2.19",
|
"@vue/compiler-sfc": "~3.2.22",
|
||||||
"core-js": "~3.18.1",
|
"babel-plugin-rewire": "~1.2.0",
|
||||||
|
"core-js": "~3.18.3",
|
||||||
"cross-env": "~7.0.3",
|
"cross-env": "~7.0.3",
|
||||||
"dns2": "~2.0.1",
|
"dns2": "~2.0.1",
|
||||||
"eslint": "~7.32.0",
|
"eslint": "~7.32.0",
|
||||||
"eslint-plugin-vue": "~7.18.0",
|
"eslint-plugin-vue": "~7.18.0",
|
||||||
"jest": "~27.2.4",
|
"jest": "~27.2.5",
|
||||||
"jest-puppeteer": "~6.0.0",
|
"jest-puppeteer": "~6.0.0",
|
||||||
"puppeteer": "~10.4.0",
|
"puppeteer": "~10.4.0",
|
||||||
"sass": "~1.42.1",
|
"sass": "~1.42.1",
|
||||||
"stylelint": "~13.13.1",
|
"stylelint": "~13.13.1",
|
||||||
"stylelint-config-standard": "~22.0.0",
|
"stylelint-config-standard": "~22.0.0",
|
||||||
"typescript": "~4.4.3",
|
"typescript": "~4.4.4",
|
||||||
"vite": "~2.6.4"
|
"vite": "~2.6.14"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
14
server/2fa.js
Normal file
14
server/2fa.js
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
const { checkLogin } = require("./util-server");
|
||||||
|
const { R } = require("redbean-node");
|
||||||
|
|
||||||
|
class TwoFA {
|
||||||
|
|
||||||
|
static async disable2FA(userID) {
|
||||||
|
return await R.exec("UPDATE `user` SET twofa_status = 0 WHERE id = ? ", [
|
||||||
|
userID,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = TwoFA;
|
@ -1,8 +1,9 @@
|
|||||||
const basicAuth = require("express-basic-auth")
|
const basicAuth = require("express-basic-auth");
|
||||||
const passwordHash = require("./password-hash");
|
const passwordHash = require("./password-hash");
|
||||||
const { R } = require("redbean-node");
|
const { R } = require("redbean-node");
|
||||||
const { setting } = require("./util-server");
|
const { setting } = require("./util-server");
|
||||||
const { debug } = require("../src/util");
|
const { debug } = require("../src/util");
|
||||||
|
const { loginRateLimiter } = require("./rate-limiter");
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
@ -13,7 +14,7 @@ const { debug } = require("../src/util");
|
|||||||
exports.login = async function (username, password) {
|
exports.login = async function (username, password) {
|
||||||
let user = await R.findOne("user", " username = ? AND active = 1 ", [
|
let user = await R.findOne("user", " username = ? AND active = 1 ", [
|
||||||
username,
|
username,
|
||||||
])
|
]);
|
||||||
|
|
||||||
if (user && passwordHash.verify(password, user.password)) {
|
if (user && passwordHash.verify(password, user.password)) {
|
||||||
// Upgrade the hash to bcrypt
|
// Upgrade the hash to bcrypt
|
||||||
@ -27,21 +28,30 @@ exports.login = async function (username, password) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
};
|
||||||
|
|
||||||
function myAuthorizer(username, password, callback) {
|
function myAuthorizer(username, password, callback) {
|
||||||
|
|
||||||
setting("disableAuth").then((result) => {
|
setting("disableAuth").then((result) => {
|
||||||
|
|
||||||
if (result) {
|
if (result) {
|
||||||
callback(null, true)
|
callback(null, true);
|
||||||
} else {
|
} else {
|
||||||
exports.login(username, password).then((user) => {
|
// Login Rate Limit
|
||||||
callback(null, user != null)
|
loginRateLimiter.pass(null, 0).then((pass) => {
|
||||||
})
|
if (pass) {
|
||||||
}
|
exports.login(username, password).then((user) => {
|
||||||
})
|
callback(null, user != null);
|
||||||
|
|
||||||
|
if (user == null) {
|
||||||
|
loginRateLimiter.removeTokens(1);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
callback(null, false);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
exports.basicAuth = basicAuth({
|
exports.basicAuth = basicAuth({
|
||||||
|
@ -9,18 +9,17 @@ let interval;
|
|||||||
exports.startInterval = () => {
|
exports.startInterval = () => {
|
||||||
let check = async () => {
|
let check = async () => {
|
||||||
try {
|
try {
|
||||||
const res = await axios.get("https://raw.githubusercontent.com/louislam/uptime-kuma/master/package.json");
|
const res = await axios.get("https://uptime.kuma.pet/version");
|
||||||
|
|
||||||
if (typeof res.data === "string") {
|
|
||||||
res.data = JSON.parse(res.data);
|
|
||||||
}
|
|
||||||
|
|
||||||
// For debug
|
// For debug
|
||||||
if (process.env.TEST_CHECK_VERSION === "1") {
|
if (process.env.TEST_CHECK_VERSION === "1") {
|
||||||
res.data.version = "1000.0.0";
|
res.data.slow = "1000.0.0";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (res.data.slow) {
|
||||||
|
exports.latestVersion = res.data.slow;
|
||||||
}
|
}
|
||||||
|
|
||||||
exports.latestVersion = res.data.version;
|
|
||||||
} catch (_) { }
|
} catch (_) { }
|
||||||
|
|
||||||
};
|
};
|
||||||
|
7
server/config.js
Normal file
7
server/config.js
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
const args = require("args-parser")(process.argv);
|
||||||
|
const demoMode = args["demo"] || false;
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
args,
|
||||||
|
demoMode
|
||||||
|
};
|
@ -49,10 +49,14 @@ class Database {
|
|||||||
"patch-incident-table.sql": true,
|
"patch-incident-table.sql": true,
|
||||||
"patch-group-table.sql": true,
|
"patch-group-table.sql": true,
|
||||||
"patch-monitor-push_token.sql": true,
|
"patch-monitor-push_token.sql": true,
|
||||||
|
"patch-http-monitor-method-body-and-headers.sql": true,
|
||||||
|
"patch-2fa-invalidate-used-token.sql": true,
|
||||||
|
"patch-notification_sent_history.sql": true,
|
||||||
|
"patch-monitor-basic-auth.sql": true,
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The finally version should be 10 after merged tag feature
|
* The final version should be 10 after merged tag feature
|
||||||
* @deprecated Use patchList for any new feature
|
* @deprecated Use patchList for any new feature
|
||||||
*/
|
*/
|
||||||
static latestVersion = 10;
|
static latestVersion = 10;
|
||||||
@ -76,7 +80,7 @@ class Database {
|
|||||||
console.log(`Data Dir: ${Database.dataDir}`);
|
console.log(`Data Dir: ${Database.dataDir}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
static async connect() {
|
static async connect(testMode = false) {
|
||||||
const acquireConnectionTimeout = 120 * 1000;
|
const acquireConnectionTimeout = 120 * 1000;
|
||||||
|
|
||||||
const Dialect = require("knex/lib/dialects/sqlite3/index.js");
|
const Dialect = require("knex/lib/dialects/sqlite3/index.js");
|
||||||
@ -109,9 +113,15 @@ class Database {
|
|||||||
await R.autoloadModels("./server/model");
|
await R.autoloadModels("./server/model");
|
||||||
|
|
||||||
await R.exec("PRAGMA foreign_keys = ON");
|
await R.exec("PRAGMA foreign_keys = ON");
|
||||||
// Change to WAL
|
if (testMode) {
|
||||||
await R.exec("PRAGMA journal_mode = WAL");
|
// Change to MEMORY
|
||||||
|
await R.exec("PRAGMA journal_mode = MEMORY");
|
||||||
|
} else {
|
||||||
|
// Change to WAL
|
||||||
|
await R.exec("PRAGMA journal_mode = WAL");
|
||||||
|
}
|
||||||
await R.exec("PRAGMA cache_size = -12000");
|
await R.exec("PRAGMA cache_size = -12000");
|
||||||
|
await R.exec("PRAGMA auto_vacuum = FULL");
|
||||||
|
|
||||||
console.log("SQLite config:");
|
console.log("SQLite config:");
|
||||||
console.log(await R.getAll("PRAGMA journal_mode"));
|
console.log(await R.getAll("PRAGMA journal_mode"));
|
||||||
@ -130,7 +140,7 @@ class Database {
|
|||||||
console.info("Latest database version: " + this.latestVersion);
|
console.info("Latest database version: " + this.latestVersion);
|
||||||
|
|
||||||
if (version === this.latestVersion) {
|
if (version === this.latestVersion) {
|
||||||
console.info("Database no need to patch");
|
console.info("Database patch not needed");
|
||||||
} else if (version > this.latestVersion) {
|
} else if (version > this.latestVersion) {
|
||||||
console.info("Warning: Database version is newer than expected");
|
console.info("Warning: Database version is newer than expected");
|
||||||
} else {
|
} else {
|
||||||
@ -151,8 +161,8 @@ class Database {
|
|||||||
await Database.close();
|
await Database.close();
|
||||||
|
|
||||||
console.error(ex);
|
console.error(ex);
|
||||||
console.error("Start Uptime-Kuma failed due to patch db failed");
|
console.error("Start Uptime-Kuma failed due to issue patching the database");
|
||||||
console.error("Please submit the bug report if you still encounter the problem after restart: https://github.com/louislam/uptime-kuma/issues");
|
console.error("Please submit a bug report if you still encounter the problem after restart: https://github.com/louislam/uptime-kuma/issues");
|
||||||
|
|
||||||
this.restore();
|
this.restore();
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
@ -190,7 +200,7 @@ class Database {
|
|||||||
await Database.close();
|
await Database.close();
|
||||||
|
|
||||||
console.error(ex);
|
console.error(ex);
|
||||||
console.error("Start Uptime-Kuma failed due to patch db failed");
|
console.error("Start Uptime-Kuma failed due to issue patching the database");
|
||||||
console.error("Please submit the bug report if you still encounter the problem after restart: https://github.com/louislam/uptime-kuma/issues");
|
console.error("Please submit the bug report if you still encounter the problem after restart: https://github.com/louislam/uptime-kuma/issues");
|
||||||
|
|
||||||
this.restore();
|
this.restore();
|
||||||
@ -231,7 +241,7 @@ class Database {
|
|||||||
this.patched = true;
|
this.patched = true;
|
||||||
await this.importSQLFile("./db/" + sqlFilename);
|
await this.importSQLFile("./db/" + sqlFilename);
|
||||||
databasePatchedFiles[sqlFilename] = true;
|
databasePatchedFiles[sqlFilename] = true;
|
||||||
console.log(sqlFilename + " is patched successfully");
|
console.log(sqlFilename + " was patched successfully");
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
debug(sqlFilename + " is already patched, skip");
|
debug(sqlFilename + " is already patched, skip");
|
||||||
@ -286,7 +296,7 @@ class Database {
|
|||||||
};
|
};
|
||||||
process.addListener("unhandledRejection", listener);
|
process.addListener("unhandledRejection", listener);
|
||||||
|
|
||||||
console.log("Closing DB");
|
console.log("Closing the database");
|
||||||
|
|
||||||
while (true) {
|
while (true) {
|
||||||
Database.noReject = true;
|
Database.noReject = true;
|
||||||
@ -296,7 +306,7 @@ class Database {
|
|||||||
if (Database.noReject) {
|
if (Database.noReject) {
|
||||||
break;
|
break;
|
||||||
} else {
|
} else {
|
||||||
console.log("Waiting to close the db");
|
console.log("Waiting to close the database");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
console.log("SQLite closed");
|
console.log("SQLite closed");
|
||||||
@ -311,7 +321,7 @@ class Database {
|
|||||||
*/
|
*/
|
||||||
static backup(version) {
|
static backup(version) {
|
||||||
if (! this.backupPath) {
|
if (! this.backupPath) {
|
||||||
console.info("Backup the db");
|
console.info("Backing up the database");
|
||||||
this.backupPath = this.dataDir + "kuma.db.bak" + version;
|
this.backupPath = this.dataDir + "kuma.db.bak" + version;
|
||||||
fs.copyFileSync(Database.path, this.backupPath);
|
fs.copyFileSync(Database.path, this.backupPath);
|
||||||
|
|
||||||
@ -334,7 +344,7 @@ class Database {
|
|||||||
*/
|
*/
|
||||||
static restore() {
|
static restore() {
|
||||||
if (this.backupPath) {
|
if (this.backupPath) {
|
||||||
console.error("Patch db failed!!! Restoring the backup");
|
console.error("Patching the database failed!!! Restoring the backup");
|
||||||
|
|
||||||
const shmPath = Database.path + "-shm";
|
const shmPath = Database.path + "-shm";
|
||||||
const walPath = Database.path + "-wal";
|
const walPath = Database.path + "-wal";
|
||||||
@ -353,7 +363,7 @@ class Database {
|
|||||||
fs.unlinkSync(walPath);
|
fs.unlinkSync(walPath);
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.log("Restore failed, you may need to restore the backup manually");
|
console.log("Restore failed; you may need to restore the backup manually");
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -372,6 +382,17 @@ class Database {
|
|||||||
console.log("Nothing to restore");
|
console.log("Nothing to restore");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static getSize() {
|
||||||
|
debug("Database.getSize()");
|
||||||
|
let stats = fs.statSync(Database.path);
|
||||||
|
debug(stats);
|
||||||
|
return stats.size;
|
||||||
|
}
|
||||||
|
|
||||||
|
static async shrink() {
|
||||||
|
await R.exec("VACUUM");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = Database;
|
module.exports = Database;
|
||||||
|
31
server/jobs.js
Normal file
31
server/jobs.js
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
const path = require("path");
|
||||||
|
const Bree = require("bree");
|
||||||
|
const { SHARE_ENV } = require("worker_threads");
|
||||||
|
|
||||||
|
const jobs = [
|
||||||
|
{
|
||||||
|
name: "clear-old-data",
|
||||||
|
interval: "at 03:14",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const initBackgroundJobs = function (args) {
|
||||||
|
const bree = new Bree({
|
||||||
|
root: path.resolve("server", "jobs"),
|
||||||
|
jobs,
|
||||||
|
worker: {
|
||||||
|
env: SHARE_ENV,
|
||||||
|
workerData: args,
|
||||||
|
},
|
||||||
|
workerMessageHandler: (message) => {
|
||||||
|
console.log("[Background Job]:", message);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
bree.start();
|
||||||
|
return bree;
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
initBackgroundJobs
|
||||||
|
};
|
40
server/jobs/clear-old-data.js
Normal file
40
server/jobs/clear-old-data.js
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
const { log, exit, connectDb } = require("./util-worker");
|
||||||
|
const { R } = require("redbean-node");
|
||||||
|
const { setSetting, setting } = require("../util-server");
|
||||||
|
|
||||||
|
const DEFAULT_KEEP_PERIOD = 180;
|
||||||
|
|
||||||
|
(async () => {
|
||||||
|
await connectDb();
|
||||||
|
|
||||||
|
let period = await setting("keepDataPeriodDays");
|
||||||
|
|
||||||
|
// Set Default Period
|
||||||
|
if (period == null) {
|
||||||
|
await setSetting("keepDataPeriodDays", DEFAULT_KEEP_PERIOD, "general");
|
||||||
|
period = DEFAULT_KEEP_PERIOD;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try parse setting
|
||||||
|
let parsedPeriod;
|
||||||
|
try {
|
||||||
|
parsedPeriod = parseInt(period);
|
||||||
|
} catch (_) {
|
||||||
|
log("Failed to parse setting, resetting to default..");
|
||||||
|
await setSetting("keepDataPeriodDays", DEFAULT_KEEP_PERIOD, "general");
|
||||||
|
parsedPeriod = DEFAULT_KEEP_PERIOD;
|
||||||
|
}
|
||||||
|
|
||||||
|
log(`Clearing Data older than ${parsedPeriod} days...`);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await R.exec(
|
||||||
|
"DELETE FROM heartbeat WHERE time < DATETIME('now', '-' || ? || ' days') ",
|
||||||
|
[parsedPeriod]
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
log(`Failed to clear old data: ${e.message}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
exit();
|
||||||
|
})();
|
39
server/jobs/util-worker.js
Normal file
39
server/jobs/util-worker.js
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
const { parentPort, workerData } = require("worker_threads");
|
||||||
|
const Database = require("../database");
|
||||||
|
const path = require("path");
|
||||||
|
|
||||||
|
const log = function (any) {
|
||||||
|
if (parentPort) {
|
||||||
|
parentPort.postMessage(any);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const exit = function (error) {
|
||||||
|
if (error && error != 0) {
|
||||||
|
process.exit(error);
|
||||||
|
} else {
|
||||||
|
if (parentPort) {
|
||||||
|
parentPort.postMessage("done");
|
||||||
|
} else {
|
||||||
|
process.exit(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const connectDb = async function () {
|
||||||
|
const dbPath = path.join(
|
||||||
|
process.env.DATA_DIR || workerData["data-dir"] || "./data/"
|
||||||
|
);
|
||||||
|
|
||||||
|
Database.init({
|
||||||
|
"data-dir": dbPath,
|
||||||
|
});
|
||||||
|
|
||||||
|
await Database.connect();
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
log,
|
||||||
|
exit,
|
||||||
|
connectDb,
|
||||||
|
};
|
@ -7,11 +7,11 @@ dayjs.extend(timezone);
|
|||||||
const axios = require("axios");
|
const axios = require("axios");
|
||||||
const { Prometheus } = require("../prometheus");
|
const { Prometheus } = require("../prometheus");
|
||||||
const { debug, UP, DOWN, PENDING, flipStatus, TimeLogger } = require("../../src/util");
|
const { debug, UP, DOWN, PENDING, flipStatus, TimeLogger } = require("../../src/util");
|
||||||
const { tcping, ping, dnsResolve, checkCertificate, checkStatusCode, getTotalClientInRoom } = require("../util-server");
|
const { tcping, ping, dnsResolve, checkCertificate, checkStatusCode, getTotalClientInRoom, setting, errorLog } = require("../util-server");
|
||||||
const { R } = require("redbean-node");
|
const { R } = require("redbean-node");
|
||||||
const { BeanModel } = require("redbean-node/dist/bean-model");
|
const { BeanModel } = require("redbean-node/dist/bean-model");
|
||||||
const { Notification } = require("../notification");
|
const { Notification } = require("../notification");
|
||||||
const { demoMode } = require("../server");
|
const { demoMode } = require("../config");
|
||||||
const version = require("../../package.json").version;
|
const version = require("../../package.json").version;
|
||||||
const apicache = require("../modules/apicache");
|
const apicache = require("../modules/apicache");
|
||||||
|
|
||||||
@ -55,6 +55,11 @@ class Monitor extends BeanModel {
|
|||||||
id: this.id,
|
id: this.id,
|
||||||
name: this.name,
|
name: this.name,
|
||||||
url: this.url,
|
url: this.url,
|
||||||
|
method: this.method,
|
||||||
|
body: this.body,
|
||||||
|
headers: this.headers,
|
||||||
|
basic_auth_user: this.basic_auth_user,
|
||||||
|
basic_auth_pass: this.basic_auth_pass,
|
||||||
hostname: this.hostname,
|
hostname: this.hostname,
|
||||||
port: this.port,
|
port: this.port,
|
||||||
maxretries: this.maxretries,
|
maxretries: this.maxretries,
|
||||||
@ -77,6 +82,15 @@ class Monitor extends BeanModel {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Encode user and password to Base64 encoding
|
||||||
|
* for HTTP "basic" auth, as per RFC-7617
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
encodeBase64(user, pass) {
|
||||||
|
return Buffer.from(user + ":" + pass).toString("base64");
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Parse to boolean
|
* Parse to boolean
|
||||||
* @returns {boolean}
|
* @returns {boolean}
|
||||||
@ -138,11 +152,26 @@ class Monitor extends BeanModel {
|
|||||||
// Do not do any queries/high loading things before the "bean.ping"
|
// Do not do any queries/high loading things before the "bean.ping"
|
||||||
let startTime = dayjs().valueOf();
|
let startTime = dayjs().valueOf();
|
||||||
|
|
||||||
let res = await axios.get(this.url, {
|
// HTTP basic auth
|
||||||
|
let basicAuthHeader = {};
|
||||||
|
if (this.basic_auth_user) {
|
||||||
|
basicAuthHeader = {
|
||||||
|
"Authorization": "Basic " + this.encodeBase64(this.basic_auth_user, this.basic_auth_pass),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
debug(`[${this.name}] Prepare Options for axios`);
|
||||||
|
|
||||||
|
const options = {
|
||||||
|
url: this.url,
|
||||||
|
method: (this.method || "get").toLowerCase(),
|
||||||
|
...(this.body ? { data: JSON.parse(this.body) } : {}),
|
||||||
timeout: this.interval * 1000 * 0.8,
|
timeout: this.interval * 1000 * 0.8,
|
||||||
headers: {
|
headers: {
|
||||||
"Accept": "*/*",
|
"Accept": "*/*",
|
||||||
"User-Agent": "Uptime-Kuma/" + version,
|
"User-Agent": "Uptime-Kuma/" + version,
|
||||||
|
...(this.headers ? JSON.parse(this.headers) : {}),
|
||||||
|
...(basicAuthHeader),
|
||||||
},
|
},
|
||||||
httpsAgent: new https.Agent({
|
httpsAgent: new https.Agent({
|
||||||
maxCachedSessions: 0, // Use Custom agent to disable session reuse (https://github.com/nodejs/node/issues/3940)
|
maxCachedSessions: 0, // Use Custom agent to disable session reuse (https://github.com/nodejs/node/issues/3940)
|
||||||
@ -152,15 +181,26 @@ class Monitor extends BeanModel {
|
|||||||
validateStatus: (status) => {
|
validateStatus: (status) => {
|
||||||
return checkStatusCode(status, this.getAcceptedStatuscodes());
|
return checkStatusCode(status, this.getAcceptedStatuscodes());
|
||||||
},
|
},
|
||||||
});
|
};
|
||||||
|
|
||||||
|
debug(`[${this.name}] Axios Request`);
|
||||||
|
let res = await axios.request(options);
|
||||||
bean.msg = `${res.status} - ${res.statusText}`;
|
bean.msg = `${res.status} - ${res.statusText}`;
|
||||||
bean.ping = dayjs().valueOf() - startTime;
|
bean.ping = dayjs().valueOf() - startTime;
|
||||||
|
|
||||||
// Check certificate if https is used
|
// Check certificate if https is used
|
||||||
let certInfoStartTime = dayjs().valueOf();
|
let certInfoStartTime = dayjs().valueOf();
|
||||||
if (this.getUrl()?.protocol === "https:") {
|
if (this.getUrl()?.protocol === "https:") {
|
||||||
|
debug(`[${this.name}] Check cert`);
|
||||||
try {
|
try {
|
||||||
tlsInfo = await this.updateTlsInfo(checkCertificate(res));
|
let tlsInfoObject = checkCertificate(res);
|
||||||
|
tlsInfo = await this.updateTlsInfo(tlsInfoObject);
|
||||||
|
|
||||||
|
if (!this.getIgnoreTls()) {
|
||||||
|
debug(`[${this.name}] call sendCertNotification`);
|
||||||
|
await this.sendCertNotification(tlsInfoObject);
|
||||||
|
}
|
||||||
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (e.message !== "No TLS certificate in response") {
|
if (e.message !== "No TLS certificate in response") {
|
||||||
console.error(e.message);
|
console.error(e.message);
|
||||||
@ -172,6 +212,10 @@ class Monitor extends BeanModel {
|
|||||||
debug("Cert Info Query Time: " + (dayjs().valueOf() - certInfoStartTime) + "ms");
|
debug("Cert Info Query Time: " + (dayjs().valueOf() - certInfoStartTime) + "ms");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (process.env.UPTIME_KUMA_LOG_RESPONSE_BODY_MONITOR_ID == this.id) {
|
||||||
|
console.log(res.data);
|
||||||
|
}
|
||||||
|
|
||||||
if (this.type === "http") {
|
if (this.type === "http") {
|
||||||
bean.status = UP;
|
bean.status = UP;
|
||||||
} else {
|
} else {
|
||||||
@ -252,6 +296,9 @@ class Monitor extends BeanModel {
|
|||||||
debug("heartbeatCount" + heartbeatCount + " " + time);
|
debug("heartbeatCount" + heartbeatCount + " " + time);
|
||||||
|
|
||||||
if (heartbeatCount <= 0) {
|
if (heartbeatCount <= 0) {
|
||||||
|
// Fix #922, since previous heartbeat could be inserted by api, it should get from database
|
||||||
|
previousBeat = await Monitor.getPreviousHeartbeat(this.id);
|
||||||
|
|
||||||
throw new Error("No heartbeat in the time window");
|
throw new Error("No heartbeat in the time window");
|
||||||
} else {
|
} else {
|
||||||
// No need to insert successful heartbeat for push type, so end here
|
// No need to insert successful heartbeat for push type, so end here
|
||||||
@ -260,6 +307,46 @@ class Monitor extends BeanModel {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
} else if (this.type === "steam") {
|
||||||
|
const steamApiUrl = "https://api.steampowered.com/IGameServersService/GetServerList/v1/";
|
||||||
|
const steamAPIKey = await setting("steamAPIKey");
|
||||||
|
const filter = `addr\\${this.hostname}:${this.port}`;
|
||||||
|
|
||||||
|
if (!steamAPIKey) {
|
||||||
|
throw new Error("Steam API Key not found");
|
||||||
|
}
|
||||||
|
|
||||||
|
let res = await axios.get(steamApiUrl, {
|
||||||
|
timeout: this.interval * 1000 * 0.8,
|
||||||
|
headers: {
|
||||||
|
"Accept": "*/*",
|
||||||
|
"User-Agent": "Uptime-Kuma/" + version,
|
||||||
|
},
|
||||||
|
httpsAgent: new https.Agent({
|
||||||
|
maxCachedSessions: 0, // Use Custom agent to disable session reuse (https://github.com/nodejs/node/issues/3940)
|
||||||
|
rejectUnauthorized: ! this.getIgnoreTls(),
|
||||||
|
}),
|
||||||
|
maxRedirects: this.maxredirects,
|
||||||
|
validateStatus: (status) => {
|
||||||
|
return checkStatusCode(status, this.getAcceptedStatuscodes());
|
||||||
|
},
|
||||||
|
params: {
|
||||||
|
filter: filter,
|
||||||
|
key: steamAPIKey,
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (res.data.response && res.data.response.servers && res.data.response.servers.length > 0) {
|
||||||
|
bean.status = UP;
|
||||||
|
bean.msg = res.data.response.servers[0].name;
|
||||||
|
|
||||||
|
try {
|
||||||
|
bean.ping = await ping(this.hostname);
|
||||||
|
} catch (_) { }
|
||||||
|
} else {
|
||||||
|
throw new Error("Server not found on Steam");
|
||||||
|
}
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
bean.msg = "Unknown Monitor Type";
|
bean.msg = "Unknown Monitor Type";
|
||||||
bean.status = PENDING;
|
bean.status = PENDING;
|
||||||
@ -292,53 +379,20 @@ class Monitor extends BeanModel {
|
|||||||
|
|
||||||
let beatInterval = this.interval;
|
let beatInterval = this.interval;
|
||||||
|
|
||||||
// * ? -> ANY STATUS = important [isFirstBeat]
|
debug(`[${this.name}] Check isImportant`);
|
||||||
// UP -> PENDING = not important
|
let isImportant = Monitor.isImportantBeat(isFirstBeat, previousBeat?.status, bean.status);
|
||||||
// * UP -> DOWN = important
|
|
||||||
// UP -> UP = not important
|
|
||||||
// PENDING -> PENDING = not important
|
|
||||||
// * PENDING -> DOWN = important
|
|
||||||
// PENDING -> UP = not important
|
|
||||||
// DOWN -> PENDING = this case not exists
|
|
||||||
// DOWN -> DOWN = not important
|
|
||||||
// * DOWN -> UP = important
|
|
||||||
let isImportant = isFirstBeat ||
|
|
||||||
(previousBeat.status === UP && bean.status === DOWN) ||
|
|
||||||
(previousBeat.status === DOWN && bean.status === UP) ||
|
|
||||||
(previousBeat.status === PENDING && bean.status === DOWN);
|
|
||||||
|
|
||||||
// Mark as important if status changed, ignore pending pings,
|
// Mark as important if status changed, ignore pending pings,
|
||||||
// Don't notify if disrupted changes to up
|
// Don't notify if disrupted changes to up
|
||||||
if (isImportant) {
|
if (isImportant) {
|
||||||
bean.important = true;
|
bean.important = true;
|
||||||
|
|
||||||
// Send only if the first beat is DOWN
|
debug(`[${this.name}] sendNotification`);
|
||||||
if (!isFirstBeat || bean.status === DOWN) {
|
await Monitor.sendNotification(isFirstBeat, this, bean);
|
||||||
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;
|
// Clear Status Page Cache
|
||||||
if (bean.status === UP) {
|
debug(`[${this.name}] apicache clear`);
|
||||||
text = "✅ Up";
|
apicache.clear();
|
||||||
} else {
|
|
||||||
text = "🔴 Down";
|
|
||||||
}
|
|
||||||
|
|
||||||
let msg = `[${this.name}] [${text}] ${bean.msg}`;
|
|
||||||
|
|
||||||
for (let notification of notificationList) {
|
|
||||||
try {
|
|
||||||
await Notification.send(JSON.parse(notification.config), msg, await this.toJSON(), bean.toJSON());
|
|
||||||
} catch (e) {
|
|
||||||
console.error("Cannot send notification to " + notification.name);
|
|
||||||
console.log(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Clear Status Page Cache
|
|
||||||
apicache.clear();
|
|
||||||
}
|
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
bean.important = false;
|
bean.important = false;
|
||||||
@ -355,10 +409,14 @@ class Monitor extends BeanModel {
|
|||||||
console.warn(`Monitor #${this.id} '${this.name}': Failing: ${bean.msg} | Interval: ${beatInterval} seconds | Type: ${this.type}`);
|
console.warn(`Monitor #${this.id} '${this.name}': Failing: ${bean.msg} | Interval: ${beatInterval} seconds | Type: ${this.type}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
debug(`[${this.name}] Send to socket`);
|
||||||
io.to(this.user_id).emit("heartbeat", bean.toJSON());
|
io.to(this.user_id).emit("heartbeat", bean.toJSON());
|
||||||
Monitor.sendStats(io, this.id, this.user_id);
|
Monitor.sendStats(io, this.id, this.user_id);
|
||||||
|
|
||||||
|
debug(`[${this.name}] Store`);
|
||||||
await R.store(bean);
|
await R.store(bean);
|
||||||
|
|
||||||
|
debug(`[${this.name}] prometheus.update`);
|
||||||
prometheus.update(bean, tlsInfo);
|
prometheus.update(bean, tlsInfo);
|
||||||
|
|
||||||
previousBeat = bean;
|
previousBeat = bean;
|
||||||
@ -372,18 +430,36 @@ class Monitor extends BeanModel {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
this.heartbeatInterval = setTimeout(beat, beatInterval * 1000);
|
debug(`[${this.name}] SetTimeout for next check.`);
|
||||||
|
this.heartbeatInterval = setTimeout(safeBeat, beatInterval * 1000);
|
||||||
|
} else {
|
||||||
|
console.log(`[${this.name}] isStop = true, no next check.`);
|
||||||
}
|
}
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const safeBeat = async () => {
|
||||||
|
try {
|
||||||
|
await beat();
|
||||||
|
} catch (e) {
|
||||||
|
console.trace(e);
|
||||||
|
errorLog(e, false);
|
||||||
|
console.error("Please report to https://github.com/louislam/uptime-kuma/issues");
|
||||||
|
|
||||||
|
if (! this.isStop) {
|
||||||
|
console.log("Try to restart the monitor");
|
||||||
|
this.heartbeatInterval = setTimeout(safeBeat, this.interval * 1000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// Delay Push Type
|
// Delay Push Type
|
||||||
if (this.type === "push") {
|
if (this.type === "push") {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
beat();
|
safeBeat();
|
||||||
}, this.interval * 1000);
|
}, this.interval * 1000);
|
||||||
} else {
|
} else {
|
||||||
beat();
|
safeBeat();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -415,10 +491,36 @@ class Monitor extends BeanModel {
|
|||||||
let tls_info_bean = await R.findOne("monitor_tls_info", "monitor_id = ?", [
|
let tls_info_bean = await R.findOne("monitor_tls_info", "monitor_id = ?", [
|
||||||
this.id,
|
this.id,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
if (tls_info_bean == null) {
|
if (tls_info_bean == null) {
|
||||||
tls_info_bean = R.dispense("monitor_tls_info");
|
tls_info_bean = R.dispense("monitor_tls_info");
|
||||||
tls_info_bean.monitor_id = this.id;
|
tls_info_bean.monitor_id = this.id;
|
||||||
|
} else {
|
||||||
|
|
||||||
|
// Clear sent history if the cert changed.
|
||||||
|
try {
|
||||||
|
let oldCertInfo = JSON.parse(tls_info_bean.info_json);
|
||||||
|
|
||||||
|
let isValidObjects = oldCertInfo && oldCertInfo.certInfo && checkCertificateResult && checkCertificateResult.certInfo;
|
||||||
|
|
||||||
|
if (isValidObjects) {
|
||||||
|
if (oldCertInfo.certInfo.fingerprint256 !== checkCertificateResult.certInfo.fingerprint256) {
|
||||||
|
debug("Resetting sent_history");
|
||||||
|
await R.exec("DELETE FROM notification_sent_history WHERE type = 'certificate' AND monitor_id = ?", [
|
||||||
|
this.id
|
||||||
|
]);
|
||||||
|
} else {
|
||||||
|
debug("No need to reset sent_history");
|
||||||
|
debug(oldCertInfo.certInfo.fingerprint256);
|
||||||
|
debug(checkCertificateResult.certInfo.fingerprint256);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
debug("Not valid object");
|
||||||
|
}
|
||||||
|
} catch (e) { }
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
tls_info_bean.info_json = JSON.stringify(checkCertificateResult);
|
tls_info_bean.info_json = JSON.stringify(checkCertificateResult);
|
||||||
await R.store(tls_info_bean);
|
await R.store(tls_info_bean);
|
||||||
|
|
||||||
@ -546,6 +648,121 @@ class Monitor extends BeanModel {
|
|||||||
io.to(userID).emit("uptime", monitorID, duration, uptime);
|
io.to(userID).emit("uptime", monitorID, duration, uptime);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static isImportantBeat(isFirstBeat, previousBeatStatus, currentBeatStatus) {
|
||||||
|
// * ? -> ANY STATUS = important [isFirstBeat]
|
||||||
|
// UP -> PENDING = not important
|
||||||
|
// * UP -> DOWN = important
|
||||||
|
// UP -> UP = not important
|
||||||
|
// PENDING -> PENDING = not important
|
||||||
|
// * PENDING -> DOWN = important
|
||||||
|
// PENDING -> UP = not important
|
||||||
|
// DOWN -> PENDING = this case not exists
|
||||||
|
// DOWN -> DOWN = not important
|
||||||
|
// * DOWN -> UP = important
|
||||||
|
let isImportant = isFirstBeat ||
|
||||||
|
(previousBeatStatus === UP && currentBeatStatus === DOWN) ||
|
||||||
|
(previousBeatStatus === DOWN && currentBeatStatus === UP) ||
|
||||||
|
(previousBeatStatus === PENDING && currentBeatStatus === DOWN);
|
||||||
|
return isImportant;
|
||||||
|
}
|
||||||
|
|
||||||
|
static async sendNotification(isFirstBeat, monitor, bean) {
|
||||||
|
if (!isFirstBeat || bean.status === DOWN) {
|
||||||
|
const notificationList = await Monitor.getNotificationList(monitor);
|
||||||
|
|
||||||
|
let text;
|
||||||
|
if (bean.status === UP) {
|
||||||
|
text = "✅ Up";
|
||||||
|
} else {
|
||||||
|
text = "🔴 Down";
|
||||||
|
}
|
||||||
|
|
||||||
|
let msg = `[${monitor.name}] [${text}] ${bean.msg}`;
|
||||||
|
|
||||||
|
for (let notification of notificationList) {
|
||||||
|
try {
|
||||||
|
await Notification.send(JSON.parse(notification.config), msg, await monitor.toJSON(), bean.toJSON());
|
||||||
|
} catch (e) {
|
||||||
|
console.error("Cannot send notification to " + notification.name);
|
||||||
|
console.log(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static async getNotificationList(monitor) {
|
||||||
|
let notificationList = await R.getAll("SELECT notification.* FROM notification, monitor_notification WHERE monitor_id = ? AND monitor_notification.notification_id = notification.id ", [
|
||||||
|
monitor.id,
|
||||||
|
]);
|
||||||
|
return notificationList;
|
||||||
|
}
|
||||||
|
|
||||||
|
async sendCertNotification(tlsInfoObject) {
|
||||||
|
if (tlsInfoObject && tlsInfoObject.certInfo && tlsInfoObject.certInfo.daysRemaining) {
|
||||||
|
const notificationList = await Monitor.getNotificationList(this);
|
||||||
|
|
||||||
|
debug("call sendCertNotificationByTargetDays");
|
||||||
|
await this.sendCertNotificationByTargetDays(tlsInfoObject.certInfo.daysRemaining, 21, notificationList);
|
||||||
|
await this.sendCertNotificationByTargetDays(tlsInfoObject.certInfo.daysRemaining, 14, notificationList);
|
||||||
|
await this.sendCertNotificationByTargetDays(tlsInfoObject.certInfo.daysRemaining, 7, notificationList);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async sendCertNotificationByTargetDays(daysRemaining, targetDays, notificationList) {
|
||||||
|
|
||||||
|
if (daysRemaining > targetDays) {
|
||||||
|
debug(`No need to send cert notification. ${daysRemaining} > ${targetDays}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (notificationList.length > 0) {
|
||||||
|
|
||||||
|
let row = await R.getRow("SELECT * FROM notification_sent_history WHERE type = ? AND monitor_id = ? AND days = ?", [
|
||||||
|
"certificate",
|
||||||
|
this.id,
|
||||||
|
targetDays,
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Sent already, no need to send again
|
||||||
|
if (row) {
|
||||||
|
debug("Sent already, no need to send again");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let sent = false;
|
||||||
|
debug("Send certificate notification");
|
||||||
|
|
||||||
|
for (let notification of notificationList) {
|
||||||
|
try {
|
||||||
|
debug("Sending to " + notification.name);
|
||||||
|
await Notification.send(JSON.parse(notification.config), `[${this.name}][${this.url}] Certificate will be expired in ${daysRemaining} days`);
|
||||||
|
sent = true;
|
||||||
|
} catch (e) {
|
||||||
|
console.error("Cannot send cert notification to " + notification.name);
|
||||||
|
console.error(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sent) {
|
||||||
|
await R.exec("INSERT INTO notification_sent_history (type, monitor_id, days) VALUES(?, ?, ?)", [
|
||||||
|
"certificate",
|
||||||
|
this.id,
|
||||||
|
targetDays,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
debug("No notification, no need to send cert notification");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static async getPreviousHeartbeat(monitorID) {
|
||||||
|
return await R.getRow(`
|
||||||
|
SELECT status, time FROM heartbeat
|
||||||
|
WHERE id = (select MAX(id) from heartbeat where monitor_id = ?)
|
||||||
|
`, [
|
||||||
|
monitorID
|
||||||
|
]);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = Monitor;
|
module.exports = Monitor;
|
||||||
|
108
server/notification-providers/aliyun-sms.js
Normal file
108
server/notification-providers/aliyun-sms.js
Normal file
@ -0,0 +1,108 @@
|
|||||||
|
const NotificationProvider = require("./notification-provider");
|
||||||
|
const { DOWN, UP } = require("../../src/util");
|
||||||
|
const { default: axios } = require("axios");
|
||||||
|
const Crypto = require("crypto");
|
||||||
|
const qs = require("qs");
|
||||||
|
|
||||||
|
class AliyunSMS extends NotificationProvider {
|
||||||
|
name = "AliyunSMS";
|
||||||
|
|
||||||
|
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
|
||||||
|
let okMsg = "Sent Successfully.";
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (heartbeatJSON != null) {
|
||||||
|
let msgBody = JSON.stringify({
|
||||||
|
name: monitorJSON["name"],
|
||||||
|
time: heartbeatJSON["time"],
|
||||||
|
status: this.statusToString(heartbeatJSON["status"]),
|
||||||
|
msg: heartbeatJSON["msg"],
|
||||||
|
});
|
||||||
|
if (this.sendSms(notification, msgBody)) {
|
||||||
|
return okMsg;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
let msgBody = JSON.stringify({
|
||||||
|
name: "",
|
||||||
|
time: "",
|
||||||
|
status: "",
|
||||||
|
msg: msg,
|
||||||
|
});
|
||||||
|
if (this.sendSms(notification, msgBody)) {
|
||||||
|
return okMsg;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
this.throwGeneralAxiosError(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async sendSms(notification, msgbody) {
|
||||||
|
let params = {
|
||||||
|
PhoneNumbers: notification.phonenumber,
|
||||||
|
TemplateCode: notification.templateCode,
|
||||||
|
SignName: notification.signName,
|
||||||
|
TemplateParam: msgbody,
|
||||||
|
AccessKeyId: notification.accessKeyId,
|
||||||
|
Format: "JSON",
|
||||||
|
SignatureMethod: "HMAC-SHA1",
|
||||||
|
SignatureVersion: "1.0",
|
||||||
|
SignatureNonce: Math.random().toString(),
|
||||||
|
Timestamp: new Date().toISOString(),
|
||||||
|
Action: "SendSms",
|
||||||
|
Version: "2017-05-25",
|
||||||
|
};
|
||||||
|
|
||||||
|
params.Signature = this.sign(params, notification.secretAccessKey);
|
||||||
|
let config = {
|
||||||
|
method: "POST",
|
||||||
|
url: "http://dysmsapi.aliyuncs.com/",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/x-www-form-urlencoded",
|
||||||
|
},
|
||||||
|
data: qs.stringify(params),
|
||||||
|
};
|
||||||
|
|
||||||
|
let result = await axios(config);
|
||||||
|
if (result.data.Message == "OK") {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Aliyun request sign */
|
||||||
|
sign(param, AccessKeySecret) {
|
||||||
|
let param2 = {};
|
||||||
|
let data = [];
|
||||||
|
|
||||||
|
let oa = Object.keys(param).sort();
|
||||||
|
|
||||||
|
for (let i = 0; i < oa.length; i++) {
|
||||||
|
let key = oa[i];
|
||||||
|
param2[key] = param[key];
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let key in param2) {
|
||||||
|
data.push(`${encodeURIComponent(key)}=${encodeURIComponent(param2[key])}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
let StringToSign = `POST&${encodeURIComponent("/")}&${encodeURIComponent(data.join("&"))}`;
|
||||||
|
return Crypto
|
||||||
|
.createHmac("sha1", `${AccessKeySecret}&`)
|
||||||
|
.update(Buffer.from(StringToSign))
|
||||||
|
.digest("base64");
|
||||||
|
}
|
||||||
|
|
||||||
|
statusToString(status) {
|
||||||
|
switch (status) {
|
||||||
|
case DOWN:
|
||||||
|
return "DOWN";
|
||||||
|
case UP:
|
||||||
|
return "UP";
|
||||||
|
default:
|
||||||
|
return status;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = AliyunSMS;
|
89
server/notification-providers/bark.js
Normal file
89
server/notification-providers/bark.js
Normal file
@ -0,0 +1,89 @@
|
|||||||
|
//
|
||||||
|
// bark.js
|
||||||
|
// UptimeKuma
|
||||||
|
//
|
||||||
|
// Created by Lakr Aream on 2021/10/24.
|
||||||
|
// Copyright © 2021 Lakr Aream. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
const NotificationProvider = require("./notification-provider");
|
||||||
|
const { DOWN, UP } = require("../../src/util");
|
||||||
|
const { default: axios } = require("axios");
|
||||||
|
|
||||||
|
// bark is an APN bridge that sends notifications to Apple devices.
|
||||||
|
|
||||||
|
const barkNotificationGroup = "UptimeKuma";
|
||||||
|
const barkNotificationAvatar = "https://github.com/louislam/uptime-kuma/raw/master/public/icon.png";
|
||||||
|
const barkNotificationSound = "telegraph";
|
||||||
|
const successMessage = "Successes!";
|
||||||
|
|
||||||
|
class Bark extends NotificationProvider {
|
||||||
|
name = "Bark";
|
||||||
|
|
||||||
|
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
|
||||||
|
try {
|
||||||
|
var barkEndpoint = notification.barkEndpoint;
|
||||||
|
|
||||||
|
// check if the endpoint has a "/" suffix, if so, delete it first
|
||||||
|
if (barkEndpoint.endsWith("/")) {
|
||||||
|
barkEndpoint = barkEndpoint.substring(0, barkEndpoint.length - 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (msg != null && heartbeatJSON != null && heartbeatJSON["status"] == UP) {
|
||||||
|
let title = "UptimeKuma Monitor Up";
|
||||||
|
return await this.postNotification(title, msg, barkEndpoint);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (msg != null && heartbeatJSON != null && heartbeatJSON["status"] == DOWN) {
|
||||||
|
let title = "UptimeKuma Monitor Down";
|
||||||
|
return await this.postNotification(title, msg, barkEndpoint);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (msg != null) {
|
||||||
|
let title = "UptimeKuma Message";
|
||||||
|
return await this.postNotification(title, msg, barkEndpoint);
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// add additional parameter for better on device styles (iOS 15 optimized)
|
||||||
|
appendAdditionalParameters(postUrl) {
|
||||||
|
// grouping all our notifications
|
||||||
|
postUrl += "?group=" + barkNotificationGroup;
|
||||||
|
// set icon to uptime kuma icon, 11kb should be fine
|
||||||
|
postUrl += "&icon=" + barkNotificationAvatar;
|
||||||
|
// picked a sound, this should follow system's mute status when arrival
|
||||||
|
postUrl += "&sound=" + barkNotificationSound;
|
||||||
|
return postUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
// thrown if failed to check result, result code should be in range 2xx
|
||||||
|
checkResult(result) {
|
||||||
|
if (result.status == null) {
|
||||||
|
throw new Error("Bark notification failed with invalid response!");
|
||||||
|
}
|
||||||
|
if (result.status < 200 || result.status >= 300) {
|
||||||
|
throw new Error("Bark notification failed with status code " + result.status);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async postNotification(title, subtitle, endpoint) {
|
||||||
|
// url encode title and subtitle
|
||||||
|
title = encodeURIComponent(title);
|
||||||
|
subtitle = encodeURIComponent(subtitle);
|
||||||
|
let postUrl = endpoint + "/" + title + "/" + subtitle;
|
||||||
|
postUrl = this.appendAdditionalParameters(postUrl);
|
||||||
|
let result = await axios.get(postUrl);
|
||||||
|
this.checkResult(result);
|
||||||
|
if (result.statusText != null) {
|
||||||
|
return "Bark notification succeed: " + result.statusText;
|
||||||
|
}
|
||||||
|
// because returned in range 200 ..< 300
|
||||||
|
return successMessage;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = Bark;
|
42
server/notification-providers/clicksendsms.js
Normal file
42
server/notification-providers/clicksendsms.js
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
const NotificationProvider = require("./notification-provider");
|
||||||
|
const axios = require("axios");
|
||||||
|
|
||||||
|
class ClickSendSMS extends NotificationProvider {
|
||||||
|
|
||||||
|
name = "clicksendsms";
|
||||||
|
|
||||||
|
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
|
||||||
|
let okMsg = "Sent Successfully.";
|
||||||
|
try {
|
||||||
|
console.log({ notification });
|
||||||
|
let config = {
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
"Authorization": "Basic " + Buffer.from(notification.clicksendsmsLogin + ":" + notification.clicksendsmsPassword).toString('base64'),
|
||||||
|
"Accept": "text/json",
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let data = {
|
||||||
|
messages: [
|
||||||
|
{
|
||||||
|
"body": msg.replace(/[^\x00-\x7F]/g, ""),
|
||||||
|
"to": notification.clicksendsmsToNumber,
|
||||||
|
"source": "uptime-kuma",
|
||||||
|
"from": notification.clicksendsmsSenderName,
|
||||||
|
}
|
||||||
|
]
|
||||||
|
};
|
||||||
|
let resp = await axios.post("https://rest.clicksend.com/v3/sms/send", data, config);
|
||||||
|
if (resp.data.data.messages[0].status !== "SUCCESS") {
|
||||||
|
let error = "Something gone wrong. Api returned " + resp.data.data.messages[0].status + ".";
|
||||||
|
this.throwGeneralAxiosError(error);
|
||||||
|
}
|
||||||
|
|
||||||
|
return okMsg;
|
||||||
|
} catch (error) {
|
||||||
|
this.throwGeneralAxiosError(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = ClickSendSMS;
|
79
server/notification-providers/dingding.js
Normal file
79
server/notification-providers/dingding.js
Normal file
@ -0,0 +1,79 @@
|
|||||||
|
const NotificationProvider = require("./notification-provider");
|
||||||
|
const { DOWN, UP } = require("../../src/util");
|
||||||
|
const { default: axios } = require("axios");
|
||||||
|
const Crypto = require("crypto");
|
||||||
|
|
||||||
|
class DingDing extends NotificationProvider {
|
||||||
|
name = "DingDing";
|
||||||
|
|
||||||
|
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
|
||||||
|
let okMsg = "Sent Successfully.";
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (heartbeatJSON != null) {
|
||||||
|
let params = {
|
||||||
|
msgtype: "markdown",
|
||||||
|
markdown: {
|
||||||
|
title: `[${this.statusToString(heartbeatJSON["status"])}] ${monitorJSON["name"]}`,
|
||||||
|
text: `## [${this.statusToString(heartbeatJSON["status"])}] ${monitorJSON["name"]} \n > ${heartbeatJSON["msg"]} \n > Time(UTC):${heartbeatJSON["time"]}`,
|
||||||
|
}
|
||||||
|
};
|
||||||
|
if (this.sendToDingDing(notification, params)) {
|
||||||
|
return okMsg;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
let params = {
|
||||||
|
msgtype: "text",
|
||||||
|
text: {
|
||||||
|
content: msg
|
||||||
|
}
|
||||||
|
};
|
||||||
|
if (this.sendToDingDing(notification, params)) {
|
||||||
|
return okMsg;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
this.throwGeneralAxiosError(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async sendToDingDing(notification, params) {
|
||||||
|
let timestamp = Date.now();
|
||||||
|
|
||||||
|
let config = {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
url: `${notification.webHookUrl}×tamp=${timestamp}&sign=${encodeURIComponent(this.sign(timestamp, notification.secretKey))}`,
|
||||||
|
data: JSON.stringify(params),
|
||||||
|
};
|
||||||
|
|
||||||
|
let result = await axios(config);
|
||||||
|
if (result.data.errmsg == "ok") {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** DingDing sign */
|
||||||
|
sign(timestamp, secretKey) {
|
||||||
|
return Crypto
|
||||||
|
.createHmac("sha256", Buffer.from(secretKey, "utf8"))
|
||||||
|
.update(Buffer.from(`${timestamp}\n${secretKey}`, "utf8"))
|
||||||
|
.digest("base64");
|
||||||
|
}
|
||||||
|
|
||||||
|
statusToString(status) {
|
||||||
|
switch (status) {
|
||||||
|
case DOWN:
|
||||||
|
return "DOWN";
|
||||||
|
case UP:
|
||||||
|
return "UP";
|
||||||
|
default:
|
||||||
|
return status;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = DingDing;
|
@ -27,7 +27,7 @@ class Feishu extends NotificationProvider {
|
|||||||
content: {
|
content: {
|
||||||
post: {
|
post: {
|
||||||
zh_cn: {
|
zh_cn: {
|
||||||
title: "UptimeKuma Alert: " + monitorJSON["name"],
|
title: "UptimeKuma Alert: [Down] " + monitorJSON["name"],
|
||||||
content: [
|
content: [
|
||||||
[
|
[
|
||||||
{
|
{
|
||||||
@ -54,7 +54,7 @@ class Feishu extends NotificationProvider {
|
|||||||
content: {
|
content: {
|
||||||
post: {
|
post: {
|
||||||
zh_cn: {
|
zh_cn: {
|
||||||
title: "UptimeKuma Alert: " + monitorJSON["name"],
|
title: "UptimeKuma Alert: [Up] " + monitorJSON["name"],
|
||||||
content: [
|
content: [
|
||||||
[
|
[
|
||||||
{
|
{
|
||||||
|
@ -7,12 +7,12 @@ class Pushover extends NotificationProvider {
|
|||||||
|
|
||||||
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
|
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
|
||||||
let okMsg = "Sent Successfully.";
|
let okMsg = "Sent Successfully.";
|
||||||
let pushoverlink = "https://api.pushover.net/1/messages.json"
|
let pushoverlink = "https://api.pushover.net/1/messages.json";
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (heartbeatJSON == null) {
|
if (heartbeatJSON == null) {
|
||||||
let data = {
|
let data = {
|
||||||
"message": "<b>Uptime Kuma Pushover testing successful.</b>",
|
"message": msg,
|
||||||
"user": notification.pushoveruserkey,
|
"user": notification.pushoveruserkey,
|
||||||
"token": notification.pushoverapptoken,
|
"token": notification.pushoverapptoken,
|
||||||
"sound": notification.pushoversounds,
|
"sound": notification.pushoversounds,
|
||||||
@ -21,8 +21,8 @@ class Pushover extends NotificationProvider {
|
|||||||
"retry": "30",
|
"retry": "30",
|
||||||
"expire": "3600",
|
"expire": "3600",
|
||||||
"html": 1,
|
"html": 1,
|
||||||
}
|
};
|
||||||
await axios.post(pushoverlink, data)
|
await axios.post(pushoverlink, data);
|
||||||
return okMsg;
|
return okMsg;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -36,11 +36,11 @@ class Pushover extends NotificationProvider {
|
|||||||
"retry": "30",
|
"retry": "30",
|
||||||
"expire": "3600",
|
"expire": "3600",
|
||||||
"html": 1,
|
"html": 1,
|
||||||
}
|
};
|
||||||
await axios.post(pushoverlink, data)
|
await axios.post(pushoverlink, data);
|
||||||
return okMsg;
|
return okMsg;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.throwGeneralAxiosError(error)
|
this.throwGeneralAxiosError(error);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
44
server/notification-providers/serwersms.js
Normal file
44
server/notification-providers/serwersms.js
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
const NotificationProvider = require("./notification-provider");
|
||||||
|
const axios = require("axios");
|
||||||
|
|
||||||
|
class SerwerSMS extends NotificationProvider {
|
||||||
|
|
||||||
|
name = "serwersms";
|
||||||
|
|
||||||
|
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
|
||||||
|
let okMsg = "Sent Successfully.";
|
||||||
|
|
||||||
|
try {
|
||||||
|
let config = {
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let data = {
|
||||||
|
"username": notification.serwersmsUsername,
|
||||||
|
"password": notification.serwersmsPassword,
|
||||||
|
"phone": notification.serwersmsPhoneNumber,
|
||||||
|
"text": msg.replace(/[^\x00-\x7F]/g, ""),
|
||||||
|
"sender": notification.serwersmsSenderName,
|
||||||
|
};
|
||||||
|
|
||||||
|
let resp = await axios.post("https://api2.serwersms.pl/messages/send_sms", data, config);
|
||||||
|
|
||||||
|
if (!resp.data.success) {
|
||||||
|
if (resp.data.error) {
|
||||||
|
let error = `SerwerSMS.pl API returned error code ${resp.data.error.code} (${resp.data.error.type}) with error message: ${resp.data.error.message}`;
|
||||||
|
this.throwGeneralAxiosError(error);
|
||||||
|
} else {
|
||||||
|
let error = "SerwerSMS.pl API returned an unexpected response";
|
||||||
|
this.throwGeneralAxiosError(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return okMsg;
|
||||||
|
} catch (error) {
|
||||||
|
this.throwGeneralAxiosError(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = SerwerSMS;
|
@ -39,8 +39,9 @@ class Slack extends NotificationProvider {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const time = heartbeatJSON["time"];
|
const time = heartbeatJSON["time"];
|
||||||
|
const textMsg = "Uptime Kuma Alert";
|
||||||
let data = {
|
let data = {
|
||||||
"text": "Uptime Kuma Alert",
|
"text": monitorJSON ? textMsg + `: ${monitorJSON.name}` : textMsg,
|
||||||
"channel": notification.slackchannel,
|
"channel": notification.slackchannel,
|
||||||
"username": notification.slackusername,
|
"username": notification.slackusername,
|
||||||
"icon_emoji": notification.slackiconemo,
|
"icon_emoji": notification.slackiconemo,
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
const nodemailer = require("nodemailer");
|
const nodemailer = require("nodemailer");
|
||||||
const NotificationProvider = require("./notification-provider");
|
const NotificationProvider = require("./notification-provider");
|
||||||
|
const { DOWN, UP } = require("../../src/util");
|
||||||
|
|
||||||
class SMTP extends NotificationProvider {
|
class SMTP extends NotificationProvider {
|
||||||
|
|
||||||
@ -11,6 +12,9 @@ class SMTP extends NotificationProvider {
|
|||||||
host: notification.smtpHost,
|
host: notification.smtpHost,
|
||||||
port: notification.smtpPort,
|
port: notification.smtpPort,
|
||||||
secure: notification.smtpSecure,
|
secure: notification.smtpSecure,
|
||||||
|
tls: {
|
||||||
|
rejectUnauthorized: notification.smtpIgnoreTLSError || false,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
// Should fix the issue in https://github.com/louislam/uptime-kuma/issues/26#issuecomment-896373904
|
// Should fix the issue in https://github.com/louislam/uptime-kuma/issues/26#issuecomment-896373904
|
||||||
@ -20,6 +24,56 @@ class SMTP extends NotificationProvider {
|
|||||||
pass: notification.smtpPassword,
|
pass: notification.smtpPassword,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
// Lets start with default subject and empty string for custom one
|
||||||
|
let subject = msg;
|
||||||
|
|
||||||
|
// Change the subject if:
|
||||||
|
// - The msg ends with "Testing" or
|
||||||
|
// - Actual Up/Down Notification
|
||||||
|
if ((monitorJSON && heartbeatJSON) || msg.endsWith("Testing")) {
|
||||||
|
let customSubject = "";
|
||||||
|
|
||||||
|
// Our subject cannot end with whitespace it's often raise spam score
|
||||||
|
// Once I got "Cannot read property 'trim' of undefined", better be safe than sorry
|
||||||
|
if (notification.customSubject) {
|
||||||
|
customSubject = notification.customSubject.trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
// If custom subject is not empty, change subject for notification
|
||||||
|
if (customSubject !== "") {
|
||||||
|
|
||||||
|
// Replace "MACROS" with corresponding variable
|
||||||
|
let replaceName = new RegExp("{{NAME}}", "g");
|
||||||
|
let replaceHostnameOrURL = new RegExp("{{HOSTNAME_OR_URL}}", "g");
|
||||||
|
let replaceStatus = new RegExp("{{STATUS}}", "g");
|
||||||
|
|
||||||
|
// Lets start with dummy values to simplify code
|
||||||
|
let monitorName = "Test";
|
||||||
|
let monitorHostnameOrURL = "testing.hostname";
|
||||||
|
let serviceStatus = "⚠️ Test";
|
||||||
|
|
||||||
|
if (monitorJSON !== null) {
|
||||||
|
monitorName = monitorJSON["name"];
|
||||||
|
|
||||||
|
if (monitorJSON["type"] === "http" || monitorJSON["type"] === "keyword") {
|
||||||
|
monitorHostnameOrURL = monitorJSON["url"];
|
||||||
|
} else {
|
||||||
|
monitorHostnameOrURL = monitorJSON["hostname"];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (heartbeatJSON !== null) {
|
||||||
|
serviceStatus = (heartbeatJSON["status"] === DOWN) ? "🔴 Down" : "✅ Up";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Break replace to one by line for better readability
|
||||||
|
customSubject = customSubject.replace(replaceStatus, serviceStatus);
|
||||||
|
customSubject = customSubject.replace(replaceName, monitorName);
|
||||||
|
customSubject = customSubject.replace(replaceHostnameOrURL, monitorHostnameOrURL);
|
||||||
|
|
||||||
|
subject = customSubject;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let transporter = nodemailer.createTransport(config);
|
let transporter = nodemailer.createTransport(config);
|
||||||
|
|
||||||
@ -34,11 +88,8 @@ class SMTP extends NotificationProvider {
|
|||||||
cc: notification.smtpCC,
|
cc: notification.smtpCC,
|
||||||
bcc: notification.smtpBCC,
|
bcc: notification.smtpBCC,
|
||||||
to: notification.smtpTo,
|
to: notification.smtpTo,
|
||||||
subject: msg,
|
subject: subject,
|
||||||
text: bodyTextContent,
|
text: bodyTextContent,
|
||||||
tls: {
|
|
||||||
rejectUnauthorized: notification.smtpIgnoreTLSError || false,
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
return "Sent Successfully.";
|
return "Sent Successfully.";
|
||||||
|
41
server/notification-providers/stackfield.js
Normal file
41
server/notification-providers/stackfield.js
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
const NotificationProvider = require("./notification-provider");
|
||||||
|
const axios = require("axios");
|
||||||
|
const { setting } = require("../util-server");
|
||||||
|
const { getMonitorRelativeURL } = require("../../src/util");
|
||||||
|
|
||||||
|
class Stackfield extends NotificationProvider {
|
||||||
|
|
||||||
|
name = "stackfield";
|
||||||
|
|
||||||
|
async send(notification, msg, monitorJSON = null) {
|
||||||
|
let okMsg = "Sent Successfully.";
|
||||||
|
try {
|
||||||
|
// Stackfield message formatting: https://www.stackfield.com/help/formatting-messages-2001
|
||||||
|
|
||||||
|
let textMsg = "+Uptime Kuma Alert+";
|
||||||
|
|
||||||
|
if (monitorJSON && monitorJSON.name) {
|
||||||
|
textMsg += `\n*${monitorJSON.name}*`;
|
||||||
|
}
|
||||||
|
|
||||||
|
textMsg += `\n${msg}`;
|
||||||
|
|
||||||
|
const baseURL = await setting("primaryBaseURL");
|
||||||
|
if (baseURL) {
|
||||||
|
textMsg += `\n${baseURL + getMonitorRelativeURL(monitorJSON.id)}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = {
|
||||||
|
"Title": textMsg,
|
||||||
|
};
|
||||||
|
|
||||||
|
await axios.post(notification.stackfieldwebhookURL, data);
|
||||||
|
return okMsg;
|
||||||
|
} catch (error) {
|
||||||
|
this.throwGeneralAxiosError(error);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = Stackfield;
|
@ -8,6 +8,7 @@ const Mattermost = require("./notification-providers/mattermost");
|
|||||||
const Matrix = require("./notification-providers/matrix");
|
const Matrix = require("./notification-providers/matrix");
|
||||||
const Octopush = require("./notification-providers/octopush");
|
const Octopush = require("./notification-providers/octopush");
|
||||||
const PromoSMS = require("./notification-providers/promosms");
|
const PromoSMS = require("./notification-providers/promosms");
|
||||||
|
const ClickSendSMS = require("./notification-providers/clicksendsms");
|
||||||
const Pushbullet = require("./notification-providers/pushbullet");
|
const Pushbullet = require("./notification-providers/pushbullet");
|
||||||
const Pushover = require("./notification-providers/pushover");
|
const Pushover = require("./notification-providers/pushover");
|
||||||
const Pushy = require("./notification-providers/pushy");
|
const Pushy = require("./notification-providers/pushy");
|
||||||
@ -19,6 +20,11 @@ const Teams = require("./notification-providers/teams");
|
|||||||
const Telegram = require("./notification-providers/telegram");
|
const Telegram = require("./notification-providers/telegram");
|
||||||
const Webhook = require("./notification-providers/webhook");
|
const Webhook = require("./notification-providers/webhook");
|
||||||
const Feishu = require("./notification-providers/feishu");
|
const Feishu = require("./notification-providers/feishu");
|
||||||
|
const AliyunSms = require("./notification-providers/aliyun-sms");
|
||||||
|
const DingDing = require("./notification-providers/dingding");
|
||||||
|
const Bark = require("./notification-providers/bark");
|
||||||
|
const SerwerSMS = require("./notification-providers/serwersms");
|
||||||
|
const Stackfield = require("./notification-providers/stackfield");
|
||||||
|
|
||||||
class Notification {
|
class Notification {
|
||||||
|
|
||||||
@ -31,6 +37,8 @@ class Notification {
|
|||||||
|
|
||||||
const list = [
|
const list = [
|
||||||
new Apprise(),
|
new Apprise(),
|
||||||
|
new AliyunSms(),
|
||||||
|
new DingDing(),
|
||||||
new Discord(),
|
new Discord(),
|
||||||
new Teams(),
|
new Teams(),
|
||||||
new Gotify(),
|
new Gotify(),
|
||||||
@ -41,6 +49,7 @@ class Notification {
|
|||||||
new Matrix(),
|
new Matrix(),
|
||||||
new Octopush(),
|
new Octopush(),
|
||||||
new PromoSMS(),
|
new PromoSMS(),
|
||||||
|
new ClickSendSMS(),
|
||||||
new Pushbullet(),
|
new Pushbullet(),
|
||||||
new Pushover(),
|
new Pushover(),
|
||||||
new Pushy(),
|
new Pushy(),
|
||||||
@ -50,6 +59,9 @@ class Notification {
|
|||||||
new SMTP(),
|
new SMTP(),
|
||||||
new Telegram(),
|
new Telegram(),
|
||||||
new Webhook(),
|
new Webhook(),
|
||||||
|
new Bark(),
|
||||||
|
new SerwerSMS(),
|
||||||
|
new Stackfield(),
|
||||||
];
|
];
|
||||||
|
|
||||||
for (let item of list) {
|
for (let item of list) {
|
||||||
|
@ -4,10 +4,7 @@ const net = require("net");
|
|||||||
const spawn = require("child_process").spawn;
|
const spawn = require("child_process").spawn;
|
||||||
const events = require("events");
|
const events = require("events");
|
||||||
const fs = require("fs");
|
const fs = require("fs");
|
||||||
const WIN = /^win/.test(process.platform);
|
const util = require("./util-server");
|
||||||
const LIN = /^linux/.test(process.platform);
|
|
||||||
const MAC = /^darwin/.test(process.platform);
|
|
||||||
const FBSD = /^freebsd/.test(process.platform);
|
|
||||||
|
|
||||||
module.exports = Ping;
|
module.exports = Ping;
|
||||||
|
|
||||||
@ -23,12 +20,12 @@ function Ping(host, options) {
|
|||||||
|
|
||||||
const timeout = 10;
|
const timeout = 10;
|
||||||
|
|
||||||
if (WIN) {
|
if (util.WIN) {
|
||||||
this._bin = "c:/windows/system32/ping.exe";
|
this._bin = "c:/windows/system32/ping.exe";
|
||||||
this._args = (options.args) ? options.args : [ "-n", "1", "-w", timeout * 1000, host ];
|
this._args = (options.args) ? options.args : [ "-n", "1", "-w", timeout * 1000, host ];
|
||||||
this._regmatch = /[><=]([0-9.]+?)ms/;
|
this._regmatch = /[><=]([0-9.]+?)ms/;
|
||||||
|
|
||||||
} else if (LIN) {
|
} else if (util.LIN) {
|
||||||
this._bin = "/bin/ping";
|
this._bin = "/bin/ping";
|
||||||
|
|
||||||
const defaultArgs = [ "-n", "-w", timeout, "-c", "1", host ];
|
const defaultArgs = [ "-n", "-w", timeout, "-c", "1", host ];
|
||||||
@ -40,7 +37,7 @@ function Ping(host, options) {
|
|||||||
this._args = (options.args) ? options.args : defaultArgs;
|
this._args = (options.args) ? options.args : defaultArgs;
|
||||||
this._regmatch = /=([0-9.]+?) ms/;
|
this._regmatch = /=([0-9.]+?) ms/;
|
||||||
|
|
||||||
} else if (MAC) {
|
} else if (util.MAC) {
|
||||||
|
|
||||||
if (net.isIPv6(host) || options.ipv6) {
|
if (net.isIPv6(host) || options.ipv6) {
|
||||||
this._bin = "/sbin/ping6";
|
this._bin = "/sbin/ping6";
|
||||||
@ -51,7 +48,7 @@ function Ping(host, options) {
|
|||||||
this._args = (options.args) ? options.args : [ "-n", "-t", timeout, "-c", "1", host ];
|
this._args = (options.args) ? options.args : [ "-n", "-t", timeout, "-c", "1", host ];
|
||||||
this._regmatch = /=([0-9.]+?) ms/;
|
this._regmatch = /=([0-9.]+?) ms/;
|
||||||
|
|
||||||
} else if (FBSD) {
|
} else if (util.FBSD) {
|
||||||
this._bin = "/sbin/ping";
|
this._bin = "/sbin/ping";
|
||||||
|
|
||||||
const defaultArgs = [ "-n", "-t", timeout, "-c", "1", host ];
|
const defaultArgs = [ "-n", "-t", timeout, "-c", "1", host ];
|
||||||
@ -101,6 +98,9 @@ Ping.prototype.send = function (callback) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
this._ping.stdout.on("data", function (data) { // log stdout
|
this._ping.stdout.on("data", function (data) { // log stdout
|
||||||
|
if (util.WIN) {
|
||||||
|
data = convertOutput(data);
|
||||||
|
}
|
||||||
this._stdout = (this._stdout || "") + data;
|
this._stdout = (this._stdout || "") + data;
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -112,6 +112,9 @@ Ping.prototype.send = function (callback) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
this._ping.stderr.on("data", function (data) { // log stderr
|
this._ping.stderr.on("data", function (data) { // log stderr
|
||||||
|
if (util.WIN) {
|
||||||
|
data = convertOutput(data);
|
||||||
|
}
|
||||||
this._stderr = (this._stderr || "") + data;
|
this._stderr = (this._stderr || "") + data;
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -157,3 +160,19 @@ Ping.prototype.start = function (callback) {
|
|||||||
Ping.prototype.stop = function () {
|
Ping.prototype.stop = function () {
|
||||||
clearInterval(this._i);
|
clearInterval(this._i);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Try to convert to UTF-8 for Windows, as the ping's output on Windows is not UTF-8 and could be in other languages
|
||||||
|
* Thank @pemassi
|
||||||
|
* https://github.com/louislam/uptime-kuma/issues/570#issuecomment-941984094
|
||||||
|
* @param data
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
function convertOutput(data) {
|
||||||
|
if (util.WIN) {
|
||||||
|
if (data) {
|
||||||
|
return util.convertToUTF8(data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
39
server/rate-limiter.js
Normal file
39
server/rate-limiter.js
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
const { RateLimiter } = require("limiter");
|
||||||
|
const { debug } = require("../src/util");
|
||||||
|
|
||||||
|
class KumaRateLimiter {
|
||||||
|
constructor(config) {
|
||||||
|
this.errorMessage = config.errorMessage;
|
||||||
|
this.rateLimiter = new RateLimiter(config);
|
||||||
|
}
|
||||||
|
|
||||||
|
async pass(callback, num = 1) {
|
||||||
|
const remainingRequests = await this.removeTokens(num);
|
||||||
|
debug("Rate Limit (remainingRequests):" + remainingRequests);
|
||||||
|
if (remainingRequests < 0) {
|
||||||
|
if (callback) {
|
||||||
|
callback({
|
||||||
|
ok: false,
|
||||||
|
msg: this.errorMessage,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
async removeTokens(num = 1) {
|
||||||
|
return await this.rateLimiter.removeTokens(num);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const loginRateLimiter = new KumaRateLimiter({
|
||||||
|
tokensPerInterval: 20,
|
||||||
|
interval: "minute",
|
||||||
|
fireImmediately: true,
|
||||||
|
errorMessage: "Too frequently, try again later."
|
||||||
|
});
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
loginRateLimiter
|
||||||
|
};
|
@ -5,7 +5,7 @@ const server = require("../server");
|
|||||||
const apicache = require("../modules/apicache");
|
const apicache = require("../modules/apicache");
|
||||||
const Monitor = require("../model/monitor");
|
const Monitor = require("../model/monitor");
|
||||||
const dayjs = require("dayjs");
|
const dayjs = require("dayjs");
|
||||||
const { UP } = require("../../src/util");
|
const { UP, flipStatus, debug } = require("../../src/util");
|
||||||
let router = express.Router();
|
let router = express.Router();
|
||||||
|
|
||||||
let cache = apicache.middleware;
|
let cache = apicache.middleware;
|
||||||
@ -18,9 +18,10 @@ router.get("/api/entry-page", async (_, response) => {
|
|||||||
|
|
||||||
router.get("/api/push/:pushToken", async (request, response) => {
|
router.get("/api/push/:pushToken", async (request, response) => {
|
||||||
try {
|
try {
|
||||||
|
|
||||||
let pushToken = request.params.pushToken;
|
let pushToken = request.params.pushToken;
|
||||||
let msg = request.query.msg || "OK";
|
let msg = request.query.msg || "OK";
|
||||||
let ping = request.query.ping;
|
let ping = request.query.ping || null;
|
||||||
|
|
||||||
let monitor = await R.findOne("monitor", " push_token = ? AND active = 1 ", [
|
let monitor = await R.findOne("monitor", " push_token = ? AND active = 1 ", [
|
||||||
pushToken
|
pushToken
|
||||||
@ -30,12 +31,35 @@ router.get("/api/push/:pushToken", async (request, response) => {
|
|||||||
throw new Error("Monitor not found or not active.");
|
throw new Error("Monitor not found or not active.");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const previousHeartbeat = await Monitor.getPreviousHeartbeat(monitor.id);
|
||||||
|
|
||||||
|
let status = UP;
|
||||||
|
if (monitor.isUpsideDown()) {
|
||||||
|
status = flipStatus(status);
|
||||||
|
}
|
||||||
|
|
||||||
|
let isFirstBeat = true;
|
||||||
|
let previousStatus = status;
|
||||||
|
let duration = 0;
|
||||||
|
|
||||||
let bean = R.dispense("heartbeat");
|
let bean = R.dispense("heartbeat");
|
||||||
bean.monitor_id = monitor.id;
|
|
||||||
bean.time = R.isoDateTime(dayjs.utc());
|
bean.time = R.isoDateTime(dayjs.utc());
|
||||||
bean.status = UP;
|
|
||||||
|
if (previousHeartbeat) {
|
||||||
|
isFirstBeat = false;
|
||||||
|
previousStatus = previousHeartbeat.status;
|
||||||
|
duration = dayjs(bean.time).diff(dayjs(previousHeartbeat.time), "second");
|
||||||
|
}
|
||||||
|
|
||||||
|
debug("PreviousStatus: " + previousStatus);
|
||||||
|
debug("Current Status: " + status);
|
||||||
|
|
||||||
|
bean.important = Monitor.isImportantBeat(isFirstBeat, previousStatus, status);
|
||||||
|
bean.monitor_id = monitor.id;
|
||||||
|
bean.status = status;
|
||||||
bean.msg = msg;
|
bean.msg = msg;
|
||||||
bean.ping = ping;
|
bean.ping = ping;
|
||||||
|
bean.duration = duration;
|
||||||
|
|
||||||
await R.store(bean);
|
await R.store(bean);
|
||||||
|
|
||||||
@ -45,6 +69,11 @@ router.get("/api/push/:pushToken", async (request, response) => {
|
|||||||
response.json({
|
response.json({
|
||||||
ok: true,
|
ok: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (bean.important) {
|
||||||
|
await Monitor.sendNotification(isFirstBeat, monitor, bean);
|
||||||
|
}
|
||||||
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
response.json({
|
response.json({
|
||||||
ok: false,
|
ok: false,
|
||||||
@ -67,6 +96,10 @@ router.get("/api/status-page/config", async (_request, response) => {
|
|||||||
config.statusPagePublished = true;
|
config.statusPagePublished = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (! config.statusPageTags) {
|
||||||
|
config.statusPageTags = false;
|
||||||
|
}
|
||||||
|
|
||||||
if (! config.title) {
|
if (! config.title) {
|
||||||
config.title = "Uptime Kuma";
|
config.title = "Uptime Kuma";
|
||||||
}
|
}
|
||||||
@ -106,10 +139,28 @@ router.get("/api/status-page/monitor-list", cache("5 minutes"), async (_request,
|
|||||||
try {
|
try {
|
||||||
await checkPublished();
|
await checkPublished();
|
||||||
const publicGroupList = [];
|
const publicGroupList = [];
|
||||||
let list = await R.find("group", " public = 1 ORDER BY weight ");
|
const tagsVisible = (await getSettings("statusPage")).statusPageTags;
|
||||||
|
const list = await R.find("group", " public = 1 ORDER BY weight ");
|
||||||
for (let groupBean of list) {
|
for (let groupBean of list) {
|
||||||
publicGroupList.push(await groupBean.toPublicJSON());
|
let monitorGroup = await groupBean.toPublicJSON();
|
||||||
|
if (tagsVisible) {
|
||||||
|
monitorGroup.monitorList = await Promise.all(monitorGroup.monitorList.map(async (monitor) => {
|
||||||
|
// Includes tags as an array in response, allows for tags to be displayed on public status page
|
||||||
|
const tags = await R.getAll(
|
||||||
|
`SELECT monitor_tag.monitor_id, monitor_tag.value, tag.name, tag.color
|
||||||
|
FROM monitor_tag
|
||||||
|
JOIN tag
|
||||||
|
ON monitor_tag.tag_id = tag.id
|
||||||
|
WHERE monitor_tag.monitor_id = ?`, [monitor.id]
|
||||||
|
);
|
||||||
|
return {
|
||||||
|
...monitor,
|
||||||
|
tags: tags
|
||||||
|
};
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
publicGroupList.push(monitorGroup);
|
||||||
}
|
}
|
||||||
|
|
||||||
response.json(publicGroupList);
|
response.json(publicGroupList);
|
||||||
|
177
server/server.js
177
server/server.js
@ -1,6 +1,7 @@
|
|||||||
console.log("Welcome to Uptime Kuma");
|
console.log("Welcome to Uptime Kuma");
|
||||||
const args = require("args-parser")(process.argv);
|
const args = require("args-parser")(process.argv);
|
||||||
const { sleep, debug, getRandomInt, genSecret } = require("../src/util");
|
const { sleep, debug, getRandomInt, genSecret } = require("../src/util");
|
||||||
|
const config = require("./config");
|
||||||
|
|
||||||
debug(args);
|
debug(args);
|
||||||
|
|
||||||
@ -8,10 +9,6 @@ if (! process.env.NODE_ENV) {
|
|||||||
process.env.NODE_ENV = "production";
|
process.env.NODE_ENV = "production";
|
||||||
}
|
}
|
||||||
|
|
||||||
// Demo Mode?
|
|
||||||
const demoMode = args["demo"] || false;
|
|
||||||
exports.demoMode = demoMode;
|
|
||||||
|
|
||||||
console.log("Node Env: " + process.env.NODE_ENV);
|
console.log("Node Env: " + process.env.NODE_ENV);
|
||||||
|
|
||||||
console.log("Importing Node libraries");
|
console.log("Importing Node libraries");
|
||||||
@ -34,6 +31,7 @@ debug("Importing prometheus-api-metrics");
|
|||||||
const prometheusAPIMetrics = require("prometheus-api-metrics");
|
const prometheusAPIMetrics = require("prometheus-api-metrics");
|
||||||
debug("Importing compare-versions");
|
debug("Importing compare-versions");
|
||||||
const compareVersions = require("compare-versions");
|
const compareVersions = require("compare-versions");
|
||||||
|
const { passwordStrength } = require("check-password-strength");
|
||||||
|
|
||||||
debug("Importing 2FA Modules");
|
debug("Importing 2FA Modules");
|
||||||
const notp = require("notp");
|
const notp = require("notp");
|
||||||
@ -43,7 +41,7 @@ console.log("Importing this project modules");
|
|||||||
debug("Importing Monitor");
|
debug("Importing Monitor");
|
||||||
const Monitor = require("./model/monitor");
|
const Monitor = require("./model/monitor");
|
||||||
debug("Importing Settings");
|
debug("Importing Settings");
|
||||||
const { getSettings, setSettings, setting, initJWTSecret, checkLogin, startUnitTest } = require("./util-server");
|
const { getSettings, setSettings, setting, initJWTSecret, checkLogin, startUnitTest, FBSD, errorLog } = require("./util-server");
|
||||||
|
|
||||||
debug("Importing Notification");
|
debug("Importing Notification");
|
||||||
const { Notification } = require("./notification");
|
const { Notification } = require("./notification");
|
||||||
@ -52,6 +50,10 @@ Notification.init();
|
|||||||
debug("Importing Database");
|
debug("Importing Database");
|
||||||
const Database = require("./database");
|
const Database = require("./database");
|
||||||
|
|
||||||
|
debug("Importing Background Jobs");
|
||||||
|
const { initBackgroundJobs } = require("./jobs");
|
||||||
|
const { loginRateLimiter } = require("./rate-limiter");
|
||||||
|
|
||||||
const { basicAuth } = require("./auth");
|
const { basicAuth } = require("./auth");
|
||||||
const { login } = require("./auth");
|
const { login } = require("./auth");
|
||||||
const passwordHash = require("./password-hash");
|
const passwordHash = require("./password-hash");
|
||||||
@ -61,12 +63,29 @@ console.info("Version: " + checkVersion.version);
|
|||||||
|
|
||||||
// If host is omitted, the server will accept connections on the unspecified IPv6 address (::) when IPv6 is available and the unspecified IPv4 address (0.0.0.0) otherwise.
|
// If host is omitted, the server will accept connections on the unspecified IPv6 address (::) when IPv6 is available and the unspecified IPv4 address (0.0.0.0) otherwise.
|
||||||
// Dual-stack support for (::)
|
// Dual-stack support for (::)
|
||||||
const hostname = process.env.HOST || args.host;
|
let hostname = process.env.UPTIME_KUMA_HOST || args.host;
|
||||||
const port = parseInt(process.env.PORT || args.port || 3001);
|
|
||||||
|
// Also read HOST if not FreeBSD, as HOST is a system environment variable in FreeBSD
|
||||||
|
if (!hostname && !FBSD) {
|
||||||
|
hostname = process.env.HOST;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hostname) {
|
||||||
|
console.log("Custom hostname: " + hostname);
|
||||||
|
}
|
||||||
|
|
||||||
|
const port = parseInt(process.env.UPTIME_KUMA_PORT || process.env.PORT || args.port || 3001);
|
||||||
|
|
||||||
// SSL
|
// SSL
|
||||||
const sslKey = process.env.SSL_KEY || args["ssl-key"] || undefined;
|
const sslKey = process.env.UPTIME_KUMA_SSL_KEY || process.env.SSL_KEY || args["ssl-key"] || undefined;
|
||||||
const sslCert = process.env.SSL_CERT || args["ssl-cert"] || undefined;
|
const sslCert = process.env.UPTIME_KUMA_SSL_CERT || process.env.SSL_CERT || args["ssl-cert"] || undefined;
|
||||||
|
const disableFrameSameOrigin = !!process.env.UPTIME_KUMA_DISABLE_FRAME_SAMEORIGIN || args["disable-frame-sameorigin"] || false;
|
||||||
|
|
||||||
|
// 2FA / notp verification defaults
|
||||||
|
const twofa_verification_opts = {
|
||||||
|
"window": 1,
|
||||||
|
"time": 30
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Run unit test after the server is ready
|
* Run unit test after the server is ready
|
||||||
@ -74,7 +93,7 @@ const sslCert = process.env.SSL_CERT || args["ssl-cert"] || undefined;
|
|||||||
*/
|
*/
|
||||||
const testMode = !!args["test"] || false;
|
const testMode = !!args["test"] || false;
|
||||||
|
|
||||||
if (demoMode) {
|
if (config.demoMode) {
|
||||||
console.log("==== Demo Mode ====");
|
console.log("==== Demo Mode ====");
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -100,9 +119,20 @@ module.exports.io = io;
|
|||||||
// Must be after io instantiation
|
// Must be after io instantiation
|
||||||
const { sendNotificationList, sendHeartbeatList, sendImportantHeartbeatList, sendInfo } = require("./client");
|
const { sendNotificationList, sendHeartbeatList, sendImportantHeartbeatList, sendInfo } = require("./client");
|
||||||
const { statusPageSocketHandler } = require("./socket-handlers/status-page-socket-handler");
|
const { statusPageSocketHandler } = require("./socket-handlers/status-page-socket-handler");
|
||||||
|
const databaseSocketHandler = require("./socket-handlers/database-socket-handler");
|
||||||
|
const TwoFA = require("./2fa");
|
||||||
|
|
||||||
app.use(express.json());
|
app.use(express.json());
|
||||||
|
|
||||||
|
// Global Middleware
|
||||||
|
app.use(function (req, res, next) {
|
||||||
|
if (!disableFrameSameOrigin) {
|
||||||
|
res.setHeader("X-Frame-Options", "SAMEORIGIN");
|
||||||
|
}
|
||||||
|
res.removeHeader("X-Powered-By");
|
||||||
|
next();
|
||||||
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Total WebSocket client connected to server currently, no actual use
|
* Total WebSocket client connected to server currently, no actual use
|
||||||
* @type {number}
|
* @type {number}
|
||||||
@ -131,13 +161,23 @@ let needSetup = false;
|
|||||||
* Cache Index HTML
|
* Cache Index HTML
|
||||||
* @type {string}
|
* @type {string}
|
||||||
*/
|
*/
|
||||||
let indexHTML = fs.readFileSync("./dist/index.html").toString();
|
let indexHTML = "";
|
||||||
|
|
||||||
|
try {
|
||||||
|
indexHTML = fs.readFileSync("./dist/index.html").toString();
|
||||||
|
} catch (e) {
|
||||||
|
// "dist/index.html" is not necessary for development
|
||||||
|
if (process.env.NODE_ENV !== "development") {
|
||||||
|
console.error("Error: Cannot find 'dist/index.html', did you install correctly?");
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
exports.entryPage = "dashboard";
|
exports.entryPage = "dashboard";
|
||||||
|
|
||||||
(async () => {
|
(async () => {
|
||||||
Database.init(args);
|
Database.init(args);
|
||||||
await initDatabase();
|
await initDatabase(testMode);
|
||||||
|
|
||||||
exports.entryPage = await setting("entryPage");
|
exports.entryPage = await setting("entryPage");
|
||||||
|
|
||||||
@ -147,6 +187,15 @@ exports.entryPage = "dashboard";
|
|||||||
// Normal Router here
|
// Normal Router here
|
||||||
// ***************************
|
// ***************************
|
||||||
|
|
||||||
|
// Entry Page
|
||||||
|
app.get("/", async (_request, response) => {
|
||||||
|
if (exports.entryPage === "statusPage") {
|
||||||
|
response.redirect("/status");
|
||||||
|
} else {
|
||||||
|
response.redirect("/dashboard");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// Robots.txt
|
// Robots.txt
|
||||||
app.get("/robots.txt", async (_request, response) => {
|
app.get("/robots.txt", async (_request, response) => {
|
||||||
let txt = "User-agent: *\nDisallow:";
|
let txt = "User-agent: *\nDisallow:";
|
||||||
@ -176,7 +225,7 @@ exports.entryPage = "dashboard";
|
|||||||
const apiRouter = require("./routers/api-router");
|
const apiRouter = require("./routers/api-router");
|
||||||
app.use(apiRouter);
|
app.use(apiRouter);
|
||||||
|
|
||||||
// Universal Route Handler, must be at the end of all express route.
|
// Universal Route Handler, must be at the end of all express routes.
|
||||||
app.get("*", async (_request, response) => {
|
app.get("*", async (_request, response) => {
|
||||||
if (_request.originalUrl.startsWith("/upload/")) {
|
if (_request.originalUrl.startsWith("/upload/")) {
|
||||||
response.status(404).send("File not found.");
|
response.status(404).send("File not found.");
|
||||||
@ -244,12 +293,16 @@ exports.entryPage = "dashboard";
|
|||||||
socket.on("login", async (data, callback) => {
|
socket.on("login", async (data, callback) => {
|
||||||
console.log("Login");
|
console.log("Login");
|
||||||
|
|
||||||
|
// Login Rate Limit
|
||||||
|
if (! await loginRateLimiter.pass(callback)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
let user = await login(data.username, data.password);
|
let user = await login(data.username, data.password);
|
||||||
|
|
||||||
if (user) {
|
if (user) {
|
||||||
afterLogin(socket, user);
|
if (user.twofa_status == 0) {
|
||||||
|
afterLogin(socket, user);
|
||||||
if (user.twofaStatus == 0) {
|
|
||||||
callback({
|
callback({
|
||||||
ok: true,
|
ok: true,
|
||||||
token: jwt.sign({
|
token: jwt.sign({
|
||||||
@ -258,16 +311,23 @@ exports.entryPage = "dashboard";
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (user.twofaStatus == 1 && !data.token) {
|
if (user.twofa_status == 1 && !data.token) {
|
||||||
callback({
|
callback({
|
||||||
tokenRequired: true,
|
tokenRequired: true,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (data.token) {
|
if (data.token) {
|
||||||
let verify = notp.totp.verify(data.token, user.twofa_secret);
|
let verify = notp.totp.verify(data.token, user.twofa_secret, twofa_verification_opts);
|
||||||
|
|
||||||
|
if (user.twofa_last_token !== data.token && verify) {
|
||||||
|
afterLogin(socket, user);
|
||||||
|
|
||||||
|
await R.exec("UPDATE `user` SET twofa_last_token = ? WHERE id = ? ", [
|
||||||
|
data.token,
|
||||||
|
socket.userID,
|
||||||
|
]);
|
||||||
|
|
||||||
if (verify && verify.delta == 0) {
|
|
||||||
callback({
|
callback({
|
||||||
ok: true,
|
ok: true,
|
||||||
token: jwt.sign({
|
token: jwt.sign({
|
||||||
@ -305,7 +365,7 @@ exports.entryPage = "dashboard";
|
|||||||
]);
|
]);
|
||||||
|
|
||||||
if (user.twofa_status == 0) {
|
if (user.twofa_status == 0) {
|
||||||
let newSecret = await genSecret();
|
let newSecret = genSecret();
|
||||||
let encodedSecret = base32.encode(newSecret);
|
let encodedSecret = base32.encode(newSecret);
|
||||||
|
|
||||||
// Google authenticator doesn't like equal signs
|
// Google authenticator doesn't like equal signs
|
||||||
@ -361,10 +421,7 @@ exports.entryPage = "dashboard";
|
|||||||
socket.on("disable2FA", async (callback) => {
|
socket.on("disable2FA", async (callback) => {
|
||||||
try {
|
try {
|
||||||
checkLogin(socket);
|
checkLogin(socket);
|
||||||
|
await TwoFA.disable2FA(socket.userID);
|
||||||
await R.exec("UPDATE `user` SET twofa_status = 0 WHERE id = ? ", [
|
|
||||||
socket.userID,
|
|
||||||
]);
|
|
||||||
|
|
||||||
callback({
|
callback({
|
||||||
ok: true,
|
ok: true,
|
||||||
@ -383,9 +440,9 @@ exports.entryPage = "dashboard";
|
|||||||
socket.userID,
|
socket.userID,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
let verify = notp.totp.verify(token, user.twofa_secret);
|
let verify = notp.totp.verify(token, user.twofa_secret, twofa_verification_opts);
|
||||||
|
|
||||||
if (verify && verify.delta == 0) {
|
if (user.twofa_last_token !== token && verify) {
|
||||||
callback({
|
callback({
|
||||||
ok: true,
|
ok: true,
|
||||||
valid: true,
|
valid: true,
|
||||||
@ -432,8 +489,12 @@ exports.entryPage = "dashboard";
|
|||||||
|
|
||||||
socket.on("setup", async (username, password, callback) => {
|
socket.on("setup", async (username, password, callback) => {
|
||||||
try {
|
try {
|
||||||
|
if (passwordStrength(password).value === "Too weak") {
|
||||||
|
throw new Error("Password is too weak. It should contain alphabetic and numeric characters. It must be at least 6 characters in length.");
|
||||||
|
}
|
||||||
|
|
||||||
if ((await R.count("user")) !== 0) {
|
if ((await R.count("user")) !== 0) {
|
||||||
throw new Error("Uptime Kuma has been setup. If you want to setup again, please delete the database.");
|
throw new Error("Uptime Kuma has been initialized. If you want to run setup again, please delete the database.");
|
||||||
}
|
}
|
||||||
|
|
||||||
let user = R.dispense("user");
|
let user = R.dispense("user");
|
||||||
@ -478,8 +539,8 @@ exports.entryPage = "dashboard";
|
|||||||
|
|
||||||
await updateMonitorNotification(bean.id, notificationIDList);
|
await updateMonitorNotification(bean.id, notificationIDList);
|
||||||
|
|
||||||
await startMonitor(socket.userID, bean.id);
|
|
||||||
await sendMonitorList(socket);
|
await sendMonitorList(socket);
|
||||||
|
await startMonitor(socket.userID, bean.id);
|
||||||
|
|
||||||
callback({
|
callback({
|
||||||
ok: true,
|
ok: true,
|
||||||
@ -509,6 +570,11 @@ exports.entryPage = "dashboard";
|
|||||||
bean.name = monitor.name;
|
bean.name = monitor.name;
|
||||||
bean.type = monitor.type;
|
bean.type = monitor.type;
|
||||||
bean.url = monitor.url;
|
bean.url = monitor.url;
|
||||||
|
bean.method = monitor.method;
|
||||||
|
bean.body = monitor.body;
|
||||||
|
bean.headers = monitor.headers;
|
||||||
|
bean.basic_auth_user = monitor.basic_auth_user;
|
||||||
|
bean.basic_auth_pass = monitor.basic_auth_pass;
|
||||||
bean.interval = monitor.interval;
|
bean.interval = monitor.interval;
|
||||||
bean.retryInterval = monitor.retryInterval;
|
bean.retryInterval = monitor.retryInterval;
|
||||||
bean.hostname = monitor.hostname;
|
bean.hostname = monitor.hostname;
|
||||||
@ -588,6 +654,38 @@ exports.entryPage = "dashboard";
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
socket.on("getMonitorBeats", async (monitorID, period, callback) => {
|
||||||
|
try {
|
||||||
|
checkLogin(socket);
|
||||||
|
|
||||||
|
console.log(`Get Monitor Beats: ${monitorID} User ID: ${socket.userID}`);
|
||||||
|
|
||||||
|
if (period == null) {
|
||||||
|
throw new Error("Invalid period.");
|
||||||
|
}
|
||||||
|
|
||||||
|
let list = await R.getAll(`
|
||||||
|
SELECT * FROM heartbeat
|
||||||
|
WHERE monitor_id = ? AND
|
||||||
|
time > DATETIME('now', '-' || ? || ' hours')
|
||||||
|
ORDER BY time ASC
|
||||||
|
`, [
|
||||||
|
monitorID,
|
||||||
|
period,
|
||||||
|
]);
|
||||||
|
|
||||||
|
callback({
|
||||||
|
ok: true,
|
||||||
|
data: list,
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
callback({
|
||||||
|
ok: false,
|
||||||
|
msg: e.message,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// Start or Resume the monitor
|
// Start or Resume the monitor
|
||||||
socket.on("resumeMonitor", async (monitorID, callback) => {
|
socket.on("resumeMonitor", async (monitorID, callback) => {
|
||||||
try {
|
try {
|
||||||
@ -818,10 +916,14 @@ exports.entryPage = "dashboard";
|
|||||||
try {
|
try {
|
||||||
checkLogin(socket);
|
checkLogin(socket);
|
||||||
|
|
||||||
if (! password.currentPassword) {
|
if (! password.newPassword) {
|
||||||
throw new Error("Invalid new password");
|
throw new Error("Invalid new password");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (passwordStrength(password.newPassword).value === "Too weak") {
|
||||||
|
throw new Error("Password is too weak. It should contain alphabetic and numeric characters. It must be at least 6 characters in length.");
|
||||||
|
}
|
||||||
|
|
||||||
let user = await R.findOne("user", " id = ? AND active = 1 ", [
|
let user = await R.findOne("user", " id = ? AND active = 1 ", [
|
||||||
socket.userID,
|
socket.userID,
|
||||||
]);
|
]);
|
||||||
@ -1034,6 +1136,11 @@ exports.entryPage = "dashboard";
|
|||||||
name: monitorListData[i].name,
|
name: monitorListData[i].name,
|
||||||
type: monitorListData[i].type,
|
type: monitorListData[i].type,
|
||||||
url: monitorListData[i].url,
|
url: monitorListData[i].url,
|
||||||
|
method: monitorListData[i].method || "GET",
|
||||||
|
body: monitorListData[i].body,
|
||||||
|
headers: monitorListData[i].headers,
|
||||||
|
basic_auth_user: monitorListData[i].basic_auth_user,
|
||||||
|
basic_auth_pass: monitorListData[i].basic_auth_pass,
|
||||||
interval: monitorListData[i].interval,
|
interval: monitorListData[i].interval,
|
||||||
retryInterval: retryInterval,
|
retryInterval: retryInterval,
|
||||||
hostname: monitorListData[i].hostname,
|
hostname: monitorListData[i].hostname,
|
||||||
@ -1200,6 +1307,7 @@ exports.entryPage = "dashboard";
|
|||||||
|
|
||||||
// Status Page Socket Handler for admin only
|
// Status Page Socket Handler for admin only
|
||||||
statusPageSocketHandler(socket);
|
statusPageSocketHandler(socket);
|
||||||
|
databaseSocketHandler(socket);
|
||||||
|
|
||||||
debug("added all socket handlers");
|
debug("added all socket handlers");
|
||||||
|
|
||||||
@ -1239,6 +1347,8 @@ exports.entryPage = "dashboard";
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
initBackgroundJobs(args);
|
||||||
|
|
||||||
})();
|
})();
|
||||||
|
|
||||||
async function updateMonitorNotification(monitorID, notificationIDList) {
|
async function updateMonitorNotification(monitorID, notificationIDList) {
|
||||||
@ -1309,14 +1419,14 @@ async function getMonitorJSONList(userID) {
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function initDatabase() {
|
async function initDatabase(testMode = false) {
|
||||||
if (! fs.existsSync(Database.path)) {
|
if (! fs.existsSync(Database.path)) {
|
||||||
console.log("Copying Database");
|
console.log("Copying Database");
|
||||||
fs.copyFileSync(Database.templatePath, Database.path);
|
fs.copyFileSync(Database.templatePath, Database.path);
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log("Connecting to Database");
|
console.log("Connecting to the Database");
|
||||||
await Database.connect();
|
await Database.connect(testMode);
|
||||||
console.log("Connected");
|
console.log("Connected");
|
||||||
|
|
||||||
// Patch the database
|
// Patch the database
|
||||||
@ -1415,7 +1525,7 @@ async function shutdownFunction(signal) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function finalFunction() {
|
function finalFunction() {
|
||||||
console.log("Graceful shutdown successfully!");
|
console.log("Graceful shutdown successful!");
|
||||||
}
|
}
|
||||||
|
|
||||||
gracefulShutdown(server, {
|
gracefulShutdown(server, {
|
||||||
@ -1430,5 +1540,6 @@ gracefulShutdown(server, {
|
|||||||
// Catch unexpected errors here
|
// Catch unexpected errors here
|
||||||
process.addListener("unhandledRejection", (error, promise) => {
|
process.addListener("unhandledRejection", (error, promise) => {
|
||||||
console.trace(error);
|
console.trace(error);
|
||||||
|
errorLog(error, false);
|
||||||
console.error("If you keep encountering errors, please report to https://github.com/louislam/uptime-kuma/issues");
|
console.error("If you keep encountering errors, please report to https://github.com/louislam/uptime-kuma/issues");
|
||||||
});
|
});
|
||||||
|
37
server/socket-handlers/database-socket-handler.js
Normal file
37
server/socket-handlers/database-socket-handler.js
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
const { checkLogin } = require("../util-server");
|
||||||
|
const Database = require("../database");
|
||||||
|
|
||||||
|
module.exports = (socket) => {
|
||||||
|
|
||||||
|
// Post or edit incident
|
||||||
|
socket.on("getDatabaseSize", async (callback) => {
|
||||||
|
try {
|
||||||
|
checkLogin(socket);
|
||||||
|
callback({
|
||||||
|
ok: true,
|
||||||
|
size: Database.getSize(),
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
callback({
|
||||||
|
ok: false,
|
||||||
|
msg: error.message,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.on("shrinkDatabase", async (callback) => {
|
||||||
|
try {
|
||||||
|
checkLogin(socket);
|
||||||
|
Database.shrink();
|
||||||
|
callback({
|
||||||
|
ok: true,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
callback({
|
||||||
|
ok: false,
|
||||||
|
msg: error.message,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
};
|
@ -6,6 +6,16 @@ const passwordHash = require("./password-hash");
|
|||||||
const dayjs = require("dayjs");
|
const dayjs = require("dayjs");
|
||||||
const { Resolver } = require("dns");
|
const { Resolver } = require("dns");
|
||||||
const child_process = require("child_process");
|
const child_process = require("child_process");
|
||||||
|
const iconv = require("iconv-lite");
|
||||||
|
const chardet = require("chardet");
|
||||||
|
const fs = require("fs");
|
||||||
|
const nodeJsUtil = require("util");
|
||||||
|
|
||||||
|
// From ping-lite
|
||||||
|
exports.WIN = /^win/.test(process.platform);
|
||||||
|
exports.LIN = /^linux/.test(process.platform);
|
||||||
|
exports.MAC = /^darwin/.test(process.platform);
|
||||||
|
exports.FBSD = /^freebsd/.test(process.platform);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Init or reset JWT secret
|
* Init or reset JWT secret
|
||||||
@ -116,7 +126,7 @@ exports.setting = async function (key) {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
exports.setSetting = async function (key, value) {
|
exports.setSetting = async function (key, value, type = null) {
|
||||||
let bean = await R.findOne("setting", " `key` = ? ", [
|
let bean = await R.findOne("setting", " `key` = ? ", [
|
||||||
key,
|
key,
|
||||||
]);
|
]);
|
||||||
@ -124,6 +134,7 @@ exports.setSetting = async function (key, value) {
|
|||||||
bean = R.dispense("setting");
|
bean = R.dispense("setting");
|
||||||
bean.key = key;
|
bean.key = key;
|
||||||
}
|
}
|
||||||
|
bean.type = type;
|
||||||
bean.value = JSON.stringify(value);
|
bean.value = JSON.stringify(value);
|
||||||
await R.store(bean);
|
await R.store(bean);
|
||||||
};
|
};
|
||||||
@ -190,8 +201,13 @@ const getDaysRemaining = (validFrom, validTo) => {
|
|||||||
// param: info - the chain obtained from getPeerCertificate()
|
// param: info - the chain obtained from getPeerCertificate()
|
||||||
const parseCertificateInfo = function (info) {
|
const parseCertificateInfo = function (info) {
|
||||||
let link = info;
|
let link = info;
|
||||||
|
let i = 0;
|
||||||
|
|
||||||
|
const existingList = {};
|
||||||
|
|
||||||
while (link) {
|
while (link) {
|
||||||
|
debug(`[${i}] ${link.fingerprint}`);
|
||||||
|
|
||||||
if (!link.valid_from || !link.valid_to) {
|
if (!link.valid_from || !link.valid_to) {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@ -199,15 +215,24 @@ const parseCertificateInfo = function (info) {
|
|||||||
link.validFor = link.subjectaltname?.replace(/DNS:|IP Address:/g, "").split(", ");
|
link.validFor = link.subjectaltname?.replace(/DNS:|IP Address:/g, "").split(", ");
|
||||||
link.daysRemaining = getDaysRemaining(new Date(), link.validTo);
|
link.daysRemaining = getDaysRemaining(new Date(), link.validTo);
|
||||||
|
|
||||||
|
existingList[link.fingerprint] = true;
|
||||||
|
|
||||||
// Move up the chain until loop is encountered
|
// Move up the chain until loop is encountered
|
||||||
if (link.issuerCertificate == null) {
|
if (link.issuerCertificate == null) {
|
||||||
break;
|
break;
|
||||||
} else if (link.fingerprint == link.issuerCertificate.fingerprint) {
|
} else if (link.issuerCertificate.fingerprint in existingList) {
|
||||||
|
debug(`[Last] ${link.issuerCertificate.fingerprint}`);
|
||||||
link.issuerCertificate = null;
|
link.issuerCertificate = null;
|
||||||
break;
|
break;
|
||||||
} else {
|
} else {
|
||||||
link = link.issuerCertificate;
|
link = link.issuerCertificate;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Should be no use, but just in case.
|
||||||
|
if (i > 500) {
|
||||||
|
throw new Error("Dead loop occurred in parseCertificateInfo");
|
||||||
|
}
|
||||||
|
i++;
|
||||||
}
|
}
|
||||||
|
|
||||||
return info;
|
return info;
|
||||||
@ -217,6 +242,7 @@ exports.checkCertificate = function (res) {
|
|||||||
const info = res.request.res.socket.getPeerCertificate(true);
|
const info = res.request.res.socket.getPeerCertificate(true);
|
||||||
const valid = res.request.res.socket.authorized || false;
|
const valid = res.request.res.socket.authorized || false;
|
||||||
|
|
||||||
|
debug("Parsing Certificate Info");
|
||||||
const parsedInfo = parseCertificateInfo(info);
|
const parsedInfo = parseCertificateInfo(info);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@ -312,3 +338,35 @@ exports.startUnitTest = async () => {
|
|||||||
process.exit(code);
|
process.exit(code);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param body : Buffer
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
exports.convertToUTF8 = (body) => {
|
||||||
|
const guessEncoding = chardet.detect(body);
|
||||||
|
//debug("Guess Encoding: " + guessEncoding);
|
||||||
|
const str = iconv.decode(body, guessEncoding);
|
||||||
|
return str.toString();
|
||||||
|
};
|
||||||
|
|
||||||
|
let logFile;
|
||||||
|
|
||||||
|
try {
|
||||||
|
logFile = fs.createWriteStream("./data/error.log", {
|
||||||
|
flags: "a"
|
||||||
|
});
|
||||||
|
} catch (_) { }
|
||||||
|
|
||||||
|
exports.errorLog = (error, outputToConsole = true) => {
|
||||||
|
try {
|
||||||
|
if (logFile) {
|
||||||
|
const dateTime = R.isoDateTime();
|
||||||
|
logFile.write(`[${dateTime}] ` + nodeJsUtil.format(error) + "\n");
|
||||||
|
|
||||||
|
if (outputToConsole) {
|
||||||
|
console.error(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (_) { }
|
||||||
|
};
|
||||||
|
@ -14,6 +14,10 @@ h2 {
|
|||||||
font-size: 26px;
|
font-size: 26px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
textarea.form-control {
|
||||||
|
border-radius: 19px;
|
||||||
|
}
|
||||||
|
|
||||||
::-webkit-scrollbar {
|
::-webkit-scrollbar {
|
||||||
width: 10px;
|
width: 10px;
|
||||||
}
|
}
|
||||||
@ -185,7 +189,7 @@ h2 {
|
|||||||
opacity: 1;
|
opacity: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.table-hover > tbody > tr:hover {
|
.table-hover > tbody > tr:hover > * {
|
||||||
--bs-table-accent-bg: #070a10;
|
--bs-table-accent-bg: #070a10;
|
||||||
color: $dark-font-color;
|
color: $dark-font-color;
|
||||||
}
|
}
|
||||||
@ -342,6 +346,10 @@ h2 {
|
|||||||
&.active {
|
&.active {
|
||||||
background-color: #cdf8f4;
|
background-color: #cdf8f4;
|
||||||
}
|
}
|
||||||
|
.tags {
|
||||||
|
// Removes margin to line up tags list with uptime percentage
|
||||||
|
margin-left: -0.25rem;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -21,7 +21,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.multiselect__tag {
|
.multiselect__tag {
|
||||||
border-radius: 50rem;
|
border-radius: $border-radius;
|
||||||
margin-bottom: 0;
|
margin-bottom: 0;
|
||||||
padding: 6px 26px 6px 10px;
|
padding: 6px 26px 6px 10px;
|
||||||
background: $primary !important;
|
background: $primary !important;
|
||||||
|
@ -12,6 +12,7 @@ $dark-font-color2: #020b05;
|
|||||||
$dark-bg: #0d1117;
|
$dark-bg: #0d1117;
|
||||||
$dark-bg2: #070a10;
|
$dark-bg2: #070a10;
|
||||||
$dark-border-color: #1d2634;
|
$dark-border-color: #1d2634;
|
||||||
|
$dark-header-bg: #161b22;
|
||||||
|
|
||||||
$easing-in: cubic-bezier(0.54, 0.78, 0.55, 0.97);
|
$easing-in: cubic-bezier(0.54, 0.78, 0.55, 0.97);
|
||||||
$easing-out: cubic-bezier(0.25, 0.46, 0.45, 0.94);
|
$easing-out: cubic-bezier(0.25, 0.46, 0.45, 0.94);
|
||||||
|
@ -167,7 +167,7 @@ export default {
|
|||||||
},
|
},
|
||||||
|
|
||||||
getBeatTitle(beat) {
|
getBeatTitle(beat) {
|
||||||
return `${this.$root.datetime(beat.time)} - ${beat.msg}`;
|
return `${this.$root.datetime(beat.time)}` + ((beat.msg) ? ` - ${beat.msg}` : ``);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@ -186,7 +186,7 @@ export default {
|
|||||||
.beat {
|
.beat {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
background-color: $primary;
|
background-color: $primary;
|
||||||
border-radius: 50rem;
|
border-radius: $border-radius;
|
||||||
|
|
||||||
&.empty {
|
&.empty {
|
||||||
background-color: aliceblue;
|
background-color: aliceblue;
|
||||||
|
@ -137,7 +137,7 @@ export default {
|
|||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
|
|
||||||
.dark & {
|
.dark & {
|
||||||
background-color: #161b22;
|
background-color: $dark-header-bg;
|
||||||
border-bottom: 0;
|
border-bottom: 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,16 +1,34 @@
|
|||||||
<template>
|
<template>
|
||||||
<LineChart :chart-data="chartData" :options="chartOptions" />
|
<div>
|
||||||
|
<div class="period-options">
|
||||||
|
<button type="button" class="btn btn-light dropdown-toggle btn-period-toggle" data-bs-toggle="dropdown" aria-expanded="false">
|
||||||
|
{{ chartPeriodOptions[chartPeriodHrs] }}
|
||||||
|
</button>
|
||||||
|
<ul class="dropdown-menu dropdown-menu-end">
|
||||||
|
<li v-for="(item, key) in chartPeriodOptions" :key="key">
|
||||||
|
<a class="dropdown-item" :class="{ active: chartPeriodHrs == key }" href="#" @click="chartPeriodHrs = key">{{ item }}</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<div class="chart-wrapper" :class="{ loading : loading}">
|
||||||
|
<LineChart :chart-data="chartData" :options="chartOptions" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script lang="ts">
|
||||||
import { BarController, BarElement, Chart, Filler, LinearScale, LineController, LineElement, PointElement, TimeScale, Tooltip } from "chart.js";
|
import { BarController, BarElement, Chart, Filler, LinearScale, LineController, LineElement, PointElement, TimeScale, Tooltip } from "chart.js";
|
||||||
import dayjs from "dayjs";
|
import dayjs from "dayjs";
|
||||||
import utc from "dayjs/plugin/utc";
|
import utc from "dayjs/plugin/utc";
|
||||||
import timezone from "dayjs/plugin/timezone";
|
import timezone from "dayjs/plugin/timezone";
|
||||||
import "chartjs-adapter-dayjs";
|
import "chartjs-adapter-dayjs";
|
||||||
import { LineChart } from "vue-chart-3";
|
import { LineChart } from "vue-chart-3";
|
||||||
|
import { useToast } from "vue-toastification";
|
||||||
|
import { UP, DOWN, PENDING } from "../util.ts";
|
||||||
|
|
||||||
dayjs.extend(utc);
|
dayjs.extend(utc);
|
||||||
dayjs.extend(timezone);
|
dayjs.extend(timezone);
|
||||||
|
const toast = useToast();
|
||||||
|
|
||||||
Chart.register(LineController, BarController, LineElement, PointElement, TimeScale, BarElement, LinearScale, Tooltip, Filler);
|
Chart.register(LineController, BarController, LineElement, PointElement, TimeScale, BarElement, LinearScale, Tooltip, Filler);
|
||||||
|
|
||||||
@ -24,8 +42,23 @@ export default {
|
|||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
|
|
||||||
|
loading: false,
|
||||||
|
|
||||||
// Configurable filtering on top of the returned data
|
// Configurable filtering on top of the returned data
|
||||||
chartPeriodHrs: 6,
|
chartPeriodHrs: 0,
|
||||||
|
|
||||||
|
chartPeriodOptions: {
|
||||||
|
0: this.$t("recent"),
|
||||||
|
3: "3h",
|
||||||
|
6: "6h",
|
||||||
|
24: "24h",
|
||||||
|
168: "1w",
|
||||||
|
},
|
||||||
|
|
||||||
|
// A heartbeatList for 3h, 6h, 24h, 1w
|
||||||
|
// Uses the $root.heartbeatList when value is null
|
||||||
|
heartbeatList: null
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
@ -117,7 +150,7 @@ export default {
|
|||||||
},
|
},
|
||||||
callbacks: {
|
callbacks: {
|
||||||
label: (context) => {
|
label: (context) => {
|
||||||
return ` ${new Intl.NumberFormat().format(context.parsed.y)} ms`
|
return ` ${new Intl.NumberFormat().format(context.parsed.y)} ms`;
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -125,27 +158,36 @@ export default {
|
|||||||
display: false,
|
display: false,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
};
|
||||||
},
|
},
|
||||||
chartData() {
|
chartData() {
|
||||||
let pingData = []; // Ping Data for Line Chart, y-axis contains ping time
|
let pingData = []; // Ping Data for Line Chart, y-axis contains ping time
|
||||||
let downData = []; // Down Data for Bar Chart, y-axis is 1 if target is down, 0 if target is up
|
let downData = []; // Down Data for Bar Chart, y-axis is 1 if target is down, 0 if target is up
|
||||||
if (this.monitorId in this.$root.heartbeatList) {
|
|
||||||
this.$root.heartbeatList[this.monitorId]
|
let heartbeatList = this.heartbeatList ||
|
||||||
.filter(
|
(this.monitorId in this.$root.heartbeatList && this.$root.heartbeatList[this.monitorId]) ||
|
||||||
(beat) => dayjs.utc(beat.time).tz(this.$root.timezone).isAfter(dayjs().subtract(this.chartPeriodHrs, "hours")))
|
[];
|
||||||
.map((beat) => {
|
|
||||||
const x = this.$root.datetime(beat.time);
|
heartbeatList
|
||||||
pingData.push({
|
.filter(
|
||||||
x,
|
// Filtering as data gets appended
|
||||||
y: beat.ping,
|
// not the most efficient, but works for now
|
||||||
});
|
(beat) => dayjs.utc(beat.time).tz(this.$root.timezone).isAfter(
|
||||||
downData.push({
|
dayjs().subtract(Math.max(this.chartPeriodHrs, 6), "hours")
|
||||||
x,
|
)
|
||||||
y: beat.status === 0 ? 1 : 0,
|
)
|
||||||
})
|
.map((beat) => {
|
||||||
|
const x = this.$root.datetime(beat.time);
|
||||||
|
pingData.push({
|
||||||
|
x,
|
||||||
|
y: beat.ping,
|
||||||
});
|
});
|
||||||
}
|
downData.push({
|
||||||
|
x,
|
||||||
|
y: beat.status === DOWN ? 1 : 0,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
datasets: [
|
datasets: [
|
||||||
{
|
{
|
||||||
@ -172,5 +214,110 @@ export default {
|
|||||||
};
|
};
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
watch: {
|
||||||
|
// Update chart data when the selected chart period changes
|
||||||
|
chartPeriodHrs: function (newPeriod) {
|
||||||
|
if (newPeriod == "0") {
|
||||||
|
newPeriod = null;
|
||||||
|
this.heartbeatList = null;
|
||||||
|
} else {
|
||||||
|
this.loading = true;
|
||||||
|
|
||||||
|
this.$root.getMonitorBeats(this.monitorId, newPeriod, (res) => {
|
||||||
|
if (!res.ok) {
|
||||||
|
toast.error(res.msg);
|
||||||
|
} else {
|
||||||
|
this.heartbeatList = res.data;
|
||||||
|
}
|
||||||
|
this.loading = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
created() {
|
||||||
|
// Setup Watcher on the root heartbeatList,
|
||||||
|
// And mirror latest change to this.heartbeatList
|
||||||
|
this.$watch(() => this.$root.heartbeatList[this.monitorId],
|
||||||
|
(heartbeatList) => {
|
||||||
|
if (this.chartPeriodHrs != 0) {
|
||||||
|
const newBeat = heartbeatList.at(-1);
|
||||||
|
if (newBeat && dayjs.utc(newBeat.time) > dayjs.utc(this.heartbeatList.at(-1)?.time)) {
|
||||||
|
this.heartbeatList.push(heartbeatList.at(-1));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ deep: true }
|
||||||
|
);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
@import "../assets/vars.scss";
|
||||||
|
|
||||||
|
.form-select {
|
||||||
|
width: unset;
|
||||||
|
display: inline-flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.period-options {
|
||||||
|
padding: 0.1em 1em;
|
||||||
|
margin-bottom: -1.2em;
|
||||||
|
float: right;
|
||||||
|
position: relative;
|
||||||
|
z-index: 10;
|
||||||
|
|
||||||
|
.dropdown-menu {
|
||||||
|
padding: 0;
|
||||||
|
min-width: 50px;
|
||||||
|
font-size: 0.9em;
|
||||||
|
|
||||||
|
.dark & {
|
||||||
|
background: $dark-bg;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown-item {
|
||||||
|
border-radius: 0.3rem;
|
||||||
|
padding: 2px 16px 4px 16px;
|
||||||
|
|
||||||
|
.dark & {
|
||||||
|
background: $dark-bg;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark &:hover {
|
||||||
|
background: $dark-font-color;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark & .dropdown-item.active {
|
||||||
|
background: $primary;
|
||||||
|
color: $dark-font-color2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-period-toggle {
|
||||||
|
padding: 2px 15px;
|
||||||
|
background: transparent;
|
||||||
|
border: 0;
|
||||||
|
color: $link-color;
|
||||||
|
opacity: 0.7;
|
||||||
|
font-size: 0.9em;
|
||||||
|
|
||||||
|
&::after {
|
||||||
|
vertical-align: 0.155em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark & {
|
||||||
|
color: $dark-font-color;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.chart-wrapper {
|
||||||
|
margin-bottom: 0.5em;
|
||||||
|
|
||||||
|
&.loading {
|
||||||
|
filter: blur(10px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
@ -41,6 +41,9 @@
|
|||||||
<Uptime :monitor="monitor.element" type="24" :pill="true" />
|
<Uptime :monitor="monitor.element" type="24" :pill="true" />
|
||||||
{{ monitor.element.name }}
|
{{ monitor.element.name }}
|
||||||
</div>
|
</div>
|
||||||
|
<div class="tags">
|
||||||
|
<Tag v-for="tag in monitor.element.tags" :key="tag" :item="tag" :size="'sm'" />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div :key="$root.userHeartbeatBar" class="col-3 col-md-4">
|
<div :key="$root.userHeartbeatBar" class="col-3 col-md-4">
|
||||||
<HeartbeatBar size="small" :monitor-id="monitor.element.id" />
|
<HeartbeatBar size="small" :monitor-id="monitor.element.id" />
|
||||||
@ -59,12 +62,14 @@
|
|||||||
import Draggable from "vuedraggable";
|
import Draggable from "vuedraggable";
|
||||||
import HeartbeatBar from "./HeartbeatBar.vue";
|
import HeartbeatBar from "./HeartbeatBar.vue";
|
||||||
import Uptime from "./Uptime.vue";
|
import Uptime from "./Uptime.vue";
|
||||||
|
import Tag from "./Tag.vue";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
Draggable,
|
Draggable,
|
||||||
HeartbeatBar,
|
HeartbeatBar,
|
||||||
Uptime,
|
Uptime,
|
||||||
|
Tag,
|
||||||
},
|
},
|
||||||
props: {
|
props: {
|
||||||
editMode: {
|
editMode: {
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<span :class="className">{{ uptime }}</span>
|
<span :class="className" :title="24 + $t('-hour')">{{ uptime }}</span>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
25
src/components/notifications/AliyunSms.vue
Normal file
25
src/components/notifications/AliyunSms.vue
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
<template>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="accessKeyId" class="form-label">{{ $t("AccessKeyId") }}<span style="color: red;"><sup>*</sup></span></label>
|
||||||
|
<input id="accessKeyId" v-model="$parent.notification.accessKeyId" type="text" class="form-control" required>
|
||||||
|
|
||||||
|
<label for="secretAccessKey" class="form-label">{{ $t("SecretAccessKey") }}<span style="color: red;"><sup>*</sup></span></label>
|
||||||
|
<input id="secretAccessKey" v-model="$parent.notification.secretAccessKey" type="text" class="form-control" required>
|
||||||
|
|
||||||
|
<label for="phonenumber" class="form-label">{{ $t("Phonenumber") }}<span style="color: red;"><sup>*</sup></span></label>
|
||||||
|
<input id="phonenumber" v-model="$parent.notification.phonenumber" type="text" class="form-control" required>
|
||||||
|
|
||||||
|
<label for="templateCode" class="form-label">{{ $t("TemplateCode") }}<span style="color: red;"><sup>*</sup></span></label>
|
||||||
|
<input id="templateCode" v-model="$parent.notification.templateCode" type="text" class="form-control" required>
|
||||||
|
|
||||||
|
<label for="signName" class="form-label">{{ $t("SignName") }}<span style="color: red;"><sup>*</sup></span></label>
|
||||||
|
<input id="signName" v-model="$parent.notification.signName" type="text" class="form-control" required>
|
||||||
|
|
||||||
|
<div class="form-text">
|
||||||
|
<p>Sms template must contain parameters: <br> <code>${name} ${time} ${status} ${msg}</code></p>
|
||||||
|
<i18n-t tag="p" keypath="Read more:">
|
||||||
|
<a href="https://help.aliyun.com/document_detail/101414.html" target="_blank">https://help.aliyun.com/document_detail/101414.html</a>
|
||||||
|
</i18n-t>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
15
src/components/notifications/Bark.vue
Normal file
15
src/components/notifications/Bark.vue
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
<template>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="Bark Endpoint" class="form-label">{{ $t("Bark Endpoint") }}<span style="color: red;"><sup>*</sup></span></label>
|
||||||
|
<input id="Bark Endpoint" v-model="$parent.notification.barkEndpoint" type="text" class="form-control" required>
|
||||||
|
<div class="form-text">
|
||||||
|
<p><span style="color: red;"><sup>*</sup></span>{{ $t("Required") }}</p>
|
||||||
|
</div>
|
||||||
|
<i18n-t tag="div" keypath="wayToGetTeamsURL" class="form-text">
|
||||||
|
<a
|
||||||
|
href="https://github.com/Finb/Bark"
|
||||||
|
target="_blank"
|
||||||
|
>{{ $t("here") }}</a>
|
||||||
|
</i18n-t>
|
||||||
|
</div>
|
||||||
|
</template>
|
38
src/components/notifications/ClickSendSMS.vue
Normal file
38
src/components/notifications/ClickSendSMS.vue
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
<template>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="clicksendsms-login" class="form-label">API Username</label>
|
||||||
|
<div class="form-text">
|
||||||
|
{{ $t("apiCredentials") }}
|
||||||
|
<a href="http://dashboard.clicksend.com/account/subaccounts" target="_blank">{{ $t("here") }}</a>
|
||||||
|
</div>
|
||||||
|
<input id="clicksendsms-login" v-model="$parent.notification.clicksendsmsLogin" type="text" class="form-control" required>
|
||||||
|
<label for="clicksendsms-key" class="form-label">API Key</label>
|
||||||
|
<HiddenInput id="clicksendsms-key" v-model="$parent.notification.clicksendsmsPassword" :required="true" autocomplete="one-time-code"></HiddenInput>
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<div class="form-text">
|
||||||
|
{{ $t("checkPrice", [$t("clicksendsms")]) }}
|
||||||
|
<a href="https://www.clicksend.com/us/pricing" target="_blank">https://clicksend.com/us/pricing</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="clicksendsms-to-number" class="form-label">Recipient Number</label>
|
||||||
|
<input id="clicksendsms-to-number" v-model="$parent.notification.clicksendsmsToNumber" type="text" minlength="8" maxlength="14" class="form-control" required>
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="clicksendsms-sender-name" class="form-label">From Name/Number -
|
||||||
|
<a href="https://help.clicksend.com/article/4kgj7krx00-what-is-a-sender-id-or-sender-number" target="_blank">More Info</a>
|
||||||
|
</label>
|
||||||
|
<input id="clicksendsms-sender-name" v-model="$parent.notification.clicksendsmsSenderName" type="text" minlength="3" maxlength="11" class="form-control">
|
||||||
|
<div class="form-text">Leave blank to use a shared sender number.</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<script>
|
||||||
|
import HiddenInput from "../HiddenInput.vue";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
components: {
|
||||||
|
HiddenInput,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
16
src/components/notifications/DingDing.vue
Normal file
16
src/components/notifications/DingDing.vue
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
<template>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="WebHookUrl" class="form-label">{{ $t("WebHookUrl") }}<span style="color: red;"><sup>*</sup></span></label>
|
||||||
|
<input id="WebHookUrl" v-model="$parent.notification.webHookUrl" type="text" class="form-control" required>
|
||||||
|
|
||||||
|
<label for="secretKey" class="form-label">{{ $t("SecretKey") }}<span style="color: red;"><sup>*</sup></span></label>
|
||||||
|
<input id="secretKey" v-model="$parent.notification.secretKey" type="text" class="form-control" required>
|
||||||
|
|
||||||
|
<div class="form-text">
|
||||||
|
<p>For safety, must use secret key</p>
|
||||||
|
<i18n-t tag="p" keypath="Read more:">
|
||||||
|
<a href="https://developers.dingtalk.com/document/robots/custom-robot-access" target="_blank">https://developers.dingtalk.com/document/robots/custom-robot-access</a>
|
||||||
|
</i18n-t>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
@ -5,7 +5,7 @@
|
|||||||
<div class="form-text">
|
<div class="form-text">
|
||||||
<p><span style="color: red;"><sup>*</sup></span>{{ $t("Required") }}</p>
|
<p><span style="color: red;"><sup>*</sup></span>{{ $t("Required") }}</p>
|
||||||
</div>
|
</div>
|
||||||
<i18n-t tag="div" keypath="wayToGetTeamsURL" class="form-text">
|
<i18n-t tag="div" keypath="wayToGetTeamsURL" class="form-text">
|
||||||
<a
|
<a
|
||||||
href="https://www.feishu.cn/hc/zh-CN/articles/360024984973"
|
href="https://www.feishu.cn/hc/zh-CN/articles/360024984973"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
|
@ -1,25 +1,25 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label for="homeserver-url" class="form-label">Homeserver URL (with http(s):// and optionally port)</label><span style="color: red;"><sup>*</sup></span>
|
<label for="homeserver-url" class="form-label">{{ $t("matrixHomeserverURL") }}</label><span style="color: red;"><sup>*</sup></span>
|
||||||
<input id="homeserver-url" v-model="$parent.notification.homeserverUrl" type="text" class="form-control" :required="true">
|
<input id="homeserver-url" v-model="$parent.notification.homeserverUrl" type="text" class="form-control" :required="true">
|
||||||
</div>
|
</div>
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label for="internal-room-id" class="form-label">Internal Room Id</label><span style="color: red;"><sup>*</sup></span>
|
<label for="internal-room-id" class="form-label">{{ $t("Internal Room Id") }}</label><span style="color: red;"><sup>*</sup></span>
|
||||||
<input id="internal-room-id" v-model="$parent.notification.internalRoomId" type="text" class="form-control" required="true">
|
<input id="internal-room-id" v-model="$parent.notification.internalRoomId" type="text" class="form-control" required="true">
|
||||||
</div>
|
</div>
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label for="access-token" class="form-label">Access Token</label><span style="color: red;"><sup>*</sup></span>
|
<label for="access-token" class="form-label">{{ $t("Access Token") }}</label><span style="color: red;"><sup>*</sup></span>
|
||||||
<HiddenInput id="access-token" v-model="$parent.notification.accessToken" :required="true" autocomplete="one-time-code" :maxlength="500"></HiddenInput>
|
<HiddenInput id="access-token" v-model="$parent.notification.accessToken" :required="true" autocomplete="one-time-code" :maxlength="500"></HiddenInput>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-text">
|
<div class="form-text">
|
||||||
<span style="color: red;"><sup>*</sup></span>Required
|
<span style="color: red;"><sup>*</sup></span>{{ $t("Required") }}
|
||||||
<p style="margin-top: 8px;">
|
<p style="margin-top: 8px;">
|
||||||
You can find the internal room ID by looking in the advanced section of the room settings in your Matrix client. It should look like !QMdRCpUIfLwsfjxye6:home.server.
|
{{ $t("matrixDesc1") }}
|
||||||
</p>
|
|
||||||
<p style="margin-top: 8px;">
|
|
||||||
It is highly recommended you create a new user and do not use your own Matrix user's access token as it will allow full access to your account and all the rooms you joined. Instead, create a new user and only invite it to the room that you want to receive the notification in. You can get the access token by running <code>curl -XPOST -d '{"type": "m.login.password", "identifier": {"user": "botusername", "type": "m.id.user"}, "password": "passwordforuser"}' "https://home.server/_matrix/client/r0/login"</code>.
|
|
||||||
</p>
|
</p>
|
||||||
|
<i18n-t tag="p" keypath="matrixDesc2" style="margin-top: 8px;">
|
||||||
|
<code>curl -XPOST -d '{"type": "m.login.password", "identifier": {"user": "botusername", "type": "m.id.user"}, "password": "passwordforuser"}' "https://home.server/_matrix/client/r0/login"</code>.
|
||||||
|
</i18n-t>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@ -30,5 +30,5 @@ export default {
|
|||||||
components: {
|
components: {
|
||||||
HiddenInput,
|
HiddenInput,
|
||||||
},
|
},
|
||||||
}
|
};
|
||||||
</script>
|
</script>
|
||||||
|
@ -6,7 +6,7 @@
|
|||||||
<option value="1">Legacy Octopush-DM (endpoint: www.octopush-dm.com)</option>
|
<option value="1">Legacy Octopush-DM (endpoint: www.octopush-dm.com)</option>
|
||||||
</select>
|
</select>
|
||||||
<div class="form-text">
|
<div class="form-text">
|
||||||
Do you use the legacy version of Octopush (2011-2020) or the new version?
|
{{ $t("octopushLegacyHint") }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
|
@ -10,12 +10,13 @@
|
|||||||
<select id="promosms-type-sms" v-model="$parent.notification.promosmsSMSType" class="form-select">
|
<select id="promosms-type-sms" v-model="$parent.notification.promosmsSMSType" class="form-select">
|
||||||
<option value="0">{{ $t("promosmsTypeFlash") }}</option>
|
<option value="0">{{ $t("promosmsTypeFlash") }}</option>
|
||||||
<option value="1">{{ $t("promosmsTypeEco") }}</option>
|
<option value="1">{{ $t("promosmsTypeEco") }}</option>
|
||||||
<option value="2">{{ $t("promosmsTypeFull") }}</option>
|
<option value="3">{{ $t("promosmsTypeFull") }}</option>
|
||||||
<option value="3">{{ $t("promosmsTypeSpeed") }}</option>
|
<option value="4">{{ $t("promosmsTypeSpeed") }}</option>
|
||||||
</select>
|
</select>
|
||||||
<i18n-t tag="div" keypath="Check PromoSMS prices" class="form-text">
|
<div class="form-text">
|
||||||
|
{{ $t("checkPrice", [$t("promosms")]) }}
|
||||||
<a href="https://promosms.com/cennik/" target="_blank">https://promosms.com/cennik/</a>
|
<a href="https://promosms.com/cennik/" target="_blank">https://promosms.com/cennik/</a>
|
||||||
</i18n-t>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label for="promosms-phone-number" class="form-label">{{ $t("promosmsPhoneNumber") }}</label>
|
<label for="promosms-phone-number" class="form-label">{{ $t("promosmsPhoneNumber") }}</label>
|
||||||
@ -25,7 +26,6 @@
|
|||||||
<label for="promosms-sender-name" class="form-label">{{ $t("promosmsSMSSender") }}</label>
|
<label for="promosms-sender-name" class="form-label">{{ $t("promosmsSMSSender") }}</label>
|
||||||
<input id="promosms-sender-name" v-model="$parent.notification.promosmsSenderName" type="text" minlength="3" maxlength="11" class="form-control">
|
<input id="promosms-sender-name" v-model="$parent.notification.promosmsSenderName" type="text" minlength="3" maxlength="11" class="form-control">
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
@ -11,7 +11,7 @@
|
|||||||
<div class="form-text">
|
<div class="form-text">
|
||||||
<span style="color: red;"><sup>*</sup></span>{{ $t("Required") }}
|
<span style="color: red;"><sup>*</sup></span>{{ $t("Required") }}
|
||||||
<i18n-t tag="p" keypath="aboutWebhooks" style="margin-top: 8px;">
|
<i18n-t tag="p" keypath="aboutWebhooks" style="margin-top: 8px;">
|
||||||
<a href="https://docs.rocket.chat/guides/administration/administration/integrations" target="_blank">https://api.slack.com/messaging/webhooks</a>
|
<a href="https://docs.rocket.chat/guides/administration/administration/integrations" target="_blank">https://docs.rocket.chat/guides/administration/administration/integrations</a>
|
||||||
</i18n-t>
|
</i18n-t>
|
||||||
<p style="margin-top: 8px;">
|
<p style="margin-top: 8px;">
|
||||||
{{ $t("aboutChannelName", [$t("rocket.chat")]) }}
|
{{ $t("aboutChannelName", [$t("rocket.chat")]) }}
|
||||||
|
@ -10,7 +10,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label for="secure" class="form-label">Secure</label>
|
<label for="secure" class="form-label">{{ $t("Security") }}</label>
|
||||||
<select id="secure" v-model="$parent.notification.smtpSecure" class="form-select">
|
<select id="secure" v-model="$parent.notification.smtpSecure" class="form-select">
|
||||||
<option :value="false">{{ $t("secureOptionNone") }}</option>
|
<option :value="false">{{ $t("secureOptionNone") }}</option>
|
||||||
<option :value="true">{{ $t("secureOptionTLS") }}</option>
|
<option :value="true">{{ $t("secureOptionTLS") }}</option>
|
||||||
@ -57,6 +57,18 @@
|
|||||||
<label for="to-bcc" class="form-label">{{ $t("smtpBCC") }}</label>
|
<label for="to-bcc" class="form-label">{{ $t("smtpBCC") }}</label>
|
||||||
<input id="to-bcc" v-model="$parent.notification.smtpBCC" type="text" class="form-control" autocomplete="false" :required="!hasRecipient">
|
<input id="to-bcc" v-model="$parent.notification.smtpBCC" type="text" class="form-control" autocomplete="false" :required="!hasRecipient">
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="subject-email" class="form-label">{{ $t("emailCustomSubject") }}</label>
|
||||||
|
<input id="subject-email" v-model="$parent.notification.customSubject" type="text" class="form-control" autocomplete="false" placeholder="">
|
||||||
|
<div v-pre class="form-text">
|
||||||
|
(leave blank for default one)<br />
|
||||||
|
{{NAME}}: Service Name<br />
|
||||||
|
{{HOSTNAME_OR_URL}}: Hostname or URL<br />
|
||||||
|
{{URL}}: URL<br />
|
||||||
|
{{STATUS}}: Status<br />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
28
src/components/notifications/SerwerSMS.vue
Normal file
28
src/components/notifications/SerwerSMS.vue
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
<template>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="serwersms-username" class="form-label">{{ $t('serwersmsAPIUser') }}</label>
|
||||||
|
<input id="serwersms-username" v-model="$parent.notification.serwersmsUsername" type="text" class="form-control" required>
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="serwersms-key" class="form-label">{{ $t('serwersmsAPIPassword') }}</label>
|
||||||
|
<HiddenInput id="serwersms-key" v-model="$parent.notification.serwersmsPassword" :required="true" autocomplete="one-time-code"></HiddenInput>
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="serwersms-phone-number" class="form-label">{{ $t("serwersmsPhoneNumber") }}</label>
|
||||||
|
<input id="serwersms-phone-number" v-model="$parent.notification.serwersmsPhoneNumber" type="text" class="form-control" required>
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="serwersms-sender-name" class="form-label">{{ $t("serwersmsSenderName") }}</label>
|
||||||
|
<input id="serwersms-sender-name" v-model="$parent.notification.serwersmsSenderName" type="text" minlength="3" maxlength="11" class="form-control">
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import HiddenInput from "../HiddenInput.vue";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
components: {
|
||||||
|
HiddenInput,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
13
src/components/notifications/Stackfield.vue
Normal file
13
src/components/notifications/Stackfield.vue
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
<template>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="stackfield-webhook-url" class="form-label">{{ $t("Webhook URL") }}<span style="color: red;"><sup>*</sup></span></label>
|
||||||
|
<input id="stackfield-webhook-url" v-model="$parent.notification.stackfieldwebhookURL" type="text" class="form-control" required>
|
||||||
|
|
||||||
|
<div class="form-text">
|
||||||
|
<span style="color: red;"><sup>*</sup></span>{{ $t("Required") }}
|
||||||
|
<i18n-t tag="p" keypath="aboutWebhooks" style="margin-top: 8px;">
|
||||||
|
<a href="https://www.stackfield.com/developer-api#AnchorAPI2" target="_blank">https://www.stackfield.com/developer-api#AnchorAPI2</a>
|
||||||
|
</i18n-t>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
@ -2,9 +2,9 @@
|
|||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label for="telegram-bot-token" class="form-label">{{ $t("Bot Token") }}</label>
|
<label for="telegram-bot-token" class="form-label">{{ $t("Bot Token") }}</label>
|
||||||
<HiddenInput id="telegram-bot-token" v-model="$parent.notification.telegramBotToken" :required="true" autocomplete="one-time-code"></HiddenInput>
|
<HiddenInput id="telegram-bot-token" v-model="$parent.notification.telegramBotToken" :required="true" autocomplete="one-time-code"></HiddenInput>
|
||||||
<div class="form-text">
|
<i18n-t tag="div" keypath="wayToGetTelegramToken" class="form-text">
|
||||||
{{ $t("You can get a token from") }} <a href="https://t.me/BotFather" target="_blank">https://t.me/BotFather</a>.
|
<a href="https://t.me/BotFather" target="_blank">https://t.me/BotFather</a>
|
||||||
</div>
|
</i18n-t>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
@ -25,13 +25,7 @@
|
|||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p style="margin-top: 8px;">
|
<p style="margin-top: 8px;">
|
||||||
<template v-if="$parent.notification.telegramBotToken">
|
<a :href="telegramGetUpdatesURL('withToken')" target="_blank" style="word-break: break-word;">{{ telegramGetUpdatesURL("masked") }}</a>
|
||||||
<a :href="telegramGetUpdatesURL" target="_blank" style="word-break: break-word;">{{ telegramGetUpdatesURL }}</a>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<template v-else>
|
|
||||||
{{ telegramGetUpdatesURL }}
|
|
||||||
</template>
|
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -40,49 +34,51 @@
|
|||||||
<script>
|
<script>
|
||||||
import HiddenInput from "../HiddenInput.vue";
|
import HiddenInput from "../HiddenInput.vue";
|
||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
import { useToast } from "vue-toastification"
|
import { useToast } from "vue-toastification";
|
||||||
const toast = useToast();
|
const toast = useToast();
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
HiddenInput,
|
HiddenInput,
|
||||||
},
|
},
|
||||||
computed: {
|
methods: {
|
||||||
telegramGetUpdatesURL() {
|
telegramGetUpdatesURL(mode = "masked") {
|
||||||
let token = `<${this.$t("YOUR BOT TOKEN HERE")}>`
|
let token = `<${this.$t("YOUR BOT TOKEN HERE")}>`;
|
||||||
|
|
||||||
if (this.$parent.notification.telegramBotToken) {
|
if (this.$parent.notification.telegramBotToken) {
|
||||||
token = this.$parent.notification.telegramBotToken;
|
if (mode === "withToken") {
|
||||||
|
token = this.$parent.notification.telegramBotToken;
|
||||||
|
} else if (mode === "masked") {
|
||||||
|
token = "*".repeat(this.$parent.notification.telegramBotToken.length);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return `https://api.telegram.org/bot${token}/getUpdates`;
|
return `https://api.telegram.org/bot${token}/getUpdates`;
|
||||||
},
|
},
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
async autoGetTelegramChatID() {
|
async autoGetTelegramChatID() {
|
||||||
try {
|
try {
|
||||||
let res = await axios.get(this.telegramGetUpdatesURL)
|
let res = await axios.get(this.telegramGetUpdatesURL("withToken"));
|
||||||
|
|
||||||
if (res.data.result.length >= 1) {
|
if (res.data.result.length >= 1) {
|
||||||
let update = res.data.result[res.data.result.length - 1]
|
let update = res.data.result[res.data.result.length - 1];
|
||||||
|
|
||||||
if (update.channel_post) {
|
if (update.channel_post) {
|
||||||
this.notification.telegramChatID = update.channel_post.chat.id;
|
this.notification.telegramChatID = update.channel_post.chat.id;
|
||||||
} else if (update.message) {
|
} else if (update.message) {
|
||||||
this.notification.telegramChatID = update.message.chat.id;
|
this.notification.telegramChatID = update.message.chat.id;
|
||||||
} else {
|
} else {
|
||||||
throw new Error(this.$t("chatIDNotFound"))
|
throw new Error(this.$t("chatIDNotFound"));
|
||||||
}
|
}
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
throw new Error(this.$t("chatIDNotFound"))
|
throw new Error(this.$t("chatIDNotFound"));
|
||||||
}
|
}
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
toast.error(error.message)
|
toast.error(error.message);
|
||||||
}
|
}
|
||||||
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
</script>
|
</script>
|
||||||
|
@ -16,7 +16,7 @@
|
|||||||
</select>
|
</select>
|
||||||
|
|
||||||
<div class="form-text">
|
<div class="form-text">
|
||||||
<p>"application/json" is good for any modern http servers such as express.js</p>
|
<p>{{ $t("webhookJsonDesc", ["\"application/json\""]) }}</p>
|
||||||
<i18n-t tag="p" keypath="webhookFormDataDesc">
|
<i18n-t tag="p" keypath="webhookFormDataDesc">
|
||||||
<template #multipart>"multipart/form-data"</template>
|
<template #multipart>"multipart/form-data"</template>
|
||||||
<template #decodeFunction>
|
<template #decodeFunction>
|
||||||
|
@ -11,6 +11,7 @@ import Pushover from "./Pushover.vue";
|
|||||||
import Pushy from "./Pushy.vue";
|
import Pushy from "./Pushy.vue";
|
||||||
import Octopush from "./Octopush.vue";
|
import Octopush from "./Octopush.vue";
|
||||||
import PromoSMS from "./PromoSMS.vue";
|
import PromoSMS from "./PromoSMS.vue";
|
||||||
|
import ClickSendSMS from "./ClickSendSMS.vue";
|
||||||
import LunaSea from "./LunaSea.vue";
|
import LunaSea from "./LunaSea.vue";
|
||||||
import Feishu from "./Feishu.vue";
|
import Feishu from "./Feishu.vue";
|
||||||
import Apprise from "./Apprise.vue";
|
import Apprise from "./Apprise.vue";
|
||||||
@ -18,6 +19,11 @@ import Pushbullet from "./Pushbullet.vue";
|
|||||||
import Line from "./Line.vue";
|
import Line from "./Line.vue";
|
||||||
import Mattermost from "./Mattermost.vue";
|
import Mattermost from "./Mattermost.vue";
|
||||||
import Matrix from "./Matrix.vue";
|
import Matrix from "./Matrix.vue";
|
||||||
|
import AliyunSMS from "./AliyunSms.vue";
|
||||||
|
import DingDing from "./DingDing.vue";
|
||||||
|
import Bark from "./Bark.vue";
|
||||||
|
import SerwerSMS from "./SerwerSMS.vue";
|
||||||
|
import Stackfield from './Stackfield.vue';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Manage all notification form.
|
* Manage all notification form.
|
||||||
@ -38,13 +44,19 @@ const NotificationFormList = {
|
|||||||
"pushy": Pushy,
|
"pushy": Pushy,
|
||||||
"octopush": Octopush,
|
"octopush": Octopush,
|
||||||
"promosms": PromoSMS,
|
"promosms": PromoSMS,
|
||||||
|
"clicksendsms": ClickSendSMS,
|
||||||
"lunasea": LunaSea,
|
"lunasea": LunaSea,
|
||||||
"Feishu": Feishu,
|
"Feishu": Feishu,
|
||||||
|
"AliyunSMS": AliyunSMS,
|
||||||
"apprise": Apprise,
|
"apprise": Apprise,
|
||||||
"pushbullet": Pushbullet,
|
"pushbullet": Pushbullet,
|
||||||
"line": Line,
|
"line": Line,
|
||||||
"mattermost": Mattermost,
|
"mattermost": Mattermost,
|
||||||
"matrix": Matrix,
|
"matrix": Matrix,
|
||||||
|
"DingDing": DingDing,
|
||||||
|
"Bark": Bark,
|
||||||
|
"serwersms": SerwerSMS,
|
||||||
|
"stackfield": Stackfield,
|
||||||
}
|
}
|
||||||
|
|
||||||
export default NotificationFormList
|
export default NotificationFormList
|
||||||
|
25
src/components/settings/About.vue
Normal file
25
src/components/settings/About.vue
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
<template>
|
||||||
|
<div class="d-flex justify-content-center align-items-center">
|
||||||
|
<div class="logo d-flex flex-column justify-content-center align-items-center">
|
||||||
|
<object class="my-4" width="200" height="200" data="/icon.svg" />
|
||||||
|
<div class="fs-4 fw-bold">Uptime Kuma</div>
|
||||||
|
<div>{{ $t("Version") }}: {{ $root.info.version }}</div>
|
||||||
|
<div class="my-1 update-link"><a href="https://github.com/louislam/uptime-kuma/releases" target="_blank" rel="noopener">{{ $t("Check Update On GitHub") }}</a></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.logo {
|
||||||
|
margin: 4em 1em;
|
||||||
|
}
|
||||||
|
.update-link {
|
||||||
|
font-size: 0.9em;
|
||||||
|
}
|
||||||
|
</style>
|
143
src/components/settings/Appearance.vue
Normal file
143
src/components/settings/Appearance.vue
Normal file
@ -0,0 +1,143 @@
|
|||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<div class="my-4">
|
||||||
|
<label for="language" class="form-label">
|
||||||
|
{{ $t("Language") }}
|
||||||
|
</label>
|
||||||
|
<select id="language" v-model="$root.language" class="form-select">
|
||||||
|
<option
|
||||||
|
v-for="(lang, i) in $i18n.availableLocales"
|
||||||
|
:key="`Lang${i}`"
|
||||||
|
:value="lang"
|
||||||
|
>
|
||||||
|
{{ $i18n.messages[lang].languageName }}
|
||||||
|
</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="my-4">
|
||||||
|
<label for="timezone" class="form-label">{{ $t("Theme") }}</label>
|
||||||
|
<div>
|
||||||
|
<div
|
||||||
|
class="btn-group"
|
||||||
|
role="group"
|
||||||
|
aria-label="Basic checkbox toggle button group"
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
id="btncheck1"
|
||||||
|
v-model="$root.userTheme"
|
||||||
|
type="radio"
|
||||||
|
class="btn-check"
|
||||||
|
name="theme"
|
||||||
|
autocomplete="off"
|
||||||
|
value="light"
|
||||||
|
/>
|
||||||
|
<label class="btn btn-outline-primary" for="btncheck1">
|
||||||
|
{{ $t("Light") }}
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<input
|
||||||
|
id="btncheck2"
|
||||||
|
v-model="$root.userTheme"
|
||||||
|
type="radio"
|
||||||
|
class="btn-check"
|
||||||
|
name="theme"
|
||||||
|
autocomplete="off"
|
||||||
|
value="dark"
|
||||||
|
/>
|
||||||
|
<label class="btn btn-outline-primary" for="btncheck2">
|
||||||
|
{{ $t("Dark") }}
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<input
|
||||||
|
id="btncheck3"
|
||||||
|
v-model="$root.userTheme"
|
||||||
|
type="radio"
|
||||||
|
class="btn-check"
|
||||||
|
name="theme"
|
||||||
|
autocomplete="off"
|
||||||
|
value="auto"
|
||||||
|
/>
|
||||||
|
<label class="btn btn-outline-primary" for="btncheck3">
|
||||||
|
{{ $t("Auto") }}
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="my-4">
|
||||||
|
<label class="form-label">{{ $t("Theme - Heartbeat Bar") }}</label>
|
||||||
|
<div>
|
||||||
|
<div
|
||||||
|
class="btn-group"
|
||||||
|
role="group"
|
||||||
|
aria-label="Basic checkbox toggle button group"
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
id="btncheck4"
|
||||||
|
v-model="$root.userHeartbeatBar"
|
||||||
|
type="radio"
|
||||||
|
class="btn-check"
|
||||||
|
name="heartbeatBarTheme"
|
||||||
|
autocomplete="off"
|
||||||
|
value="normal"
|
||||||
|
/>
|
||||||
|
<label class="btn btn-outline-primary" for="btncheck4">
|
||||||
|
{{ $t("Normal") }}
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<input
|
||||||
|
id="btncheck5"
|
||||||
|
v-model="$root.userHeartbeatBar"
|
||||||
|
type="radio"
|
||||||
|
class="btn-check"
|
||||||
|
name="heartbeatBarTheme"
|
||||||
|
autocomplete="off"
|
||||||
|
value="bottom"
|
||||||
|
/>
|
||||||
|
<label class="btn btn-outline-primary" for="btncheck5">
|
||||||
|
{{ $t("Bottom") }}
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<input
|
||||||
|
id="btncheck6"
|
||||||
|
v-model="$root.userHeartbeatBar"
|
||||||
|
type="radio"
|
||||||
|
class="btn-check"
|
||||||
|
name="heartbeatBarTheme"
|
||||||
|
autocomplete="off"
|
||||||
|
value="none"
|
||||||
|
/>
|
||||||
|
<label class="btn btn-outline-primary" for="btncheck6">
|
||||||
|
{{ $t("None") }}
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
@import "../../assets/vars.scss";
|
||||||
|
|
||||||
|
.btn-check:active + .btn-outline-primary,
|
||||||
|
.btn-check:checked + .btn-outline-primary,
|
||||||
|
.btn-check:hover + .btn-outline-primary {
|
||||||
|
color: #fff;
|
||||||
|
|
||||||
|
.dark & {
|
||||||
|
color: #000;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark {
|
||||||
|
.list-group-item {
|
||||||
|
background-color: $dark-bg2;
|
||||||
|
color: $dark-font-color;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
213
src/components/settings/Backup.vue
Normal file
213
src/components/settings/Backup.vue
Normal file
@ -0,0 +1,213 @@
|
|||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<div class="my-4">
|
||||||
|
<h4 class="mt-4 mb-2">{{ $t("Export Backup") }}</h4>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
{{ $t("backupDescription") }} <br />
|
||||||
|
({{ $t("backupDescription2") }}) <br />
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div class="mb-2">
|
||||||
|
<button class="btn btn-primary" @click="downloadBackup">
|
||||||
|
{{ $t("Export") }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
<strong>{{ $t("backupDescription3") }}</strong>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="my-4">
|
||||||
|
<h4 class="mt-4 mb-2">{{ $t("Import Backup") }}</h4>
|
||||||
|
|
||||||
|
<label class="form-label">{{ $t("Options") }}:</label>
|
||||||
|
<br />
|
||||||
|
<div class="form-check form-check-inline">
|
||||||
|
<input
|
||||||
|
id="radioKeep"
|
||||||
|
v-model="importHandle"
|
||||||
|
class="form-check-input"
|
||||||
|
type="radio"
|
||||||
|
name="radioImportHandle"
|
||||||
|
value="keep"
|
||||||
|
/>
|
||||||
|
<label class="form-check-label" for="radioKeep">
|
||||||
|
{{ $t("Keep both") }}
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div class="form-check form-check-inline">
|
||||||
|
<input
|
||||||
|
id="radioSkip"
|
||||||
|
v-model="importHandle"
|
||||||
|
class="form-check-input"
|
||||||
|
type="radio"
|
||||||
|
name="radioImportHandle"
|
||||||
|
value="skip"
|
||||||
|
/>
|
||||||
|
<label class="form-check-label" for="radioSkip">
|
||||||
|
{{ $t("Skip existing") }}
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div class="form-check form-check-inline">
|
||||||
|
<input
|
||||||
|
id="radioOverwrite"
|
||||||
|
v-model="importHandle"
|
||||||
|
class="form-check-input"
|
||||||
|
type="radio"
|
||||||
|
name="radioImportHandle"
|
||||||
|
value="overwrite"
|
||||||
|
/>
|
||||||
|
<label class="form-check-label" for="radioOverwrite">
|
||||||
|
{{ $t("Overwrite") }}
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div class="form-text mb-2">
|
||||||
|
{{ $t("importHandleDescription") }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-2">
|
||||||
|
<input
|
||||||
|
id="importBackup"
|
||||||
|
type="file"
|
||||||
|
class="form-control"
|
||||||
|
accept="application/json"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="input-group mb-2 justify-content-end">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="btn btn-outline-primary"
|
||||||
|
:disabled="processing"
|
||||||
|
@click="confirmImport"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
v-if="processing"
|
||||||
|
class="spinner-border spinner-border-sm me-1"
|
||||||
|
></div>
|
||||||
|
{{ $t("Import") }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
v-if="importAlert"
|
||||||
|
class="alert alert-danger mt-3"
|
||||||
|
style="padding: 6px 16px"
|
||||||
|
>
|
||||||
|
{{ importAlert }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Confirm
|
||||||
|
ref="confirmImport"
|
||||||
|
btn-style="btn-danger"
|
||||||
|
:yes-text="$t('Yes')"
|
||||||
|
:no-text="$t('No')"
|
||||||
|
@yes="importBackup"
|
||||||
|
>
|
||||||
|
{{ $t("confirmImportMsg") }}
|
||||||
|
</Confirm>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import Confirm from "../../components/Confirm.vue";
|
||||||
|
import dayjs from "dayjs";
|
||||||
|
import { useToast } from "vue-toastification";
|
||||||
|
|
||||||
|
const toast = useToast();
|
||||||
|
|
||||||
|
export default {
|
||||||
|
components: {
|
||||||
|
Confirm,
|
||||||
|
},
|
||||||
|
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
processing: false,
|
||||||
|
importHandle: "skip",
|
||||||
|
importAlert: null,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
confirmImport() {
|
||||||
|
this.$refs.confirmImport.show();
|
||||||
|
},
|
||||||
|
|
||||||
|
downloadBackup() {
|
||||||
|
let time = dayjs().format("YYYY_MM_DD-hh_mm_ss");
|
||||||
|
let fileName = `Uptime_Kuma_Backup_${time}.json`;
|
||||||
|
let monitorList = Object.values(this.$root.monitorList);
|
||||||
|
let exportData = {
|
||||||
|
version: this.$root.info.version,
|
||||||
|
notificationList: this.$root.notificationList,
|
||||||
|
monitorList: monitorList,
|
||||||
|
};
|
||||||
|
exportData = JSON.stringify(exportData, null, 4);
|
||||||
|
let downloadItem = document.createElement("a");
|
||||||
|
downloadItem.setAttribute(
|
||||||
|
"href",
|
||||||
|
"data:application/json;charset=utf-8," +
|
||||||
|
encodeURIComponent(exportData)
|
||||||
|
);
|
||||||
|
downloadItem.setAttribute("download", fileName);
|
||||||
|
downloadItem.click();
|
||||||
|
},
|
||||||
|
|
||||||
|
importBackup() {
|
||||||
|
this.processing = true;
|
||||||
|
let uploadItem = document.getElementById("importBackup").files;
|
||||||
|
|
||||||
|
if (uploadItem.length <= 0) {
|
||||||
|
this.processing = false;
|
||||||
|
return (this.importAlert = this.$t("alertNoFile"));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (uploadItem.item(0).type !== "application/json") {
|
||||||
|
this.processing = false;
|
||||||
|
return (this.importAlert = this.$t("alertWrongFileType"));
|
||||||
|
}
|
||||||
|
|
||||||
|
let fileReader = new FileReader();
|
||||||
|
fileReader.readAsText(uploadItem.item(0));
|
||||||
|
|
||||||
|
fileReader.onload = (item) => {
|
||||||
|
this.$root.uploadBackup(
|
||||||
|
item.target.result,
|
||||||
|
this.importHandle,
|
||||||
|
(res) => {
|
||||||
|
this.processing = false;
|
||||||
|
|
||||||
|
if (res.ok) {
|
||||||
|
toast.success(res.msg);
|
||||||
|
} else {
|
||||||
|
toast.error(res.msg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
};
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
@import "../../assets/vars.scss";
|
||||||
|
|
||||||
|
.dark {
|
||||||
|
#importBackup {
|
||||||
|
&::file-selector-button {
|
||||||
|
color: $primary;
|
||||||
|
background-color: $dark-bg;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover:not(:disabled):not([readonly])::file-selector-button {
|
||||||
|
color: $dark-font-color2;
|
||||||
|
background-color: $primary;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user