mirror of
https://github.com/louislam/uptime-kuma.git
synced 2024-10-01 01:25:45 -04:00
Merge branch 'master' into Revyn112_master
# Conflicts: # server/model/monitor.js # src/languages/en.js # src/pages/EditMonitor.vue
This commit is contained in:
commit
ae31eb6ba9
@ -19,7 +19,6 @@ README.md
|
||||
.eslint*
|
||||
.stylelint*
|
||||
/.github
|
||||
package-lock.json
|
||||
yarn.lock
|
||||
app.json
|
||||
CODE_OF_CONDUCT.md
|
||||
@ -28,7 +27,8 @@ CNAME
|
||||
install.sh
|
||||
SECURITY.md
|
||||
tsconfig.json
|
||||
|
||||
.env
|
||||
/tmp
|
||||
|
||||
### .gitignore content (commented rules are duplicated)
|
||||
|
||||
|
17
.eslintrc.js
17
.eslintrc.js
@ -91,6 +91,23 @@ module.exports = {
|
||||
"rules": {
|
||||
"comma-dangle": ["error", "always-multiline"],
|
||||
}
|
||||
},
|
||||
|
||||
// Override for jest puppeteer
|
||||
{
|
||||
"files": [
|
||||
"**/*.spec.js",
|
||||
"**/*.spec.jsx"
|
||||
],
|
||||
env: {
|
||||
jest: true,
|
||||
},
|
||||
globals: {
|
||||
page: true,
|
||||
browser: true,
|
||||
context: true,
|
||||
jestPuppeteer: true,
|
||||
},
|
||||
}
|
||||
]
|
||||
};
|
||||
|
2
.github/FUNDING.yml
vendored
2
.github/FUNDING.yml
vendored
@ -1,6 +1,6 @@
|
||||
# These are supported funding model platforms
|
||||
|
||||
#github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
|
||||
github: louislam # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
|
||||
#patreon: # Replace with a single Patreon username
|
||||
open_collective: uptime-kuma # Replace with a single Open Collective username
|
||||
#ko_fi: # Replace with a single Ko-fi username
|
||||
|
3
.github/ISSUE_TEMPLATE/ask-for-help.md
vendored
3
.github/ISSUE_TEMPLATE/ask-for-help.md
vendored
@ -9,6 +9,9 @@ assignees: ''
|
||||
**Is it a duplicate question?**
|
||||
Please search in Issues without filters: https://github.com/louislam/uptime-kuma/issues?q=
|
||||
|
||||
**Describe your problem**
|
||||
Please describe what you are asking for
|
||||
|
||||
**Info**
|
||||
Uptime Kuma Version:
|
||||
Using Docker?: Yes/No
|
||||
|
35
.github/workflows/auto-test.yml
vendored
Normal file
35
.github/workflows/auto-test.yml
vendored
Normal file
@ -0,0 +1,35 @@
|
||||
# This workflow will do a clean install of node dependencies, cache/restore them, build the source code and run tests across different versions of node
|
||||
# For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions
|
||||
|
||||
name: Auto Test
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ master ]
|
||||
pull_request:
|
||||
branches: [ master ]
|
||||
|
||||
jobs:
|
||||
auto-test:
|
||||
runs-on: ${{ matrix.os }}
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
os: [macos-latest, ubuntu-latest, windows-latest]
|
||||
node-version: [14.x, 16.x]
|
||||
# See supported Node.js release schedule at https://nodejs.org/en/about/releases/
|
||||
|
||||
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 run install-legacy
|
||||
- run: npm run build
|
||||
- run: npm test
|
||||
env:
|
||||
HEADLESS_TEST: 1
|
||||
JUST_FOR_TEST: ${{ secrets.JUST_FOR_TEST }}
|
2
.gitignore
vendored
2
.gitignore
vendored
@ -11,3 +11,5 @@ dist-ssr
|
||||
|
||||
/private
|
||||
/out
|
||||
/tmp
|
||||
.env
|
||||
|
136
CONTRIBUTING.md
136
CONTRIBUTING.md
@ -4,54 +4,73 @@ First of all, thank you everyone who made pull requests for Uptime Kuma, I never
|
||||
|
||||
The project was created with vite.js (vue3). Then I created a sub-directory called "server" for server part. Both frontend and backend share the same package.json.
|
||||
|
||||
The frontend code build into "dist" directory. The server uses "dist" as root. This is how production is working.
|
||||
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.
|
||||
|
||||
# Can I create a pull request for Uptime Kuma?
|
||||
## Key Technical Skills
|
||||
|
||||
Generally, if the pull request is working fine and it do not affect any existing logic, workflow and perfomance, I will merge to the master branch once it is tested.
|
||||
- Node.js (You should know what are promise, async/await and arrow function etc.)
|
||||
- Socket.io
|
||||
- SCSS
|
||||
- Vue.js
|
||||
- Bootstrap
|
||||
- SQLite
|
||||
|
||||
## Directories
|
||||
|
||||
- data (App data)
|
||||
- dist (Frontend build)
|
||||
- extra (Extra useful scripts)
|
||||
- public (Frontend resources for dev only)
|
||||
- server (Server source code)
|
||||
- src (Frontend source code)
|
||||
- test (unit test)
|
||||
|
||||
## 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.
|
||||
|
||||
If you are not sure, feel free to create an empty pull request draft first.
|
||||
|
||||
## Pull Request Examples
|
||||
### Pull Request Examples
|
||||
|
||||
### ✅ High - Medium Priority
|
||||
#### ✅ High - Medium Priority
|
||||
|
||||
- Add a new notification
|
||||
- Add a chart
|
||||
- Fix a bug
|
||||
- Translations
|
||||
|
||||
### *️⃣ Requires one more reviewer
|
||||
#### *️⃣ Requires one more reviewer
|
||||
|
||||
I do not have such knowledge to test it.
|
||||
|
||||
- Add k8s supports
|
||||
|
||||
### *️⃣ Low Priority
|
||||
#### *️⃣ Low Priority
|
||||
|
||||
It changed my current workflow and require further studies.
|
||||
|
||||
- Change my release approach
|
||||
|
||||
### ❌ Won't Merge
|
||||
#### ❌ Won't Merge
|
||||
|
||||
- Duplicated pull request
|
||||
- Buggy
|
||||
- Existing logic is completely modified or deleted
|
||||
- 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.
|
||||
|
||||
For example, recently, because I am not a python expert, I spent a 2 hours to resolve all problems in order to install and use the Apprise cli. Apprise requires so many hidden requirements, I have to figure out myself how to solve the problems by Google search for my OS. That is painful. I do not want Uptime Kuma to be like this way, so:
|
||||
|
||||
- Easy to install for non-Docker users, no native build dependency is needed (at least for x86_64), no extra config, no extra effort to get it run
|
||||
- Single container for Docker users, no very complex docker-composer file. Just map the volume and expose the port, then good to go
|
||||
- All settings in frontend.
|
||||
- Single container for Docker users, no very complex docker-compose file. Just map the volume and expose the port, then good to go
|
||||
- Settings should be configurable in the frontend. Env var is not encouraged.
|
||||
- Easy to use
|
||||
|
||||
# Coding Styles
|
||||
## Coding Styles
|
||||
|
||||
- 4 spaces indentation
|
||||
- Follow `.editorconfig`
|
||||
- Follow ESLint
|
||||
|
||||
@ -61,26 +80,20 @@ For example, recently, because I am not a python expert, I spent a 2 hours to re
|
||||
- SQLite: underscore_type
|
||||
- CSS/SCSS: dash-type
|
||||
|
||||
# Tools
|
||||
## Tools
|
||||
|
||||
- Node.js >= 14
|
||||
- Git
|
||||
- IDE that supports EditorConfig and ESLint (I am using Intellji Idea)
|
||||
- A SQLite tool (I am using SQLite Expert Personal)
|
||||
- IDE that supports ESLint and EditorConfig (I am using Intellji Idea)
|
||||
- A SQLite tool (SQLite Expert Personal is suggested)
|
||||
|
||||
# Install dependencies
|
||||
## Install dependencies
|
||||
|
||||
```bash
|
||||
npm install --dev
|
||||
npm ci
|
||||
```
|
||||
|
||||
For npm@7, you need --legacy-peer-deps
|
||||
|
||||
```bash
|
||||
npm install --legacy-peer-deps --dev
|
||||
```
|
||||
|
||||
# Backend Dev
|
||||
## How to start the Backend Dev Server
|
||||
|
||||
(2021-09-23 Update)
|
||||
|
||||
@ -90,41 +103,39 @@ npm run start-server-dev
|
||||
|
||||
It binds to `0.0.0.0:3001` by default.
|
||||
|
||||
## Backend Details
|
||||
### Backend Details
|
||||
|
||||
It is mainly a socket.io app + express.js.
|
||||
|
||||
express.js is just used for serving the frontend built files (index.html, .js and .css etc.)
|
||||
|
||||
# Frontend Dev
|
||||
- model/ (Object model, auto mapping to the database table name)
|
||||
- modules/ (Modified 3rd-party modules)
|
||||
- notification-providers/ (indivdual notification logic)
|
||||
- routers/ (Express Routers)
|
||||
- scoket-handler (Socket.io Handlers)
|
||||
- server.js (Server main logic)
|
||||
|
||||
Start frontend dev server. Hot-reload enabled in this way. It binds to `0.0.0.0:3000` by default.
|
||||
## How to start the Frontend Dev Server
|
||||
|
||||
```bash
|
||||
npm run dev
|
||||
```
|
||||
1. Set the env var `NODE_ENV` to "development".
|
||||
2. Start the frontend dev server by the following command.
|
||||
|
||||
PS: You can ignore those scss warnings, those warnings are from Bootstrap that I cannot fix.
|
||||
```bash
|
||||
npm run dev
|
||||
```
|
||||
|
||||
It binds to `0.0.0.0:3000` by default.
|
||||
|
||||
You can use Vue.js devtools Chrome extension for debugging.
|
||||
|
||||
After the frontend server started. It cannot connect to the websocket server even you have started the server. You need to tell the frontend that is a dev env by running this in DevTool console and refresh:
|
||||
|
||||
```javascript
|
||||
localStorage.dev = "dev";
|
||||
```
|
||||
|
||||
So that the frontend will try to connect websocket server in 3001.
|
||||
|
||||
Alternately, you can specific `NODE_ENV` to "development".
|
||||
|
||||
## Build the frontend
|
||||
### Build the frontend
|
||||
|
||||
```bash
|
||||
npm run build
|
||||
```
|
||||
|
||||
## Frontend Details
|
||||
### Frontend Details
|
||||
|
||||
Uptime Kuma Frontend is a single page application (SPA). Most paths are handled by Vue Router.
|
||||
|
||||
@ -134,11 +145,36 @@ 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`.
|
||||
|
||||
# Database Migration
|
||||
## Database Migration
|
||||
|
||||
1. Create `patch{num}.sql` in `./db/`
|
||||
2. Update `latestVersion` in `./server/database.js`
|
||||
1. Create `patch-{name}.sql` in `./db/`
|
||||
2. Add your patch filename in the `patchList` list in `./server/database.js`
|
||||
|
||||
# Unit Test
|
||||
## Unit Test
|
||||
|
||||
Yes, no unit test for now. I know it is very important, but at the same time my spare time is very limited. I want to implement my ideas first. I will go back to this in some points.
|
||||
It is an end-to-end testing. It is using Jest and Puppeteer.
|
||||
|
||||
```bash
|
||||
npm run build
|
||||
npm test
|
||||
```
|
||||
|
||||
By default, the Chromium window will be shown up during the test. Specifying `HEADLESS_TEST=1` for terminal environments.
|
||||
|
||||
## Update Dependencies
|
||||
|
||||
Install `ncu`
|
||||
https://github.com/raineorshine/npm-check-updates
|
||||
|
||||
```bash
|
||||
ncu -u -t patch
|
||||
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.
|
||||
|
||||
Patch release = the third digit ([Semantic Versioning](https://semver.org/))
|
||||
|
||||
## Translations
|
||||
|
||||
Please read: https://github.com/louislam/uptime-kuma/tree/master/src/languages
|
||||
|
33
README.md
33
README.md
@ -1,6 +1,6 @@
|
||||
# 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://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>
|
||||
|
||||
<div align="center" width="100%">
|
||||
<img src="./public/icon.svg" width="128" alt="" />
|
||||
@ -8,7 +8,7 @@
|
||||
|
||||
It is a self-hosted monitoring tool like "Uptime Robot".
|
||||
|
||||
<img src="https://louislam.net/uptimekuma/1.jpg" width="512" alt="" />
|
||||
<img src="https://uptime.kuma.pet/img/dark.jpg" width="700" alt="" />
|
||||
|
||||
## 🥔 Live Demo
|
||||
|
||||
@ -16,17 +16,20 @@ Try it!
|
||||
|
||||
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 at 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!
|
||||
|
||||
## ⭐ Features
|
||||
|
||||
* Monitoring uptime for HTTP(s) / TCP / Ping / DNS Record.
|
||||
* Monitoring uptime for HTTP(s) / TCP / Ping / DNS Record / Push.
|
||||
* 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/issues/284).
|
||||
* 20 seconds interval.
|
||||
* 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 second intervals.
|
||||
* [Multi Languages](https://github.com/louislam/uptime-kuma/tree/master/src/languages)
|
||||
* Simple Status Page
|
||||
* Ping Chart
|
||||
* Certificate Info
|
||||
|
||||
## 🔧 How to Install
|
||||
|
||||
@ -37,7 +40,7 @@ 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
|
||||
```
|
||||
|
||||
Browse to http://localhost:3001 after started.
|
||||
Browse to http://localhost:3001 after starting.
|
||||
|
||||
### 💪🏻 Without Docker
|
||||
|
||||
@ -55,11 +58,11 @@ npm run setup
|
||||
node server/server.js
|
||||
|
||||
# (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
|
||||
```
|
||||
|
||||
Browse to http://localhost:3001 after started.
|
||||
Browse to http://localhost:3001 after starting.
|
||||
|
||||
### Advanced Installation
|
||||
|
||||
@ -85,9 +88,13 @@ https://github.com/louislam/uptime-kuma/projects/1
|
||||
|
||||
## 🖼 More Screenshots
|
||||
|
||||
Dark Mode:
|
||||
Light Mode:
|
||||
|
||||
<img src="https://user-images.githubusercontent.com/1336778/128710166-908f8d88-9256-43f3-9c49-bfc2c56011d2.png" width="400" alt="" />
|
||||
<img src="https://uptime.kuma.pet/img/light.jpg" width="512" alt="" />
|
||||
|
||||
Status Page:
|
||||
|
||||
<img src="https://user-images.githubusercontent.com/1336778/134628766-a3fe0981-0926-4285-ab46-891a21c3e4cb.png" width="512" alt="" />
|
||||
|
||||
Settings Page:
|
||||
|
||||
@ -111,11 +118,13 @@ If you love this project, please consider giving me a ⭐.
|
||||
## 🗣️ Discussion
|
||||
|
||||
### Issues Page
|
||||
|
||||
You can discuss or ask for help in [Issues](https://github.com/louislam/uptime-kuma/issues).
|
||||
|
||||
### Subreddit
|
||||
|
||||
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/
|
||||
|
||||
## Contribute
|
||||
|
18
SECURITY.md
18
SECURITY.md
@ -5,11 +5,27 @@
|
||||
Use this section to tell people about which versions of your project are
|
||||
currently being supported with security updates.
|
||||
|
||||
### Uptime Kuma Versions
|
||||
|
||||
| Version | Supported |
|
||||
| ------- | ------------------ |
|
||||
| 1.x.x | :white_check_mark: |
|
||||
| 1.8.X | :white_check_mark: |
|
||||
| <= 1.7.X | ❌ |
|
||||
|
||||
### Upgradable Docker Tags
|
||||
|
||||
| Tag | Supported |
|
||||
| ------- | ------------------ |
|
||||
| 1 | :white_check_mark: |
|
||||
| 1-debian | :white_check_mark: |
|
||||
| 1-alpine | :white_check_mark: |
|
||||
| latest | :white_check_mark: |
|
||||
| debian | :white_check_mark: |
|
||||
| alpine | :white_check_mark: |
|
||||
| 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.
|
||||
|
11
babel.config.js
Normal file
11
babel.config.js
Normal file
@ -0,0 +1,11 @@
|
||||
const config = {};
|
||||
|
||||
if (process.env.TEST_FRONTEND) {
|
||||
config.presets = ["@babel/preset-env"];
|
||||
}
|
||||
|
||||
if (process.env.TEST_BACKEND) {
|
||||
config.plugins = ["babel-plugin-rewire"];
|
||||
}
|
||||
|
||||
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",
|
||||
};
|
||||
|
5
config/jest-frontend.config.js
Normal file
5
config/jest-frontend.config.js
Normal file
@ -0,0 +1,5 @@
|
||||
module.exports = {
|
||||
"rootDir": "..",
|
||||
"testRegex": "./test/frontend.spec.js",
|
||||
};
|
||||
|
6
config/jest-puppeteer.config.js
Normal file
6
config/jest-puppeteer.config.js
Normal file
@ -0,0 +1,6 @@
|
||||
module.exports = {
|
||||
"launch": {
|
||||
"headless": process.env.HEADLESS_TEST || false,
|
||||
"userDataDir": "./data/test-chrome-profile",
|
||||
}
|
||||
};
|
11
config/jest.config.js
Normal file
11
config/jest.config.js
Normal file
@ -0,0 +1,11 @@
|
||||
module.exports = {
|
||||
"verbose": true,
|
||||
"preset": "jest-puppeteer",
|
||||
"globals": {
|
||||
"__DEV__": true
|
||||
},
|
||||
"testRegex": "./test/e2e.spec.js",
|
||||
"rootDir": "..",
|
||||
"testTimeout": 30000,
|
||||
};
|
||||
|
24
config/vite.config.js
Normal file
24
config/vite.config.js
Normal file
@ -0,0 +1,24 @@
|
||||
import legacy from "@vitejs/plugin-legacy";
|
||||
import vue from "@vitejs/plugin-vue";
|
||||
import { defineConfig } from "vite";
|
||||
|
||||
const postCssScss = require("postcss-scss");
|
||||
const postcssRTLCSS = require("postcss-rtlcss");
|
||||
|
||||
// https://vitejs.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [
|
||||
vue(),
|
||||
legacy({
|
||||
targets: ["ie > 11"],
|
||||
additionalLegacyPolyfills: ["regenerator-runtime/runtime"]
|
||||
})
|
||||
],
|
||||
css: {
|
||||
postcss: {
|
||||
"parser": postCssScss,
|
||||
"map": false,
|
||||
"plugins": [postcssRTLCSS]
|
||||
}
|
||||
},
|
||||
});
|
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;
|
7
db/patch-monitor-push_token.sql
Normal file
7
db/patch-monitor-push_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 monitor
|
||||
ADD push_token VARCHAR(20) DEFAULT NULL;
|
||||
|
||||
COMMIT;
|
8
docker/alpine-base.dockerfile
Normal file
8
docker/alpine-base.dockerfile
Normal file
@ -0,0 +1,8 @@
|
||||
# DON'T UPDATE TO alpine3.13, 1.14, see #41.
|
||||
FROM node:14-alpine3.12
|
||||
WORKDIR /app
|
||||
|
||||
# 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 && \
|
||||
pip3 --no-cache-dir install apprise && \
|
||||
rm -rf /root/.cache
|
12
docker/debian-base.dockerfile
Normal file
12
docker/debian-base.dockerfile
Normal file
@ -0,0 +1,12 @@
|
||||
# DON'T UPDATE TO node:14-bullseye-slim, see #372.
|
||||
# If the image changed, the second stage image should be changed too
|
||||
FROM node:14-buster-slim
|
||||
WORKDIR /app
|
||||
|
||||
# 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!
|
||||
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 \
|
||||
sqlite3 iputils-ping util-linux dumb-init && \
|
||||
pip3 --no-cache-dir install apprise && \
|
||||
rm -rf /var/lib/apt/lists/*
|
51
docker/dockerfile
Normal file
51
docker/dockerfile
Normal file
@ -0,0 +1,51 @@
|
||||
FROM louislam/uptime-kuma:base-debian AS build
|
||||
WORKDIR /app
|
||||
|
||||
ENV PUPPETEER_SKIP_CHROMIUM_DOWNLOAD=1
|
||||
|
||||
COPY . .
|
||||
RUN npm ci && \
|
||||
npm run build && \
|
||||
npm ci --production && \
|
||||
chmod +x /app/extra/entrypoint.sh
|
||||
|
||||
|
||||
FROM louislam/uptime-kuma:base-debian AS release
|
||||
WORKDIR /app
|
||||
|
||||
# Copy app files from build layer
|
||||
COPY --from=build /app /app
|
||||
|
||||
EXPOSE 3001
|
||||
VOLUME ["/app/data"]
|
||||
HEALTHCHECK --interval=60s --timeout=30s --start-period=180s --retries=5 CMD node extra/healthcheck.js
|
||||
ENTRYPOINT ["/usr/bin/dumb-init", "--", "extra/entrypoint.sh"]
|
||||
CMD ["node", "server/server.js"]
|
||||
|
||||
FROM release AS nightly
|
||||
RUN npm run mark-as-nightly
|
||||
|
||||
# Upload the artifact to Github
|
||||
FROM louislam/uptime-kuma:base-debian AS upload-artifact
|
||||
WORKDIR /
|
||||
RUN apt update && \
|
||||
apt --yes install curl file
|
||||
|
||||
ARG GITHUB_TOKEN
|
||||
ARG TARGETARCH
|
||||
ARG PLATFORM=debian
|
||||
ARG VERSION
|
||||
ARG FILE=$PLATFORM-$TARGETARCH-$VERSION.tar.gz
|
||||
ARG DIST=dist.tar.gz
|
||||
|
||||
COPY --from=build /app /app
|
||||
RUN chmod +x /app/extra/upload-github-release-asset.sh
|
||||
|
||||
# Full Build
|
||||
# RUN tar -zcvf $FILE app
|
||||
# RUN /app/extra/upload-github-release-asset.sh github_api_token=$GITHUB_TOKEN owner=louislam repo=uptime-kuma tag=$VERSION filename=$FILE
|
||||
|
||||
# Dist only
|
||||
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=/app/$DIST
|
||||
|
26
docker/dockerfile-alpine
Normal file
26
docker/dockerfile-alpine
Normal file
@ -0,0 +1,26 @@
|
||||
FROM louislam/uptime-kuma:base-alpine AS build
|
||||
WORKDIR /app
|
||||
|
||||
ENV PUPPETEER_SKIP_CHROMIUM_DOWNLOAD=1
|
||||
|
||||
COPY . .
|
||||
RUN npm ci && \
|
||||
npm run build && \
|
||||
npm ci --production && \
|
||||
chmod +x /app/extra/entrypoint.sh
|
||||
|
||||
|
||||
FROM louislam/uptime-kuma:base-alpine AS release
|
||||
WORKDIR /app
|
||||
|
||||
# Copy app files from build layer
|
||||
COPY --from=build /app /app
|
||||
|
||||
EXPOSE 3001
|
||||
VOLUME ["/app/data"]
|
||||
HEALTHCHECK --interval=60s --timeout=30s --start-period=180s --retries=5 CMD node extra/healthcheck.js
|
||||
ENTRYPOINT ["/usr/bin/dumb-init", "--", "extra/entrypoint.sh"]
|
||||
CMD ["node", "server/server.js"]
|
||||
|
||||
FROM release AS nightly
|
||||
RUN npm run mark-as-nightly
|
33
dockerfile
33
dockerfile
@ -1,33 +0,0 @@
|
||||
# DON'T UPDATE TO node:14-bullseye-slim, see #372.
|
||||
# If the image changed, the second stage image should be changed too
|
||||
FROM node:14-buster-slim AS build
|
||||
WORKDIR /app
|
||||
|
||||
COPY . .
|
||||
RUN npm install --legacy-peer-deps && \
|
||||
npm run build && \
|
||||
npm prune --production && \
|
||||
chmod +x /app/extra/entrypoint.sh
|
||||
|
||||
|
||||
FROM node:14-buster-slim AS release
|
||||
WORKDIR /app
|
||||
|
||||
# Install Apprise, add sqlite3 cli for debugging in the future, iputils-ping for ping, util-linux for setpriv
|
||||
RUN apt update && \
|
||||
apt --yes install python3 python3-pip python3-cryptography python3-six python3-yaml python3-click python3-markdown python3-requests python3-requests-oauthlib \
|
||||
sqlite3 iputils-ping util-linux && \
|
||||
pip3 --no-cache-dir install apprise && \
|
||||
rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Copy app files from build layer
|
||||
COPY --from=build /app /app
|
||||
|
||||
EXPOSE 3001
|
||||
VOLUME ["/app/data"]
|
||||
HEALTHCHECK --interval=60s --timeout=30s --start-period=180s --retries=5 CMD node extra/healthcheck.js
|
||||
ENTRYPOINT ["extra/entrypoint.sh"]
|
||||
CMD ["node", "server/server.js"]
|
||||
|
||||
FROM release AS nightly
|
||||
RUN npm run mark-as-nightly
|
@ -1,30 +0,0 @@
|
||||
# DON'T UPDATE TO alpine3.13, 1.14, see #41.
|
||||
FROM node:14-alpine3.12 AS build
|
||||
WORKDIR /app
|
||||
|
||||
COPY . .
|
||||
RUN npm install --legacy-peer-deps && \
|
||||
npm run build && \
|
||||
npm prune --production && \
|
||||
chmod +x /app/extra/entrypoint.sh
|
||||
|
||||
|
||||
FROM node:14-alpine3.12 AS release
|
||||
WORKDIR /app
|
||||
|
||||
# Install apprise, iputils for non-root ping, setpriv
|
||||
RUN apk add --no-cache iputils setpriv python3 py3-cryptography py3-pip py3-six py3-yaml py3-click py3-markdown py3-requests py3-requests-oauthlib && \
|
||||
pip3 --no-cache-dir install apprise && \
|
||||
rm -rf /root/.cache
|
||||
|
||||
# Copy app files from build layer
|
||||
COPY --from=build /app /app
|
||||
|
||||
EXPOSE 3001
|
||||
VOLUME ["/app/data"]
|
||||
HEALTHCHECK --interval=60s --timeout=30s --start-period=180s --retries=5 CMD node extra/healthcheck.js
|
||||
ENTRYPOINT ["extra/entrypoint.sh"]
|
||||
CMD ["node", "server/server.js"]
|
||||
|
||||
FROM release AS nightly
|
||||
RUN npm run mark-as-nightly
|
6
ecosystem.config.js
Normal file
6
ecosystem.config.js
Normal file
@ -0,0 +1,6 @@
|
||||
module.exports = {
|
||||
apps: [{
|
||||
name: "uptime-kuma",
|
||||
script: "./server/server.js",
|
||||
}]
|
||||
}
|
57
extra/download-dist.js
Normal file
57
extra/download-dist.js
Normal file
@ -0,0 +1,57 @@
|
||||
console.log("Downloading dist");
|
||||
const https = require("https");
|
||||
const tar = require("tar");
|
||||
|
||||
const packageJSON = require("../package.json");
|
||||
const fs = require("fs");
|
||||
const version = packageJSON.version;
|
||||
|
||||
const filename = "dist.tar.gz";
|
||||
|
||||
const url = `https://github.com/louislam/uptime-kuma/releases/download/${version}/${filename}`;
|
||||
download(url);
|
||||
|
||||
function download(url) {
|
||||
console.log(url);
|
||||
|
||||
https.get(url, (response) => {
|
||||
if (response.statusCode === 200) {
|
||||
console.log("Extracting dist...");
|
||||
|
||||
if (fs.existsSync("./dist")) {
|
||||
|
||||
if (fs.existsSync("./dist-backup")) {
|
||||
fs.rmdirSync("./dist-backup", {
|
||||
recursive: true
|
||||
});
|
||||
}
|
||||
|
||||
fs.renameSync("./dist", "./dist-backup");
|
||||
}
|
||||
|
||||
const tarStream = tar.x({
|
||||
cwd: "./",
|
||||
});
|
||||
|
||||
tarStream.on("close", () => {
|
||||
fs.rmdirSync("./dist-backup", {
|
||||
recursive: true
|
||||
});
|
||||
console.log("Done");
|
||||
});
|
||||
|
||||
tarStream.on("error", () => {
|
||||
if (fs.existsSync("./dist-backup")) {
|
||||
fs.renameSync("./dist-backup", "./dist");
|
||||
}
|
||||
console.log("Done");
|
||||
});
|
||||
|
||||
response.pipe(tarStream);
|
||||
} else if (response.statusCode === 302) {
|
||||
download(response.headers.location);
|
||||
} else {
|
||||
console.log("dist not found");
|
||||
}
|
||||
});
|
||||
}
|
@ -2,8 +2,8 @@
|
||||
|
||||
# set -e Exit the script if an error happens
|
||||
set -e
|
||||
PUID=${PUID=1000}
|
||||
PGID=${PGID=1000}
|
||||
PUID=${PUID=0}
|
||||
PGID=${PGID=0}
|
||||
|
||||
files_ownership () {
|
||||
# -h Changes the ownership of an encountered symbolic link and not that of the file or directory pointed to by the symbolic link.
|
||||
|
@ -12,50 +12,59 @@ const rl = readline.createInterface({
|
||||
output: process.stdout
|
||||
});
|
||||
|
||||
(async () => {
|
||||
const main = async () => {
|
||||
Database.init(args);
|
||||
await Database.connect();
|
||||
|
||||
try {
|
||||
const user = await R.findOne("user");
|
||||
|
||||
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.");
|
||||
// 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("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) {
|
||||
console.error("Error: " + e.message);
|
||||
}
|
||||
|
||||
await Database.close();
|
||||
rl.close();
|
||||
|
||||
console.log("Finished. You should restart the Uptime Kuma server.")
|
||||
})();
|
||||
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,
|
||||
};
|
||||
|
@ -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";
|
||||
console.log("Base Lang: " + baseLangCode);
|
||||
fs.rmdirSync("./languages", { recursive: true });
|
||||
if (fs.existsSync("./languages")) {
|
||||
fs.rmdirSync("./languages", { recursive: true });
|
||||
}
|
||||
copyRecursiveSync("../../src/languages", "./languages");
|
||||
|
||||
const en = (await import("./languages/en.js")).default;
|
||||
@ -39,7 +41,7 @@ console.log("Files:", files);
|
||||
|
||||
for (const file of files) {
|
||||
if (!file.endsWith(".js")) {
|
||||
console.log("Skipping " + file)
|
||||
console.log("Skipping " + file);
|
||||
continue;
|
||||
}
|
||||
|
||||
|
@ -19,6 +19,7 @@ if (! newVersion) {
|
||||
const exists = tagExists(newVersion);
|
||||
|
||||
if (! exists) {
|
||||
|
||||
// Process package.json
|
||||
pkg.version = newVersion;
|
||||
pkg.scripts.setup = pkg.scripts.setup.replaceAll(oldVersion, newVersion);
|
||||
@ -29,8 +30,11 @@ if (! exists) {
|
||||
|
||||
commit(newVersion);
|
||||
tag(newVersion);
|
||||
|
||||
updateWiki(oldVersion, newVersion);
|
||||
|
||||
} else {
|
||||
console.log("version exists")
|
||||
console.log("version exists");
|
||||
}
|
||||
|
||||
function commit(version) {
|
||||
@ -38,16 +42,16 @@ function commit(version) {
|
||||
|
||||
let res = child_process.spawnSync("git", ["commit", "-m", msg, "-a"]);
|
||||
let stdout = res.stdout.toString().trim();
|
||||
console.log(stdout)
|
||||
console.log(stdout);
|
||||
|
||||
if (stdout.includes("no changes added to commit")) {
|
||||
throw new Error("commit error")
|
||||
throw new Error("commit error");
|
||||
}
|
||||
}
|
||||
|
||||
function tag(version) {
|
||||
let res = child_process.spawnSync("git", ["tag", version]);
|
||||
console.log(res.stdout.toString().trim())
|
||||
console.log(res.stdout.toString().trim());
|
||||
}
|
||||
|
||||
function tagExists(version) {
|
||||
@ -59,3 +63,38 @@ function tagExists(version) {
|
||||
|
||||
return res.stdout.toString().trim() === version;
|
||||
}
|
||||
|
||||
function updateWiki(oldVersion, newVersion) {
|
||||
const wikiDir = "./tmp/wiki";
|
||||
const howToUpdateFilename = "./tmp/wiki/🆙-How-to-Update.md";
|
||||
|
||||
safeDelete(wikiDir);
|
||||
|
||||
child_process.spawnSync("git", ["clone", "https://github.com/louislam/uptime-kuma.wiki.git", wikiDir]);
|
||||
let content = fs.readFileSync(howToUpdateFilename).toString();
|
||||
content = content.replaceAll(`git checkout ${oldVersion}`, `git checkout ${newVersion}`);
|
||||
fs.writeFileSync(howToUpdateFilename, content);
|
||||
|
||||
child_process.spawnSync("git", ["add", "-A"], {
|
||||
cwd: wikiDir,
|
||||
});
|
||||
|
||||
child_process.spawnSync("git", ["commit", "-m", `Update to ${newVersion} from ${oldVersion}`], {
|
||||
cwd: wikiDir,
|
||||
});
|
||||
|
||||
console.log("Pushing to Github");
|
||||
child_process.spawnSync("git", ["push"], {
|
||||
cwd: wikiDir,
|
||||
});
|
||||
|
||||
safeDelete(wikiDir);
|
||||
}
|
||||
|
||||
function safeDelete(dir) {
|
||||
if (fs.existsSync(dir)) {
|
||||
fs.rmdirSync(dir, {
|
||||
recursive: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
64
extra/upload-github-release-asset.sh
Normal file
64
extra/upload-github-release-asset.sh
Normal file
@ -0,0 +1,64 @@
|
||||
#!/usr/bin/env bash
|
||||
#
|
||||
# Author: Stefan Buck
|
||||
# License: MIT
|
||||
# https://gist.github.com/stefanbuck/ce788fee19ab6eb0b4447a85fc99f447
|
||||
#
|
||||
#
|
||||
# This script accepts the following parameters:
|
||||
#
|
||||
# * owner
|
||||
# * repo
|
||||
# * tag
|
||||
# * filename
|
||||
# * github_api_token
|
||||
#
|
||||
# Script to upload a release asset using the GitHub API v3.
|
||||
#
|
||||
# Example:
|
||||
#
|
||||
# upload-github-release-asset.sh github_api_token=TOKEN owner=stefanbuck repo=playground tag=v0.1.0 filename=./build.zip
|
||||
#
|
||||
|
||||
# Check dependencies.
|
||||
set -e
|
||||
xargs=$(which gxargs || which xargs)
|
||||
|
||||
# Validate settings.
|
||||
[ "$TRACE" ] && set -x
|
||||
|
||||
CONFIG=$@
|
||||
|
||||
for line in $CONFIG; do
|
||||
eval "$line"
|
||||
done
|
||||
|
||||
# Define variables.
|
||||
GH_API="https://api.github.com"
|
||||
GH_REPO="$GH_API/repos/$owner/$repo"
|
||||
GH_TAGS="$GH_REPO/releases/tags/$tag"
|
||||
AUTH="Authorization: token $github_api_token"
|
||||
WGET_ARGS="--content-disposition --auth-no-challenge --no-cookie"
|
||||
CURL_ARGS="-LJO#"
|
||||
|
||||
if [[ "$tag" == 'LATEST' ]]; then
|
||||
GH_TAGS="$GH_REPO/releases/latest"
|
||||
fi
|
||||
|
||||
# Validate token.
|
||||
curl -o /dev/null -sH "$AUTH" $GH_REPO || { echo "Error: Invalid repo, token or network issue!"; exit 1; }
|
||||
|
||||
# Read asset tags.
|
||||
response=$(curl -sH "$AUTH" $GH_TAGS)
|
||||
|
||||
# Get ID of the asset based on given filename.
|
||||
eval $(echo "$response" | grep -m 1 "id.:" | grep -w id | tr : = | tr -cd '[[:alnum:]]=')
|
||||
[ "$id" ] || { echo "Error: Failed to get release id for tag: $tag"; echo "$response" | awk 'length($0)<100' >&2; exit 1; }
|
||||
|
||||
# Upload asset
|
||||
echo "Uploading asset... "
|
||||
|
||||
# Construct url
|
||||
GH_ASSET="https://uploads.github.com/repos/$owner/$repo/releases/$id/assets?name=$(basename $filename)"
|
||||
|
||||
curl "$GITHUB_OAUTH_BASIC" --data-binary @"$filename" -H "Authorization: token $github_api_token" -H "Content-Type: application/octet-stream" $GH_ASSET
|
12998
package-lock.json
generated
12998
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
152
package.json
152
package.json
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "uptime-kuma",
|
||||
"version": "1.7.0",
|
||||
"version": "1.8.0",
|
||||
"license": "MIT",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
@ -15,20 +15,29 @@
|
||||
"lint:js": "eslint --ext \".js,.vue\" --ignore-path .gitignore .",
|
||||
"lint:style": "stylelint \"**/*.{vue,css,scss}\" --ignore-path .gitignore",
|
||||
"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-server": "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-with-build": "npm run build && npm test",
|
||||
"jest": "node test/prepare-jest.js && npm run jest-frontend && npm run jest-backend && jest --config=./config/jest.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",
|
||||
"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-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.7.0-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.7.0 -t louislam/uptime-kuma:debian -t louislam/uptime-kuma:1-debian -t louislam/uptime-kuma:1.7.0-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-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-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.8.0-alpine --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.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-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-amd64": "docker buildx build --platform linux/amd64 -t louislam/uptime-kuma:nightly-amd64 --target nightly . --push --progress plain",
|
||||
"setup": "git checkout 1.7.0 && npm install --legacy-peer-deps && node node_modules/esbuild/install.js && npm run build && npm prune",
|
||||
"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",
|
||||
"setup": "git checkout 1.8.0 && npm ci --production && npm run download-dist",
|
||||
"download-dist": "node extra/download-dist.js",
|
||||
"update-version": "node extra/update-version.js",
|
||||
"mark-as-nightly": "node extra/mark-as-nightly.js",
|
||||
"reset-password": "node extra/reset-password.js",
|
||||
@ -43,66 +52,77 @@
|
||||
"update-language-files": "cd extra/update-language-files && node index.js && eslint ../../src/languages/**.js --fix"
|
||||
},
|
||||
"dependencies": {
|
||||
"@fortawesome/fontawesome-svg-core": "^1.2.36",
|
||||
"@fortawesome/free-regular-svg-icons": "^5.15.4",
|
||||
"@fortawesome/free-solid-svg-icons": "^5.15.4",
|
||||
"@fortawesome/vue-fontawesome": "^3.0.0-4",
|
||||
"@louislam/sqlite3": "^5.0.6",
|
||||
"@popperjs/core": "^2.10.1",
|
||||
"args-parser": "^1.3.0",
|
||||
"axios": "^0.21.4",
|
||||
"bcryptjs": "^2.4.3",
|
||||
"bootstrap": "^5.1.1",
|
||||
"chart.js": "^3.5.1",
|
||||
"chartjs-adapter-dayjs": "^1.0.0",
|
||||
"command-exists": "^1.2.9",
|
||||
"compare-versions": "^3.6.0",
|
||||
"dayjs": "^1.10.7",
|
||||
"express": "^4.17.1",
|
||||
"express-basic-auth": "^1.2.0",
|
||||
"form-data": "^4.0.0",
|
||||
"http-graceful-shutdown": "^3.1.4",
|
||||
"jsonwebtoken": "^8.5.1",
|
||||
"nodemailer": "^6.6.5",
|
||||
"notp": "^2.0.3",
|
||||
"password-hash": "^1.2.2",
|
||||
"prom-client": "^13.2.0",
|
||||
"prometheus-api-metrics": "^3.2.0",
|
||||
"qrcode": "^1.4.4",
|
||||
"@fortawesome/fontawesome-svg-core": "~1.2.36",
|
||||
"@fortawesome/free-regular-svg-icons": "~5.15.4",
|
||||
"@fortawesome/free-solid-svg-icons": "~5.15.4",
|
||||
"@fortawesome/vue-fontawesome": "~3.0.0-4",
|
||||
"@louislam/sqlite3": "~6.0.0",
|
||||
"@popperjs/core": "~2.10.2",
|
||||
"args-parser": "~1.3.0",
|
||||
"axios": "~0.21.4",
|
||||
"bcryptjs": "~2.4.3",
|
||||
"bootstrap": "~5.1.1",
|
||||
"chardet": "^1.3.0",
|
||||
"bree": "~6.3.1",
|
||||
"chart.js": "~3.5.1",
|
||||
"chartjs-adapter-dayjs": "~1.0.0",
|
||||
"command-exists": "~1.2.9",
|
||||
"compare-versions": "~3.6.0",
|
||||
"dayjs": "~1.10.7",
|
||||
"express": "~4.17.1",
|
||||
"express-basic-auth": "~1.2.0",
|
||||
"form-data": "~4.0.0",
|
||||
"http-graceful-shutdown": "~3.1.4",
|
||||
"iconv-lite": "^0.6.3",
|
||||
"jsonwebtoken": "~8.5.1",
|
||||
"nodemailer": "~6.6.5",
|
||||
"notp": "~2.0.3",
|
||||
"password-hash": "~1.2.2",
|
||||
"postcss-rtlcss": "~3.4.1",
|
||||
"postcss-scss": "~4.0.1",
|
||||
"prom-client": "~13.2.0",
|
||||
"prometheus-api-metrics": "~3.2.0",
|
||||
"qrcode": "~1.4.4",
|
||||
"redbean-node": "0.1.2",
|
||||
"socket.io": "^4.2.0",
|
||||
"socket.io-client": "^4.2.0",
|
||||
"tcp-ping": "^0.1.1",
|
||||
"thirty-two": "^1.0.2",
|
||||
"timezones-list": "^3.0.1",
|
||||
"v-pagination-3": "^0.1.6",
|
||||
"socket.io": "~4.2.0",
|
||||
"socket.io-client": "~4.2.0",
|
||||
"tar": "^6.1.11",
|
||||
"tcp-ping": "~0.1.1",
|
||||
"thirty-two": "~1.0.2",
|
||||
"timezones-list": "~3.0.1",
|
||||
"v-pagination-3": "~0.1.6",
|
||||
"vue": "next",
|
||||
"vue-chart-3": "^0.5.8",
|
||||
"vue-confirm-dialog": "^1.0.2",
|
||||
"vue-contenteditable": "^3.0.4",
|
||||
"vue-i18n": "^9.1.7",
|
||||
"vue-image-crop-upload": "^3.0.3",
|
||||
"vue-multiselect": "^3.0.0-alpha.2",
|
||||
"vue-qrcode": "^1.0.0",
|
||||
"vue-router": "^4.0.11",
|
||||
"vue-toastification": "^2.0.0-rc.1",
|
||||
"vuedraggable": "^4.1.0"
|
||||
"vue-chart-3": "~0.5.8",
|
||||
"vue-confirm-dialog": "~1.0.2",
|
||||
"vue-contenteditable": "~3.0.4",
|
||||
"vue-i18n": "~9.1.9",
|
||||
"vue-image-crop-upload": "~3.0.3",
|
||||
"vue-multiselect": "~3.0.0-alpha.2",
|
||||
"vue-qrcode": "~1.0.0",
|
||||
"vue-router": "~4.0.11",
|
||||
"vue-toastification": "~2.0.0-rc.1",
|
||||
"vuedraggable": "~4.1.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/eslint-parser": "^7.15.7",
|
||||
"@types/bootstrap": "^5.1.6",
|
||||
"@vitejs/plugin-legacy": "^1.5.3",
|
||||
"@vitejs/plugin-vue": "^1.9.1",
|
||||
"@vue/compiler-sfc": "^3.2.16",
|
||||
"core-js": "^3.18.0",
|
||||
"cross-env": "^7.0.3",
|
||||
"dns2": "^2.0.1",
|
||||
"eslint": "^7.32.0",
|
||||
"eslint-plugin-vue": "^7.18.0",
|
||||
"sass": "^1.42.1",
|
||||
"stylelint": "^13.13.1",
|
||||
"stylelint-config-standard": "^22.0.0",
|
||||
"typescript": "^4.4.3",
|
||||
"vite": "^2.5.10"
|
||||
"@babel/eslint-parser": "~7.15.7",
|
||||
"@babel/preset-env": "^7.15.8",
|
||||
"@types/bootstrap": "~5.1.6",
|
||||
"@vitejs/plugin-legacy": "~1.6.1",
|
||||
"@vitejs/plugin-vue": "~1.9.2",
|
||||
"@vue/compiler-sfc": "~3.2.19",
|
||||
"babel-plugin-rewire": "~1.2.0",
|
||||
"core-js": "~3.18.1",
|
||||
"cross-env": "~7.0.3",
|
||||
"dns2": "~2.0.1",
|
||||
"eslint": "~7.32.0",
|
||||
"eslint-plugin-vue": "~7.18.0",
|
||||
"jest": "~27.2.4",
|
||||
"jest-puppeteer": "~6.0.0",
|
||||
"puppeteer": "~10.4.0",
|
||||
"sass": "~1.42.1",
|
||||
"stylelint": "~13.13.1",
|
||||
"stylelint-config-standard": "~22.0.0",
|
||||
"typescript": "~4.4.3",
|
||||
"vite": "~2.6.4"
|
||||
}
|
||||
}
|
||||
|
@ -1,6 +1,5 @@
|
||||
const { setSetting } = require("./util-server");
|
||||
const axios = require("axios");
|
||||
const { isDev } = require("../src/util");
|
||||
|
||||
exports.version = require("../package.json").version;
|
||||
exports.latestVersion = null;
|
||||
@ -22,7 +21,6 @@ exports.startInterval = () => {
|
||||
}
|
||||
|
||||
exports.latestVersion = res.data.version;
|
||||
console.log("Latest Version: " + exports.latestVersion);
|
||||
} catch (_) { }
|
||||
|
||||
};
|
||||
|
@ -4,6 +4,8 @@
|
||||
const { TimeLogger } = require("../src/util");
|
||||
const { R } = require("redbean-node");
|
||||
const { io } = require("./server");
|
||||
const { setting } = require("./util-server");
|
||||
const checkVersion = require("./check-version");
|
||||
|
||||
async function sendNotificationList(socket) {
|
||||
const timeLogger = new TimeLogger();
|
||||
@ -14,10 +16,10 @@ async function sendNotificationList(socket) {
|
||||
]);
|
||||
|
||||
for (let bean of list) {
|
||||
result.push(bean.export())
|
||||
result.push(bean.export());
|
||||
}
|
||||
|
||||
io.to(socket.userID).emit("notificationList", result)
|
||||
io.to(socket.userID).emit("notificationList", result);
|
||||
|
||||
timeLogger.print("Send Notification List");
|
||||
|
||||
@ -39,7 +41,7 @@ async function sendHeartbeatList(socket, monitorID, toUser = false, overwrite =
|
||||
LIMIT 100
|
||||
`, [
|
||||
monitorID,
|
||||
])
|
||||
]);
|
||||
|
||||
let result = list.reverse();
|
||||
|
||||
@ -69,7 +71,7 @@ async function sendImportantHeartbeatList(socket, monitorID, toUser = false, ove
|
||||
LIMIT 500
|
||||
`, [
|
||||
monitorID,
|
||||
])
|
||||
]);
|
||||
|
||||
timeLogger.print(`[Monitor: ${monitorID}] sendImportantHeartbeatList`);
|
||||
|
||||
@ -81,8 +83,18 @@ async function sendImportantHeartbeatList(socket, monitorID, toUser = false, ove
|
||||
|
||||
}
|
||||
|
||||
async function sendInfo(socket) {
|
||||
socket.emit("info", {
|
||||
version: checkVersion.version,
|
||||
latestVersion: checkVersion.latestVersion,
|
||||
primaryBaseURL: await setting("primaryBaseURL")
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
sendNotificationList,
|
||||
sendImportantHeartbeatList,
|
||||
sendHeartbeatList,
|
||||
}
|
||||
sendInfo
|
||||
};
|
||||
|
||||
|
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,6 +49,8 @@ class Database {
|
||||
"patch-add-apikey-monitor.sql": true,
|
||||
"patch-incident-table.sql": true,
|
||||
"patch-group-table.sql": true,
|
||||
"patch-monitor-push_token.sql": true,
|
||||
"patch-http-monitor-method-body-and-headers.sql": true,
|
||||
}
|
||||
|
||||
/**
|
||||
@ -108,6 +110,7 @@ class Database {
|
||||
R.freeze(true);
|
||||
await R.autoloadModels("./server/model");
|
||||
|
||||
await R.exec("PRAGMA foreign_keys = ON");
|
||||
// Change to WAL
|
||||
await R.exec("PRAGMA journal_mode = WAL");
|
||||
await R.exec("PRAGMA cache_size = -12000");
|
||||
|
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,
|
||||
};
|
@ -11,7 +11,9 @@ const { tcping, ping, dnsResolve, checkCertificate, checkStatusCode, getTotalCli
|
||||
const { R } = require("redbean-node");
|
||||
const { BeanModel } = require("redbean-node/dist/bean-model");
|
||||
const { Notification } = require("../notification");
|
||||
const { demoMode } = require("../config");
|
||||
const version = require("../../package.json").version;
|
||||
const apicache = require("../modules/apicache");
|
||||
|
||||
/**
|
||||
* status:
|
||||
@ -53,13 +55,15 @@ class Monitor extends BeanModel {
|
||||
id: this.id,
|
||||
name: this.name,
|
||||
url: this.url,
|
||||
method: this.method,
|
||||
body: this.body,
|
||||
headers: this.headers,
|
||||
hostname: this.hostname,
|
||||
port: this.port,
|
||||
maxretries: this.maxretries,
|
||||
weight: this.weight,
|
||||
active: this.active,
|
||||
type: this.type,
|
||||
apikey: this.apikey,
|
||||
interval: this.interval,
|
||||
retryInterval: this.retryInterval,
|
||||
keyword: this.keyword,
|
||||
@ -70,6 +74,7 @@ class Monitor extends BeanModel {
|
||||
dns_resolve_type: this.dns_resolve_type,
|
||||
dns_resolve_server: this.dns_resolve_server,
|
||||
dns_last_result: this.dns_last_result,
|
||||
pushToken: this.pushToken,
|
||||
notificationIDList,
|
||||
tags: tags,
|
||||
};
|
||||
@ -136,11 +141,15 @@ class Monitor extends BeanModel {
|
||||
// Do not do any queries/high loading things before the "bean.ping"
|
||||
let startTime = dayjs().valueOf();
|
||||
|
||||
let res = await axios.get(this.url, {
|
||||
const options = {
|
||||
url: this.url,
|
||||
method: (this.method || "get").toLowerCase(),
|
||||
...(this.body ? { data: JSON.parse(this.body) } : {}),
|
||||
timeout: this.interval * 1000 * 0.8,
|
||||
headers: {
|
||||
"Accept": "*/*",
|
||||
"User-Agent": "Uptime-Kuma/" + version,
|
||||
...(this.headers ? JSON.parse(this.headers) : {}),
|
||||
},
|
||||
httpsAgent: new https.Agent({
|
||||
maxCachedSessions: 0, // Use Custom agent to disable session reuse (https://github.com/nodejs/node/issues/3940)
|
||||
@ -150,7 +159,8 @@ class Monitor extends BeanModel {
|
||||
validateStatus: (status) => {
|
||||
return checkStatusCode(status, this.getAcceptedStatuscodes());
|
||||
},
|
||||
});
|
||||
};
|
||||
let res = await axios.request(options);
|
||||
bean.msg = `${res.status} - ${res.statusText}`;
|
||||
bean.ping = dayjs().valueOf() - startTime;
|
||||
|
||||
@ -166,7 +176,13 @@ class Monitor extends BeanModel {
|
||||
}
|
||||
}
|
||||
|
||||
debug("Cert Info Query Time: " + (dayjs().valueOf() - certInfoStartTime) + "ms");
|
||||
if (process.env.TIMELOGGER === "1") {
|
||||
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") {
|
||||
bean.status = UP;
|
||||
@ -237,6 +253,25 @@ class Monitor extends BeanModel {
|
||||
|
||||
bean.msg = dnsMessage;
|
||||
bean.status = UP;
|
||||
} else if (this.type === "push") { // Type: Push
|
||||
const time = R.isoDateTime(dayjs.utc().subtract(this.interval, "second"));
|
||||
|
||||
let heartbeatCount = await R.count("heartbeat", " monitor_id = ? AND time > ? ", [
|
||||
this.id,
|
||||
time
|
||||
]);
|
||||
|
||||
debug("heartbeatCount" + heartbeatCount + " " + time);
|
||||
|
||||
if (heartbeatCount <= 0) {
|
||||
throw new Error("No heartbeat in the time window");
|
||||
} else {
|
||||
// No need to insert successful heartbeat for push type, so end here
|
||||
retries = 0;
|
||||
this.heartbeatInterval = setTimeout(beat, this.interval * 1000);
|
||||
return;
|
||||
}
|
||||
|
||||
} else if (this.type === "steam") {
|
||||
const steamApiUrl = "https://api.steampowered.com/IGameServersService/GetServerList/v1/";
|
||||
const filter = `addr\\${this.hostname}:${this.port}`;
|
||||
@ -277,6 +312,9 @@ class Monitor extends BeanModel {
|
||||
} else {
|
||||
throw new Error(bean.msg + ", but server is not found");
|
||||
}
|
||||
} else {
|
||||
bean.msg = "Unknown Monitor Type";
|
||||
bean.status = PENDING;
|
||||
}
|
||||
|
||||
if (this.isUpsideDown()) {
|
||||
@ -304,61 +342,23 @@ class Monitor extends BeanModel {
|
||||
}
|
||||
}
|
||||
|
||||
// * ? -> 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 ||
|
||||
(previousBeat.status === UP && bean.status === DOWN) ||
|
||||
(previousBeat.status === DOWN && bean.status === UP) ||
|
||||
(previousBeat.status === PENDING && bean.status === DOWN);
|
||||
let beatInterval = this.interval;
|
||||
|
||||
let isImportant = Monitor.isImportantBeat(isFirstBeat, previousBeat?.status, bean.status);
|
||||
|
||||
// Mark as important if status changed, ignore pending pings,
|
||||
// Don't notify if disrupted changes to up
|
||||
if (isImportant) {
|
||||
bean.important = true;
|
||||
|
||||
// Send only if the first beat is DOWN
|
||||
if (!isFirstBeat || bean.status === DOWN) {
|
||||
let notificationList = await R.getAll("SELECT notification.* FROM notification, monitor_notification WHERE monitor_id = ? AND monitor_notification.notification_id = notification.id ", [
|
||||
this.id,
|
||||
]);
|
||||
|
||||
let text;
|
||||
if (bean.status === UP) {
|
||||
text = "✅ Up";
|
||||
} 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
await Monitor.sendNotification(isFirstBeat, this, bean);
|
||||
} else {
|
||||
bean.important = false;
|
||||
}
|
||||
|
||||
let beatInterval = this.interval;
|
||||
|
||||
if (bean.status === UP) {
|
||||
console.info(`Monitor #${this.id} '${this.name}': Successful Response: ${bean.ping} ms | Interval: ${beatInterval} seconds | Type: ${this.type}`);
|
||||
} else if (bean.status === PENDING) {
|
||||
if (this.retryInterval !== this.interval) {
|
||||
if (this.retryInterval > 0) {
|
||||
beatInterval = this.retryInterval;
|
||||
}
|
||||
console.warn(`Monitor #${this.id} '${this.name}': Pending: ${bean.msg} | Max retries: ${this.maxretries} | Retry: ${retries} | Retry Interval: ${beatInterval} seconds | Type: ${this.type}`);
|
||||
@ -375,12 +375,27 @@ class Monitor extends BeanModel {
|
||||
previousBeat = bean;
|
||||
|
||||
if (! this.isStop) {
|
||||
|
||||
if (demoMode) {
|
||||
if (beatInterval < 20) {
|
||||
console.log("beat interval too low, reset to 20s");
|
||||
beatInterval = 20;
|
||||
}
|
||||
}
|
||||
|
||||
this.heartbeatInterval = setTimeout(beat, beatInterval * 1000);
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
beat();
|
||||
// Delay Push Type
|
||||
if (this.type === "push") {
|
||||
setTimeout(() => {
|
||||
beat();
|
||||
}, this.interval * 1000);
|
||||
} else {
|
||||
beat();
|
||||
}
|
||||
}
|
||||
|
||||
stop() {
|
||||
@ -541,6 +556,54 @@ class Monitor extends BeanModel {
|
||||
const uptime = await this.calcUptime(duration, monitorID);
|
||||
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) {
|
||||
let notificationList = await R.getAll("SELECT notification.* FROM notification, monitor_notification WHERE monitor_id = ? AND monitor_notification.notification_id = notification.id ", [
|
||||
monitor.id,
|
||||
]);
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
// Clear Status Page Cache
|
||||
apicache.clear();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
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;
|
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: monitorJSON["name"],
|
||||
text: `## [${this.statusToString(heartbeatJSON["status"])}] \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;
|
@ -7,7 +7,7 @@ class Discord extends NotificationProvider {
|
||||
name = "discord";
|
||||
|
||||
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
|
||||
let okMsg = "Sent Successfully. ";
|
||||
let okMsg = "Sent Successfully.";
|
||||
|
||||
try {
|
||||
const discordDisplayName = notification.discordUsername || "Uptime Kuma";
|
||||
|
83
server/notification-providers/feishu.js
Normal file
83
server/notification-providers/feishu.js
Normal file
@ -0,0 +1,83 @@
|
||||
const NotificationProvider = require("./notification-provider");
|
||||
const axios = require("axios");
|
||||
const { DOWN, UP } = require("../../src/util");
|
||||
|
||||
class Feishu extends NotificationProvider {
|
||||
name = "Feishu";
|
||||
|
||||
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
|
||||
let okMsg = "Sent Successfully.";
|
||||
let feishuWebHookUrl = notification.feishuWebHookUrl;
|
||||
|
||||
try {
|
||||
if (heartbeatJSON == null) {
|
||||
let testdata = {
|
||||
msg_type: "text",
|
||||
content: {
|
||||
text: msg,
|
||||
},
|
||||
};
|
||||
await axios.post(feishuWebHookUrl, testdata);
|
||||
return okMsg;
|
||||
}
|
||||
|
||||
if (heartbeatJSON["status"] == DOWN) {
|
||||
let downdata = {
|
||||
msg_type: "post",
|
||||
content: {
|
||||
post: {
|
||||
zh_cn: {
|
||||
title: "UptimeKuma Alert: " + monitorJSON["name"],
|
||||
content: [
|
||||
[
|
||||
{
|
||||
tag: "text",
|
||||
text:
|
||||
"[Down] " +
|
||||
heartbeatJSON["msg"] +
|
||||
"\nTime (UTC): " +
|
||||
heartbeatJSON["time"],
|
||||
},
|
||||
],
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
await axios.post(feishuWebHookUrl, downdata);
|
||||
return okMsg;
|
||||
}
|
||||
|
||||
if (heartbeatJSON["status"] == UP) {
|
||||
let updata = {
|
||||
msg_type: "post",
|
||||
content: {
|
||||
post: {
|
||||
zh_cn: {
|
||||
title: "UptimeKuma Alert: " + monitorJSON["name"],
|
||||
content: [
|
||||
[
|
||||
{
|
||||
tag: "text",
|
||||
text:
|
||||
"[Up] " +
|
||||
heartbeatJSON["msg"] +
|
||||
"\nTime (UTC): " +
|
||||
heartbeatJSON["time"],
|
||||
},
|
||||
],
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
await axios.post(feishuWebHookUrl, updata);
|
||||
return okMsg;
|
||||
}
|
||||
} catch (error) {
|
||||
this.throwGeneralAxiosError(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Feishu;
|
@ -6,7 +6,7 @@ class Gotify extends NotificationProvider {
|
||||
name = "gotify";
|
||||
|
||||
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
|
||||
let okMsg = "Sent Successfully. ";
|
||||
let okMsg = "Sent Successfully.";
|
||||
try {
|
||||
if (notification.gotifyserverurl && notification.gotifyserverurl.endsWith("/")) {
|
||||
notification.gotifyserverurl = notification.gotifyserverurl.slice(0, -1);
|
||||
|
@ -7,7 +7,7 @@ class Line extends NotificationProvider {
|
||||
name = "line";
|
||||
|
||||
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
|
||||
let okMsg = "Sent Successfully. ";
|
||||
let okMsg = "Sent Successfully.";
|
||||
try {
|
||||
let lineAPIUrl = "https://api.line.me/v2/bot/message/push";
|
||||
let config = {
|
||||
|
@ -7,7 +7,7 @@ class LunaSea extends NotificationProvider {
|
||||
name = "lunasea";
|
||||
|
||||
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
|
||||
let okMsg = "Sent Successfully. ";
|
||||
let okMsg = "Sent Successfully.";
|
||||
let lunaseadevice = "https://notify.lunasea.app/v1/custom/device/" + notification.lunaseaDevice
|
||||
|
||||
try {
|
||||
|
45
server/notification-providers/matrix.js
Normal file
45
server/notification-providers/matrix.js
Normal file
@ -0,0 +1,45 @@
|
||||
const NotificationProvider = require("./notification-provider");
|
||||
const axios = require("axios");
|
||||
const Crypto = require("crypto");
|
||||
const { debug } = require("../../src/util");
|
||||
|
||||
class Matrix extends NotificationProvider {
|
||||
name = "matrix";
|
||||
|
||||
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
|
||||
let okMsg = "Sent Successfully.";
|
||||
|
||||
const size = 20;
|
||||
const randomString = encodeURIComponent(
|
||||
Crypto
|
||||
.randomBytes(size)
|
||||
.toString("base64")
|
||||
.slice(0, size)
|
||||
);
|
||||
|
||||
debug("Random String: " + randomString);
|
||||
|
||||
const roomId = encodeURIComponent(notification.internalRoomId);
|
||||
|
||||
debug("Matrix Room ID: " + roomId);
|
||||
|
||||
try {
|
||||
let config = {
|
||||
headers: {
|
||||
"Authorization": `Bearer ${notification.accessToken}`,
|
||||
}
|
||||
};
|
||||
let data = {
|
||||
"msgtype": "m.text",
|
||||
"body": msg
|
||||
};
|
||||
|
||||
await axios.put(`${notification.homeserverUrl}/_matrix/client/r0/rooms/${roomId}/send/m.room.message/${randomString}`, data, config);
|
||||
return okMsg;
|
||||
} catch (error) {
|
||||
this.throwGeneralAxiosError(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Matrix;
|
@ -7,7 +7,7 @@ class Mattermost extends NotificationProvider {
|
||||
name = "mattermost";
|
||||
|
||||
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
|
||||
let okMsg = "Sent Successfully. ";
|
||||
let okMsg = "Sent Successfully.";
|
||||
try {
|
||||
const mattermostUserName = notification.mattermostusername || "Uptime Kuma";
|
||||
// If heartbeatJSON is null, assume we're testing.
|
||||
|
@ -6,30 +6,54 @@ class Octopush extends NotificationProvider {
|
||||
name = "octopush";
|
||||
|
||||
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
|
||||
let okMsg = "Sent Successfully. ";
|
||||
let okMsg = "Sent Successfully.";
|
||||
|
||||
try {
|
||||
let config = {
|
||||
headers: {
|
||||
"api-key": notification.octopushAPIKey,
|
||||
"api-login": notification.octopushLogin,
|
||||
"cache-control": "no-cache"
|
||||
}
|
||||
};
|
||||
let data = {
|
||||
"recipients": [
|
||||
{
|
||||
"phone_number": notification.octopushPhoneNumber
|
||||
// Default - V2
|
||||
if (notification.octopushVersion == 2 || !notification.octopushVersion) {
|
||||
let config = {
|
||||
headers: {
|
||||
"api-key": notification.octopushAPIKey,
|
||||
"api-login": notification.octopushLogin,
|
||||
"cache-control": "no-cache"
|
||||
}
|
||||
],
|
||||
//octopush not supporting non ascii char
|
||||
"text": msg.replace(/[^\x00-\x7F]/g, ""),
|
||||
"type": notification.octopushSMSType,
|
||||
"purpose": "alert",
|
||||
"sender": notification.octopushSenderName
|
||||
};
|
||||
};
|
||||
let data = {
|
||||
"recipients": [
|
||||
{
|
||||
"phone_number": notification.octopushPhoneNumber
|
||||
}
|
||||
],
|
||||
//octopush not supporting non ascii char
|
||||
"text": msg.replace(/[^\x00-\x7F]/g, ""),
|
||||
"type": notification.octopushSMSType,
|
||||
"purpose": "alert",
|
||||
"sender": notification.octopushSenderName
|
||||
};
|
||||
await axios.post("https://api.octopush.com/v1/public/sms-campaign/send", data, config)
|
||||
} else if (notification.octopushVersion == 1) {
|
||||
let data = {
|
||||
"user_login": notification.octopushDMLogin,
|
||||
"api_key": notification.octopushDMAPIKey,
|
||||
"sms_recipients": notification.octopushDMPhoneNumber,
|
||||
"sms_sender": notification.octopushDMSenderName,
|
||||
"sms_type": (notification.octopushDMSMSType == "sms_premium") ? "FR" : "XXX",
|
||||
"transactional": "1",
|
||||
//octopush not supporting non ascii char
|
||||
"sms_text": msg.replace(/[^\x00-\x7F]/g, ""),
|
||||
};
|
||||
|
||||
let config = {
|
||||
headers: {
|
||||
"cache-control": "no-cache"
|
||||
},
|
||||
params: data
|
||||
};
|
||||
await axios.post("https://www.octopush-dm.com/api/sms/json", {}, config)
|
||||
} else {
|
||||
throw new Error("Unknown Octopush version!");
|
||||
}
|
||||
|
||||
await axios.post("https://api.octopush.com/v1/public/sms-campaign/send", data, config)
|
||||
return okMsg;
|
||||
} catch (error) {
|
||||
this.throwGeneralAxiosError(error);
|
||||
|
41
server/notification-providers/promosms.js
Normal file
41
server/notification-providers/promosms.js
Normal file
@ -0,0 +1,41 @@
|
||||
const NotificationProvider = require("./notification-provider");
|
||||
const axios = require("axios");
|
||||
|
||||
class PromoSMS extends NotificationProvider {
|
||||
|
||||
name = "promosms";
|
||||
|
||||
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
|
||||
let okMsg = "Sent Successfully.";
|
||||
|
||||
try {
|
||||
let config = {
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"Authorization": "Basic " + Buffer.from(notification.promosmsLogin + ":" + notification.promosmsPassword).toString('base64'),
|
||||
"Accept": "text/json",
|
||||
}
|
||||
};
|
||||
let data = {
|
||||
"recipients": [ notification.promosmsPhoneNumber ],
|
||||
//Lets remove non ascii char
|
||||
"text": msg.replace(/[^\x00-\x7F]/g, ""),
|
||||
"type": Number(notification.promosmsSMSType),
|
||||
"sender": notification.promosmsSenderName
|
||||
};
|
||||
|
||||
let resp = await axios.post("https://promosms.com/api/rest/v3_2/sms", data, config);
|
||||
|
||||
if (resp.data.response.status !== 0) {
|
||||
let error = "Something gone wrong. Api returned " + resp.data.response.status + ".";
|
||||
this.throwGeneralAxiosError(error);
|
||||
}
|
||||
|
||||
return okMsg;
|
||||
} catch (error) {
|
||||
this.throwGeneralAxiosError(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = PromoSMS;
|
@ -8,7 +8,7 @@ class Pushbullet extends NotificationProvider {
|
||||
name = "pushbullet";
|
||||
|
||||
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
|
||||
let okMsg = "Sent Successfully. ";
|
||||
let okMsg = "Sent Successfully.";
|
||||
|
||||
try {
|
||||
let pushbulletUrl = "https://api.pushbullet.com/v2/pushes";
|
||||
|
@ -6,7 +6,7 @@ class Pushover extends NotificationProvider {
|
||||
name = "pushover";
|
||||
|
||||
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"
|
||||
|
||||
try {
|
||||
|
@ -6,7 +6,7 @@ class Pushy extends NotificationProvider {
|
||||
name = "pushy";
|
||||
|
||||
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
|
||||
let okMsg = "Sent Successfully. ";
|
||||
let okMsg = "Sent Successfully.";
|
||||
|
||||
try {
|
||||
await axios.post(`https://api.pushy.me/push?api_key=${notification.pushyAPIKey}`, {
|
||||
|
@ -1,25 +1,29 @@
|
||||
const NotificationProvider = require("./notification-provider");
|
||||
const axios = require("axios");
|
||||
const Slack = require("./slack");
|
||||
const { setting } = require("../util-server");
|
||||
const { getMonitorRelativeURL, UP, DOWN } = require("../../src/util");
|
||||
|
||||
class RocketChat extends NotificationProvider {
|
||||
|
||||
name = "rocket.chat";
|
||||
|
||||
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
|
||||
let okMsg = "Sent Successfully. ";
|
||||
let okMsg = "Sent Successfully.";
|
||||
try {
|
||||
if (heartbeatJSON == null) {
|
||||
let data = {
|
||||
"text": "Uptime Kuma Rocket.chat testing successful.",
|
||||
"text": msg,
|
||||
"channel": notification.rocketchannel,
|
||||
"username": notification.rocketusername,
|
||||
"icon_emoji": notification.rocketiconemo,
|
||||
}
|
||||
await axios.post(notification.rocketwebhookURL, data)
|
||||
};
|
||||
await axios.post(notification.rocketwebhookURL, data);
|
||||
return okMsg;
|
||||
}
|
||||
|
||||
const time = heartbeatJSON["time"];
|
||||
|
||||
let data = {
|
||||
"text": "Uptime Kuma Alert",
|
||||
"channel": notification.rocketchannel,
|
||||
@ -28,16 +32,32 @@ class RocketChat extends NotificationProvider {
|
||||
"attachments": [
|
||||
{
|
||||
"title": "Uptime Kuma Alert *Time (UTC)*\n" + time,
|
||||
"title_link": notification.rocketbutton,
|
||||
"text": "*Message*\n" + msg,
|
||||
"color": "#32cd32"
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
// Color
|
||||
if (heartbeatJSON.status === DOWN) {
|
||||
data.attachments[0].color = "#ff0000";
|
||||
} else {
|
||||
data.attachments[0].color = "#32cd32";
|
||||
}
|
||||
await axios.post(notification.rocketwebhookURL, data)
|
||||
|
||||
if (notification.rocketbutton) {
|
||||
await Slack.deprecateURL(notification.rocketbutton);
|
||||
}
|
||||
|
||||
const baseURL = await setting("primaryBaseURL");
|
||||
|
||||
if (baseURL) {
|
||||
data.attachments[0].title_link = baseURL + getMonitorRelativeURL(monitorJSON.id);
|
||||
}
|
||||
|
||||
await axios.post(notification.rocketwebhookURL, data);
|
||||
return okMsg;
|
||||
} catch (error) {
|
||||
this.throwGeneralAxiosError(error)
|
||||
this.throwGeneralAxiosError(error);
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -6,7 +6,7 @@ class Signal extends NotificationProvider {
|
||||
name = "signal";
|
||||
|
||||
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
|
||||
let okMsg = "Sent Successfully. ";
|
||||
let okMsg = "Sent Successfully.";
|
||||
|
||||
try {
|
||||
let data = {
|
||||
|
@ -1,27 +1,47 @@
|
||||
const NotificationProvider = require("./notification-provider");
|
||||
const axios = require("axios");
|
||||
const { setSettings, setting } = require("../util-server");
|
||||
const { getMonitorRelativeURL } = require("../../src/util");
|
||||
|
||||
class Slack extends NotificationProvider {
|
||||
|
||||
name = "slack";
|
||||
|
||||
/**
|
||||
* Deprecated property notification.slackbutton
|
||||
* Set it as primary base url if this is not yet set.
|
||||
*/
|
||||
static async deprecateURL(url) {
|
||||
let currentPrimaryBaseURL = await setting("primaryBaseURL");
|
||||
|
||||
if (!currentPrimaryBaseURL) {
|
||||
console.log("Move the url to be the primary base URL");
|
||||
await setSettings("general", {
|
||||
primaryBaseURL: url,
|
||||
});
|
||||
} else {
|
||||
console.log("Already there, no need to move the primary base URL");
|
||||
}
|
||||
}
|
||||
|
||||
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
|
||||
let okMsg = "Sent Successfully. ";
|
||||
let okMsg = "Sent Successfully.";
|
||||
try {
|
||||
if (heartbeatJSON == null) {
|
||||
let data = {
|
||||
"text": "Uptime Kuma Slack testing successful.",
|
||||
"text": msg,
|
||||
"channel": notification.slackchannel,
|
||||
"username": notification.slackusername,
|
||||
"icon_emoji": notification.slackiconemo,
|
||||
}
|
||||
await axios.post(notification.slackwebhookURL, data)
|
||||
};
|
||||
await axios.post(notification.slackwebhookURL, data);
|
||||
return okMsg;
|
||||
}
|
||||
|
||||
const time = heartbeatJSON["time"];
|
||||
const textMsg = "Uptime Kuma Alert";
|
||||
let data = {
|
||||
"text": "Uptime Kuma Alert",
|
||||
"text": monitorJSON ? textMsg + `: ${monitorJSON.name}` : textMsg,
|
||||
"channel": notification.slackchannel,
|
||||
"username": notification.slackusername,
|
||||
"icon_emoji": notification.slackiconemo,
|
||||
@ -42,26 +62,35 @@ class Slack extends NotificationProvider {
|
||||
"type": "mrkdwn",
|
||||
"text": "*Time (UTC)*\n" + time,
|
||||
}],
|
||||
},
|
||||
{
|
||||
"type": "actions",
|
||||
"elements": [
|
||||
{
|
||||
"type": "button",
|
||||
"text": {
|
||||
"type": "plain_text",
|
||||
"text": "Visit Uptime Kuma",
|
||||
},
|
||||
"value": "Uptime-Kuma",
|
||||
"url": notification.slackbutton || "https://github.com/louislam/uptime-kuma",
|
||||
},
|
||||
],
|
||||
}],
|
||||
};
|
||||
|
||||
if (notification.slackbutton) {
|
||||
await Slack.deprecateURL(notification.slackbutton);
|
||||
}
|
||||
await axios.post(notification.slackwebhookURL, data)
|
||||
|
||||
const baseURL = await setting("primaryBaseURL");
|
||||
|
||||
// Button
|
||||
if (baseURL) {
|
||||
data.blocks.push({
|
||||
"type": "actions",
|
||||
"elements": [{
|
||||
"type": "button",
|
||||
"text": {
|
||||
"type": "plain_text",
|
||||
"text": "Visit Uptime Kuma",
|
||||
},
|
||||
"value": "Uptime-Kuma",
|
||||
"url": baseURL + getMonitorRelativeURL(monitorJSON.id),
|
||||
}],
|
||||
});
|
||||
}
|
||||
|
||||
await axios.post(notification.slackwebhookURL, data);
|
||||
return okMsg;
|
||||
} catch (error) {
|
||||
this.throwGeneralAxiosError(error)
|
||||
this.throwGeneralAxiosError(error);
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -1,5 +1,6 @@
|
||||
const nodemailer = require("nodemailer");
|
||||
const NotificationProvider = require("./notification-provider");
|
||||
const { DOWN, UP } = require("../../src/util");
|
||||
|
||||
class SMTP extends NotificationProvider {
|
||||
|
||||
@ -20,6 +21,56 @@ class SMTP extends NotificationProvider {
|
||||
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);
|
||||
|
||||
@ -34,7 +85,7 @@ class SMTP extends NotificationProvider {
|
||||
cc: notification.smtpCC,
|
||||
bcc: notification.smtpBCC,
|
||||
to: notification.smtpTo,
|
||||
subject: msg,
|
||||
subject: subject,
|
||||
text: bodyTextContent,
|
||||
tls: {
|
||||
rejectUnauthorized: notification.smtpIgnoreTLSError || false,
|
||||
|
@ -87,7 +87,7 @@ class Teams extends NotificationProvider {
|
||||
};
|
||||
|
||||
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
|
||||
let okMsg = "Sent Successfully. ";
|
||||
let okMsg = "Sent Successfully.";
|
||||
|
||||
try {
|
||||
if (heartbeatJSON == null) {
|
||||
|
@ -6,7 +6,7 @@ class Telegram extends NotificationProvider {
|
||||
name = "telegram";
|
||||
|
||||
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
|
||||
let okMsg = "Sent Successfully. ";
|
||||
let okMsg = "Sent Successfully.";
|
||||
|
||||
try {
|
||||
await axios.get(`https://api.telegram.org/bot${notification.telegramBotToken}/sendMessage`, {
|
||||
|
@ -7,7 +7,7 @@ class Webhook extends NotificationProvider {
|
||||
name = "webhook";
|
||||
|
||||
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
|
||||
let okMsg = "Sent Successfully. ";
|
||||
let okMsg = "Sent Successfully.";
|
||||
|
||||
try {
|
||||
let data = {
|
||||
|
@ -5,7 +5,9 @@ const Gotify = require("./notification-providers/gotify");
|
||||
const Line = require("./notification-providers/line");
|
||||
const LunaSea = require("./notification-providers/lunasea");
|
||||
const Mattermost = require("./notification-providers/mattermost");
|
||||
const Matrix = require("./notification-providers/matrix");
|
||||
const Octopush = require("./notification-providers/octopush");
|
||||
const PromoSMS = require("./notification-providers/promosms");
|
||||
const Pushbullet = require("./notification-providers/pushbullet");
|
||||
const Pushover = require("./notification-providers/pushover");
|
||||
const Pushy = require("./notification-providers/pushy");
|
||||
@ -16,6 +18,9 @@ const SMTP = require("./notification-providers/smtp");
|
||||
const Teams = require("./notification-providers/teams");
|
||||
const Telegram = require("./notification-providers/telegram");
|
||||
const Webhook = require("./notification-providers/webhook");
|
||||
const Feishu = require("./notification-providers/feishu");
|
||||
const AliyunSms = require("./notification-providers/aliyun-sms");
|
||||
const DingDing = require("./notification-providers/dingding");
|
||||
|
||||
class Notification {
|
||||
|
||||
@ -28,13 +33,18 @@ class Notification {
|
||||
|
||||
const list = [
|
||||
new Apprise(),
|
||||
new AliyunSms(),
|
||||
new DingDing(),
|
||||
new Discord(),
|
||||
new Teams(),
|
||||
new Gotify(),
|
||||
new Line(),
|
||||
new LunaSea(),
|
||||
new Feishu(),
|
||||
new Mattermost(),
|
||||
new Matrix(),
|
||||
new Octopush(),
|
||||
new PromoSMS(),
|
||||
new Pushbullet(),
|
||||
new Pushover(),
|
||||
new Pushy(),
|
||||
|
@ -4,10 +4,7 @@ const net = require("net");
|
||||
const spawn = require("child_process").spawn;
|
||||
const events = require("events");
|
||||
const fs = require("fs");
|
||||
const WIN = /^win/.test(process.platform);
|
||||
const LIN = /^linux/.test(process.platform);
|
||||
const MAC = /^darwin/.test(process.platform);
|
||||
const FBSD = /^freebsd/.test(process.platform);
|
||||
const util = require("./util-server");
|
||||
|
||||
module.exports = Ping;
|
||||
|
||||
@ -23,12 +20,12 @@ function Ping(host, options) {
|
||||
|
||||
const timeout = 10;
|
||||
|
||||
if (WIN) {
|
||||
if (util.WIN) {
|
||||
this._bin = "c:/windows/system32/ping.exe";
|
||||
this._args = (options.args) ? options.args : [ "-n", "1", "-w", timeout * 1000, host ];
|
||||
this._regmatch = /[><=]([0-9.]+?)ms/;
|
||||
|
||||
} else if (LIN) {
|
||||
} else if (util.LIN) {
|
||||
this._bin = "/bin/ping";
|
||||
|
||||
const defaultArgs = [ "-n", "-w", timeout, "-c", "1", host ];
|
||||
@ -40,7 +37,7 @@ function Ping(host, options) {
|
||||
this._args = (options.args) ? options.args : defaultArgs;
|
||||
this._regmatch = /=([0-9.]+?) ms/;
|
||||
|
||||
} else if (MAC) {
|
||||
} else if (util.MAC) {
|
||||
|
||||
if (net.isIPv6(host) || options.ipv6) {
|
||||
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._regmatch = /=([0-9.]+?) ms/;
|
||||
|
||||
} else if (FBSD) {
|
||||
} else if (util.FBSD) {
|
||||
this._bin = "/sbin/ping";
|
||||
|
||||
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
|
||||
if (util.WIN) {
|
||||
data = convertOutput(data);
|
||||
}
|
||||
this._stdout = (this._stdout || "") + data;
|
||||
});
|
||||
|
||||
@ -112,6 +112,9 @@ Ping.prototype.send = function (callback) {
|
||||
});
|
||||
|
||||
this._ping.stderr.on("data", function (data) { // log stderr
|
||||
if (util.WIN) {
|
||||
data = convertOutput(data);
|
||||
}
|
||||
this._stderr = (this._stderr || "") + data;
|
||||
});
|
||||
|
||||
@ -157,3 +160,19 @@ Ping.prototype.start = function (callback) {
|
||||
Ping.prototype.stop = function () {
|
||||
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;
|
||||
}
|
||||
|
@ -6,7 +6,7 @@ const commonLabels = [
|
||||
"monitor_url",
|
||||
"monitor_hostname",
|
||||
"monitor_port",
|
||||
]
|
||||
];
|
||||
|
||||
const monitor_cert_days_remaining = new PrometheusClient.Gauge({
|
||||
name: "monitor_cert_days_remaining",
|
||||
@ -41,45 +41,46 @@ class Prometheus {
|
||||
monitor_url: monitor.url,
|
||||
monitor_hostname: monitor.hostname,
|
||||
monitor_port: monitor.port
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
update(heartbeat, tlsInfo) {
|
||||
|
||||
if (typeof tlsInfo !== "undefined") {
|
||||
try {
|
||||
let is_valid = 0
|
||||
let is_valid = 0;
|
||||
if (tlsInfo.valid == true) {
|
||||
is_valid = 1
|
||||
is_valid = 1;
|
||||
} else {
|
||||
is_valid = 0
|
||||
is_valid = 0;
|
||||
}
|
||||
monitor_cert_is_valid.set(this.monitorLabelValues, is_valid)
|
||||
monitor_cert_is_valid.set(this.monitorLabelValues, is_valid);
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
console.error(e);
|
||||
}
|
||||
|
||||
try {
|
||||
monitor_cert_days_remaining.set(this.monitorLabelValues, tlsInfo.daysRemaining)
|
||||
monitor_cert_days_remaining.set(this.monitorLabelValues, tlsInfo.certInfo.daysRemaining);
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
console.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
monitor_status.set(this.monitorLabelValues, heartbeat.status)
|
||||
monitor_status.set(this.monitorLabelValues, heartbeat.status);
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
console.error(e);
|
||||
}
|
||||
|
||||
try {
|
||||
if (typeof heartbeat.ping === "number") {
|
||||
monitor_response_time.set(this.monitorLabelValues, heartbeat.ping)
|
||||
monitor_response_time.set(this.monitorLabelValues, heartbeat.ping);
|
||||
} else {
|
||||
// Is it good?
|
||||
monitor_response_time.set(this.monitorLabelValues, -1)
|
||||
monitor_response_time.set(this.monitorLabelValues, -1);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
console.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
@ -87,4 +88,4 @@ class Prometheus {
|
||||
|
||||
module.exports = {
|
||||
Prometheus
|
||||
}
|
||||
};
|
||||
|
@ -4,15 +4,89 @@ const { R } = require("redbean-node");
|
||||
const server = require("../server");
|
||||
const apicache = require("../modules/apicache");
|
||||
const Monitor = require("../model/monitor");
|
||||
const dayjs = require("dayjs");
|
||||
const { UP, flipStatus, debug } = require("../../src/util");
|
||||
let router = express.Router();
|
||||
|
||||
let cache = apicache.middleware;
|
||||
let io = server.io;
|
||||
|
||||
router.get("/api/entry-page", async (_, response) => {
|
||||
allowDevAllOrigin(response);
|
||||
response.json(server.entryPage);
|
||||
});
|
||||
|
||||
router.get("/api/push/:pushToken", async (request, response) => {
|
||||
try {
|
||||
|
||||
let pushToken = request.params.pushToken;
|
||||
let msg = request.query.msg || "OK";
|
||||
let ping = request.query.ping || null;
|
||||
|
||||
let monitor = await R.findOne("monitor", " push_token = ? AND active = 1 ", [
|
||||
pushToken
|
||||
]);
|
||||
|
||||
if (! monitor) {
|
||||
throw new Error("Monitor not found or not active.");
|
||||
}
|
||||
|
||||
const previousHeartbeat = await R.getRow(`
|
||||
SELECT status, time FROM heartbeat
|
||||
WHERE id = (select MAX(id) from heartbeat where monitor_id = ?)
|
||||
`, [
|
||||
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");
|
||||
bean.time = R.isoDateTime(dayjs.utc());
|
||||
|
||||
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.ping = ping;
|
||||
bean.duration = duration;
|
||||
|
||||
await R.store(bean);
|
||||
|
||||
io.to(monitor.user_id).emit("heartbeat", bean.toJSON());
|
||||
Monitor.sendStats(io, monitor.id, monitor.user_id);
|
||||
|
||||
response.json({
|
||||
ok: true,
|
||||
});
|
||||
|
||||
if (bean.important) {
|
||||
await Monitor.sendNotification(isFirstBeat, monitor, bean);
|
||||
}
|
||||
|
||||
} catch (e) {
|
||||
response.json({
|
||||
ok: false,
|
||||
msg: e.message
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Status Page Config
|
||||
router.get("/api/status-page/config", async (_request, response) => {
|
||||
allowDevAllOrigin(response);
|
||||
|
@ -1,4 +1,9 @@
|
||||
console.log("Welcome to Uptime Kuma");
|
||||
const args = require("args-parser")(process.argv);
|
||||
const { sleep, debug, getRandomInt, genSecret } = require("../src/util");
|
||||
const config = require("./config");
|
||||
|
||||
debug(args);
|
||||
|
||||
if (! process.env.NODE_ENV) {
|
||||
process.env.NODE_ENV = "production";
|
||||
@ -6,8 +11,6 @@ if (! process.env.NODE_ENV) {
|
||||
|
||||
console.log("Node Env: " + process.env.NODE_ENV);
|
||||
|
||||
const { sleep, debug, TimeLogger, getRandomInt } = require("../src/util");
|
||||
|
||||
console.log("Importing Node libraries");
|
||||
const fs = require("fs");
|
||||
const http = require("http");
|
||||
@ -37,7 +40,7 @@ console.log("Importing this project modules");
|
||||
debug("Importing Monitor");
|
||||
const Monitor = require("./model/monitor");
|
||||
debug("Importing Settings");
|
||||
const { getSettings, setSettings, setting, initJWTSecret, genSecret, allowDevAllOrigin, checkLogin } = require("./util-server");
|
||||
const { getSettings, setSettings, setting, initJWTSecret, checkLogin, startUnitTest, FBSD } = require("./util-server");
|
||||
|
||||
debug("Importing Notification");
|
||||
const { Notification } = require("./notification");
|
||||
@ -46,32 +49,52 @@ Notification.init();
|
||||
debug("Importing Database");
|
||||
const Database = require("./database");
|
||||
|
||||
debug("Importing Background Jobs");
|
||||
const { initBackgroundJobs } = require("./jobs");
|
||||
|
||||
const { basicAuth } = require("./auth");
|
||||
const { login } = require("./auth");
|
||||
const passwordHash = require("./password-hash");
|
||||
|
||||
const args = require("args-parser")(process.argv);
|
||||
|
||||
const checkVersion = require("./check-version");
|
||||
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.
|
||||
// Dual-stack support for (::)
|
||||
const hostname = process.env.HOST || args.host;
|
||||
const port = parseInt(process.env.PORT || args.port || 3001);
|
||||
let hostname = process.env.UPTIME_KUMA_HOST || args.host;
|
||||
|
||||
// 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
|
||||
const sslKey = process.env.SSL_KEY || args["ssl-key"] || undefined;
|
||||
const sslCert = process.env.SSL_CERT || args["ssl-cert"] || undefined;
|
||||
const sslKey = process.env.UPTIME_KUMA_SSL_KEY || process.env.SSL_KEY || args["ssl-key"] || undefined;
|
||||
const sslCert = process.env.UPTIME_KUMA_SSL_CERT || process.env.SSL_CERT || args["ssl-cert"] || undefined;
|
||||
|
||||
// Demo Mode?
|
||||
const demoMode = args["demo"] || false;
|
||||
// 2FA / notp verification defaults
|
||||
const twofa_verification_opts = {
|
||||
"window": 1,
|
||||
"time": 30
|
||||
}
|
||||
|
||||
if (demoMode) {
|
||||
/**
|
||||
* Run unit test after the server is ready
|
||||
* @type {boolean}
|
||||
*/
|
||||
const testMode = !!args["test"] || false;
|
||||
|
||||
if (config.demoMode) {
|
||||
console.log("==== Demo Mode ====");
|
||||
}
|
||||
|
||||
console.log("Creating express and socket.io instance")
|
||||
console.log("Creating express and socket.io instance");
|
||||
const app = express();
|
||||
|
||||
let server;
|
||||
@ -91,7 +114,7 @@ const io = new Server(server);
|
||||
module.exports.io = io;
|
||||
|
||||
// Must be after io instantiation
|
||||
const { sendNotificationList, sendHeartbeatList, sendImportantHeartbeatList } = require("./client");
|
||||
const { sendNotificationList, sendHeartbeatList, sendImportantHeartbeatList, sendInfo } = require("./client");
|
||||
const { statusPageSocketHandler } = require("./socket-handlers/status-page-socket-handler");
|
||||
|
||||
app.use(express.json());
|
||||
@ -181,10 +204,7 @@ exports.entryPage = "dashboard";
|
||||
console.log("Adding socket handler");
|
||||
io.on("connection", async (socket) => {
|
||||
|
||||
socket.emit("info", {
|
||||
version: checkVersion.version,
|
||||
latestVersion: checkVersion.latestVersion,
|
||||
});
|
||||
sendInfo(socket);
|
||||
|
||||
totalClient++;
|
||||
|
||||
@ -261,7 +281,7 @@ exports.entryPage = "dashboard";
|
||||
}
|
||||
|
||||
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 (verify && verify.delta == 0) {
|
||||
callback({
|
||||
@ -303,6 +323,12 @@ exports.entryPage = "dashboard";
|
||||
if (user.twofa_status == 0) {
|
||||
let newSecret = await genSecret();
|
||||
let encodedSecret = base32.encode(newSecret);
|
||||
|
||||
// Google authenticator doesn't like equal signs
|
||||
// The fix is found at https://github.com/guyht/notp
|
||||
// Related issue: https://github.com/louislam/uptime-kuma/issues/486
|
||||
encodedSecret = encodedSecret.toString().replace(/=/g, "");
|
||||
|
||||
let uri = `otpauth://totp/Uptime%20Kuma:${user.username}?secret=${encodedSecret}`;
|
||||
|
||||
await R.exec("UPDATE `user` SET twofa_secret = ? WHERE id = ? ", [
|
||||
@ -373,7 +399,7 @@ exports.entryPage = "dashboard";
|
||||
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) {
|
||||
callback({
|
||||
@ -499,6 +525,9 @@ exports.entryPage = "dashboard";
|
||||
bean.name = monitor.name;
|
||||
bean.type = monitor.type;
|
||||
bean.url = monitor.url;
|
||||
bean.method = monitor.method;
|
||||
bean.body = monitor.body;
|
||||
bean.headers = monitor.headers;
|
||||
bean.interval = monitor.interval;
|
||||
bean.retryInterval = monitor.retryInterval;
|
||||
bean.hostname = monitor.hostname;
|
||||
@ -512,6 +541,7 @@ exports.entryPage = "dashboard";
|
||||
bean.accepted_statuscodes_json = JSON.stringify(monitor.accepted_statuscodes);
|
||||
bean.dns_resolve_type = monitor.dns_resolve_type;
|
||||
bean.dns_resolve_server = monitor.dns_resolve_server;
|
||||
bean.pushToken = monitor.pushToken;
|
||||
|
||||
await R.store(bean);
|
||||
|
||||
@ -639,6 +669,8 @@ exports.entryPage = "dashboard";
|
||||
});
|
||||
|
||||
await sendMonitorList(socket);
|
||||
// Clear heartbeat list on client
|
||||
await sendImportantHeartbeatList(socket, monitorID, true, true);
|
||||
|
||||
} catch (e) {
|
||||
callback({
|
||||
@ -863,6 +895,8 @@ exports.entryPage = "dashboard";
|
||||
msg: "Saved"
|
||||
});
|
||||
|
||||
sendInfo(socket);
|
||||
|
||||
} catch (e) {
|
||||
callback({
|
||||
ok: false,
|
||||
@ -1020,6 +1054,9 @@ exports.entryPage = "dashboard";
|
||||
name: monitorListData[i].name,
|
||||
type: monitorListData[i].type,
|
||||
url: monitorListData[i].url,
|
||||
method: monitorListData[i].method || "GET",
|
||||
body: monitorListData[i].body,
|
||||
headers: monitorListData[i].headers,
|
||||
interval: monitorListData[i].interval,
|
||||
retryInterval: retryInterval,
|
||||
hostname: monitorListData[i].hostname,
|
||||
@ -1035,6 +1072,10 @@ exports.entryPage = "dashboard";
|
||||
notificationIDList: {},
|
||||
};
|
||||
|
||||
if (monitorListData[i].pushToken) {
|
||||
monitor.pushToken = monitorListData[i].pushToken;
|
||||
}
|
||||
|
||||
let bean = R.dispense("monitor");
|
||||
|
||||
let notificationIDList = monitor.notificationIDList;
|
||||
@ -1215,8 +1256,14 @@ exports.entryPage = "dashboard";
|
||||
}
|
||||
startMonitors();
|
||||
checkVersion.startInterval();
|
||||
|
||||
if (testMode) {
|
||||
startUnitTest();
|
||||
}
|
||||
});
|
||||
|
||||
initBackgroundJobs(args);
|
||||
|
||||
})();
|
||||
|
||||
async function updateMonitorNotification(monitorID, notificationIDList) {
|
||||
|
@ -5,6 +5,15 @@ const { debug } = require("../src/util");
|
||||
const passwordHash = require("./password-hash");
|
||||
const dayjs = require("dayjs");
|
||||
const { Resolver } = require("dns");
|
||||
const child_process = require("child_process");
|
||||
const iconv = require("iconv-lite");
|
||||
const chardet = require("chardet");
|
||||
|
||||
// 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
|
||||
@ -115,7 +124,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` = ? ", [
|
||||
key,
|
||||
]);
|
||||
@ -123,6 +132,7 @@ exports.setSetting = async function (key, value) {
|
||||
bean = R.dispense("setting");
|
||||
bean.key = key;
|
||||
}
|
||||
bean.type = type;
|
||||
bean.value = JSON.stringify(value);
|
||||
await R.store(bean);
|
||||
};
|
||||
@ -185,38 +195,42 @@ const getDaysRemaining = (validFrom, validTo) => {
|
||||
return daysRemaining;
|
||||
};
|
||||
|
||||
exports.checkCertificate = function (res) {
|
||||
const {
|
||||
valid_from,
|
||||
valid_to,
|
||||
subjectaltname,
|
||||
issuer,
|
||||
fingerprint,
|
||||
} = res.request.res.socket.getPeerCertificate(false);
|
||||
// Fix certificate Info for display
|
||||
// param: info - the chain obtained from getPeerCertificate()
|
||||
const parseCertificateInfo = function (info) {
|
||||
let link = info;
|
||||
|
||||
if (!valid_from || !valid_to || !subjectaltname) {
|
||||
throw {
|
||||
message: "No TLS certificate in response",
|
||||
};
|
||||
while (link) {
|
||||
if (!link.valid_from || !link.valid_to) {
|
||||
break;
|
||||
}
|
||||
link.validTo = new Date(link.valid_to);
|
||||
link.validFor = link.subjectaltname?.replace(/DNS:|IP Address:/g, "").split(", ");
|
||||
link.daysRemaining = getDaysRemaining(new Date(), link.validTo);
|
||||
|
||||
// Move up the chain until loop is encountered
|
||||
if (link.issuerCertificate == null) {
|
||||
break;
|
||||
} else if (link.fingerprint == link.issuerCertificate.fingerprint) {
|
||||
link.issuerCertificate = null;
|
||||
break;
|
||||
} else {
|
||||
link = link.issuerCertificate;
|
||||
}
|
||||
}
|
||||
|
||||
return info;
|
||||
};
|
||||
|
||||
exports.checkCertificate = function (res) {
|
||||
const info = res.request.res.socket.getPeerCertificate(true);
|
||||
const valid = res.request.res.socket.authorized || false;
|
||||
|
||||
const validTo = new Date(valid_to);
|
||||
|
||||
const validFor = subjectaltname
|
||||
.replace(/DNS:|IP Address:/g, "")
|
||||
.split(", ");
|
||||
|
||||
const daysRemaining = getDaysRemaining(new Date(), validTo);
|
||||
const parsedInfo = parseCertificateInfo(info);
|
||||
|
||||
return {
|
||||
valid,
|
||||
validFor,
|
||||
validTo,
|
||||
daysRemaining,
|
||||
issuer,
|
||||
fingerprint,
|
||||
valid: valid,
|
||||
certInfo: parsedInfo
|
||||
};
|
||||
};
|
||||
|
||||
@ -272,16 +286,6 @@ exports.getTotalClientInRoom = (io, roomName) => {
|
||||
}
|
||||
};
|
||||
|
||||
exports.genSecret = () => {
|
||||
let secret = "";
|
||||
let chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
|
||||
let charsLength = chars.length;
|
||||
for ( let i = 0; i < 64; i++ ) {
|
||||
secret += chars.charAt(Math.floor(Math.random() * charsLength));
|
||||
}
|
||||
return secret;
|
||||
};
|
||||
|
||||
exports.allowDevAllOrigin = (res) => {
|
||||
if (process.env.NODE_ENV === "development") {
|
||||
exports.allowAllOrigin(res);
|
||||
@ -298,3 +302,33 @@ exports.checkLogin = (socket) => {
|
||||
throw new Error("You are not logged in.");
|
||||
}
|
||||
};
|
||||
|
||||
exports.startUnitTest = async () => {
|
||||
console.log("Starting unit test...");
|
||||
const npm = /^win/.test(process.platform) ? "npm.cmd" : "npm";
|
||||
const child = child_process.spawn(npm, ["run", "jest"]);
|
||||
|
||||
child.stdout.on("data", (data) => {
|
||||
console.log(data.toString());
|
||||
});
|
||||
|
||||
child.stderr.on("data", (data) => {
|
||||
console.log(data.toString());
|
||||
});
|
||||
|
||||
child.on("close", function (code) {
|
||||
console.log("Jest exit code: " + 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();
|
||||
};
|
||||
|
@ -1,7 +1,12 @@
|
||||
<template>
|
||||
<router-view />
|
||||
<router-view />
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {}
|
||||
import { setPageLocale } from "./util-frontend";
|
||||
export default {
|
||||
created() {
|
||||
setPageLocale();
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
@ -1,8 +1,9 @@
|
||||
@import "vars.scss";
|
||||
@import "multiselect.scss";
|
||||
@import "node_modules/bootstrap/scss/bootstrap";
|
||||
|
||||
#app {
|
||||
font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, segoe ui, Roboto, helvetica neue, Arial, noto sans, sans-serif, apple color emoji, segoe ui emoji, segoe ui symbol, noto color emoji;
|
||||
font-family: BlinkMacSystemFont, segoe ui, Roboto, helvetica neue, Arial, noto sans, sans-serif, apple color emoji, segoe ui emoji, segoe ui symbol, noto color emoji;
|
||||
}
|
||||
|
||||
h1 {
|
||||
@ -13,6 +14,10 @@ h2 {
|
||||
font-size: 26px;
|
||||
}
|
||||
|
||||
textarea.form-control {
|
||||
border-radius: 19px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar {
|
||||
width: 10px;
|
||||
}
|
||||
@ -179,6 +184,11 @@ h2 {
|
||||
border-color: $dark-border-color;
|
||||
}
|
||||
|
||||
.form-control:disabled, .form-control[readonly] {
|
||||
background-color: #232f3b;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.table-hover > tbody > tr:hover {
|
||||
--bs-table-accent-bg: #070a10;
|
||||
color: $dark-font-color;
|
||||
@ -233,30 +243,6 @@ h2 {
|
||||
color: $dark-font-color;
|
||||
}
|
||||
|
||||
// Multiselect
|
||||
.multiselect__tags {
|
||||
background-color: $dark-bg2;
|
||||
border-color: $dark-border-color;
|
||||
}
|
||||
|
||||
.multiselect__input, .multiselect__single {
|
||||
background-color: $dark-bg2;
|
||||
color: $dark-font-color;
|
||||
}
|
||||
|
||||
.multiselect__content-wrapper {
|
||||
background-color: $dark-bg2;
|
||||
border-color: $dark-border-color;
|
||||
}
|
||||
|
||||
.multiselect--above .multiselect__content-wrapper {
|
||||
border-color: $dark-border-color;
|
||||
}
|
||||
|
||||
.multiselect__option--selected {
|
||||
background-color: $dark-bg;
|
||||
}
|
||||
|
||||
.monitor-list {
|
||||
.item {
|
||||
&:hover {
|
||||
@ -428,3 +414,7 @@ h2 {
|
||||
.vue-image-crop-upload .vicp-wrap {
|
||||
border-radius: 10px !important;
|
||||
}
|
||||
|
||||
// Localization
|
||||
|
||||
@import "localization.scss";
|
||||
|
5
src/assets/localization.scss
Normal file
5
src/assets/localization.scss
Normal file
@ -0,0 +1,5 @@
|
||||
html[lang='fa'] {
|
||||
#app {
|
||||
font-family: 'IRANSans', 'Iranian Sans','B Nazanin', 'Tahoma', ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, segoe ui, Roboto, helvetica neue, Arial, noto sans, sans-serif, apple color emoji, segoe ui emoji, segoe ui symbol, noto color emoji;
|
||||
}
|
||||
}
|
73
src/assets/multiselect.scss
Normal file
73
src/assets/multiselect.scss
Normal file
@ -0,0 +1,73 @@
|
||||
@import "vars.scss";
|
||||
@import "node_modules/vue-multiselect/dist/vue-multiselect";
|
||||
|
||||
.multiselect__tags {
|
||||
border-radius: 1.5rem;
|
||||
border: 1px solid #ced4da;
|
||||
min-height: 38px;
|
||||
padding: 6px 40px 0 8px;
|
||||
}
|
||||
|
||||
.multiselect--active .multiselect__tags {
|
||||
border-radius: 1rem;
|
||||
}
|
||||
|
||||
.multiselect__option--highlight {
|
||||
background: $primary !important;
|
||||
}
|
||||
|
||||
.multiselect__option--highlight::after {
|
||||
background: $primary !important;
|
||||
}
|
||||
|
||||
.multiselect__tag {
|
||||
border-radius: $border-radius;
|
||||
margin-bottom: 0;
|
||||
padding: 6px 26px 6px 10px;
|
||||
background: $primary !important;
|
||||
}
|
||||
|
||||
.multiselect__placeholder {
|
||||
font-size: 1rem;
|
||||
padding-left: 6px;
|
||||
padding-top: 0;
|
||||
padding-bottom: 0;
|
||||
margin-bottom: 0;
|
||||
opacity: 0.67;
|
||||
}
|
||||
|
||||
.multiselect__input,
|
||||
.multiselect__single {
|
||||
line-height: 14px;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.dark {
|
||||
.multiselect__tag {
|
||||
color: $dark-font-color2;
|
||||
}
|
||||
|
||||
.multiselect__tags {
|
||||
background-color: $dark-bg2;
|
||||
border-color: $dark-border-color;
|
||||
}
|
||||
|
||||
.multiselect__input,
|
||||
.multiselect__single {
|
||||
background-color: $dark-bg2;
|
||||
color: $dark-font-color;
|
||||
}
|
||||
|
||||
.multiselect__content-wrapper {
|
||||
background-color: $dark-bg2;
|
||||
border-color: $dark-border-color;
|
||||
}
|
||||
|
||||
.multiselect--above .multiselect__content-wrapper {
|
||||
border-color: $dark-border-color;
|
||||
}
|
||||
|
||||
.multiselect__option--selected {
|
||||
background-color: $dark-bg;
|
||||
}
|
||||
}
|
52
src/components/CertificateInfo.vue
Normal file
52
src/components/CertificateInfo.vue
Normal file
@ -0,0 +1,52 @@
|
||||
<template>
|
||||
<div>
|
||||
<h4>{{ $t("Certificate Info") }}</h4>
|
||||
{{ $t("Certificate Chain") }}:
|
||||
<div
|
||||
v-if="valid"
|
||||
class="rounded d-inline-flex ms-2 text-white tag-valid"
|
||||
>
|
||||
{{ $t("Valid") }}
|
||||
</div>
|
||||
<div
|
||||
v-if="!valid"
|
||||
class="rounded d-inline-flex ms-2 text-white tag-invalid"
|
||||
>
|
||||
{{ $t("Invalid") }}
|
||||
</div>
|
||||
<certificate-info-row :cert="certInfo" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import CertificateInfoRow from "./CertificateInfoRow.vue";
|
||||
export default {
|
||||
components: {
|
||||
CertificateInfoRow,
|
||||
},
|
||||
props: {
|
||||
certInfo: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
valid: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import "../assets/vars.scss";
|
||||
|
||||
.tag-valid {
|
||||
padding: 2px 25px;
|
||||
background-color: $primary;
|
||||
}
|
||||
|
||||
.tag-invalid {
|
||||
padding: 2px 25px;
|
||||
background-color: $danger;
|
||||
}
|
||||
</style>
|
122
src/components/CertificateInfoRow.vue
Normal file
122
src/components/CertificateInfoRow.vue
Normal file
@ -0,0 +1,122 @@
|
||||
<template>
|
||||
<div>
|
||||
<div class="d-flex flex-row align-items-center p-1 overflow-hidden">
|
||||
<div class="m-3 ps-3">
|
||||
<div class="cert-icon">
|
||||
<font-awesome-icon icon="file" />
|
||||
<font-awesome-icon class="award-icon" icon="award" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="m-3">
|
||||
<table class="text-start">
|
||||
<tbody>
|
||||
<tr class="my-3">
|
||||
<td class="px-3">Subject:</td>
|
||||
<td>{{ formatSubject(cert.subject) }}</td>
|
||||
</tr>
|
||||
<tr class="my-3">
|
||||
<td class="px-3">Valid To:</td>
|
||||
<td><Datetime :value="cert.validTo" /></td>
|
||||
</tr>
|
||||
<tr class="my-3">
|
||||
<td class="px-3">Days Remaining:</td>
|
||||
<td>{{ cert.daysRemaining }}</td>
|
||||
</tr>
|
||||
<tr class="my-3">
|
||||
<td class="px-3">Issuer:</td>
|
||||
<td>{{ formatSubject(cert.issuer) }}</td>
|
||||
</tr>
|
||||
<tr class="my-3">
|
||||
<td class="px-3">Fingerprint:</td>
|
||||
<td>{{ cert.fingerprint }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<div class="d-flex">
|
||||
<font-awesome-icon
|
||||
v-if="cert.issuerCertificate"
|
||||
class="m-2 ps-6 link-icon"
|
||||
icon="link"
|
||||
/>
|
||||
</div>
|
||||
<certificate-info-row
|
||||
v-if="cert.issuerCertificate"
|
||||
:cert="cert.issuerCertificate"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Datetime from "../components/Datetime.vue";
|
||||
export default {
|
||||
name: "CertificateInfoRow",
|
||||
components: {
|
||||
Datetime,
|
||||
},
|
||||
props: {
|
||||
cert: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
formatSubject(subject) {
|
||||
if (subject.O && subject.CN && subject.C) {
|
||||
return `${subject.CN} - ${subject.O} (${subject.C})`;
|
||||
} else if (subject.O && subject.CN) {
|
||||
return `${subject.CN} - ${subject.O}`;
|
||||
} else if (subject.CN) {
|
||||
return subject.CN;
|
||||
} else {
|
||||
return "no info";
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import "../assets/vars.scss";
|
||||
|
||||
table {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.cert-icon {
|
||||
position: relative;
|
||||
font-size: 70px;
|
||||
color: $link-color;
|
||||
opacity: 0.5;
|
||||
|
||||
.dark & {
|
||||
color: $dark-font-color;
|
||||
opacity: 0.3;
|
||||
}
|
||||
}
|
||||
|
||||
.award-icon {
|
||||
position: absolute;
|
||||
font-size: 0.5em;
|
||||
bottom: 20%;
|
||||
left: 12%;
|
||||
color: white;
|
||||
|
||||
.dark & {
|
||||
color: $dark-bg;
|
||||
}
|
||||
}
|
||||
|
||||
.link-icon {
|
||||
font-size: 20px;
|
||||
margin-left: 50px !important;
|
||||
color: $link-color;
|
||||
opacity: 0.5;
|
||||
|
||||
.dark & {
|
||||
color: $dark-font-color;
|
||||
opacity: 0.3;
|
||||
}
|
||||
}
|
||||
</style>
|
122
src/components/CopyableInput.vue
Normal file
122
src/components/CopyableInput.vue
Normal file
@ -0,0 +1,122 @@
|
||||
<template>
|
||||
<div class="input-group">
|
||||
<input
|
||||
:id="id"
|
||||
ref="input"
|
||||
v-model="model"
|
||||
:type="type"
|
||||
class="form-control"
|
||||
:placeholder="placeholder"
|
||||
:autocomplete="autocomplete"
|
||||
:required="required"
|
||||
:readonly="readonly"
|
||||
:disabled="disabled"
|
||||
>
|
||||
|
||||
<a class="btn btn-outline-primary" @click="copyToClipboard(model)">
|
||||
<font-awesome-icon :icon="icon" />
|
||||
</a>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
let timeout;
|
||||
|
||||
export default {
|
||||
props: {
|
||||
id: {
|
||||
type: String,
|
||||
default: ""
|
||||
},
|
||||
type: {
|
||||
type: String,
|
||||
default: "text"
|
||||
},
|
||||
modelValue: {
|
||||
type: String,
|
||||
default: ""
|
||||
},
|
||||
placeholder: {
|
||||
type: String,
|
||||
default: ""
|
||||
},
|
||||
autocomplete: {
|
||||
type: String,
|
||||
default: undefined,
|
||||
},
|
||||
required: {
|
||||
type: Boolean
|
||||
},
|
||||
readonly: {
|
||||
type: String,
|
||||
default: undefined,
|
||||
},
|
||||
disabled: {
|
||||
type: String,
|
||||
default: undefined,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
visibility: "password",
|
||||
icon: "copy",
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
model: {
|
||||
get() {
|
||||
return this.modelValue;
|
||||
},
|
||||
set(value) {
|
||||
this.$emit("update:modelValue", value);
|
||||
}
|
||||
}
|
||||
},
|
||||
created() {
|
||||
|
||||
},
|
||||
methods: {
|
||||
|
||||
showInput() {
|
||||
this.visibility = "text";
|
||||
},
|
||||
|
||||
hideInput() {
|
||||
this.visibility = "password";
|
||||
},
|
||||
|
||||
copyToClipboard(textToCopy) {
|
||||
this.icon = "check";
|
||||
|
||||
clearTimeout(timeout);
|
||||
timeout = setTimeout(() => {
|
||||
this.icon = "copy";
|
||||
}, 3000);
|
||||
|
||||
// navigator clipboard api needs a secure context (https)
|
||||
if (navigator.clipboard && window.isSecureContext) {
|
||||
// navigator clipboard api method'
|
||||
return navigator.clipboard.writeText(textToCopy);
|
||||
} else {
|
||||
// text area method
|
||||
let textArea = document.createElement("textarea");
|
||||
textArea.value = textToCopy;
|
||||
// make the textarea out of viewport
|
||||
textArea.style.position = "fixed";
|
||||
textArea.style.left = "-999999px";
|
||||
textArea.style.top = "-999999px";
|
||||
document.body.appendChild(textArea);
|
||||
textArea.focus();
|
||||
textArea.select();
|
||||
return new Promise((res, rej) => {
|
||||
// here the magic happens
|
||||
document.execCommand("copy") ? res() : rej();
|
||||
textArea.remove();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
};
|
||||
</script>
|
@ -186,7 +186,7 @@ export default {
|
||||
.beat {
|
||||
display: inline-block;
|
||||
background-color: $primary;
|
||||
border-radius: 50rem;
|
||||
border-radius: $border-radius;
|
||||
|
||||
&.empty {
|
||||
background-color: aliceblue;
|
||||
|
@ -52,7 +52,7 @@ export default {
|
||||
token: "",
|
||||
res: null,
|
||||
tokenRequired: false,
|
||||
}
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
submit() {
|
||||
@ -60,21 +60,19 @@ export default {
|
||||
|
||||
this.$root.login(this.username, this.password, this.token, (res) => {
|
||||
this.processing = false;
|
||||
console.log(res)
|
||||
|
||||
if (res.tokenRequired) {
|
||||
this.tokenRequired = true;
|
||||
} else {
|
||||
this.res = res;
|
||||
}
|
||||
})
|
||||
});
|
||||
},
|
||||
},
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.form-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@ -82,8 +80,17 @@ export default {
|
||||
padding-bottom: 40px;
|
||||
}
|
||||
|
||||
.form {
|
||||
.form-floating {
|
||||
> label {
|
||||
padding-left: 1.3rem;
|
||||
}
|
||||
|
||||
> .form-control {
|
||||
padding-left: 1.3rem;
|
||||
}
|
||||
}
|
||||
|
||||
.form {
|
||||
width: 100%;
|
||||
max-width: 330px;
|
||||
padding: 15px;
|
||||
|
@ -19,7 +19,7 @@
|
||||
|
||||
<router-link v-for="(item, index) in sortedMonitorList" :key="index" :to="monitorURL(item.id)" class="item" :class="{ 'disabled': ! item.active }">
|
||||
<div class="row">
|
||||
<div class="col-6 col-md-8 small-padding" :class="{ 'monitorItem': $root.userHeartbeatBar == 'bottom' || $root.userHeartbeatBar == 'none' }">
|
||||
<div class="col-9 col-md-8 small-padding" :class="{ 'monitorItem': $root.userHeartbeatBar == 'bottom' || $root.userHeartbeatBar == 'none' }">
|
||||
<div class="info">
|
||||
<Uptime :monitor="item" type="24" :pill="true" />
|
||||
{{ item.name }}
|
||||
@ -28,7 +28,7 @@
|
||||
<Tag v-for="tag in item.tags" :key="tag" :item="tag" :size="'sm'" />
|
||||
</div>
|
||||
</div>
|
||||
<div v-show="$root.userHeartbeatBar == 'normal'" :key="$root.userHeartbeatBar" class="col-6 col-md-4">
|
||||
<div v-show="$root.userHeartbeatBar == 'normal'" :key="$root.userHeartbeatBar" class="col-3 col-md-4">
|
||||
<HeartbeatBar size="small" :monitor-id="item.id" />
|
||||
</div>
|
||||
</div>
|
||||
@ -47,6 +47,7 @@
|
||||
import HeartbeatBar from "../components/HeartbeatBar.vue";
|
||||
import Uptime from "../components/Uptime.vue";
|
||||
import Tag from "../components/Tag.vue";
|
||||
import { getMonitorRelativeURL } from "../util.ts";
|
||||
|
||||
export default {
|
||||
components: {
|
||||
@ -62,7 +63,7 @@ export default {
|
||||
data() {
|
||||
return {
|
||||
searchText: "",
|
||||
}
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
sortedMonitorList() {
|
||||
@ -91,7 +92,7 @@ export default {
|
||||
}
|
||||
|
||||
return m1.name.localeCompare(m2.name);
|
||||
})
|
||||
});
|
||||
|
||||
// Simple filter by search text
|
||||
// finds monitor name, tag name or tag value
|
||||
@ -100,8 +101,8 @@ export default {
|
||||
result = result.filter(monitor => {
|
||||
return monitor.name.toLowerCase().includes(loweredSearchText)
|
||||
|| monitor.tags.find(tag => tag.name.toLowerCase().includes(loweredSearchText)
|
||||
|| tag.value?.toLowerCase().includes(loweredSearchText))
|
||||
})
|
||||
|| tag.value?.toLowerCase().includes(loweredSearchText));
|
||||
});
|
||||
}
|
||||
|
||||
return result;
|
||||
@ -109,13 +110,13 @@ export default {
|
||||
},
|
||||
methods: {
|
||||
monitorURL(id) {
|
||||
return "/dashboard/" + id;
|
||||
return getMonitorRelativeURL(id);
|
||||
},
|
||||
clearSearchText() {
|
||||
this.searchText = "";
|
||||
}
|
||||
},
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
@ -13,23 +13,7 @@
|
||||
<div class="mb-3">
|
||||
<label for="notification-type" class="form-label">{{ $t("Notification Type") }}</label>
|
||||
<select id="notification-type" v-model="notification.type" class="form-select">
|
||||
<option value="telegram">Telegram</option>
|
||||
<option value="webhook">Webhook</option>
|
||||
<option value="smtp">{{ $t("Email") }} (SMTP)</option>
|
||||
<option value="discord">Discord</option>
|
||||
<option value="teams">Microsoft Teams</option>
|
||||
<option value="signal">Signal</option>
|
||||
<option value="gotify">Gotify</option>
|
||||
<option value="slack">Slack</option>
|
||||
<option value="rocket.chat">Rocket.chat</option>
|
||||
<option value="pushover">Pushover</option>
|
||||
<option value="pushy">Pushy</option>
|
||||
<option value="octopush">Octopush</option>
|
||||
<option value="lunasea">LunaSea</option>
|
||||
<option value="apprise">Apprise (Support 50+ Notification services)</option>
|
||||
<option value="pushbullet">Pushbullet</option>
|
||||
<option value="line">Line Messenger</option>
|
||||
<option value="mattermost">Mattermost</option>
|
||||
<option v-for="type in notificationTypes" :key="type" :value="type">{{ $t(type) }}</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
@ -38,370 +22,8 @@
|
||||
<input id="notification-name" v-model="notification.name" type="text" class="form-control" required>
|
||||
</div>
|
||||
|
||||
<Telegram v-if="notification.type === 'telegram'" />
|
||||
|
||||
<!-- TODO: Convert all into vue components, but not an easy task. -->
|
||||
|
||||
<template v-if="notification.type === 'webhook'">
|
||||
<div class="mb-3">
|
||||
<label for="webhook-url" class="form-label">Post URL</label>
|
||||
<input id="webhook-url" v-model="notification.webhookURL" type="url" pattern="https?://.+" class="form-control" required>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="webhook-content-type" class="form-label">Content Type</label>
|
||||
<select id="webhook-content-type" v-model="notification.webhookContentType" class="form-select" required>
|
||||
<option value="json">
|
||||
application/json
|
||||
</option>
|
||||
<option value="form-data">
|
||||
multipart/form-data
|
||||
</option>
|
||||
</select>
|
||||
|
||||
<div class="form-text">
|
||||
<p>"application/json" is good for any modern http servers such as express.js</p>
|
||||
<p>"multipart/form-data" is good for PHP, you just need to parse the json by <strong>json_decode($_POST['data'])</strong></p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<SMTP v-if="notification.type === 'smtp'" />
|
||||
|
||||
<template v-if="notification.type === 'discord'">
|
||||
<div class="mb-3">
|
||||
<label for="discord-webhook-url" class="form-label">Discord Webhook URL</label>
|
||||
<input id="discord-webhook-url" v-model="notification.discordWebhookUrl" type="text" class="form-control" required autocomplete="false">
|
||||
<div class="form-text">
|
||||
You can get this by going to Server Settings -> Integrations -> Create Webhook
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="discord-username" class="form-label">Bot Display Name</label>
|
||||
<input id="discord-username" v-model="notification.discordUsername" type="text" class="form-control" autocomplete="false" :placeholder="$root.appName">
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="discord-prefix-message" class="form-label">Prefix Custom Message</label>
|
||||
<input id="discord-prefix-message" v-model="notification.discordPrefixMessage" type="text" class="form-control" autocomplete="false" placeholder="Hello @everyone is...">
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template v-if="notification.type === 'signal'">
|
||||
<div class="mb-3">
|
||||
<label for="signal-url" class="form-label">Post URL</label>
|
||||
<input id="signal-url" v-model="notification.signalURL" type="url" pattern="https?://.+" class="form-control" required>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="signal-number" class="form-label">Number</label>
|
||||
<input id="signal-number" v-model="notification.signalNumber" type="text" class="form-control" required>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="signal-recipients" class="form-label">Recipients</label>
|
||||
<input id="signal-recipients" v-model="notification.signalRecipients" type="text" class="form-control" required>
|
||||
|
||||
<div class="form-text">
|
||||
You need to have a signal client with REST API.
|
||||
|
||||
<p style="margin-top: 8px;">
|
||||
You can check this url to view how to setup one:
|
||||
</p>
|
||||
|
||||
<p style="margin-top: 8px;">
|
||||
<a href="https://github.com/bbernhard/signal-cli-rest-api" target="_blank">https://github.com/bbernhard/signal-cli-rest-api</a>
|
||||
</p>
|
||||
|
||||
<p style="margin-top: 8px;">
|
||||
IMPORTANT: You cannot mix groups and numbers in recipients!
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template v-if="notification.type === 'gotify'">
|
||||
<div class="mb-3">
|
||||
<label for="gotify-application-token" class="form-label">Application Token</label>
|
||||
<HiddenInput id="gotify-application-token" v-model="notification.gotifyapplicationToken" :required="true" autocomplete="one-time-code"></HiddenInput>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="gotify-server-url" class="form-label">Server URL</label>
|
||||
<div class="input-group mb-3">
|
||||
<input id="gotify-server-url" v-model="notification.gotifyserverurl" type="text" class="form-control" required>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="gotify-priority" class="form-label">Priority</label>
|
||||
<input id="gotify-priority" v-model="notification.gotifyPriority" type="number" class="form-control" required min="0" max="10" step="1">
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template v-if="notification.type === 'slack'">
|
||||
<div class="mb-3">
|
||||
<label for="slack-webhook-url" class="form-label">Webhook URL<span style="color: red;"><sup>*</sup></span></label>
|
||||
<input id="slack-webhook-url" v-model="notification.slackwebhookURL" type="text" class="form-control" required>
|
||||
<label for="slack-username" class="form-label">Username</label>
|
||||
<input id="slack-username" v-model="notification.slackusername" type="text" class="form-control">
|
||||
<label for="slack-iconemo" class="form-label">Icon Emoji</label>
|
||||
<input id="slack-iconemo" v-model="notification.slackiconemo" type="text" class="form-control">
|
||||
<label for="slack-channel" class="form-label">Channel Name</label>
|
||||
<input id="slack-channel-name" v-model="notification.slackchannel" type="text" class="form-control">
|
||||
<label for="slack-button-url" class="form-label">Uptime Kuma URL</label>
|
||||
<input id="slack-button" v-model="notification.slackbutton" type="text" class="form-control">
|
||||
<div class="form-text">
|
||||
<span style="color: red;"><sup>*</sup></span>Required
|
||||
<p style="margin-top: 8px;">
|
||||
More info about webhooks on: <a href="https://api.slack.com/messaging/webhooks" target="_blank">https://api.slack.com/messaging/webhooks</a>
|
||||
</p>
|
||||
<p style="margin-top: 8px;">
|
||||
Enter the channel name on Slack Channel Name field if you want to bypass the webhook channel. Ex: #other-channel
|
||||
</p>
|
||||
<p style="margin-top: 8px;">
|
||||
If you leave the Uptime Kuma URL field blank, it will default to the Project Github page.
|
||||
</p>
|
||||
<p style="margin-top: 8px;">
|
||||
Emoji cheat sheet: <a href="https://www.webfx.com/tools/emoji-cheat-sheet/" target="_blank">https://www.webfx.com/tools/emoji-cheat-sheet/</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template v-if="notification.type === 'rocket.chat'">
|
||||
<div class="mb-3">
|
||||
<label for="rocket-webhook-url" class="form-label">Webhook URL<span style="color: red;"><sup>*</sup></span></label>
|
||||
<input id="rocket-webhook-url" v-model="notification.rocketwebhookURL" type="text" class="form-control" required>
|
||||
<label for="rocket-username" class="form-label">Username</label>
|
||||
<input id="rocket-username" v-model="notification.rocketusername" type="text" class="form-control">
|
||||
<label for="rocket-iconemo" class="form-label">Icon Emoji</label>
|
||||
<input id="rocket-iconemo" v-model="notification.rocketiconemo" type="text" class="form-control">
|
||||
<label for="rocket-channel" class="form-label">Channel Name</label>
|
||||
<input id="rocket-channel-name" v-model="notification.rocketchannel" type="text" class="form-control">
|
||||
<label for="rocket-button-url" class="form-label">Uptime Kuma URL</label>
|
||||
<input id="rocket-button" v-model="notification.rocketbutton" type="text" class="form-control">
|
||||
<div class="form-text">
|
||||
<span style="color: red;"><sup>*</sup></span>Required
|
||||
<p style="margin-top: 8px;">
|
||||
More info about webhooks on: <a href="https://docs.rocket.chat/guides/administration/administration/integrations" target="_blank">https://api.slack.com/messaging/webhooks</a>
|
||||
</p>
|
||||
<p style="margin-top: 8px;">
|
||||
Enter the channel name on Rocket.chat Channel Name field if you want to bypass the webhook channel. Ex: #other-channel
|
||||
</p>
|
||||
<p style="margin-top: 8px;">
|
||||
If you leave the Uptime Kuma URL field blank, it will default to the Project Github page.
|
||||
</p>
|
||||
<p style="margin-top: 8px;">
|
||||
Emoji cheat sheet: <a href="https://www.webfx.com/tools/emoji-cheat-sheet/" target="_blank">https://www.webfx.com/tools/emoji-cheat-sheet/</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template v-if="notification.type === 'mattermost'">
|
||||
<div class="mb-3">
|
||||
<label for="mattermost-webhook-url" class="form-label">Webhook URL<span style="color:red;"><sup>*</sup></span></label>
|
||||
<input id="mattermost-webhook-url" v-model="notification.mattermostWebhookUrl" type="text" class="form-control" required>
|
||||
<label for="mattermost-username" class="form-label">Username</label>
|
||||
<input id="mattermost-username" v-model="notification.mattermostusername" type="text" class="form-control">
|
||||
<label for="mattermost-iconurl" class="form-label">Icon URL</label>
|
||||
<input id="mattermost-iconurl" v-model="notification.mattermosticonurl" type="text" class="form-control">
|
||||
<label for="mattermost-iconemo" class="form-label">Icon Emoji</label>
|
||||
<input id="mattermost-iconemo" v-model="notification.mattermosticonemo" type="text" class="form-control">
|
||||
<label for="mattermost-channel" class="form-label">Channel Name</label>
|
||||
<input id="mattermost-channel-name" v-model="notification.mattermostchannel" type="text" class="form-control">
|
||||
<div class="form-text">
|
||||
<span style="color:red;"><sup>*</sup></span>Required
|
||||
<p style="margin-top: 8px;">
|
||||
More info about webhooks on: <a href="https://docs.mattermost.com/developer/webhooks-incoming.html" target="_blank">https://docs.mattermost.com/developer/webhooks-incoming.html</a>
|
||||
</p>
|
||||
<p style="margin-top: 8px;">
|
||||
You can override the default channel that webhook posts to by entering the channel name into "Channel Name" field. This needs to be enabled in Mattermost webhook settings. Ex: #other-channel
|
||||
</p>
|
||||
<p style="margin-top: 8px;">
|
||||
If you leave the Uptime Kuma URL field blank, it will default to the Project Github page.
|
||||
</p>
|
||||
<p style="margin-top: 8px;">
|
||||
You can provide a link to a picture in "Icon URL" to override the default profile picture. Will not be used if Icon Emoji is set.
|
||||
</p>
|
||||
<p style="margin-top: 8px;">
|
||||
Emoji cheat sheet: <a href="https://www.webfx.com/tools/emoji-cheat-sheet/" target="_blank">https://www.webfx.com/tools/emoji-cheat-sheet/</a> Note: emoji takes preference over Icon URL.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template v-if="notification.type === 'pushy'">
|
||||
<div class="mb-3">
|
||||
<label for="pushy-app-token" class="form-label">API_KEY</label>
|
||||
<HiddenInput id="pushy-app-token" v-model="notification.pushyAPIKey" :required="true" autocomplete="one-time-code"></HiddenInput>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="pushy-user-key" class="form-label">USER_TOKEN</label>
|
||||
<div class="input-group mb-3">
|
||||
<HiddenInput id="pushy-user-key" v-model="notification.pushyToken" :required="true" autocomplete="one-time-code"></HiddenInput>
|
||||
</div>
|
||||
</div>
|
||||
<p style="margin-top: 8px;">
|
||||
More info on: <a href="https://pushy.me/docs/api/send-notifications" target="_blank">https://pushy.me/docs/api/send-notifications</a>
|
||||
</p>
|
||||
</template>
|
||||
|
||||
<template v-if="notification.type === 'octopush'">
|
||||
<div class="mb-3">
|
||||
<label for="octopush-key" class="form-label">API KEY</label>
|
||||
<HiddenInput id="octopush-key" v-model="notification.octopushAPIKey" :required="true" autocomplete="one-time-code"></HiddenInput>
|
||||
<label for="octopush-login" class="form-label">API LOGIN</label>
|
||||
<input id="octopush-login" v-model="notification.octopushLogin" type="text" class="form-control" required>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="octopush-type-sms" class="form-label">SMS Type</label>
|
||||
<select id="octopush-type-sms" v-model="notification.octopushSMSType" class="form-select">
|
||||
<option value="sms_premium">Premium (Fast - recommended for alerting)</option>
|
||||
<option value="sms_low_cost">Low Cost (Slow, sometimes blocked by operator)</option>
|
||||
</select>
|
||||
<div class="form-text">
|
||||
Check octopush prices <a href="https://octopush.com/tarifs-sms-international/" target="_blank">https://octopush.com/tarifs-sms-international/</a>.
|
||||
</div>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="octopush-phone-number" class="form-label">Phone number (intl format, eg : +33612345678) </label>
|
||||
<input id="octopush-phone-number" v-model="notification.octopushPhoneNumber" type="text" class="form-control" required>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="octopush-sender-name" class="form-label">SMS Sender Name : 3-11 alphanumeric characters and space (a-zA-Z0-9)</label>
|
||||
<input id="octopush-sender-name" v-model="notification.octopushSenderName" type="text" minlength="3" maxlength="11" class="form-control">
|
||||
</div>
|
||||
|
||||
<p style="margin-top: 8px;">
|
||||
More info on: <a href="https://octopush.com/api-sms-documentation/envoi-de-sms/" target="_blank">https://octopush.com/api-sms-documentation/envoi-de-sms/</a>
|
||||
</p>
|
||||
</template>
|
||||
|
||||
<template v-if="notification.type === 'pushover'">
|
||||
<div class="mb-3">
|
||||
<label for="pushover-user" class="form-label">User Key<span style="color: red;"><sup>*</sup></span></label>
|
||||
<HiddenInput id="pushover-user" v-model="notification.pushoveruserkey" :required="true" autocomplete="one-time-code"></HiddenInput>
|
||||
<label for="pushover-app-token" class="form-label">Application Token<span style="color: red;"><sup>*</sup></span></label>
|
||||
<HiddenInput id="pushover-app-token" v-model="notification.pushoverapptoken" :required="true" autocomplete="one-time-code"></HiddenInput>
|
||||
<label for="pushover-device" class="form-label">Device</label>
|
||||
<input id="pushover-device" v-model="notification.pushoverdevice" type="text" class="form-control">
|
||||
<label for="pushover-device" class="form-label">Message Title</label>
|
||||
<input id="pushover-title" v-model="notification.pushovertitle" type="text" class="form-control">
|
||||
<label for="pushover-priority" class="form-label">Priority</label>
|
||||
<select id="pushover-priority" v-model="notification.pushoverpriority" class="form-select">
|
||||
<option>-2</option>
|
||||
<option>-1</option>
|
||||
<option>0</option>
|
||||
<option>1</option>
|
||||
<option>2</option>
|
||||
</select>
|
||||
<label for="pushover-sound" class="form-label">Notification Sound</label>
|
||||
<select id="pushover-sound" v-model="notification.pushoversounds" class="form-select">
|
||||
<option>pushover</option>
|
||||
<option>bike</option>
|
||||
<option>bugle</option>
|
||||
<option>cashregister</option>
|
||||
<option>classical</option>
|
||||
<option>cosmic</option>
|
||||
<option>falling</option>
|
||||
<option>gamelan</option>
|
||||
<option>incoming</option>
|
||||
<option>intermission</option>
|
||||
<option>mechanical</option>
|
||||
<option>pianobar</option>
|
||||
<option>siren</option>
|
||||
<option>spacealarm</option>
|
||||
<option>tugboat</option>
|
||||
<option>alien</option>
|
||||
<option>climb</option>
|
||||
<option>persistent</option>
|
||||
<option>echo</option>
|
||||
<option>updown</option>
|
||||
<option>vibrate</option>
|
||||
<option>none</option>
|
||||
</select>
|
||||
<div class="form-text">
|
||||
<span style="color: red;"><sup>*</sup></span>Required
|
||||
<p style="margin-top: 8px;">
|
||||
More info on: <a href="https://pushover.net/api" target="_blank">https://pushover.net/api</a>
|
||||
</p>
|
||||
<p style="margin-top: 8px;">
|
||||
Emergency priority (2) has default 30 second timeout between retries and will expire after 1 hour.
|
||||
</p>
|
||||
<p style="margin-top: 8px;">
|
||||
If you want to send notifications to different devices, fill out Device field.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template v-if="notification.type === 'apprise'">
|
||||
<div class="mb-3">
|
||||
<label for="apprise-url" class="form-label">Apprise URL</label>
|
||||
<input id="apprise-url" v-model="notification.appriseURL" type="text" class="form-control" required>
|
||||
<div class="form-text">
|
||||
<p>Example: twilio://AccountSid:AuthToken@FromPhoneNo</p>
|
||||
<p>
|
||||
Read more: <a href="https://github.com/caronc/apprise/wiki#notification-services" target="_blank">https://github.com/caronc/apprise/wiki#notification-services</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<p>
|
||||
Status:
|
||||
<span v-if="appriseInstalled" class="text-primary">Apprise is installed</span>
|
||||
<span v-else class="text-danger">Apprise is not installed. <a href="https://github.com/caronc/apprise" target="_blank">Read more</a></span>
|
||||
</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template v-if="notification.type === 'lunasea'">
|
||||
<div class="mb-3">
|
||||
<label for="lunasea-device" class="form-label">LunaSea Device ID<span style="color: red;"><sup>*</sup></span></label>
|
||||
<input id="lunasea-device" v-model="notification.lunaseaDevice" type="text" class="form-control" required>
|
||||
<div class="form-text">
|
||||
<p><span style="color: red;"><sup>*</sup></span>Required</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template v-if="notification.type === 'pushbullet'">
|
||||
<div class="mb-3">
|
||||
<label for="pushbullet-access-token" class="form-label">Access Token</label>
|
||||
<HiddenInput id="pushbullet-access-token" v-model="notification.pushbulletAccessToken" :required="true" autocomplete="one-time-code"></HiddenInput>
|
||||
</div>
|
||||
|
||||
<p style="margin-top: 8px;">
|
||||
More info on: <a href="https://docs.pushbullet.com" target="_blank">https://docs.pushbullet.com</a>
|
||||
</p>
|
||||
</template>
|
||||
|
||||
<template v-if="notification.type === 'line'">
|
||||
<div class="mb-3">
|
||||
<label for="line-channel-access-token" class="form-label">Channel access token</label>
|
||||
<HiddenInput id="line-channel-access-token" v-model="notification.lineChannelAccessToken" :required="true" autocomplete="one-time-code"></HiddenInput>
|
||||
</div>
|
||||
<div class="form-text">
|
||||
Line Developers Console - <b>Basic Settings</b>
|
||||
</div>
|
||||
<div class="mb-3" style="margin-top: 12px;">
|
||||
<label for="line-user-id" class="form-label">User ID</label>
|
||||
<input id="line-user-id" v-model="notification.lineUserID" type="text" class="form-control" required>
|
||||
</div>
|
||||
<div class="form-text">
|
||||
Line Developers Console - <b>Messaging API</b>
|
||||
</div>
|
||||
<div class="form-text" style="margin-top: 8px;">
|
||||
First access the <a href="https://developers.line.biz/console/" target="_blank">Line Developers Console</a>, create a provider and channel (Messaging API), then you can get the channel access token and user id from the above mentioned menu items.
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- DEPRECATED! Please create vue component in "./src/components/notifications/{notification name}.vue" -->
|
||||
|
||||
<Teams v-if="notification.type === 'teams'" />
|
||||
<!-- form body -->
|
||||
<component :is="currentForm" />
|
||||
|
||||
<div class="mb-3 mt-4">
|
||||
<hr class="dropdown-divider mb-4">
|
||||
@ -446,22 +68,15 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { Modal } from "bootstrap"
|
||||
import { ucfirst } from "../util.ts"
|
||||
import { Modal } from "bootstrap";
|
||||
import { ucfirst } from "../util.ts";
|
||||
|
||||
import Confirm from "./Confirm.vue";
|
||||
import HiddenInput from "./HiddenInput.vue";
|
||||
import Telegram from "./notifications/Telegram.vue";
|
||||
import Teams from "./notifications/Teams.vue";
|
||||
import SMTP from "./notifications/SMTP.vue";
|
||||
import NotificationFormList from "./notifications";
|
||||
|
||||
export default {
|
||||
components: {
|
||||
Confirm,
|
||||
HiddenInput,
|
||||
Telegram,
|
||||
Teams,
|
||||
SMTP,
|
||||
},
|
||||
props: {},
|
||||
emits: ["added"],
|
||||
@ -470,43 +85,48 @@ export default {
|
||||
model: null,
|
||||
processing: false,
|
||||
id: null,
|
||||
notificationTypes: Object.keys(NotificationFormList),
|
||||
notification: {
|
||||
name: "",
|
||||
/** @type { null | keyof NotificationFormList } */
|
||||
type: null,
|
||||
isDefault: false,
|
||||
// Do not set default value here, please scroll to show()
|
||||
},
|
||||
appriseInstalled: false,
|
||||
}
|
||||
};
|
||||
},
|
||||
|
||||
computed: {
|
||||
currentForm() {
|
||||
if (!this.notification.type) {
|
||||
return null;
|
||||
}
|
||||
return NotificationFormList[this.notification.type];
|
||||
}
|
||||
},
|
||||
|
||||
watch: {
|
||||
"notification.type"(to, from) {
|
||||
let oldName;
|
||||
|
||||
if (from) {
|
||||
oldName = `My ${ucfirst(from)} Alert (1)`;
|
||||
oldName = this.getUniqueDefaultName(from);
|
||||
} else {
|
||||
oldName = "";
|
||||
}
|
||||
|
||||
if (! this.notification.name || this.notification.name === oldName) {
|
||||
this.notification.name = `My ${ucfirst(to)} Alert (1)`
|
||||
this.notification.name = this.getUniqueDefaultName(to);
|
||||
}
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.modal = new Modal(this.$refs.modal)
|
||||
|
||||
this.$root.getSocket().emit("checkApprise", (installed) => {
|
||||
this.appriseInstalled = installed;
|
||||
})
|
||||
this.modal = new Modal(this.$refs.modal);
|
||||
},
|
||||
methods: {
|
||||
|
||||
deleteConfirm() {
|
||||
this.modal.hide();
|
||||
this.$refs.confirmDelete.show()
|
||||
this.$refs.confirmDelete.show();
|
||||
},
|
||||
|
||||
show(notificationID) {
|
||||
@ -525,21 +145,19 @@ export default {
|
||||
name: "",
|
||||
type: null,
|
||||
isDefault: false,
|
||||
}
|
||||
};
|
||||
|
||||
// Set Default value here
|
||||
this.notification.type = "telegram";
|
||||
this.notification.gotifyPriority = 8;
|
||||
this.notification.smtpSecure = false;
|
||||
this.notification.type = this.notificationTypes[0];
|
||||
}
|
||||
|
||||
this.modal.show()
|
||||
this.modal.show();
|
||||
},
|
||||
|
||||
submit() {
|
||||
this.processing = true;
|
||||
this.$root.getSocket().emit("addNotification", this.notification, this.id, (res) => {
|
||||
this.$root.toastRes(res)
|
||||
this.$root.toastRes(res);
|
||||
this.processing = false;
|
||||
|
||||
if (res.ok) {
|
||||
@ -551,30 +169,45 @@ export default {
|
||||
}
|
||||
|
||||
}
|
||||
})
|
||||
});
|
||||
},
|
||||
|
||||
test() {
|
||||
this.processing = true;
|
||||
this.$root.getSocket().emit("testNotification", this.notification, (res) => {
|
||||
this.$root.toastRes(res)
|
||||
this.$root.toastRes(res);
|
||||
this.processing = false;
|
||||
})
|
||||
});
|
||||
},
|
||||
|
||||
deleteNotification() {
|
||||
this.processing = true;
|
||||
this.$root.getSocket().emit("deleteNotification", this.id, (res) => {
|
||||
this.$root.toastRes(res)
|
||||
this.$root.toastRes(res);
|
||||
this.processing = false;
|
||||
|
||||
if (res.ok) {
|
||||
this.modal.hide()
|
||||
this.modal.hide();
|
||||
}
|
||||
})
|
||||
});
|
||||
},
|
||||
/**
|
||||
* @param {keyof NotificationFormList} notificationKey
|
||||
* @return {string}
|
||||
*/
|
||||
getUniqueDefaultName(notificationKey) {
|
||||
let index = 1;
|
||||
let name = "";
|
||||
do {
|
||||
name = this.$t("defaultNotificationName", {
|
||||
notification: this.$t(notificationKey).replace(/\(.+\)/, "").trim(),
|
||||
number: index++
|
||||
});
|
||||
} while (this.$root.notificationList.find(it => it.name === name));
|
||||
return name;
|
||||
}
|
||||
},
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
@ -11,18 +11,18 @@ export default {
|
||||
computed: {
|
||||
color() {
|
||||
if (this.status === 0) {
|
||||
return "danger"
|
||||
return "danger";
|
||||
}
|
||||
|
||||
if (this.status === 1) {
|
||||
return "primary"
|
||||
return "primary";
|
||||
}
|
||||
|
||||
if (this.status === 2) {
|
||||
return "warning"
|
||||
return "warning";
|
||||
}
|
||||
|
||||
return "secondary"
|
||||
return "secondary";
|
||||
},
|
||||
|
||||
text() {
|
||||
@ -41,11 +41,11 @@ export default {
|
||||
return this.$t("Unknown");
|
||||
},
|
||||
},
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
span {
|
||||
width: 64px;
|
||||
min-width: 64px;
|
||||
}
|
||||
</style>
|
||||
|
@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<div>
|
||||
<h4 class="mb-3">{{ $t("Tags") }}</h4>
|
||||
<div class="mb-3 p-1">
|
||||
<h4 class="mt-5 mb-3">{{ $t("Tags") }}</h4>
|
||||
<div v-if="selectedTags.length > 0" class="mb-2 p-1">
|
||||
<tag
|
||||
v-for="item in selectedTags"
|
||||
:key="item.id"
|
||||
@ -124,8 +124,8 @@
|
||||
import { Modal } from "bootstrap";
|
||||
import VueMultiselect from "vue-multiselect";
|
||||
import Tag from "../components/Tag.vue";
|
||||
import { useToast } from "vue-toastification"
|
||||
const toast = useToast()
|
||||
import { useToast } from "vue-toastification";
|
||||
const toast = useToast();
|
||||
|
||||
export default {
|
||||
components: {
|
||||
@ -186,7 +186,7 @@ export default {
|
||||
color: "#7C3AED" },
|
||||
{ name: this.$t("Pink"),
|
||||
color: "#DB2777" },
|
||||
]
|
||||
];
|
||||
},
|
||||
validateDraftTag() {
|
||||
let nameInvalid = false;
|
||||
@ -227,7 +227,7 @@ export default {
|
||||
invalid,
|
||||
nameInvalid,
|
||||
valueInvalid,
|
||||
}
|
||||
};
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
@ -243,7 +243,7 @@ export default {
|
||||
if (res.ok) {
|
||||
this.existingTags = res.tags;
|
||||
} else {
|
||||
toast.error(res.msg)
|
||||
toast.error(res.msg);
|
||||
}
|
||||
});
|
||||
},
|
||||
@ -277,7 +277,7 @@ export default {
|
||||
name: this.newDraftTag.select.name,
|
||||
value: this.newDraftTag.value,
|
||||
new: true,
|
||||
})
|
||||
});
|
||||
}
|
||||
} else {
|
||||
// Add new Tag
|
||||
@ -286,7 +286,7 @@ export default {
|
||||
name: this.newDraftTag.name.trim(),
|
||||
value: this.newDraftTag.value,
|
||||
new: true,
|
||||
})
|
||||
});
|
||||
}
|
||||
this.clearDraftTag();
|
||||
},
|
||||
@ -348,7 +348,7 @@ export default {
|
||||
if (tag.name == newTag.name && tag.color == newTag.color) {
|
||||
tag.id = newTagResult.id;
|
||||
}
|
||||
})
|
||||
});
|
||||
} else {
|
||||
tagId = newTag.id;
|
||||
}
|
||||
|
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>
|
35
src/components/notifications/Apprise.vue
Normal file
35
src/components/notifications/Apprise.vue
Normal file
@ -0,0 +1,35 @@
|
||||
<template>
|
||||
<div class="mb-3">
|
||||
<label for="apprise-url" class="form-label">{{ $t("Apprise URL") }}</label>
|
||||
<input id="apprise-url" v-model="$parent.notification.appriseURL" type="text" class="form-control" required>
|
||||
<div class="form-text">
|
||||
<p>{{ $t("Example:", ["twilio://AccountSid:AuthToken@FromPhoneNo"]) }}</p>
|
||||
<i18n-t tag="p" keypath="Read more:">
|
||||
<a href="https://github.com/caronc/apprise/wiki#notification-services" target="_blank">https://github.com/caronc/apprise/wiki#notification-services</a>
|
||||
</i18n-t>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<i18n-t tag="p" keypath="Status:">
|
||||
<span v-if="appriseInstalled" class="text-primary">{{ $t("appriseInstalled") }}</span>
|
||||
<i18n-t v-else tag="span" keypath="appriseNotInstalled" class="text-danger">
|
||||
<a href="https://github.com/caronc/apprise" target="_blank">{{ $t("Read more") }}</a>
|
||||
</i18n-t>
|
||||
</i18n-t>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
appriseInstalled: false
|
||||
};
|
||||
},
|
||||
mounted() {
|
||||
this.$root.getSocket().emit("checkApprise", (installed) => {
|
||||
this.appriseInstalled = installed;
|
||||
});
|
||||
},
|
||||
};
|
||||
</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>
|
19
src/components/notifications/Discord.vue
Normal file
19
src/components/notifications/Discord.vue
Normal file
@ -0,0 +1,19 @@
|
||||
<template>
|
||||
<div class="mb-3">
|
||||
<label for="discord-webhook-url" class="form-label">{{ $t("Discord Webhook URL") }}</label>
|
||||
<input id="discord-webhook-url" v-model="$parent.notification.discordWebhookUrl" type="text" class="form-control" required autocomplete="false">
|
||||
<div class="form-text">
|
||||
{{ $t("wayToGetDiscordURL") }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="discord-username" class="form-label">{{ $t("Bot Display Name") }}</label>
|
||||
<input id="discord-username" v-model="$parent.notification.discordUsername" type="text" class="form-control" autocomplete="false" :placeholder="$root.appName">
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="discord-prefix-message" class="form-label">{{ $t("Prefix Custom Message") }}</label>
|
||||
<input id="discord-prefix-message" v-model="$parent.notification.discordPrefixMessage" type="text" class="form-control" autocomplete="false" :placeholder="$t('Hello @everyone is...')">
|
||||
</div>
|
||||
</template>
|
15
src/components/notifications/Feishu.vue
Normal file
15
src/components/notifications/Feishu.vue
Normal file
@ -0,0 +1,15 @@
|
||||
<template>
|
||||
<div class="mb-3">
|
||||
<label for="Feishu-WebHookUrl" class="form-label">{{ $t("Feishu WebHookUrl") }}<span style="color: red;"><sup>*</sup></span></label>
|
||||
<input id="Feishu-WebHookUrl" v-model="$parent.notification.feishuWebHookUrl" 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://www.feishu.cn/hc/zh-CN/articles/360024984973"
|
||||
target="_blank"
|
||||
>{{ $t("here") }}</a>
|
||||
</i18n-t>
|
||||
</div>
|
||||
</template>
|
32
src/components/notifications/Gotify.vue
Normal file
32
src/components/notifications/Gotify.vue
Normal file
@ -0,0 +1,32 @@
|
||||
<template>
|
||||
<div class="mb-3">
|
||||
<label for="gotify-application-token" class="form-label">{{ $t("Application Token") }}</label>
|
||||
<HiddenInput id="gotify-application-token" v-model="$parent.notification.gotifyapplicationToken" :required="true" autocomplete="one-time-code"></HiddenInput>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="gotify-server-url" class="form-label">{{ $t("Server URL") }}</label>
|
||||
<div class="input-group mb-3">
|
||||
<input id="gotify-server-url" v-model="$parent.notification.gotifyserverurl" type="text" class="form-control" required>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="gotify-priority" class="form-label">{{ $t("Priority") }}</label>
|
||||
<input id="gotify-priority" v-model="$parent.notification.gotifyPriority" type="number" class="form-control" required min="0" max="10" step="1">
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import HiddenInput from "../HiddenInput.vue";
|
||||
|
||||
export default {
|
||||
components: {
|
||||
HiddenInput,
|
||||
},
|
||||
mounted() {
|
||||
if (typeof this.$parent.notification.gotifyPriority === "undefined") {
|
||||
this.$parent.notification.gotifyPriority = 8;
|
||||
}
|
||||
},
|
||||
}
|
||||
</script>
|
29
src/components/notifications/Line.vue
Normal file
29
src/components/notifications/Line.vue
Normal file
@ -0,0 +1,29 @@
|
||||
<template>
|
||||
<div class="mb-3">
|
||||
<label for="line-channel-access-token" class="form-label">{{ $t("Channel access token") }}</label>
|
||||
<HiddenInput id="line-channel-access-token" v-model="$parent.notification.lineChannelAccessToken" :required="true" autocomplete="one-time-code"></HiddenInput>
|
||||
</div>
|
||||
<i18n-t tag="div" keypath="lineDevConsoleTo" class="form-text">
|
||||
<b>{{ $t("Basic Settings") }}</b>
|
||||
</i18n-t>
|
||||
<div class="mb-3" style="margin-top: 12px;">
|
||||
<label for="line-user-id" class="form-label">User ID</label>
|
||||
<input id="line-user-id" v-model="$parent.notification.lineUserID" type="text" class="form-control" required>
|
||||
</div>
|
||||
<i18n-t tag="div" keypath="lineDevConsoleTo" class="form-text">
|
||||
<b>{{ $t("Messaging API") }}</b>
|
||||
</i18n-t>
|
||||
<i18n-t tag="div" keypath="wayToGetLineChannelToken" class="form-text" style="margin-top: 8px;">
|
||||
<a href="https://developers.line.biz/console/" target="_blank">{{ $t("Line Developers Console") }}</a>
|
||||
</i18n-t>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import HiddenInput from "../HiddenInput.vue";
|
||||
|
||||
export default {
|
||||
components: {
|
||||
HiddenInput,
|
||||
},
|
||||
};
|
||||
</script>
|
9
src/components/notifications/LunaSea.vue
Normal file
9
src/components/notifications/LunaSea.vue
Normal file
@ -0,0 +1,9 @@
|
||||
<template>
|
||||
<div class="mb-3">
|
||||
<label for="lunasea-device" class="form-label">{{ $t("LunaSea Device ID") }}<span style="color: red;"><sup>*</sup></span></label>
|
||||
<input id="lunasea-device" v-model="$parent.notification.lunaseaDevice" type="text" class="form-control" required>
|
||||
<div class="form-text">
|
||||
<p><span style="color: red;"><sup>*</sup></span>{{ $t("Required") }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
34
src/components/notifications/Matrix.vue
Normal file
34
src/components/notifications/Matrix.vue
Normal file
@ -0,0 +1,34 @@
|
||||
<template>
|
||||
<div class="mb-3">
|
||||
<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">
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<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">
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<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>
|
||||
</div>
|
||||
|
||||
<div class="form-text">
|
||||
<span style="color: red;"><sup>*</sup></span>{{ $t("Required") }}
|
||||
<p style="margin-top: 8px;">
|
||||
{{ $t("matrixDesc1") }}
|
||||
</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>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import HiddenInput from "../HiddenInput.vue";
|
||||
|
||||
export default {
|
||||
components: {
|
||||
HiddenInput,
|
||||
},
|
||||
};
|
||||
</script>
|
32
src/components/notifications/Mattermost.vue
Normal file
32
src/components/notifications/Mattermost.vue
Normal file
@ -0,0 +1,32 @@
|
||||
<template>
|
||||
<div class="mb-3">
|
||||
<label for="mattermost-webhook-url" class="form-label">{{ $t("Webhook URL") }}<span style="color:red;"><sup>*</sup></span></label>
|
||||
<input id="mattermost-webhook-url" v-model="$parent.notification.mattermostWebhookUrl" type="text" class="form-control" required>
|
||||
<label for="mattermost-username" class="form-label">{{ $t("Username") }}</label>
|
||||
<input id="mattermost-username" v-model="$parent.notification.mattermostusername" type="text" class="form-control">
|
||||
<label for="mattermost-iconurl" class="form-label">{{ $t("Icon URL") }}</label>
|
||||
<input id="mattermost-iconurl" v-model="$parent.notification.mattermosticonurl" type="text" class="form-control">
|
||||
<label for="mattermost-iconemo" class="form-label">{{ $t("Icon Emoji") }}</label>
|
||||
<input id="mattermost-iconemo" v-model="$parent.notification.mattermosticonemo" type="text" class="form-control">
|
||||
<label for="mattermost-channel" class="form-label">{{ $t("Channel Name") }}</label>
|
||||
<input id="mattermost-channel-name" v-model="$parent.notification.mattermostchannel" type="text" class="form-control">
|
||||
<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://docs.mattermost.com/developer/webhooks-incoming.html" target="_blank">https://docs.mattermost.com/developer/webhooks-incoming.html</a>
|
||||
</i18n-t>
|
||||
<p style="margin-top: 8px;">
|
||||
{{ $t("aboutMattermostChannelName") }}
|
||||
</p>
|
||||
<p style="margin-top: 8px;">
|
||||
{{ $t("aboutKumaURL") }}
|
||||
</p>
|
||||
<p style="margin-top: 8px;">
|
||||
{{ $t("aboutIconURL") }}
|
||||
</p>
|
||||
<i18n-t tag="p" keypath="emojiCheatSheet" style="margin-top: 8px;">
|
||||
<a href="https://www.webfx.com/tools/emoji-cheat-sheet/" target="_blank">https://www.webfx.com/tools/emoji-cheat-sheet/</a>
|
||||
</i18n-t>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
50
src/components/notifications/Octopush.vue
Normal file
50
src/components/notifications/Octopush.vue
Normal file
@ -0,0 +1,50 @@
|
||||
<template>
|
||||
<div class="mb-3">
|
||||
<label for="octopush-version" class="form-label">Octopush API Version</label>
|
||||
<select id="octopush-version" v-model="$parent.notification.octopushVersion" class="form-select">
|
||||
<option value="2">Octopush (endpoint: api.octopush.com)</option>
|
||||
<option value="1">Legacy Octopush-DM (endpoint: www.octopush-dm.com)</option>
|
||||
</select>
|
||||
<div class="form-text">
|
||||
{{ $t("octopushLegacyHint") }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="octopush-key" class="form-label">API KEY</label>
|
||||
<HiddenInput id="octopush-key" v-model="$parent.notification.octopushAPIKey" :required="true" autocomplete="one-time-code"></HiddenInput>
|
||||
<label for="octopush-login" class="form-label">API LOGIN</label>
|
||||
<input id="octopush-login" v-model="$parent.notification.octopushLogin" type="text" class="form-control" required>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="octopush-type-sms" class="form-label">{{ $t("SMS Type") }}</label>
|
||||
<select id="octopush-type-sms" v-model="$parent.notification.octopushSMSType" class="form-select">
|
||||
<option value="sms_premium">{{ $t("octopushTypePremium") }}</option>
|
||||
<option value="sms_low_cost">{{ $t("octopushTypeLowCost") }}</option>
|
||||
</select>
|
||||
<i18n-t tag="div" keypath="Check octopush prices" class="form-text">
|
||||
<a href="https://octopush.com/tarifs-sms-international/" target="_blank">https://octopush.com/tarifs-sms-international/</a>
|
||||
</i18n-t>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="octopush-phone-number" class="form-label">{{ $t("octopushPhoneNumber") }}</label>
|
||||
<input id="octopush-phone-number" v-model="$parent.notification.octopushPhoneNumber" type="text" class="form-control" required>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="octopush-sender-name" class="form-label">{{ $t("octopushSMSSender") }}</label>
|
||||
<input id="octopush-sender-name" v-model="$parent.notification.octopushSenderName" type="text" minlength="3" maxlength="11" class="form-control">
|
||||
</div>
|
||||
|
||||
<i18n-t tag="p" keypath="More info on:" style="margin-top: 8px;">
|
||||
<a href="https://octopush.com/api-sms-documentation/envoi-de-sms/" target="_blank">https://octopush.com/api-sms-documentation/envoi-de-sms/</a>
|
||||
</i18n-t>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import HiddenInput from "../HiddenInput.vue";
|
||||
|
||||
export default {
|
||||
components: {
|
||||
HiddenInput,
|
||||
},
|
||||
};
|
||||
</script>
|
39
src/components/notifications/PromoSMS.vue
Normal file
39
src/components/notifications/PromoSMS.vue
Normal file
@ -0,0 +1,39 @@
|
||||
<template>
|
||||
<div class="mb-3">
|
||||
<label for="promosms-login" class="form-label">API LOGIN</label>
|
||||
<input id="promosms-login" v-model="$parent.notification.promosmsLogin" type="text" class="form-control" required>
|
||||
<label for="promosms-key" class="form-label">API PASSWORD</label>
|
||||
<HiddenInput id="promosms-key" v-model="$parent.notification.promosmsPassword" :required="true" autocomplete="one-time-code"></HiddenInput>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="promosms-type-sms" class="form-label">{{ $t("SMS Type") }}</label>
|
||||
<select id="promosms-type-sms" v-model="$parent.notification.promosmsSMSType" class="form-select">
|
||||
<option value="0">{{ $t("promosmsTypeFlash") }}</option>
|
||||
<option value="1">{{ $t("promosmsTypeEco") }}</option>
|
||||
<option value="2">{{ $t("promosmsTypeFull") }}</option>
|
||||
<option value="3">{{ $t("promosmsTypeSpeed") }}</option>
|
||||
</select>
|
||||
<div class="form-text">
|
||||
{{ $t("checkPrice", [$t("promosms")]) }}
|
||||
<a href="https://promosms.com/cennik/" target="_blank">https://promosms.com/cennik/</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="promosms-phone-number" class="form-label">{{ $t("promosmsPhoneNumber") }}</label>
|
||||
<input id="promosms-phone-number" v-model="$parent.notification.promosmsPhoneNumber" type="text" class="form-control" required>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<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">
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import HiddenInput from "../HiddenInput.vue";
|
||||
|
||||
export default {
|
||||
components: {
|
||||
HiddenInput,
|
||||
},
|
||||
};
|
||||
</script>
|
20
src/components/notifications/Pushbullet.vue
Normal file
20
src/components/notifications/Pushbullet.vue
Normal file
@ -0,0 +1,20 @@
|
||||
<template>
|
||||
<div class="mb-3">
|
||||
<label for="pushbullet-access-token" class="form-label">{{ $t("Access Token") }}</label>
|
||||
<HiddenInput id="pushbullet-access-token" v-model="$parent.notification.pushbulletAccessToken" :required="true" autocomplete="one-time-code"></HiddenInput>
|
||||
</div>
|
||||
|
||||
<i18n-t tag="p" keypath="More info on:" style="margin-top: 8px;">
|
||||
<a href="https://docs.pushbullet.com" target="_blank">https://docs.pushbullet.com</a>
|
||||
</i18n-t>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import HiddenInput from "../HiddenInput.vue";
|
||||
|
||||
export default {
|
||||
components: {
|
||||
HiddenInput,
|
||||
},
|
||||
};
|
||||
</script>
|
67
src/components/notifications/Pushover.vue
Normal file
67
src/components/notifications/Pushover.vue
Normal file
@ -0,0 +1,67 @@
|
||||
<template>
|
||||
<div class="mb-3">
|
||||
<label for="pushover-user" class="form-label">{{ $t("User Key") }}<span style="color: red;"><sup>*</sup></span></label>
|
||||
<HiddenInput id="pushover-user" v-model="$parent.notification.pushoveruserkey" :required="true" autocomplete="one-time-code"></HiddenInput>
|
||||
<label for="pushover-app-token" class="form-label">{{ $t("Application Token") }}<span style="color: red;"><sup>*</sup></span></label>
|
||||
<HiddenInput id="pushover-app-token" v-model="$parent.notification.pushoverapptoken" :required="true" autocomplete="one-time-code"></HiddenInput>
|
||||
<label for="pushover-device" class="form-label">{{ $t("Device") }}</label>
|
||||
<input id="pushover-device" v-model="$parent.notification.pushoverdevice" type="text" class="form-control">
|
||||
<label for="pushover-device" class="form-label">{{ $t("Message Title") }}</label>
|
||||
<input id="pushover-title" v-model="$parent.notification.pushovertitle" type="text" class="form-control">
|
||||
<label for="pushover-priority" class="form-label">{{ $t("Priority") }}</label>
|
||||
<select id="pushover-priority" v-model="$parent.notification.pushoverpriority" class="form-select">
|
||||
<option>-2</option>
|
||||
<option>-1</option>
|
||||
<option>0</option>
|
||||
<option>1</option>
|
||||
<option>2</option>
|
||||
</select>
|
||||
<label for="pushover-sound" class="form-label">{{ $t("Notification Sound") }}</label>
|
||||
<select id="pushover-sound" v-model="$parent.notification.pushoversounds" class="form-select">
|
||||
<option>pushover</option>
|
||||
<option>bike</option>
|
||||
<option>bugle</option>
|
||||
<option>cashregister</option>
|
||||
<option>classical</option>
|
||||
<option>cosmic</option>
|
||||
<option>falling</option>
|
||||
<option>gamelan</option>
|
||||
<option>incoming</option>
|
||||
<option>intermission</option>
|
||||
<option>mechanical</option>
|
||||
<option>pianobar</option>
|
||||
<option>siren</option>
|
||||
<option>spacealarm</option>
|
||||
<option>tugboat</option>
|
||||
<option>alien</option>
|
||||
<option>climb</option>
|
||||
<option>persistent</option>
|
||||
<option>echo</option>
|
||||
<option>updown</option>
|
||||
<option>vibrate</option>
|
||||
<option>none</option>
|
||||
</select>
|
||||
<div class="form-text">
|
||||
<span style="color: red;"><sup>*</sup></span>{{ $t("Required") }}
|
||||
<i18n-t tag="p" keypath="More info on:" style="margin-top: 8px;">
|
||||
<a href="https://pushover.net/api" target="_blank">https://pushover.net/api</a>
|
||||
</i18n-t>
|
||||
<p style="margin-top: 8px;">
|
||||
{{ $t("pushoverDesc1") }}
|
||||
</p>
|
||||
<p style="margin-top: 8px;">
|
||||
{{ $t("pushoverDesc2") }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import HiddenInput from "../HiddenInput.vue";
|
||||
|
||||
export default {
|
||||
components: {
|
||||
HiddenInput,
|
||||
},
|
||||
}
|
||||
</script>
|
26
src/components/notifications/Pushy.vue
Normal file
26
src/components/notifications/Pushy.vue
Normal file
@ -0,0 +1,26 @@
|
||||
<template>
|
||||
<div class="mb-3">
|
||||
<label for="pushy-app-token" class="form-label">API_KEY</label>
|
||||
<HiddenInput id="pushy-app-token" v-model="$parent.notification.pushyAPIKey" :required="true" autocomplete="one-time-code"></HiddenInput>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="pushy-user-key" class="form-label">USER_TOKEN</label>
|
||||
<div class="input-group mb-3">
|
||||
<HiddenInput id="pushy-user-key" v-model="$parent.notification.pushyToken" :required="true" autocomplete="one-time-code"></HiddenInput>
|
||||
</div>
|
||||
</div>
|
||||
<i18n-t tag="p" keypath="More info on:" style="margin-top: 8px;">
|
||||
<a href="https://pushy.me/docs/api/send-notifications" target="_blank">https://pushy.me/docs/api/send-notifications</a>
|
||||
</i18n-t>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import HiddenInput from "../HiddenInput.vue";
|
||||
|
||||
export default {
|
||||
components: {
|
||||
HiddenInput,
|
||||
},
|
||||
};
|
||||
</script>
|
27
src/components/notifications/RocketChat.vue
Normal file
27
src/components/notifications/RocketChat.vue
Normal file
@ -0,0 +1,27 @@
|
||||
<template>
|
||||
<div class="mb-3">
|
||||
<label for="rocket-webhook-url" class="form-label">{{ $t("Webhook URL") }}<span style="color: red;"><sup>*</sup></span></label>
|
||||
<input id="rocket-webhook-url" v-model="$parent.notification.rocketwebhookURL" type="text" class="form-control" required>
|
||||
<label for="rocket-username" class="form-label">{{ $t("Username") }}</label>
|
||||
<input id="rocket-username" v-model="$parent.notification.rocketusername" type="text" class="form-control">
|
||||
<label for="rocket-iconemo" class="form-label">{{ $t("Icon Emoji") }}</label>
|
||||
<input id="rocket-iconemo" v-model="$parent.notification.rocketiconemo" type="text" class="form-control">
|
||||
<label for="rocket-channel" class="form-label">{{ $t("Channel Name") }}</label>
|
||||
<input id="rocket-channel-name" v-model="$parent.notification.rocketchannel" type="text" class="form-control">
|
||||
<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://docs.rocket.chat/guides/administration/administration/integrations" target="_blank">https://api.slack.com/messaging/webhooks</a>
|
||||
</i18n-t>
|
||||
<p style="margin-top: 8px;">
|
||||
{{ $t("aboutChannelName", [$t("rocket.chat")]) }}
|
||||
</p>
|
||||
<p style="margin-top: 8px;">
|
||||
{{ $t("aboutKumaURL") }}
|
||||
</p>
|
||||
<i18n-t tag="p" keypath="emojiCheatSheet" style="margin-top: 8px;">
|
||||
<a href="https://www.webfx.com/tools/emoji-cheat-sheet/" target="_blank">https://www.webfx.com/tools/emoji-cheat-sheet/</a>
|
||||
</i18n-t>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
@ -12,8 +12,8 @@
|
||||
<div class="mb-3">
|
||||
<label for="secure" class="form-label">Secure</label>
|
||||
<select id="secure" v-model="$parent.notification.smtpSecure" class="form-select">
|
||||
<option :value="false">None / STARTTLS (25, 587)</option>
|
||||
<option :value="true">TLS (465)</option>
|
||||
<option :value="false">{{ $t("secureOptionNone") }}</option>
|
||||
<option :value="true">{{ $t("secureOptionTLS") }}</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
@ -21,7 +21,7 @@
|
||||
<div class="form-check">
|
||||
<input id="ignore-tls-error" v-model="$parent.notification.smtpIgnoreTLSError" class="form-check-input" type="checkbox" value="">
|
||||
<label class="form-check-label" for="ignore-tls-error">
|
||||
Ignore TLS Error
|
||||
{{ $t("Ignore TLS Error") }}
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
@ -37,25 +37,37 @@
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="from-email" class="form-label">From Email</label>
|
||||
<label for="from-email" class="form-label">{{ $t("From Email") }}</label>
|
||||
<input id="from-email" v-model="$parent.notification.smtpFrom" type="text" class="form-control" required autocomplete="false" placeholder=""Uptime Kuma" <example@kuma.pet>">
|
||||
<div class="form-text">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="to-email" class="form-label">To Email</label>
|
||||
<input id="to-email" v-model="$parent.notification.smtpTo" type="text" class="form-control" required autocomplete="false" placeholder="example2@kuma.pet, example3@kuma.pet">
|
||||
<label for="to-email" class="form-label">{{ $t("To Email") }}</label>
|
||||
<input id="to-email" v-model="$parent.notification.smtpTo" type="text" class="form-control" autocomplete="false" placeholder="example2@kuma.pet, example3@kuma.pet" :required="!hasRecipient">
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="to-cc" class="form-label">CC</label>
|
||||
<input id="to-cc" v-model="$parent.notification.smtpCC" type="text" class="form-control" autocomplete="false">
|
||||
<label for="to-cc" class="form-label">{{ $t("smtpCC") }}</label>
|
||||
<input id="to-cc" v-model="$parent.notification.smtpCC" type="text" class="form-control" autocomplete="false" :required="!hasRecipient">
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="to-bcc" class="form-label">BCC</label>
|
||||
<input id="to-bcc" v-model="$parent.notification.smtpBCC" type="text" class="form-control" autocomplete="false">
|
||||
<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">
|
||||
</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>
|
||||
|
||||
@ -66,10 +78,19 @@ export default {
|
||||
components: {
|
||||
HiddenInput,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
name: "smtp",
|
||||
computed: {
|
||||
hasRecipient() {
|
||||
if (this.$parent.notification.smtpTo || this.$parent.notification.smtpCC || this.$parent.notification.smtpBCC) {
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
mounted() {
|
||||
if (typeof this.$parent.notification.smtpSecure === "undefined") {
|
||||
this.$parent.notification.smtpSecure = false;
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
34
src/components/notifications/Signal.vue
Normal file
34
src/components/notifications/Signal.vue
Normal file
@ -0,0 +1,34 @@
|
||||
<template>
|
||||
<div class="mb-3">
|
||||
<label for="signal-url" class="form-label">{{ $t("Post URL") }}</label>
|
||||
<input id="signal-url" v-model="$parent.notification.signalURL" type="url" pattern="https?://.+" class="form-control" required>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="signal-number" class="form-label">{{ $t("Number") }}</label>
|
||||
<input id="signal-number" v-model="$parent.notification.signalNumber" type="text" class="form-control" required>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="signal-recipients" class="form-label">{{ $t("Recipients") }}</label>
|
||||
<input id="signal-recipients" v-model="$parent.notification.signalRecipients" type="text" class="form-control" required>
|
||||
|
||||
<div class="form-text">
|
||||
<p style="margin-top: 8px;">
|
||||
{{ $t("needSignalAPI") }}
|
||||
</p>
|
||||
|
||||
<p style="margin-top: 8px;">
|
||||
{{ $t("wayToCheckSignalURL") }}
|
||||
</p>
|
||||
|
||||
<p style="margin-top: 8px;">
|
||||
<a href="https://github.com/bbernhard/signal-cli-rest-api" target="_blank">https://github.com/bbernhard/signal-cli-rest-api</a>
|
||||
</p>
|
||||
|
||||
<p style="margin-top: 8px;">
|
||||
{{ $t("signalImportant") }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
28
src/components/notifications/Slack.vue
Normal file
28
src/components/notifications/Slack.vue
Normal file
@ -0,0 +1,28 @@
|
||||
<template>
|
||||
<div class="mb-3">
|
||||
<label for="slack-webhook-url" class="form-label">{{ $t("Webhook URL") }}<span style="color: red;"><sup>*</sup></span></label>
|
||||
<input id="slack-webhook-url" v-model="$parent.notification.slackwebhookURL" type="text" class="form-control" required>
|
||||
<label for="slack-username" class="form-label">{{ $t("Username") }}</label>
|
||||
<input id="slack-username" v-model="$parent.notification.slackusername" type="text" class="form-control">
|
||||
<label for="slack-iconemo" class="form-label">{{ $t("Icon Emoji") }}</label>
|
||||
<input id="slack-iconemo" v-model="$parent.notification.slackiconemo" type="text" class="form-control">
|
||||
<label for="slack-channel" class="form-label">{{ $t("Channel Name") }}</label>
|
||||
<input id="slack-channel-name" v-model="$parent.notification.slackchannel" type="text" class="form-control">
|
||||
|
||||
<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://api.slack.com/messaging/webhooks" target="_blank">https://api.slack.com/messaging/webhooks</a>
|
||||
</i18n-t>
|
||||
<p style="margin-top: 8px;">
|
||||
{{ $t("aboutChannelName", [$t("slack")]) }}
|
||||
</p>
|
||||
<p style="margin-top: 8px;">
|
||||
{{ $t("aboutKumaURL") }}
|
||||
</p>
|
||||
<i18n-t tag="p" keypath="emojiCheatSheet" style="margin-top: 8px;">
|
||||
<a href="https://www.webfx.com/tools/emoji-cheat-sheet/" target="_blank">https://www.webfx.com/tools/emoji-cheat-sheet/</a>
|
||||
</i18n-t>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user