mirror of
https://github.com/louislam/uptime-kuma.git
synced 2024-11-27 16:54:04 +00:00
Merge remote-tracking branch 'origin/master' into karelkryda_master
# Conflicts: # server/database.js # server/model/monitor.js # server/routers/api-router.js # server/server.js # src/components/HeartbeatBar.vue # src/components/MonitorList.vue # src/icon.js # src/layouts/Layout.vue # src/mixins/datetime.js # src/mixins/socket.js # src/router.js # src/util.js
This commit is contained in:
commit
90761cf831
161 changed files with 15369 additions and 8111 deletions
|
@ -1,6 +1,7 @@
|
||||||
/.idea
|
/.idea
|
||||||
/node_modules
|
/node_modules
|
||||||
/data
|
/data
|
||||||
|
/cypress
|
||||||
/out
|
/out
|
||||||
/test
|
/test
|
||||||
/kubernetes
|
/kubernetes
|
||||||
|
|
2
.github/PULL_REQUEST_TEMPLATE.md
vendored
2
.github/PULL_REQUEST_TEMPLATE.md
vendored
|
@ -1,3 +1,5 @@
|
||||||
|
👉 Delete this line if you have read and agree our pull request rules and guidelines: https://github.com/louislam/uptime-kuma/blob/master/CONTRIBUTING.md#can-i-create-a-pull-request-for-uptime-kuma
|
||||||
|
|
||||||
# Description
|
# Description
|
||||||
|
|
||||||
Fixes #(issue)
|
Fixes #(issue)
|
||||||
|
|
16
.github/workflows/auto-test.yml
vendored
16
.github/workflows/auto-test.yml
vendored
|
@ -50,3 +50,19 @@ jobs:
|
||||||
cache: 'npm'
|
cache: 'npm'
|
||||||
- run: npm install
|
- run: npm install
|
||||||
- run: npm run lint
|
- run: npm run lint
|
||||||
|
|
||||||
|
e2e-tests:
|
||||||
|
needs: [ check-linters ]
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- run: git config --global core.autocrlf false # Mainly for Windows
|
||||||
|
- uses: actions/checkout@v3
|
||||||
|
|
||||||
|
- name: Use Node.js 14
|
||||||
|
uses: actions/setup-node@v3
|
||||||
|
with:
|
||||||
|
node-version: 14
|
||||||
|
cache: 'npm'
|
||||||
|
- run: npm install
|
||||||
|
- run: npm run build
|
||||||
|
- run: npm run cy:test
|
||||||
|
|
22
.github/workflows/stale-bot.yml
vendored
Normal file
22
.github/workflows/stale-bot.yml
vendored
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
name: 'Automatically close stale issues and PRs'
|
||||||
|
on:
|
||||||
|
schedule:
|
||||||
|
- cron: '0 0 * * *'
|
||||||
|
#Run once a day at midnight
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
stale:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/stale@v5
|
||||||
|
with:
|
||||||
|
stale-issue-message: 'We are clearing up our old issues and your ticket has been open for 3 months with no activity. Remove stale label or comment or this will be closed in 7 days.'
|
||||||
|
stale-pr-message: 'We are clearing up our old Pull Requests and yours has been open for 3 months with no activity. Remove stale label or comment or this will be closed in 7 days.'
|
||||||
|
close-issue-message: 'This issue was closed because it has been stalled for 7 days with no activity.'
|
||||||
|
close-pr-message: 'This PR was closed because it has been stalled for 7 days with no activity.'
|
||||||
|
days-before-stale: 90
|
||||||
|
days-before-close: 7
|
||||||
|
exempt-issue-labels: 'News,Medium,High,discussion,bug,doc,feature-request'
|
||||||
|
exempt-pr-labels: 'awaiting-approval,work-in-progress,enhancement,feature-request'
|
||||||
|
exempt-issue-assignees: 'louislam'
|
||||||
|
exempt-pr-assignees: 'louislam'
|
3
.gitignore
vendored
3
.gitignore
vendored
|
@ -13,3 +13,6 @@ dist-ssr
|
||||||
/out
|
/out
|
||||||
/tmp
|
/tmp
|
||||||
.env
|
.env
|
||||||
|
|
||||||
|
cypress/videos
|
||||||
|
cypress/screenshots
|
||||||
|
|
|
@ -8,6 +8,7 @@
|
||||||
"declaration-empty-line-before": null,
|
"declaration-empty-line-before": null,
|
||||||
"alpha-value-notation": "number",
|
"alpha-value-notation": "number",
|
||||||
"color-function-notation": "legacy",
|
"color-function-notation": "legacy",
|
||||||
"shorthand-property-no-redundant-values": null
|
"shorthand-property-no-redundant-values": null,
|
||||||
|
"color-hex-length": null,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -27,17 +27,30 @@ The frontend code build into "dist" directory. The server (express.js) exposes t
|
||||||
|
|
||||||
## Can I create a pull request for Uptime Kuma?
|
## Can I create a pull request for Uptime Kuma?
|
||||||
|
|
||||||
(Updated 2022-04-24) Since I don't want to waste your time, be sure to create empty draft pull request, so we can discuss first.
|
Yes, you can. However, since I don't want to waste your time, be sure to **create empty draft pull request, so we can discuss first** if it is a large pull request or you don't know it will be merged or not.
|
||||||
|
|
||||||
|
Also, please don't rush or ask for ETA, because I have to understand the pull request, make sure it is no breaking changes and stick to my vision of this project, especially for large pull requests.
|
||||||
|
|
||||||
|
I will mark your pull request in the [milestones](https://github.com/louislam/uptime-kuma/milestones), if I am plan to review and merge it.
|
||||||
|
|
||||||
✅ Accept:
|
✅ Accept:
|
||||||
- Bug/Security fix
|
- Bug/Security fix
|
||||||
- Translations
|
- Translations
|
||||||
- Adding notification providers
|
- Adding notification providers
|
||||||
|
|
||||||
⚠️ Discuss First
|
⚠️ Discussion First
|
||||||
- Large pull requests
|
- Large pull requests
|
||||||
- New features
|
- New features
|
||||||
|
|
||||||
|
❌ Won't Merge
|
||||||
|
- Do not pass auto test
|
||||||
|
- Any breaking changes
|
||||||
|
- Duplicated pull request
|
||||||
|
- Buggy
|
||||||
|
- Existing logic is completely modified or deleted for no reason
|
||||||
|
- A function that is completely out of scope
|
||||||
|
|
||||||
|
|
||||||
### Recommended Pull Request Guideline
|
### Recommended Pull Request Guideline
|
||||||
|
|
||||||
Before deep into coding, discussion first is preferred. Creating an empty pull request for discussion would be recommended.
|
Before deep into coding, discussion first is preferred. Creating an empty pull request for discussion would be recommended.
|
||||||
|
@ -53,22 +66,15 @@ Before deep into coding, discussion first is preferred. Creating an empty pull r
|
||||||
1. Click "Change to draft"
|
1. Click "Change to draft"
|
||||||
1. Discussion
|
1. Discussion
|
||||||
|
|
||||||
#### ❌ Won't Merge
|
|
||||||
|
|
||||||
- Any breaking changes
|
|
||||||
- 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.
|
I personally do not like something need to learn so much and need to config so much before you can finally start the app.
|
||||||
|
|
||||||
- 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
|
- 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-compose file. Just map the volume and expose the port, then good to go
|
- 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.
|
- Settings should be configurable in the frontend. Environment variable is not encouraged, unless it is related to startup such as `DATA_DIR`.
|
||||||
- Easy to use
|
- Easy to use
|
||||||
|
- The web UI styling should be consistent and nice.
|
||||||
|
|
||||||
## Coding Styles
|
## Coding Styles
|
||||||
|
|
||||||
|
@ -80,8 +86,8 @@ I personally do not like something need to learn so much and need to config so m
|
||||||
## Name convention
|
## Name convention
|
||||||
|
|
||||||
- Javascript/Typescript: camelCaseType
|
- Javascript/Typescript: camelCaseType
|
||||||
- SQLite: underscore_type
|
- SQLite: snake_case (Underscore)
|
||||||
- CSS/SCSS: dash-type
|
- CSS/SCSS: kebab-case (Dash)
|
||||||
|
|
||||||
## Tools
|
## Tools
|
||||||
|
|
||||||
|
|
19
README.md
19
README.md
|
@ -23,7 +23,7 @@ VPS is sponsored by Uptime Kuma sponsors on [Open Collective](https://opencollec
|
||||||
|
|
||||||
## ⭐ Features
|
## ⭐ Features
|
||||||
|
|
||||||
* Monitoring uptime for HTTP(s) / TCP / HTTP(s) Keyword / Ping / DNS Record / Push / Steam Game Server.
|
* Monitoring uptime for HTTP(s) / TCP / HTTP(s) Keyword / Ping / DNS Record / Push / Steam Game Server / Docker Containers.
|
||||||
* Fancy, Reactive, Fast UI/UX.
|
* Fancy, Reactive, Fast UI/UX.
|
||||||
* Notifications via Telegram, Discord, Gotify, Slack, Pushover, Email (SMTP), and [90+ notification services, click here for the full list](https://github.com/louislam/uptime-kuma/tree/master/src/components/notifications).
|
* Notifications via Telegram, Discord, Gotify, Slack, Pushover, Email (SMTP), and [90+ notification services, click here for the full list](https://github.com/louislam/uptime-kuma/tree/master/src/components/notifications).
|
||||||
* 20 second intervals.
|
* 20 second intervals.
|
||||||
|
@ -106,7 +106,7 @@ https://github.com/louislam/uptime-kuma/milestones
|
||||||
|
|
||||||
Project Plan:
|
Project Plan:
|
||||||
|
|
||||||
https://github.com/louislam/uptime-kuma/projects/1
|
https://github.com/users/louislam/projects/4/views/1
|
||||||
|
|
||||||
## ❤️ Sponsors
|
## ❤️ Sponsors
|
||||||
|
|
||||||
|
@ -151,13 +151,20 @@ You can discuss or ask for help in [issues](https://github.com/louislam/uptime-k
|
||||||
|
|
||||||
### Subreddit
|
### Subreddit
|
||||||
|
|
||||||
My Reddit account: louislamlam
|
My Reddit account: [u/louislamlam](https://reddit.com/u/louislamlam).
|
||||||
You can mention me if you ask a question on Reddit.
|
You can mention me if you ask a question on Reddit.
|
||||||
https://www.reddit.com/r/UptimeKuma/
|
[r/Uptime kuma](https://www.reddit.com/r/UptimeKuma/)
|
||||||
|
|
||||||
## Contribute
|
## Contribute
|
||||||
|
|
||||||
### Beta Version
|
### Test Pull Requests
|
||||||
|
|
||||||
|
There are a lot of pull requests right now, but I don't have time to test them all.
|
||||||
|
|
||||||
|
If you want to help, you can check this:
|
||||||
|
https://github.com/louislam/uptime-kuma/wiki/Test-Pull-Requests
|
||||||
|
|
||||||
|
### Test Beta Version
|
||||||
|
|
||||||
Check out the latest beta release here: https://github.com/louislam/uptime-kuma/releases
|
Check out the latest beta release here: https://github.com/louislam/uptime-kuma/releases
|
||||||
|
|
||||||
|
@ -169,5 +176,5 @@ If you want to translate Uptime Kuma into your language, please read: https://gi
|
||||||
|
|
||||||
Feel free to correct my grammar in this README, source code, or wiki, as my mother language is not English and my grammar is not that great.
|
Feel free to correct my grammar in this README, source code, or wiki, as my mother language is not English and my grammar is not that great.
|
||||||
|
|
||||||
### Pull Requests
|
### Create Pull Requests
|
||||||
If you want to modify Uptime Kuma, this guideline may be useful for you: https://github.com/louislam/uptime-kuma/blob/master/CONTRIBUTING.md
|
If you want to modify Uptime Kuma, this guideline may be useful for you: https://github.com/louislam/uptime-kuma/blob/master/CONTRIBUTING.md
|
||||||
|
|
|
@ -8,9 +8,6 @@ Do not use the issue tracker or discuss it in the public as it will cause more d
|
||||||
|
|
||||||
## Supported Versions
|
## Supported Versions
|
||||||
|
|
||||||
Use this section to tell people about which versions of your project are
|
|
||||||
currently being supported with security updates.
|
|
||||||
|
|
||||||
### Uptime Kuma Versions
|
### Uptime Kuma Versions
|
||||||
|
|
||||||
You should use or upgrade to the latest version of Uptime Kuma. All `1.X.X` versions are upgradable to the lastest version.
|
You should use or upgrade to the latest version of Uptime Kuma. All `1.X.X` versions are upgradable to the lastest version.
|
||||||
|
|
|
@ -1,18 +1,38 @@
|
||||||
import legacy from "@vitejs/plugin-legacy";
|
import legacy from "@vitejs/plugin-legacy";
|
||||||
import vue from "@vitejs/plugin-vue";
|
import vue from "@vitejs/plugin-vue";
|
||||||
import { defineConfig } from "vite";
|
import { defineConfig } from "vite";
|
||||||
|
import visualizer from "rollup-plugin-visualizer";
|
||||||
|
import viteCompression from "vite-plugin-compression";
|
||||||
|
|
||||||
const postCssScss = require("postcss-scss");
|
const postCssScss = require("postcss-scss");
|
||||||
const postcssRTLCSS = require("postcss-rtlcss");
|
const postcssRTLCSS = require("postcss-rtlcss");
|
||||||
|
|
||||||
|
const viteCompressionFilter = /\.(js|mjs|json|css|html|svg)$/i;
|
||||||
|
|
||||||
// https://vitejs.dev/config/
|
// https://vitejs.dev/config/
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
|
server: {
|
||||||
|
port: 3000,
|
||||||
|
},
|
||||||
|
define: {
|
||||||
|
"FRONTEND_VERSION": JSON.stringify(process.env.npm_package_version),
|
||||||
|
},
|
||||||
plugins: [
|
plugins: [
|
||||||
vue(),
|
vue(),
|
||||||
legacy({
|
legacy({
|
||||||
targets: [ "ie > 11" ],
|
targets: [ "since 2015" ],
|
||||||
additionalLegacyPolyfills: [ "regenerator-runtime/runtime" ]
|
}),
|
||||||
})
|
visualizer({
|
||||||
|
filename: "tmp/dist-stats.html"
|
||||||
|
}),
|
||||||
|
viteCompression({
|
||||||
|
algorithm: "gzip",
|
||||||
|
filter: viteCompressionFilter,
|
||||||
|
}),
|
||||||
|
viteCompression({
|
||||||
|
algorithm: "brotliCompress",
|
||||||
|
filter: viteCompressionFilter,
|
||||||
|
}),
|
||||||
],
|
],
|
||||||
css: {
|
css: {
|
||||||
postcss: {
|
postcss: {
|
||||||
|
@ -21,4 +41,13 @@ export default defineConfig({
|
||||||
"plugins": [ postcssRTLCSS ]
|
"plugins": [ postcssRTLCSS ]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
build: {
|
||||||
|
rollupOptions: {
|
||||||
|
output: {
|
||||||
|
manualChunks(id, { getModuleInfo, getModuleIds }) {
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
15
cypress.config.ts
Normal file
15
cypress.config.ts
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
import { defineConfig } from "cypress";
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
e2e: {
|
||||||
|
baseUrl: "http://localhost:3002",
|
||||||
|
defaultCommandTimeout: 10000,
|
||||||
|
pageLoadTimeout: 60000,
|
||||||
|
viewportWidth: 1920,
|
||||||
|
viewportHeight: 1080,
|
||||||
|
specPattern: ["cypress/e2e/setup.cy.ts", "cypress/e2e/**/*.ts"],
|
||||||
|
},
|
||||||
|
env: {
|
||||||
|
baseUrl: "http://localhost:3002",
|
||||||
|
},
|
||||||
|
});
|
24
cypress/e2e/setup.cy.ts
Normal file
24
cypress/e2e/setup.cy.ts
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
import { actor } from "../support/actors/actor";
|
||||||
|
import { DEFAULT_USER_DATA } from "../support/const/user-data";
|
||||||
|
import { DashboardPage } from "../support/pages/dasboard-page";
|
||||||
|
import { SetupPage } from "../support/pages/setup-page";
|
||||||
|
|
||||||
|
describe("user can create a new account on setup page", () => {
|
||||||
|
before(() => {
|
||||||
|
cy.visit("/setup");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("user can create new account", () => {
|
||||||
|
cy.url().should("be.equal", SetupPage.url);
|
||||||
|
actor.setupTask.fillAndSubmitSetupForm(
|
||||||
|
DEFAULT_USER_DATA.username,
|
||||||
|
DEFAULT_USER_DATA.password,
|
||||||
|
DEFAULT_USER_DATA.password
|
||||||
|
);
|
||||||
|
|
||||||
|
cy.url().should("be.equal", DashboardPage.url);
|
||||||
|
cy.get('[role="alert"]')
|
||||||
|
.should("be.visible")
|
||||||
|
.and("contain.text", "Added Successfully.");
|
||||||
|
});
|
||||||
|
});
|
0
cypress/plugins/index.js
Normal file
0
cypress/plugins/index.js
Normal file
8
cypress/support/actors/actor.ts
Normal file
8
cypress/support/actors/actor.ts
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
import { SetupTask } from "../tasks/setup-task";
|
||||||
|
|
||||||
|
class Actor {
|
||||||
|
setupTask: SetupTask = new SetupTask();
|
||||||
|
}
|
||||||
|
|
||||||
|
const actor = new Actor();
|
||||||
|
export { actor };
|
0
cypress/support/commands.ts
Normal file
0
cypress/support/commands.ts
Normal file
4
cypress/support/const/user-data.ts
Normal file
4
cypress/support/const/user-data.ts
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
export const DEFAULT_USER_DATA = {
|
||||||
|
username: "testuser",
|
||||||
|
password: "testuser123",
|
||||||
|
};
|
1
cypress/support/e2e.ts
Normal file
1
cypress/support/e2e.ts
Normal file
|
@ -0,0 +1 @@
|
||||||
|
import "./commands";
|
3
cypress/support/pages/dasboard-page.ts
Normal file
3
cypress/support/pages/dasboard-page.ts
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
export const DashboardPage = {
|
||||||
|
url: Cypress.env("baseUrl") + "/dashboard",
|
||||||
|
};
|
7
cypress/support/pages/setup-page.ts
Normal file
7
cypress/support/pages/setup-page.ts
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
export const SetupPage = {
|
||||||
|
url: Cypress.env("baseUrl") + "/setup",
|
||||||
|
usernameInput: '[data-cy="username-input"]',
|
||||||
|
passWordInput: '[data-cy="password-input"]',
|
||||||
|
passwordRepeatInput: '[data-cy="password-repeat-input"]',
|
||||||
|
submitSetupForm: '[data-cy="submit-setup-form"]',
|
||||||
|
};
|
15
cypress/support/tasks/setup-task.ts
Normal file
15
cypress/support/tasks/setup-task.ts
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
import { SetupPage } from "../pages/setup-page";
|
||||||
|
|
||||||
|
export class SetupTask {
|
||||||
|
fillAndSubmitSetupForm(
|
||||||
|
username: string,
|
||||||
|
password: string,
|
||||||
|
passwordRepeat: string
|
||||||
|
) {
|
||||||
|
cy.get(SetupPage.usernameInput).type(username);
|
||||||
|
cy.get(SetupPage.passWordInput).type(password);
|
||||||
|
cy.get(SetupPage.passwordRepeatInput).type(passwordRepeat);
|
||||||
|
|
||||||
|
cy.get(SetupPage.submitSetupForm).click();
|
||||||
|
}
|
||||||
|
}
|
5
db/patch-add-clickable-status-page-link.sql
Normal file
5
db/patch-add-clickable-status-page-link.sql
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
-- You should not modify if this have pushed to Github, unless it does serious wrong with the db.
|
||||||
|
BEGIN TRANSACTION;
|
||||||
|
ALTER TABLE monitor_group
|
||||||
|
ADD send_url BOOLEAN DEFAULT 0 NOT NULL;
|
||||||
|
COMMIT;
|
18
db/patch-add-docker-columns.sql
Normal file
18
db/patch-add-docker-columns.sql
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
-- You should not modify if this have pushed to Github, unless it does serious wrong with the db.
|
||||||
|
BEGIN TRANSACTION;
|
||||||
|
|
||||||
|
CREATE TABLE docker_host (
|
||||||
|
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
|
||||||
|
user_id INT NOT NULL,
|
||||||
|
docker_daemon VARCHAR(255),
|
||||||
|
docker_type VARCHAR(255),
|
||||||
|
name VARCHAR(255)
|
||||||
|
);
|
||||||
|
|
||||||
|
ALTER TABLE monitor
|
||||||
|
ADD docker_host INTEGER REFERENCES docker_host(id);
|
||||||
|
|
||||||
|
ALTER TABLE monitor
|
||||||
|
ADD docker_container VARCHAR(255);
|
||||||
|
|
||||||
|
COMMIT;
|
18
db/patch-add-other-auth.sql
Normal file
18
db/patch-add-other-auth.sql
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
BEGIN TRANSACTION;
|
||||||
|
|
||||||
|
ALTER TABLE monitor
|
||||||
|
ADD auth_method VARCHAR(250);
|
||||||
|
|
||||||
|
ALTER TABLE monitor
|
||||||
|
ADD auth_domain TEXT;
|
||||||
|
ALTER TABLE monitor
|
||||||
|
|
||||||
|
ADD auth_workstation TEXT;
|
||||||
|
|
||||||
|
COMMIT;
|
||||||
|
|
||||||
|
BEGIN TRANSACTION;
|
||||||
|
UPDATE monitor
|
||||||
|
SET auth_method = 'basic'
|
||||||
|
WHERE basic_auth_user is not null;
|
||||||
|
COMMIT;
|
18
db/patch-add-radius-monitor.sql
Normal file
18
db/patch-add-radius-monitor.sql
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
BEGIN TRANSACTION;
|
||||||
|
|
||||||
|
ALTER TABLE monitor
|
||||||
|
ADD radius_username VARCHAR(255);
|
||||||
|
|
||||||
|
ALTER TABLE monitor
|
||||||
|
ADD radius_password VARCHAR(255);
|
||||||
|
|
||||||
|
ALTER TABLE monitor
|
||||||
|
ADD radius_calling_station_id VARCHAR(50);
|
||||||
|
|
||||||
|
ALTER TABLE monitor
|
||||||
|
ADD radius_called_station_id VARCHAR(50);
|
||||||
|
|
||||||
|
ALTER TABLE monitor
|
||||||
|
ADD radius_secret VARCHAR(255);
|
||||||
|
|
||||||
|
COMMIT
|
10
db/patch-add-sqlserver-monitor.sql
Normal file
10
db/patch-add-sqlserver-monitor.sql
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
BEGIN TRANSACTION;
|
||||||
|
|
||||||
|
ALTER TABLE monitor
|
||||||
|
ADD database_connection_string VARCHAR(2000);
|
||||||
|
|
||||||
|
ALTER TABLE monitor
|
||||||
|
ADD database_query TEXT;
|
||||||
|
|
||||||
|
|
||||||
|
COMMIT
|
10
db/patch-monitor-add-resend-interval.sql
Normal file
10
db/patch-monitor-add-resend-interval.sql
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
-- You should not modify if this have pushed to Github, unless it does serious wrong with the db.
|
||||||
|
BEGIN TRANSACTION;
|
||||||
|
|
||||||
|
ALTER TABLE monitor
|
||||||
|
ADD resend_interval INTEGER default 0 not null;
|
||||||
|
|
||||||
|
ALTER TABLE heartbeat
|
||||||
|
ADD down_count INTEGER default 0 not null;
|
||||||
|
|
||||||
|
COMMIT;
|
|
@ -4,5 +4,5 @@ WORKDIR /app
|
||||||
|
|
||||||
# Install apprise, iputils for non-root ping, setpriv
|
# Install apprise, iputils for non-root ping, setpriv
|
||||||
RUN apk add --no-cache iputils setpriv dumb-init python3 py3-cryptography py3-pip py3-six py3-yaml py3-click py3-markdown py3-requests py3-requests-oauthlib && \
|
RUN apk add --no-cache iputils setpriv dumb-init python3 py3-cryptography py3-pip py3-six py3-yaml py3-click py3-markdown py3-requests py3-requests-oauthlib && \
|
||||||
pip3 --no-cache-dir install apprise==0.9.8.3 && \
|
pip3 --no-cache-dir install apprise==1.0.0 && \
|
||||||
rm -rf /root/.cache
|
rm -rf /root/.cache
|
||||||
|
|
|
@ -11,8 +11,9 @@ WORKDIR /app
|
||||||
RUN apt update && \
|
RUN apt update && \
|
||||||
apt --yes --no-install-recommends install python3 python3-pip python3-cryptography python3-six python3-yaml python3-click python3-markdown python3-requests python3-requests-oauthlib \
|
apt --yes --no-install-recommends install python3 python3-pip python3-cryptography python3-six python3-yaml python3-click python3-markdown python3-requests python3-requests-oauthlib \
|
||||||
sqlite3 iputils-ping util-linux dumb-init && \
|
sqlite3 iputils-ping util-linux dumb-init && \
|
||||||
pip3 --no-cache-dir install apprise==0.9.8.3 && \
|
pip3 --no-cache-dir install apprise==1.0.0 && \
|
||||||
rm -rf /var/lib/apt/lists/*
|
rm -rf /var/lib/apt/lists/* && \
|
||||||
|
apt --yes autoremove
|
||||||
|
|
||||||
# Install cloudflared
|
# Install cloudflared
|
||||||
# dpkg --add-architecture arm: cloudflared do not provide armhf, this is workaround. Read more: https://github.com/cloudflare/cloudflared/issues/583
|
# dpkg --add-architecture arm: cloudflared do not provide armhf, this is workaround. Read more: https://github.com/cloudflare/cloudflared/issues/583
|
||||||
|
@ -22,5 +23,6 @@ RUN node ./extra/download-cloudflared.js $TARGETPLATFORM && \
|
||||||
apt update && \
|
apt update && \
|
||||||
apt --yes --no-install-recommends install ./cloudflared.deb && \
|
apt --yes --no-install-recommends install ./cloudflared.deb && \
|
||||||
rm -rf /var/lib/apt/lists/* && \
|
rm -rf /var/lib/apt/lists/* && \
|
||||||
rm -f cloudflared.deb
|
rm -f cloudflared.deb && \
|
||||||
|
apt --yes autoremove
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
# Simple docker-composer.yml
|
# Simple docker-compose.yml
|
||||||
# You can change your port or volume location
|
# You can change your port or volume location
|
||||||
|
|
||||||
version: '3.3'
|
version: '3.3'
|
||||||
|
|
|
@ -24,6 +24,36 @@ CMD ["node", "server/server.js"]
|
||||||
FROM release AS nightly
|
FROM release AS nightly
|
||||||
RUN npm run mark-as-nightly
|
RUN npm run mark-as-nightly
|
||||||
|
|
||||||
|
# Build an image for testing pr
|
||||||
|
FROM louislam/uptime-kuma:base-debian AS pr-test
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
ENV PUPPETEER_SKIP_CHROMIUM_DOWNLOAD=1
|
||||||
|
|
||||||
|
## Install Git
|
||||||
|
RUN apt update \
|
||||||
|
&& apt --yes --no-install-recommends install curl \
|
||||||
|
&& curl -fsSL https://cli.github.com/packages/githubcli-archive-keyring.gpg | dd of=/usr/share/keyrings/githubcli-archive-keyring.gpg \
|
||||||
|
&& chmod go+r /usr/share/keyrings/githubcli-archive-keyring.gpg \
|
||||||
|
&& echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/githubcli-archive-keyring.gpg] https://cli.github.com/packages stable main" | tee /etc/apt/sources.list.d/github-cli.list > /dev/null \
|
||||||
|
&& apt update \
|
||||||
|
&& apt --yes --no-install-recommends install git
|
||||||
|
|
||||||
|
## Empty the directory, because we have to clone the Git repo.
|
||||||
|
RUN rm -rf ./* && chown node /app
|
||||||
|
|
||||||
|
USER node
|
||||||
|
RUN git config --global user.email "no-reply@no-reply.com"
|
||||||
|
RUN git config --global user.name "PR Tester"
|
||||||
|
RUN git clone https://github.com/louislam/uptime-kuma.git .
|
||||||
|
RUN npm ci
|
||||||
|
|
||||||
|
EXPOSE 3000 3001
|
||||||
|
VOLUME ["/app/data"]
|
||||||
|
HEALTHCHECK --interval=60s --timeout=30s --start-period=180s --retries=5 CMD node extra/healthcheck.js
|
||||||
|
CMD ["npm", "run", "start-pr-test"]
|
||||||
|
|
||||||
|
|
||||||
# Upload the artifact to Github
|
# Upload the artifact to Github
|
||||||
FROM louislam/uptime-kuma:base-debian AS upload-artifact
|
FROM louislam/uptime-kuma:base-debian AS upload-artifact
|
||||||
|
|
33
extra/checkout-pr.js
Normal file
33
extra/checkout-pr.js
Normal file
|
@ -0,0 +1,33 @@
|
||||||
|
const childProcess = require("child_process");
|
||||||
|
|
||||||
|
if (!process.env.UPTIME_KUMA_GH_REPO) {
|
||||||
|
console.error("Please set a repo to the environment variable 'UPTIME_KUMA_GH_REPO' (e.g. mhkarimi1383:goalert-notification)");
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
let inputArray = process.env.UPTIME_KUMA_GH_REPO.split(":");
|
||||||
|
|
||||||
|
if (inputArray.length !== 2) {
|
||||||
|
console.error("Invalid format. Please set a repo to the environment variable 'UPTIME_KUMA_GH_REPO' (e.g. mhkarimi1383:goalert-notification)");
|
||||||
|
}
|
||||||
|
|
||||||
|
let name = inputArray[0];
|
||||||
|
let branch = inputArray[1];
|
||||||
|
|
||||||
|
console.log("Checkout pr");
|
||||||
|
|
||||||
|
// Checkout the pr
|
||||||
|
let result = childProcess.spawnSync("git", [ "remote", "add", name, `https://github.com/${name}/uptime-kuma` ]);
|
||||||
|
|
||||||
|
console.log(result.stdout.toString());
|
||||||
|
console.error(result.stderr.toString());
|
||||||
|
|
||||||
|
result = childProcess.spawnSync("git", [ "fetch", name, branch ]);
|
||||||
|
|
||||||
|
console.log(result.stdout.toString());
|
||||||
|
console.error(result.stderr.toString());
|
||||||
|
|
||||||
|
result = childProcess.spawnSync("git", [ "checkout", `${name}/${branch}`, "--force" ]);
|
||||||
|
|
||||||
|
console.log(result.stdout.toString());
|
||||||
|
console.error(result.stderr.toString());
|
|
@ -41,7 +41,7 @@ function updateWiki(newVersion) {
|
||||||
|
|
||||||
function safeDelete(dir) {
|
function safeDelete(dir) {
|
||||||
if (fs.existsSync(dir)) {
|
if (fs.existsSync(dir)) {
|
||||||
fs.rmdirSync(dir, {
|
fs.rm(dir, {
|
||||||
recursive: true,
|
recursive: true,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
14989
package-lock.json
generated
14989
package-lock.json
generated
File diff suppressed because it is too large
Load diff
116
package.json
116
package.json
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "uptime-kuma",
|
"name": "uptime-kuma",
|
||||||
"version": "1.16.0-beta.0",
|
"version": "1.18.0",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
|
@ -38,8 +38,9 @@
|
||||||
"build-docker-nightly": "npm run build && 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": "npm run build && 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 docker/dockerfile-alpine --platform linux/amd64,linux/arm64,linux/arm/v7 -t louislam/uptime-kuma:nightly-alpine --target nightly . --push",
|
"build-docker-nightly-alpine": "docker buildx build -f docker/dockerfile-alpine --platform linux/amd64,linux/arm64,linux/arm/v7 -t louislam/uptime-kuma:nightly-alpine --target nightly . --push",
|
||||||
"build-docker-nightly-amd64": "docker buildx build -f docker/dockerfile --platform linux/amd64 -t louislam/uptime-kuma:nightly-amd64 --target nightly . --push --progress plain",
|
"build-docker-nightly-amd64": "docker buildx build -f docker/dockerfile --platform linux/amd64 -t louislam/uptime-kuma:nightly-amd64 --target nightly . --push --progress plain",
|
||||||
|
"build-docker-pr-test": "docker buildx build -f docker/dockerfile --platform linux/amd64,linux/arm64 -t louislam/uptime-kuma:pr-test --target pr-test . --push",
|
||||||
"upload-artifacts": "docker buildx build -f docker/dockerfile --platform linux/amd64 -t louislam/uptime-kuma:upload-artifact --build-arg VERSION --build-arg GITHUB_TOKEN --target upload-artifact . --progress plain",
|
"upload-artifacts": "docker buildx build -f docker/dockerfile --platform linux/amd64 -t louislam/uptime-kuma:upload-artifact --build-arg VERSION --build-arg GITHUB_TOKEN --target upload-artifact . --progress plain",
|
||||||
"setup": "git checkout 1.15.1 && npm ci --production && npm run download-dist",
|
"setup": "git checkout 1.18.0 && npm ci --production && npm run download-dist",
|
||||||
"download-dist": "node extra/download-dist.js",
|
"download-dist": "node extra/download-dist.js",
|
||||||
"mark-as-nightly": "node extra/mark-as-nightly.js",
|
"mark-as-nightly": "node extra/mark-as-nightly.js",
|
||||||
"reset-password": "node extra/reset-password.js",
|
"reset-password": "node extra/reset-password.js",
|
||||||
|
@ -57,32 +58,32 @@
|
||||||
"ncu-patch": "npm-check-updates -u -t patch",
|
"ncu-patch": "npm-check-updates -u -t patch",
|
||||||
"release-final": "node extra/update-version.js && npm run build-docker && node ./extra/press-any-key.js && npm run upload-artifacts && node ./extra/update-wiki-version.js",
|
"release-final": "node extra/update-version.js && npm run build-docker && node ./extra/press-any-key.js && npm run upload-artifacts && node ./extra/update-wiki-version.js",
|
||||||
"release-beta": "node extra/beta/update-version.js && npm run build && node ./extra/env2arg.js docker buildx build -f docker/dockerfile --platform linux/amd64,linux/arm64,linux/arm/v7 -t louislam/uptime-kuma:$VERSION -t louislam/uptime-kuma:beta . --target release --push && node ./extra/press-any-key.js && npm run upload-artifacts",
|
"release-beta": "node extra/beta/update-version.js && npm run build && node ./extra/env2arg.js docker buildx build -f docker/dockerfile --platform linux/amd64,linux/arm64,linux/arm/v7 -t louislam/uptime-kuma:$VERSION -t louislam/uptime-kuma:beta . --target release --push && node ./extra/press-any-key.js && npm run upload-artifacts",
|
||||||
"git-remove-tag": "git tag -d"
|
"git-remove-tag": "git tag -d",
|
||||||
|
"build-dist-and-restart": "npm run build && npm run start-server-dev",
|
||||||
|
"start-pr-test": "node extra/checkout-pr.js && npm install && npm run dev",
|
||||||
|
"cy:test": "node test/prepare-test-server.js && node server/server.js --port=3002 --data-dir=./data/test/ --e2e",
|
||||||
|
"cy:run": "npx cypress run --browser chrome --headless"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"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-5",
|
|
||||||
"@louislam/sqlite3": "~15.0.6",
|
"@louislam/sqlite3": "~15.0.6",
|
||||||
"@popperjs/core": "~2.10.2",
|
|
||||||
"args-parser": "~1.3.0",
|
"args-parser": "~1.3.0",
|
||||||
"axios": "~0.26.1",
|
"axios": "~0.27.0",
|
||||||
|
"axios-ntlm": "^1.3.0",
|
||||||
"badge-maker": "^3.3.1",
|
"badge-maker": "^3.3.1",
|
||||||
"bcryptjs": "~2.4.3",
|
"bcryptjs": "~2.4.3",
|
||||||
"bootstrap": "5.1.3",
|
|
||||||
"bree": "~7.1.5",
|
"bree": "~7.1.5",
|
||||||
|
"cacheable-lookup": "~6.0.4",
|
||||||
"chardet": "^1.3.0",
|
"chardet": "^1.3.0",
|
||||||
"chart.js": "~3.6.2",
|
|
||||||
"chartjs-adapter-dayjs": "~1.0.0",
|
|
||||||
"check-password-strength": "^2.0.5",
|
"check-password-strength": "^2.0.5",
|
||||||
|
"cheerio": "^1.0.0-rc.10",
|
||||||
"chroma-js": "^2.1.2",
|
"chroma-js": "^2.1.2",
|
||||||
"command-exists": "~1.2.9",
|
"command-exists": "~1.2.9",
|
||||||
"compare-versions": "~3.6.0",
|
"compare-versions": "~3.6.0",
|
||||||
"dayjs": "~1.10.8",
|
"compression": "^1.7.4",
|
||||||
|
"dayjs": "^1.11.0",
|
||||||
"express": "~4.17.3",
|
"express": "~4.17.3",
|
||||||
"express-basic-auth": "~1.2.1",
|
"express-basic-auth": "~1.2.1",
|
||||||
"favico.js": "^0.3.10",
|
"express-static-gzip": "^2.1.7",
|
||||||
"form-data": "~4.0.0",
|
"form-data": "~4.0.0",
|
||||||
"http-graceful-shutdown": "~3.1.7",
|
"http-graceful-shutdown": "~3.1.7",
|
||||||
"http-proxy-agent": "^5.0.0",
|
"http-proxy-agent": "^5.0.0",
|
||||||
|
@ -92,25 +93,69 @@
|
||||||
"jwt-decode": "^3.1.2",
|
"jwt-decode": "^3.1.2",
|
||||||
"limiter": "^2.1.0",
|
"limiter": "^2.1.0",
|
||||||
"mqtt": "^4.2.8",
|
"mqtt": "^4.2.8",
|
||||||
|
"mssql": "^8.1.0",
|
||||||
"node-cloudflared-tunnel": "~1.0.9",
|
"node-cloudflared-tunnel": "~1.0.9",
|
||||||
|
"node-radius-client": "^1.0.0",
|
||||||
"nodemailer": "~6.6.5",
|
"nodemailer": "~6.6.5",
|
||||||
"notp": "~2.0.3",
|
"notp": "~2.0.3",
|
||||||
"password-hash": "~1.2.2",
|
"password-hash": "~1.2.2",
|
||||||
"postcss-rtlcss": "~3.4.1",
|
"pg": "^8.7.3",
|
||||||
"postcss-scss": "~4.0.3",
|
"pg-connection-string": "^2.5.0",
|
||||||
"prismjs": "^1.27.0",
|
|
||||||
"prom-client": "~13.2.0",
|
"prom-client": "~13.2.0",
|
||||||
"prometheus-api-metrics": "~3.2.1",
|
"prometheus-api-metrics": "~3.2.1",
|
||||||
"qrcode": "~1.5.0",
|
"redbean-node": "0.1.4",
|
||||||
"redbean-node": "0.1.3",
|
|
||||||
"socket.io": "~4.4.1",
|
"socket.io": "~4.4.1",
|
||||||
"socket.io-client": "~4.4.1",
|
"socket.io-client": "~4.4.1",
|
||||||
"socks-proxy-agent": "^6.1.1",
|
"socks-proxy-agent": "6.1.1",
|
||||||
"tar": "^6.1.11",
|
"tar": "^6.1.11",
|
||||||
"tcp-ping": "~0.1.1",
|
"tcp-ping": "~0.1.1",
|
||||||
"thirty-two": "~1.0.2",
|
"thirty-two": "~1.0.2"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@actions/github": "~5.0.1",
|
||||||
|
"@babel/eslint-parser": "~7.17.0",
|
||||||
|
"@babel/preset-env": "^7.15.8",
|
||||||
|
"@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-5",
|
||||||
|
"@popperjs/core": "~2.10.2",
|
||||||
|
"@types/bootstrap": "~5.1.9",
|
||||||
|
"@vitejs/plugin-legacy": "~2.1.0",
|
||||||
|
"@vitejs/plugin-vue": "~3.1.0",
|
||||||
|
"@vue/compiler-sfc": "~3.2.36",
|
||||||
|
"aedes": "^0.46.3",
|
||||||
|
"babel-plugin-rewire": "~1.2.0",
|
||||||
|
"bootstrap": "5.1.3",
|
||||||
|
"chart.js": "~3.6.2",
|
||||||
|
"chartjs-adapter-dayjs": "~1.0.0",
|
||||||
|
"concurrently": "^7.1.0",
|
||||||
|
"core-js": "~3.18.3",
|
||||||
|
"cross-env": "~7.0.3",
|
||||||
|
"cypress": "^10.1.0",
|
||||||
|
"delay": "^5.0.0",
|
||||||
|
"dns2": "~2.0.1",
|
||||||
|
"eslint": "~8.14.0",
|
||||||
|
"eslint-plugin-vue": "~8.7.1",
|
||||||
|
"favico.js": "^0.3.10",
|
||||||
|
"jest": "~27.2.5",
|
||||||
|
"jest-puppeteer": "~6.0.3",
|
||||||
|
"postcss-html": "~1.5.0",
|
||||||
|
"postcss-rtlcss": "~3.7.2",
|
||||||
|
"postcss-scss": "~4.0.4",
|
||||||
|
"prismjs": "^1.27.0",
|
||||||
|
"puppeteer": "~13.1.3",
|
||||||
|
"qrcode": "~1.5.0",
|
||||||
|
"rollup-plugin-visualizer": "^5.6.0",
|
||||||
|
"sass": "~1.42.1",
|
||||||
|
"stylelint": "~14.7.1",
|
||||||
|
"stylelint-config-standard": "~25.0.0",
|
||||||
|
"terser": "^5.15.0",
|
||||||
"timezones-list": "~3.0.1",
|
"timezones-list": "~3.0.1",
|
||||||
|
"typescript": "~4.4.4",
|
||||||
"v-pagination-3": "~0.1.7",
|
"v-pagination-3": "~0.1.7",
|
||||||
|
"vite": "~3.1.0",
|
||||||
|
"vite-plugin-compression": "^0.5.1",
|
||||||
"vue": "next",
|
"vue": "next",
|
||||||
"vue-chart-3": "3.0.9",
|
"vue-chart-3": "3.0.9",
|
||||||
"vue-confirm-dialog": "~1.0.2",
|
"vue-confirm-dialog": "~1.0.2",
|
||||||
|
@ -122,34 +167,7 @@
|
||||||
"vue-qrcode": "~1.0.0",
|
"vue-qrcode": "~1.0.0",
|
||||||
"vue-router": "~4.0.14",
|
"vue-router": "~4.0.14",
|
||||||
"vue-toastification": "~2.0.0-rc.5",
|
"vue-toastification": "~2.0.0-rc.5",
|
||||||
"vuedraggable": "~4.1.0"
|
"vuedraggable": "~4.1.0",
|
||||||
},
|
|
||||||
"devDependencies": {
|
|
||||||
"@actions/github": "~5.0.1",
|
|
||||||
"@babel/eslint-parser": "~7.17.0",
|
|
||||||
"@babel/preset-env": "^7.15.8",
|
|
||||||
"@types/bootstrap": "~5.1.9",
|
|
||||||
"@vitejs/plugin-legacy": "~1.6.4",
|
|
||||||
"@vitejs/plugin-vue": "~1.9.4",
|
|
||||||
"@vue/compiler-sfc": "~3.2.31",
|
|
||||||
"aedes": "^0.46.3",
|
|
||||||
"babel-plugin-rewire": "~1.2.0",
|
|
||||||
"concurrently": "^7.1.0",
|
|
||||||
"core-js": "~3.18.3",
|
|
||||||
"cross-env": "~7.0.3",
|
|
||||||
"dns2": "~2.0.1",
|
|
||||||
"eslint": "~8.14.0",
|
|
||||||
"eslint-plugin-vue": "~8.7.1",
|
|
||||||
"jest": "~27.2.5",
|
|
||||||
"jest-puppeteer": "~6.0.3",
|
|
||||||
"npm-check-updates": "^12.5.9",
|
|
||||||
"postcss-html": "^1.3.1",
|
|
||||||
"puppeteer": "~13.1.3",
|
|
||||||
"sass": "~1.42.1",
|
|
||||||
"stylelint": "~14.7.1",
|
|
||||||
"stylelint-config-standard": "~25.0.0",
|
|
||||||
"typescript": "~4.4.4",
|
|
||||||
"vite": "~2.6.14",
|
|
||||||
"wait-on": "^6.0.1"
|
"wait-on": "^6.0.1"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
File diff suppressed because one or more lines are too long
Before Width: | Height: | Size: 6.4 KiB After Width: | Height: | Size: 893 B |
54
server/cacheable-dns-http-agent.js
Normal file
54
server/cacheable-dns-http-agent.js
Normal file
|
@ -0,0 +1,54 @@
|
||||||
|
const https = require("https");
|
||||||
|
const http = require("http");
|
||||||
|
const CacheableLookup = require("cacheable-lookup");
|
||||||
|
|
||||||
|
class CacheableDnsHttpAgent {
|
||||||
|
|
||||||
|
static cacheable = new CacheableLookup();
|
||||||
|
|
||||||
|
static httpAgentList = {};
|
||||||
|
static httpsAgentList = {};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Register cacheable to global agents
|
||||||
|
*/
|
||||||
|
static registerGlobalAgent() {
|
||||||
|
this.cacheable.install(http.globalAgent);
|
||||||
|
this.cacheable.install(https.globalAgent);
|
||||||
|
}
|
||||||
|
|
||||||
|
static install(agent) {
|
||||||
|
this.cacheable.install(agent);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var {https.AgentOptions} agentOptions
|
||||||
|
* @return {https.Agent}
|
||||||
|
*/
|
||||||
|
static getHttpsAgent(agentOptions) {
|
||||||
|
let key = JSON.stringify(agentOptions);
|
||||||
|
if (!(key in this.httpsAgentList)) {
|
||||||
|
this.httpsAgentList[key] = new https.Agent(agentOptions);
|
||||||
|
this.cacheable.install(this.httpsAgentList[key]);
|
||||||
|
}
|
||||||
|
return this.httpsAgentList[key];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var {http.AgentOptions} agentOptions
|
||||||
|
* @return {https.Agents}
|
||||||
|
*/
|
||||||
|
static getHttpAgent(agentOptions) {
|
||||||
|
let key = JSON.stringify(agentOptions);
|
||||||
|
if (!(key in this.httpAgentList)) {
|
||||||
|
this.httpAgentList[key] = new http.Agent(agentOptions);
|
||||||
|
this.cacheable.install(this.httpAgentList[key]);
|
||||||
|
}
|
||||||
|
return this.httpAgentList[key];
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
CacheableDnsHttpAgent,
|
||||||
|
};
|
|
@ -22,7 +22,10 @@ async function sendNotificationList(socket) {
|
||||||
]);
|
]);
|
||||||
|
|
||||||
for (let bean of list) {
|
for (let bean of list) {
|
||||||
result.push(bean.export());
|
let notificationObject = bean.export();
|
||||||
|
notificationObject.isDefault = (notificationObject.isDefault === 1);
|
||||||
|
notificationObject.active = (notificationObject.active === 1);
|
||||||
|
result.push(notificationObject);
|
||||||
}
|
}
|
||||||
|
|
||||||
io.to(socket.userID).emit("notificationList", result);
|
io.to(socket.userID).emit("notificationList", result);
|
||||||
|
@ -122,10 +125,35 @@ async function sendInfo(socket) {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send list of docker hosts to client
|
||||||
|
* @param {Socket} socket Socket.io socket instance
|
||||||
|
* @returns {Promise<Bean[]>}
|
||||||
|
*/
|
||||||
|
async function sendDockerHostList(socket) {
|
||||||
|
const timeLogger = new TimeLogger();
|
||||||
|
|
||||||
|
let result = [];
|
||||||
|
let list = await R.find("docker_host", " user_id = ? ", [
|
||||||
|
socket.userID,
|
||||||
|
]);
|
||||||
|
|
||||||
|
for (let bean of list) {
|
||||||
|
result.push(bean.toJSON());
|
||||||
|
}
|
||||||
|
|
||||||
|
io.to(socket.userID).emit("dockerHostList", result);
|
||||||
|
|
||||||
|
timeLogger.print("Send Docker Host List");
|
||||||
|
|
||||||
|
return list;
|
||||||
|
}
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
sendNotificationList,
|
sendNotificationList,
|
||||||
sendImportantHeartbeatList,
|
sendImportantHeartbeatList,
|
||||||
sendHeartbeatList,
|
sendHeartbeatList,
|
||||||
sendProxyList,
|
sendProxyList,
|
||||||
sendInfo,
|
sendInfo,
|
||||||
|
sendDockerHostList
|
||||||
};
|
};
|
||||||
|
|
|
@ -53,12 +53,18 @@ class Database {
|
||||||
"patch-2fa-invalidate-used-token.sql": true,
|
"patch-2fa-invalidate-used-token.sql": true,
|
||||||
"patch-notification_sent_history.sql": true,
|
"patch-notification_sent_history.sql": true,
|
||||||
"patch-monitor-basic-auth.sql": true,
|
"patch-monitor-basic-auth.sql": true,
|
||||||
"patch-maintenance-table.sql": true,
|
"patch-add-docker-columns.sql": true,
|
||||||
"patch-status-page.sql": true,
|
"patch-status-page.sql": true,
|
||||||
"patch-proxy.sql": true,
|
"patch-proxy.sql": true,
|
||||||
"patch-monitor-expiry-notification.sql": true,
|
"patch-monitor-expiry-notification.sql": true,
|
||||||
"patch-status-page-footer-css.sql": true,
|
"patch-status-page-footer-css.sql": true,
|
||||||
"patch-added-mqtt-monitor.sql": true,
|
"patch-added-mqtt-monitor.sql": true,
|
||||||
|
"patch-add-clickable-status-page-link.sql": true,
|
||||||
|
"patch-add-sqlserver-monitor.sql": true,
|
||||||
|
"patch-add-other-auth.sql": { parents: [ "patch-monitor-basic-auth.sql" ] },
|
||||||
|
"patch-add-radius-monitor.sql": true,
|
||||||
|
"patch-monitor-add-resend-interval.sql": true,
|
||||||
|
"patch-maintenance-table.sql": true,
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -145,6 +151,9 @@ class Database {
|
||||||
await R.exec("PRAGMA cache_size = -12000");
|
await R.exec("PRAGMA cache_size = -12000");
|
||||||
await R.exec("PRAGMA auto_vacuum = FULL");
|
await R.exec("PRAGMA auto_vacuum = FULL");
|
||||||
|
|
||||||
|
// Avoid error "SQLITE_BUSY: database is locked" by allowing SQLITE to wait up to 5 seconds to do a write
|
||||||
|
await R.exec("PRAGMA busy_timeout = 5000");
|
||||||
|
|
||||||
// This ensures that an operating system crash or power failure will not corrupt the database.
|
// This ensures that an operating system crash or power failure will not corrupt the database.
|
||||||
// FULL synchronous is very safe, but it is also slower.
|
// FULL synchronous is very safe, but it is also slower.
|
||||||
// Read more: https://sqlite.org/pragma.html#pragma_synchronous
|
// Read more: https://sqlite.org/pragma.html#pragma_synchronous
|
||||||
|
@ -176,7 +185,13 @@ class Database {
|
||||||
} else {
|
} else {
|
||||||
log.info("db", "Database patch is needed");
|
log.info("db", "Database patch is needed");
|
||||||
|
|
||||||
|
try {
|
||||||
this.backup(version);
|
this.backup(version);
|
||||||
|
} catch (e) {
|
||||||
|
log.error("db", e);
|
||||||
|
log.error("db", "Unable to create a backup before patching the database. Please make sure you have enough space and permission.");
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
// Try catch anything here, if gone wrong, restore the backup
|
// Try catch anything here, if gone wrong, restore the backup
|
||||||
try {
|
try {
|
||||||
|
@ -444,6 +459,23 @@ class Database {
|
||||||
this.backupWalPath = walPath + ".bak" + version;
|
this.backupWalPath = walPath + ".bak" + version;
|
||||||
fs.copyFileSync(walPath, this.backupWalPath);
|
fs.copyFileSync(walPath, this.backupWalPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Double confirm if all files actually backup
|
||||||
|
if (!fs.existsSync(this.backupPath)) {
|
||||||
|
throw new Error("Backup failed! " + this.backupPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (fs.existsSync(shmPath)) {
|
||||||
|
if (!fs.existsSync(this.backupShmPath)) {
|
||||||
|
throw new Error("Backup failed! " + this.backupShmPath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (fs.existsSync(walPath)) {
|
||||||
|
if (!fs.existsSync(this.backupWalPath)) {
|
||||||
|
throw new Error("Backup failed! " + this.backupWalPath);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
106
server/docker.js
Normal file
106
server/docker.js
Normal file
|
@ -0,0 +1,106 @@
|
||||||
|
const axios = require("axios");
|
||||||
|
const { R } = require("redbean-node");
|
||||||
|
const version = require("../package.json").version;
|
||||||
|
const https = require("https");
|
||||||
|
|
||||||
|
class DockerHost {
|
||||||
|
/**
|
||||||
|
* Save a docker host
|
||||||
|
* @param {Object} dockerHost Docker host to save
|
||||||
|
* @param {?number} dockerHostID ID of the docker host to update
|
||||||
|
* @param {number} userID ID of the user who adds the docker host
|
||||||
|
* @returns {Promise<Bean>}
|
||||||
|
*/
|
||||||
|
static async save(dockerHost, dockerHostID, userID) {
|
||||||
|
let bean;
|
||||||
|
|
||||||
|
if (dockerHostID) {
|
||||||
|
bean = await R.findOne("docker_host", " id = ? AND user_id = ? ", [ dockerHostID, userID ]);
|
||||||
|
|
||||||
|
if (!bean) {
|
||||||
|
throw new Error("docker host not found");
|
||||||
|
}
|
||||||
|
|
||||||
|
} else {
|
||||||
|
bean = R.dispense("docker_host");
|
||||||
|
}
|
||||||
|
|
||||||
|
bean.user_id = userID;
|
||||||
|
bean.docker_daemon = dockerHost.dockerDaemon;
|
||||||
|
bean.docker_type = dockerHost.dockerType;
|
||||||
|
bean.name = dockerHost.name;
|
||||||
|
|
||||||
|
await R.store(bean);
|
||||||
|
|
||||||
|
return bean;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete a Docker host
|
||||||
|
* @param {number} dockerHostID ID of the Docker host to delete
|
||||||
|
* @param {number} userID ID of the user who created the Docker host
|
||||||
|
* @returns {Promise<void>}
|
||||||
|
*/
|
||||||
|
static async delete(dockerHostID, userID) {
|
||||||
|
let bean = await R.findOne("docker_host", " id = ? AND user_id = ? ", [ dockerHostID, userID ]);
|
||||||
|
|
||||||
|
if (!bean) {
|
||||||
|
throw new Error("docker host not found");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete removed proxy from monitors if exists
|
||||||
|
await R.exec("UPDATE monitor SET docker_host = null WHERE docker_host = ?", [ dockerHostID ]);
|
||||||
|
|
||||||
|
await R.trash(bean);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetches the amount of containers on the Docker host
|
||||||
|
* @param {Object} dockerHost Docker host to check for
|
||||||
|
* @returns {number} Total amount of containers on the host
|
||||||
|
*/
|
||||||
|
static async testDockerHost(dockerHost) {
|
||||||
|
const options = {
|
||||||
|
url: "/containers/json?all=true",
|
||||||
|
headers: {
|
||||||
|
"Accept": "*/*",
|
||||||
|
"User-Agent": "Uptime-Kuma/" + version
|
||||||
|
},
|
||||||
|
httpsAgent: new https.Agent({
|
||||||
|
maxCachedSessions: 0, // Use Custom agent to disable session reuse (https://github.com/nodejs/node/issues/3940)
|
||||||
|
rejectUnauthorized: false,
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
|
||||||
|
if (dockerHost.dockerType === "socket") {
|
||||||
|
options.socketPath = dockerHost.dockerDaemon;
|
||||||
|
} else if (dockerHost.dockerType === "tcp") {
|
||||||
|
options.baseURL = dockerHost.dockerDaemon;
|
||||||
|
}
|
||||||
|
|
||||||
|
let res = await axios.request(options);
|
||||||
|
|
||||||
|
if (Array.isArray(res.data)) {
|
||||||
|
|
||||||
|
if (res.data.length > 1) {
|
||||||
|
|
||||||
|
if ("ImageID" in res.data[0]) {
|
||||||
|
return res.data.length;
|
||||||
|
} else {
|
||||||
|
throw new Error("Invalid Docker response, is it Docker really a daemon?");
|
||||||
|
}
|
||||||
|
|
||||||
|
} else {
|
||||||
|
return res.data.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
} else {
|
||||||
|
throw new Error("Invalid Docker response, is it Docker really a daemon?");
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
DockerHost,
|
||||||
|
};
|
19
server/model/docker_host.js
Normal file
19
server/model/docker_host.js
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
const { BeanModel } = require("redbean-node/dist/bean-model");
|
||||||
|
|
||||||
|
class DockerHost extends BeanModel {
|
||||||
|
/**
|
||||||
|
* Returns an object that ready to parse to JSON
|
||||||
|
* @returns {Object}
|
||||||
|
*/
|
||||||
|
toJSON() {
|
||||||
|
return {
|
||||||
|
id: this.id,
|
||||||
|
userID: this.user_id,
|
||||||
|
dockerDaemon: this.docker_daemon,
|
||||||
|
dockerType: this.docker_type,
|
||||||
|
name: this.name,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = DockerHost;
|
|
@ -31,7 +31,7 @@ class Group extends BeanModel {
|
||||||
*/
|
*/
|
||||||
async getMonitorList() {
|
async getMonitorList() {
|
||||||
return R.convertToBeans("monitor", await R.getAll(`
|
return R.convertToBeans("monitor", await R.getAll(`
|
||||||
SELECT monitor.* FROM monitor, monitor_group
|
SELECT monitor.*, monitor_group.send_url FROM monitor, monitor_group
|
||||||
WHERE monitor.id = monitor_group.monitor_id
|
WHERE monitor.id = monitor_group.monitor_id
|
||||||
AND group_id = ?
|
AND group_id = ?
|
||||||
ORDER BY monitor_group.weight
|
ORDER BY monitor_group.weight
|
||||||
|
|
|
@ -7,7 +7,7 @@ dayjs.extend(timezone);
|
||||||
const axios = require("axios");
|
const axios = require("axios");
|
||||||
const { Prometheus } = require("../prometheus");
|
const { Prometheus } = require("../prometheus");
|
||||||
const { log, UP, DOWN, PENDING, MAINTENANCE, flipStatus, TimeLogger } = require("../../src/util");
|
const { log, UP, DOWN, PENDING, MAINTENANCE, flipStatus, TimeLogger } = require("../../src/util");
|
||||||
const { tcping, ping, dnsResolve, checkCertificate, checkStatusCode, getTotalClientInRoom, setting, mqttAsync } = require("../util-server");
|
const { tcping, ping, dnsResolve, checkCertificate, checkStatusCode, getTotalClientInRoom, setting, mssqlQuery, postgresQuery, mqttAsync, setSetting, httpNtlm, radius } = require("../util-server");
|
||||||
const { R } = require("redbean-node");
|
const { R } = require("redbean-node");
|
||||||
const { BeanModel } = require("redbean-node/dist/bean-model");
|
const { BeanModel } = require("redbean-node/dist/bean-model");
|
||||||
const { Notification } = require("../notification");
|
const { Notification } = require("../notification");
|
||||||
|
@ -16,6 +16,7 @@ const { demoMode } = require("../config");
|
||||||
const version = require("../../package.json").version;
|
const version = require("../../package.json").version;
|
||||||
const apicache = require("../modules/apicache");
|
const apicache = require("../modules/apicache");
|
||||||
const { UptimeKumaServer } = require("../uptime-kuma-server");
|
const { UptimeKumaServer } = require("../uptime-kuma-server");
|
||||||
|
const { CacheableDnsHttpAgent } = require("../cacheable-dns-http-agent");
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* status:
|
* status:
|
||||||
|
@ -35,8 +36,14 @@ class Monitor extends BeanModel {
|
||||||
let obj = {
|
let obj = {
|
||||||
id: this.id,
|
id: this.id,
|
||||||
name: this.name,
|
name: this.name,
|
||||||
|
sendUrl: this.sendUrl,
|
||||||
maintenance: await Monitor.isUnderMaintenance(this.id),
|
maintenance: await Monitor.isUnderMaintenance(this.id),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (this.sendUrl) {
|
||||||
|
obj.url = this.url;
|
||||||
|
}
|
||||||
|
|
||||||
if (showTags) {
|
if (showTags) {
|
||||||
obj.tags = await this.getTags();
|
obj.tags = await this.getTags();
|
||||||
}
|
}
|
||||||
|
@ -74,6 +81,7 @@ class Monitor extends BeanModel {
|
||||||
type: this.type,
|
type: this.type,
|
||||||
interval: this.interval,
|
interval: this.interval,
|
||||||
retryInterval: this.retryInterval,
|
retryInterval: this.retryInterval,
|
||||||
|
resendInterval: this.resendInterval,
|
||||||
keyword: this.keyword,
|
keyword: this.keyword,
|
||||||
expiryNotification: this.isEnabledExpiryNotification(),
|
expiryNotification: this.isEnabledExpiryNotification(),
|
||||||
ignoreTls: this.getIgnoreTls(),
|
ignoreTls: this.getIgnoreTls(),
|
||||||
|
@ -83,6 +91,9 @@ class Monitor extends BeanModel {
|
||||||
dns_resolve_type: this.dns_resolve_type,
|
dns_resolve_type: this.dns_resolve_type,
|
||||||
dns_resolve_server: this.dns_resolve_server,
|
dns_resolve_server: this.dns_resolve_server,
|
||||||
dns_last_result: this.dns_last_result,
|
dns_last_result: this.dns_last_result,
|
||||||
|
pushToken: this.pushToken,
|
||||||
|
docker_container: this.docker_container,
|
||||||
|
docker_host: this.docker_host,
|
||||||
proxyId: this.proxy_id,
|
proxyId: this.proxy_id,
|
||||||
notificationIDList,
|
notificationIDList,
|
||||||
tags: tags,
|
tags: tags,
|
||||||
|
@ -90,7 +101,17 @@ class Monitor extends BeanModel {
|
||||||
mqttUsername: this.mqttUsername,
|
mqttUsername: this.mqttUsername,
|
||||||
mqttPassword: this.mqttPassword,
|
mqttPassword: this.mqttPassword,
|
||||||
mqttTopic: this.mqttTopic,
|
mqttTopic: this.mqttTopic,
|
||||||
mqttSuccessMessage: this.mqttSuccessMessage
|
mqttSuccessMessage: this.mqttSuccessMessage,
|
||||||
|
databaseConnectionString: this.databaseConnectionString,
|
||||||
|
databaseQuery: this.databaseQuery,
|
||||||
|
authMethod: this.authMethod,
|
||||||
|
authWorkstation: this.authWorkstation,
|
||||||
|
authDomain: this.authDomain,
|
||||||
|
radiusUsername: this.radiusUsername,
|
||||||
|
radiusPassword: this.radiusPassword,
|
||||||
|
radiusCalledStationId: this.radiusCalledStationId,
|
||||||
|
radiusCallingStationId: this.radiusCallingStationId,
|
||||||
|
radiusSecret: this.radiusSecret,
|
||||||
};
|
};
|
||||||
|
|
||||||
if (includeSensitiveData) {
|
if (includeSensitiveData) {
|
||||||
|
@ -195,8 +216,9 @@ class Monitor extends BeanModel {
|
||||||
|
|
||||||
let bean = R.dispense("heartbeat");
|
let bean = R.dispense("heartbeat");
|
||||||
bean.monitor_id = this.id;
|
bean.monitor_id = this.id;
|
||||||
bean.time = R.isoDateTime(dayjs.utc());
|
bean.time = R.isoDateTimeMillis(dayjs.utc());
|
||||||
bean.status = DOWN;
|
bean.status = DOWN;
|
||||||
|
bean.downCount = previousBeat?.downCount || 0;
|
||||||
|
|
||||||
if (this.isUpsideDown()) {
|
if (this.isUpsideDown()) {
|
||||||
bean.status = flipStatus(bean.status);
|
bean.status = flipStatus(bean.status);
|
||||||
|
@ -219,7 +241,7 @@ class Monitor extends BeanModel {
|
||||||
|
|
||||||
// HTTP basic auth
|
// HTTP basic auth
|
||||||
let basicAuthHeader = {};
|
let basicAuthHeader = {};
|
||||||
if (this.basic_auth_user) {
|
if (this.auth_method === "basic") {
|
||||||
basicAuthHeader = {
|
basicAuthHeader = {
|
||||||
"Authorization": "Basic " + this.encodeBase64(this.basic_auth_user, this.basic_auth_pass),
|
"Authorization": "Basic " + this.encodeBase64(this.basic_auth_user, this.basic_auth_pass),
|
||||||
};
|
};
|
||||||
|
@ -270,7 +292,21 @@ class Monitor extends BeanModel {
|
||||||
log.debug("monitor", `[${this.name}] Axios Options: ${JSON.stringify(options)}`);
|
log.debug("monitor", `[${this.name}] Axios Options: ${JSON.stringify(options)}`);
|
||||||
log.debug("monitor", `[${this.name}] Axios Request`);
|
log.debug("monitor", `[${this.name}] Axios Request`);
|
||||||
|
|
||||||
let res = await axios.request(options);
|
let res;
|
||||||
|
if (this.auth_method === "ntlm") {
|
||||||
|
options.httpsAgent.keepAlive = true;
|
||||||
|
|
||||||
|
res = await httpNtlm(options, {
|
||||||
|
username: this.basic_auth_user,
|
||||||
|
password: this.basic_auth_pass,
|
||||||
|
domain: this.authDomain,
|
||||||
|
workstation: this.authWorkstation ? this.authWorkstation : undefined
|
||||||
|
});
|
||||||
|
|
||||||
|
} else {
|
||||||
|
res = await axios.request(options);
|
||||||
|
}
|
||||||
|
|
||||||
bean.msg = `${res.status} - ${res.statusText}`;
|
bean.msg = `${res.status} - ${res.statusText}`;
|
||||||
bean.ping = dayjs().valueOf() - startTime;
|
bean.ping = dayjs().valueOf() - startTime;
|
||||||
|
|
||||||
|
@ -318,7 +354,11 @@ class Monitor extends BeanModel {
|
||||||
bean.msg += ", keyword is found";
|
bean.msg += ", keyword is found";
|
||||||
bean.status = UP;
|
bean.status = UP;
|
||||||
} else {
|
} else {
|
||||||
throw new Error(bean.msg + ", but keyword is not found");
|
data = data.replace(/<[^>]*>?|[\n\r]|\s+/gm, " ");
|
||||||
|
if (data.length > 50) {
|
||||||
|
data = data.substring(0, 47) + "...";
|
||||||
|
}
|
||||||
|
throw new Error(bean.msg + ", but keyword is not in [" + data + "]");
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -336,7 +376,7 @@ class Monitor extends BeanModel {
|
||||||
let startTime = dayjs().valueOf();
|
let startTime = dayjs().valueOf();
|
||||||
let dnsMessage = "";
|
let dnsMessage = "";
|
||||||
|
|
||||||
let dnsRes = await dnsResolve(this.hostname, this.dns_resolve_server, this.dns_resolve_type);
|
let dnsRes = await dnsResolve(this.hostname, this.dns_resolve_server, this.port, this.dns_resolve_type);
|
||||||
bean.ping = dayjs().valueOf() - startTime;
|
bean.ping = dayjs().valueOf() - startTime;
|
||||||
|
|
||||||
if (this.dns_resolve_type === "A" || this.dns_resolve_type === "AAAA" || this.dns_resolve_type === "TXT") {
|
if (this.dns_resolve_type === "A" || this.dns_resolve_type === "AAAA" || this.dns_resolve_type === "TXT") {
|
||||||
|
@ -373,23 +413,34 @@ class Monitor extends BeanModel {
|
||||||
bean.msg = dnsMessage;
|
bean.msg = dnsMessage;
|
||||||
bean.status = UP;
|
bean.status = UP;
|
||||||
} else if (this.type === "push") { // Type: Push
|
} else if (this.type === "push") { // Type: Push
|
||||||
const time = R.isoDateTime(dayjs.utc().subtract(this.interval, "second"));
|
log.debug("monitor", `[${this.name}] Checking monitor at ${dayjs().format("YYYY-MM-DD HH:mm:ss.SSS")}`);
|
||||||
|
const bufferTime = 1000; // 1s buffer to accommodate clock differences
|
||||||
|
|
||||||
let heartbeatCount = await R.count("heartbeat", " monitor_id = ? AND time > ? ", [
|
if (previousBeat) {
|
||||||
this.id,
|
const msSinceLastBeat = dayjs.utc().valueOf() - dayjs.utc(previousBeat.time).valueOf();
|
||||||
time
|
|
||||||
]);
|
|
||||||
|
|
||||||
log.debug("monitor", "heartbeatCount" + heartbeatCount + " " + time);
|
log.debug("monitor", `[${this.name}] msSinceLastBeat = ${msSinceLastBeat}`);
|
||||||
|
|
||||||
if (heartbeatCount <= 0) {
|
// If the previous beat was down or pending we use the regular
|
||||||
|
// beatInterval/retryInterval in the setTimeout further below
|
||||||
|
if (previousBeat.status !== (this.isUpsideDown() ? DOWN : UP) || msSinceLastBeat > beatInterval * 1000 + bufferTime) {
|
||||||
throw new Error("No heartbeat in the time window");
|
throw new Error("No heartbeat in the time window");
|
||||||
} else {
|
} else {
|
||||||
|
let timeout = beatInterval * 1000 - msSinceLastBeat;
|
||||||
|
if (timeout < 0) {
|
||||||
|
timeout = bufferTime;
|
||||||
|
} else {
|
||||||
|
timeout += bufferTime;
|
||||||
|
}
|
||||||
// No need to insert successful heartbeat for push type, so end here
|
// No need to insert successful heartbeat for push type, so end here
|
||||||
retries = 0;
|
retries = 0;
|
||||||
this.heartbeatInterval = setTimeout(beat, beatInterval * 1000);
|
log.debug("monitor", `[${this.name}] timeout = ${timeout}`);
|
||||||
|
this.heartbeatInterval = setTimeout(beat, timeout);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
throw new Error("No heartbeat in the time window");
|
||||||
|
}
|
||||||
|
|
||||||
} else if (this.type === "steam") {
|
} else if (this.type === "steam") {
|
||||||
const steamApiUrl = "https://api.steampowered.com/IGameServersService/GetServerList/v1/";
|
const steamApiUrl = "https://api.steampowered.com/IGameServersService/GetServerList/v1/";
|
||||||
|
@ -406,10 +457,13 @@ class Monitor extends BeanModel {
|
||||||
"Accept": "*/*",
|
"Accept": "*/*",
|
||||||
"User-Agent": "Uptime-Kuma/" + version,
|
"User-Agent": "Uptime-Kuma/" + version,
|
||||||
},
|
},
|
||||||
httpsAgent: new https.Agent({
|
httpsAgent: CacheableDnsHttpAgent.getHttpsAgent({
|
||||||
maxCachedSessions: 0, // Use Custom agent to disable session reuse (https://github.com/nodejs/node/issues/3940)
|
maxCachedSessions: 0, // Use Custom agent to disable session reuse (https://github.com/nodejs/node/issues/3940)
|
||||||
rejectUnauthorized: !this.getIgnoreTls(),
|
rejectUnauthorized: !this.getIgnoreTls(),
|
||||||
}),
|
}),
|
||||||
|
httpAgent: CacheableDnsHttpAgent.getHttpAgent({
|
||||||
|
maxCachedSessions: 0,
|
||||||
|
}),
|
||||||
maxRedirects: this.maxredirects,
|
maxRedirects: this.maxredirects,
|
||||||
validateStatus: (status) => {
|
validateStatus: (status) => {
|
||||||
return checkStatusCode(status, this.getAcceptedStatuscodes());
|
return checkStatusCode(status, this.getAcceptedStatuscodes());
|
||||||
|
@ -430,6 +484,35 @@ class Monitor extends BeanModel {
|
||||||
} else {
|
} else {
|
||||||
throw new Error("Server not found on Steam");
|
throw new Error("Server not found on Steam");
|
||||||
}
|
}
|
||||||
|
} else if (this.type === "docker") {
|
||||||
|
log.debug(`[${this.name}] Prepare Options for Axios`);
|
||||||
|
|
||||||
|
const dockerHost = await R.load("docker_host", this.docker_host);
|
||||||
|
|
||||||
|
const options = {
|
||||||
|
url: `/containers/${this.docker_container}/json`,
|
||||||
|
headers: {
|
||||||
|
"Accept": "*/*",
|
||||||
|
"User-Agent": "Uptime-Kuma/" + version,
|
||||||
|
},
|
||||||
|
httpsAgent: new https.Agent({
|
||||||
|
maxCachedSessions: 0, // Use Custom agent to disable session reuse (https://github.com/nodejs/node/issues/3940)
|
||||||
|
rejectUnauthorized: ! this.getIgnoreTls(),
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
|
||||||
|
if (dockerHost._dockerType === "socket") {
|
||||||
|
options.socketPath = dockerHost._dockerDaemon;
|
||||||
|
} else if (dockerHost._dockerType === "tcp") {
|
||||||
|
options.baseURL = dockerHost._dockerDaemon;
|
||||||
|
}
|
||||||
|
|
||||||
|
log.debug(`[${this.name}] Axios Request`);
|
||||||
|
let res = await axios.request(options);
|
||||||
|
if (res.data.State.Running) {
|
||||||
|
bean.status = UP;
|
||||||
|
bean.msg = "";
|
||||||
|
}
|
||||||
} else if (this.type === "mqtt") {
|
} else if (this.type === "mqtt") {
|
||||||
bean.msg = await mqttAsync(this.hostname, this.mqttTopic, this.mqttSuccessMessage, {
|
bean.msg = await mqttAsync(this.hostname, this.mqttTopic, this.mqttSuccessMessage, {
|
||||||
port: this.port,
|
port: this.port,
|
||||||
|
@ -438,6 +521,46 @@ class Monitor extends BeanModel {
|
||||||
interval: this.interval,
|
interval: this.interval,
|
||||||
});
|
});
|
||||||
bean.status = UP;
|
bean.status = UP;
|
||||||
|
} else if (this.type === "sqlserver") {
|
||||||
|
let startTime = dayjs().valueOf();
|
||||||
|
|
||||||
|
await mssqlQuery(this.databaseConnectionString, this.databaseQuery);
|
||||||
|
|
||||||
|
bean.msg = "";
|
||||||
|
bean.status = UP;
|
||||||
|
bean.ping = dayjs().valueOf() - startTime;
|
||||||
|
} else if (this.type === "postgres") {
|
||||||
|
let startTime = dayjs().valueOf();
|
||||||
|
|
||||||
|
await postgresQuery(this.databaseConnectionString, this.databaseQuery);
|
||||||
|
|
||||||
|
bean.msg = "";
|
||||||
|
bean.status = UP;
|
||||||
|
bean.ping = dayjs().valueOf() - startTime;
|
||||||
|
} else if (this.type === "radius") {
|
||||||
|
let startTime = dayjs().valueOf();
|
||||||
|
try {
|
||||||
|
const resp = await radius(
|
||||||
|
this.hostname,
|
||||||
|
this.radiusUsername,
|
||||||
|
this.radiusPassword,
|
||||||
|
this.radiusCalledStationId,
|
||||||
|
this.radiusCallingStationId,
|
||||||
|
this.radiusSecret
|
||||||
|
);
|
||||||
|
if (resp.code) {
|
||||||
|
bean.msg = resp.code;
|
||||||
|
}
|
||||||
|
bean.status = UP;
|
||||||
|
} catch (error) {
|
||||||
|
bean.status = DOWN;
|
||||||
|
if (error.response?.code) {
|
||||||
|
bean.msg = error.response.code;
|
||||||
|
} else {
|
||||||
|
bean.msg = error.message;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
bean.ping = dayjs().valueOf() - startTime;
|
||||||
} else {
|
} else {
|
||||||
bean.msg = "Unknown Monitor Type";
|
bean.msg = "Unknown Monitor Type";
|
||||||
bean.status = PENDING;
|
bean.status = PENDING;
|
||||||
|
@ -483,16 +606,31 @@ class Monitor extends BeanModel {
|
||||||
log.debug("monitor", `[${this.name}] will not sendNotification because it is (or was) under maintenance`);
|
log.debug("monitor", `[${this.name}] will not sendNotification because it is (or was) under maintenance`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Reset down count
|
||||||
|
bean.downCount = 0;
|
||||||
|
|
||||||
// Clear Status Page Cache
|
// Clear Status Page Cache
|
||||||
log.debug("monitor", `[${this.name}] apicache clear`);
|
log.debug("monitor", `[${this.name}] apicache clear`);
|
||||||
apicache.clear();
|
apicache.clear();
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
bean.important = false;
|
bean.important = false;
|
||||||
|
|
||||||
|
if (bean.status === DOWN && this.resendInterval > 0) {
|
||||||
|
++bean.downCount;
|
||||||
|
if (bean.downCount >= this.resendInterval) {
|
||||||
|
// Send notification again, because we are still DOWN
|
||||||
|
log.debug("monitor", `[${this.name}] sendNotification again: Down Count: ${bean.downCount} | Resend Interval: ${this.resendInterval}`);
|
||||||
|
await Monitor.sendNotification(isFirstBeat, this, bean);
|
||||||
|
|
||||||
|
// Reset down count
|
||||||
|
bean.downCount = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (bean.status === UP) {
|
if (bean.status === UP) {
|
||||||
log.info("monitor", `Monitor #${this.id} '${this.name}': Successful Response: ${bean.ping} ms | Interval: ${beatInterval} seconds | Type: ${this.type}`);
|
log.debug("monitor", `Monitor #${this.id} '${this.name}': Successful Response: ${bean.ping} ms | Interval: ${beatInterval} seconds | Type: ${this.type}`);
|
||||||
} else if (bean.status === PENDING) {
|
} else if (bean.status === PENDING) {
|
||||||
if (this.retryInterval > 0) {
|
if (this.retryInterval > 0) {
|
||||||
beatInterval = this.retryInterval;
|
beatInterval = this.retryInterval;
|
||||||
|
@ -501,7 +639,7 @@ class Monitor extends BeanModel {
|
||||||
} else if (bean.status === MAINTENANCE) {
|
} else if (bean.status === MAINTENANCE) {
|
||||||
log.warn("monitor", `Monitor #${this.id} '${this.name}': Under Maintenance | Type: ${this.type}`);
|
log.warn("monitor", `Monitor #${this.id} '${this.name}': Under Maintenance | Type: ${this.type}`);
|
||||||
} else {
|
} else {
|
||||||
log.warn("monitor", `Monitor #${this.id} '${this.name}': Failing: ${bean.msg} | Interval: ${beatInterval} seconds | Type: ${this.type}`);
|
log.warn("monitor", `Monitor #${this.id} '${this.name}': Failing: ${bean.msg} | Interval: ${beatInterval} seconds | Type: ${this.type} | Down Count: ${bean.downCount} | Resend Interval: ${this.resendInterval}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
log.debug("monitor", `[${this.name}] Send to socket`);
|
log.debug("monitor", `[${this.name}] Send to socket`);
|
||||||
|
@ -876,10 +1014,19 @@ class Monitor extends BeanModel {
|
||||||
if (tlsInfoObject && tlsInfoObject.certInfo && tlsInfoObject.certInfo.daysRemaining) {
|
if (tlsInfoObject && tlsInfoObject.certInfo && tlsInfoObject.certInfo.daysRemaining) {
|
||||||
const notificationList = await Monitor.getNotificationList(this);
|
const notificationList = await Monitor.getNotificationList(this);
|
||||||
|
|
||||||
log.debug("monitor", "call sendCertNotificationByTargetDays");
|
let notifyDays = await setting("tlsExpiryNotifyDays");
|
||||||
await this.sendCertNotificationByTargetDays(tlsInfoObject.certInfo.daysRemaining, 21, notificationList);
|
if (notifyDays == null || !Array.isArray(notifyDays)) {
|
||||||
await this.sendCertNotificationByTargetDays(tlsInfoObject.certInfo.daysRemaining, 14, notificationList);
|
// Reset Default
|
||||||
await this.sendCertNotificationByTargetDays(tlsInfoObject.certInfo.daysRemaining, 7, notificationList);
|
setSetting("tlsExpiryNotifyDays", [ 7, 14, 21 ], "general");
|
||||||
|
notifyDays = [ 7, 14, 21 ];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (notifyDays != null && Array.isArray(notifyDays)) {
|
||||||
|
for (const day of notifyDays) {
|
||||||
|
log.debug("monitor", "call sendCertNotificationByTargetDays", day);
|
||||||
|
await this.sendCertNotificationByTargetDays(tlsInfoObject.certInfo.daysRemaining, day, notificationList);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,10 +1,109 @@
|
||||||
const { BeanModel } = require("redbean-node/dist/bean-model");
|
const { BeanModel } = require("redbean-node/dist/bean-model");
|
||||||
const { R } = require("redbean-node");
|
const { R } = require("redbean-node");
|
||||||
|
const cheerio = require("cheerio");
|
||||||
|
const { UptimeKumaServer } = require("../uptime-kuma-server");
|
||||||
|
|
||||||
class StatusPage extends BeanModel {
|
class StatusPage extends BeanModel {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Like this: { "test-uptime.kuma.pet": "default" }
|
||||||
|
* @type {{}}
|
||||||
|
*/
|
||||||
static domainMappingList = { };
|
static domainMappingList = { };
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param {Response} response
|
||||||
|
* @param {string} indexHTML
|
||||||
|
* @param {string} slug
|
||||||
|
*/
|
||||||
|
static async handleStatusPageResponse(response, indexHTML, slug) {
|
||||||
|
let statusPage = await R.findOne("status_page", " slug = ? ", [
|
||||||
|
slug
|
||||||
|
]);
|
||||||
|
|
||||||
|
if (statusPage) {
|
||||||
|
response.send(await StatusPage.renderHTML(indexHTML, statusPage));
|
||||||
|
} else {
|
||||||
|
response.status(404).send(UptimeKumaServer.getInstance().indexHTML);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* SSR for status pages
|
||||||
|
* @param {string} indexHTML
|
||||||
|
* @param {StatusPage} statusPage
|
||||||
|
*/
|
||||||
|
static async renderHTML(indexHTML, statusPage) {
|
||||||
|
const $ = cheerio.load(indexHTML);
|
||||||
|
const description155 = statusPage.description?.substring(0, 155);
|
||||||
|
|
||||||
|
$("title").text(statusPage.title);
|
||||||
|
$("meta[name=description]").attr("content", description155);
|
||||||
|
|
||||||
|
if (statusPage.icon) {
|
||||||
|
$("link[rel=icon]")
|
||||||
|
.attr("href", statusPage.icon)
|
||||||
|
.removeAttr("type");
|
||||||
|
|
||||||
|
$("link[rel=apple-touch-icon]").remove();
|
||||||
|
}
|
||||||
|
|
||||||
|
const head = $("head");
|
||||||
|
|
||||||
|
// OG Meta Tags
|
||||||
|
head.append(`<meta property="og:title" content="${statusPage.title}" />`);
|
||||||
|
head.append(`<meta property="og:description" content="${description155}" />`);
|
||||||
|
|
||||||
|
// Preload data
|
||||||
|
const json = JSON.stringify(await StatusPage.getStatusPageData(statusPage));
|
||||||
|
head.append(`
|
||||||
|
<script>
|
||||||
|
window.preloadData = ${json}
|
||||||
|
</script>
|
||||||
|
`);
|
||||||
|
|
||||||
|
// manifest.json
|
||||||
|
$("link[rel=manifest]").attr("href", `/api/status-page/${statusPage.slug}/manifest.json`);
|
||||||
|
|
||||||
|
return $.root().html();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all status page data in one call
|
||||||
|
* @param {StatusPage} statusPage
|
||||||
|
*/
|
||||||
|
static async getStatusPageData(statusPage) {
|
||||||
|
// Incident
|
||||||
|
let incident = await R.findOne("incident", " pin = 1 AND active = 1 AND status_page_id = ? ", [
|
||||||
|
statusPage.id,
|
||||||
|
]);
|
||||||
|
|
||||||
|
if (incident) {
|
||||||
|
incident = incident.toPublicJSON();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Public Group List
|
||||||
|
const publicGroupList = [];
|
||||||
|
const showTags = !!statusPage.show_tags;
|
||||||
|
|
||||||
|
const list = await R.find("group", " public = 1 AND status_page_id = ? ORDER BY weight ", [
|
||||||
|
statusPage.id
|
||||||
|
]);
|
||||||
|
|
||||||
|
for (let groupBean of list) {
|
||||||
|
let monitorGroup = await groupBean.toPublicJSON(showTags);
|
||||||
|
publicGroupList.push(monitorGroup);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Response
|
||||||
|
return {
|
||||||
|
config: await statusPage.toPublicJSON(),
|
||||||
|
incident,
|
||||||
|
publicGroupList
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Loads domain mapping from DB
|
* Loads domain mapping from DB
|
||||||
* Return object like this: { "test-uptime.kuma.pet": "default" }
|
* Return object like this: { "test-uptime.kuma.pet": "default" }
|
||||||
|
|
50
server/notification-providers/alertnow.js
Normal file
50
server/notification-providers/alertnow.js
Normal file
|
@ -0,0 +1,50 @@
|
||||||
|
const NotificationProvider = require("./notification-provider");
|
||||||
|
const axios = require("axios");
|
||||||
|
const { setting } = require("../util-server");
|
||||||
|
const { getMonitorRelativeURL, UP, DOWN } = require("../../src/util");
|
||||||
|
|
||||||
|
class AlertNow extends NotificationProvider {
|
||||||
|
|
||||||
|
name = "AlertNow";
|
||||||
|
|
||||||
|
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
|
||||||
|
let okMsg = "Sent Successfully.";
|
||||||
|
try {
|
||||||
|
let textMsg = "";
|
||||||
|
let status = "open";
|
||||||
|
let eventType = "ERROR";
|
||||||
|
let eventId = new Date().toISOString().slice(0, 10).replace(/-/g, "");
|
||||||
|
|
||||||
|
if (heartbeatJSON && heartbeatJSON.status === UP) {
|
||||||
|
textMsg = `[${heartbeatJSON.name}] ✅ Application is back online`;
|
||||||
|
status = "close";
|
||||||
|
eventType = "INFO";
|
||||||
|
eventId += `_${heartbeatJSON.name.replace(/\s/g, "")}`;
|
||||||
|
} else if (heartbeatJSON && heartbeatJSON.status === DOWN) {
|
||||||
|
textMsg = `[${heartbeatJSON.name}] 🔴 Application went down`;
|
||||||
|
}
|
||||||
|
|
||||||
|
textMsg += ` - ${msg}`;
|
||||||
|
|
||||||
|
const baseURL = await setting("primaryBaseURL");
|
||||||
|
if (baseURL && monitorJSON) {
|
||||||
|
textMsg += ` >> ${baseURL + getMonitorRelativeURL(monitorJSON.id)}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = {
|
||||||
|
"summary": textMsg,
|
||||||
|
"status": status,
|
||||||
|
"event_type": eventType,
|
||||||
|
"event_id": eventId,
|
||||||
|
};
|
||||||
|
|
||||||
|
await axios.post(notification.alertNowWebhookURL, data);
|
||||||
|
return okMsg;
|
||||||
|
} catch (error) {
|
||||||
|
this.throwGeneralAxiosError(error);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = AlertNow;
|
|
@ -12,9 +12,7 @@ const { default: axios } = require("axios");
|
||||||
|
|
||||||
// bark is an APN bridge that sends notifications to Apple devices.
|
// bark is an APN bridge that sends notifications to Apple devices.
|
||||||
|
|
||||||
const barkNotificationGroup = "UptimeKuma";
|
|
||||||
const barkNotificationAvatar = "https://github.com/louislam/uptime-kuma/raw/master/public/icon.png";
|
const barkNotificationAvatar = "https://github.com/louislam/uptime-kuma/raw/master/public/icon.png";
|
||||||
const barkNotificationSound = "telegraph";
|
|
||||||
const successMessage = "Successes!";
|
const successMessage = "Successes!";
|
||||||
|
|
||||||
class Bark extends NotificationProvider {
|
class Bark extends NotificationProvider {
|
||||||
|
@ -30,17 +28,17 @@ class Bark extends NotificationProvider {
|
||||||
|
|
||||||
if (msg != null && heartbeatJSON != null && heartbeatJSON["status"] === UP) {
|
if (msg != null && heartbeatJSON != null && heartbeatJSON["status"] === UP) {
|
||||||
let title = "UptimeKuma Monitor Up";
|
let title = "UptimeKuma Monitor Up";
|
||||||
return await this.postNotification(title, msg, barkEndpoint);
|
return await this.postNotification(notification, title, msg, barkEndpoint);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (msg != null && heartbeatJSON != null && heartbeatJSON["status"] === DOWN) {
|
if (msg != null && heartbeatJSON != null && heartbeatJSON["status"] === DOWN) {
|
||||||
let title = "UptimeKuma Monitor Down";
|
let title = "UptimeKuma Monitor Down";
|
||||||
return await this.postNotification(title, msg, barkEndpoint);
|
return await this.postNotification(notification, title, msg, barkEndpoint);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (msg != null) {
|
if (msg != null) {
|
||||||
let title = "UptimeKuma Message";
|
let title = "UptimeKuma Message";
|
||||||
return await this.postNotification(title, msg, barkEndpoint);
|
return await this.postNotification(notification, title, msg, barkEndpoint);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -50,13 +48,23 @@ class Bark extends NotificationProvider {
|
||||||
* @param {string} postUrl URL to append parameters to
|
* @param {string} postUrl URL to append parameters to
|
||||||
* @returns {string}
|
* @returns {string}
|
||||||
*/
|
*/
|
||||||
appendAdditionalParameters(postUrl) {
|
appendAdditionalParameters(notification, postUrl) {
|
||||||
// grouping all our notifications
|
|
||||||
postUrl += "?group=" + barkNotificationGroup;
|
|
||||||
// set icon to uptime kuma icon, 11kb should be fine
|
// set icon to uptime kuma icon, 11kb should be fine
|
||||||
postUrl += "&icon=" + barkNotificationAvatar;
|
postUrl += "?icon=" + barkNotificationAvatar;
|
||||||
|
// grouping all our notifications
|
||||||
|
if (notification.barkGroup != null) {
|
||||||
|
postUrl += "&group=" + notification.barkGroup;
|
||||||
|
} else {
|
||||||
|
// default name
|
||||||
|
postUrl += "&group=" + "UptimeKuma";
|
||||||
|
}
|
||||||
// picked a sound, this should follow system's mute status when arrival
|
// picked a sound, this should follow system's mute status when arrival
|
||||||
postUrl += "&sound=" + barkNotificationSound;
|
if (notification.barkSound != null) {
|
||||||
|
postUrl += "&sound=" + notification.barkSound;
|
||||||
|
} else {
|
||||||
|
// default sound
|
||||||
|
postUrl += "&sound=" + "telegraph";
|
||||||
|
}
|
||||||
return postUrl;
|
return postUrl;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -81,12 +89,12 @@ class Bark extends NotificationProvider {
|
||||||
* @param {string} endpoint Endpoint to send request to
|
* @param {string} endpoint Endpoint to send request to
|
||||||
* @returns {string}
|
* @returns {string}
|
||||||
*/
|
*/
|
||||||
async postNotification(title, subtitle, endpoint) {
|
async postNotification(notification, title, subtitle, endpoint) {
|
||||||
// url encode title and subtitle
|
// url encode title and subtitle
|
||||||
title = encodeURIComponent(title);
|
title = encodeURIComponent(title);
|
||||||
subtitle = encodeURIComponent(subtitle);
|
subtitle = encodeURIComponent(subtitle);
|
||||||
let postUrl = endpoint + "/" + title + "/" + subtitle;
|
let postUrl = endpoint + "/" + title + "/" + subtitle;
|
||||||
postUrl = this.appendAdditionalParameters(postUrl);
|
postUrl = this.appendAdditionalParameters(notification, postUrl);
|
||||||
let result = await axios.get(postUrl);
|
let result = await axios.get(postUrl);
|
||||||
this.checkResult(result);
|
this.checkResult(result);
|
||||||
if (result.statusText != null) {
|
if (result.statusText != null) {
|
||||||
|
|
|
@ -55,8 +55,8 @@ class Discord extends NotificationProvider {
|
||||||
value: monitorJSON["name"],
|
value: monitorJSON["name"],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Service URL / Address",
|
name: monitorJSON["type"] === "push" ? "Service Type" : "Service URL",
|
||||||
value: address,
|
value: monitorJSON["type"] === "push" ? "Heartbeat" : address,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Time (UTC)",
|
name: "Time (UTC)",
|
||||||
|
@ -90,8 +90,8 @@ class Discord extends NotificationProvider {
|
||||||
value: monitorJSON["name"],
|
value: monitorJSON["name"],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Service URL",
|
name: monitorJSON["type"] === "push" ? "Service Type" : "Service URL",
|
||||||
value: address.startsWith("http") ? "[Visit Service](" + address + ")" : address,
|
value: monitorJSON["type"] === "push" ? "Heartbeat" : address.startsWith("http") ? "[Visit Service](" + address + ")" : address,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Time (UTC)",
|
name: "Time (UTC)",
|
||||||
|
@ -99,7 +99,7 @@ class Discord extends NotificationProvider {
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Ping",
|
name: "Ping",
|
||||||
value: heartbeatJSON["ping"] + "ms",
|
value: heartbeatJSON["ping"] == null ? "N/A" : heartbeatJSON["ping"] + " ms",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
}],
|
}],
|
||||||
|
|
35
server/notification-providers/goalert.js
Normal file
35
server/notification-providers/goalert.js
Normal file
|
@ -0,0 +1,35 @@
|
||||||
|
const NotificationProvider = require("./notification-provider");
|
||||||
|
const axios = require("axios");
|
||||||
|
const { UP } = require("../../src/util");
|
||||||
|
|
||||||
|
class GoAlert extends NotificationProvider {
|
||||||
|
|
||||||
|
name = "GoAlert";
|
||||||
|
|
||||||
|
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
|
||||||
|
let okMsg = "Sent Successfully.";
|
||||||
|
try {
|
||||||
|
let closeAction = "close";
|
||||||
|
let data = {
|
||||||
|
summary: msg,
|
||||||
|
};
|
||||||
|
if (heartbeatJSON != null && heartbeatJSON["status"] === UP) {
|
||||||
|
data["action"] = closeAction;
|
||||||
|
}
|
||||||
|
let headers = {
|
||||||
|
"Content-Type": "multipart/form-data",
|
||||||
|
};
|
||||||
|
let config = {
|
||||||
|
headers: headers
|
||||||
|
};
|
||||||
|
await axios.post(`${notification.goAlertBaseURL}/api/v2/generic/incoming?token=${notification.goAlertToken}`, data, config);
|
||||||
|
return okMsg;
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
let msg = (error.response.data) ? error.response.data : "Error without response";
|
||||||
|
throw new Error(msg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = GoAlert;
|
38
server/notification-providers/home-assistant.js
Normal file
38
server/notification-providers/home-assistant.js
Normal file
|
@ -0,0 +1,38 @@
|
||||||
|
const NotificationProvider = require("./notification-provider");
|
||||||
|
const axios = require("axios");
|
||||||
|
|
||||||
|
const defaultNotificationService = "notify";
|
||||||
|
|
||||||
|
class HomeAssistant extends NotificationProvider {
|
||||||
|
name = "HomeAssistant";
|
||||||
|
|
||||||
|
async send(notification, message, monitor = null, heartbeat = null) {
|
||||||
|
const notificationService = notification?.notificationService || defaultNotificationService;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await axios.post(
|
||||||
|
`${notification.homeAssistantUrl}/api/services/notify/${notificationService}`,
|
||||||
|
{
|
||||||
|
title: "Uptime Kuma",
|
||||||
|
message,
|
||||||
|
...(notificationService !== "persistent_notification" && { data: {
|
||||||
|
name: monitor?.name,
|
||||||
|
status: heartbeat?.status,
|
||||||
|
} }),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${notification.longLivedAccessToken}`,
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
return "Sent Successfully.";
|
||||||
|
} catch (error) {
|
||||||
|
this.throwGeneralAxiosError(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = HomeAssistant;
|
43
server/notification-providers/linenotify.js
Normal file
43
server/notification-providers/linenotify.js
Normal file
|
@ -0,0 +1,43 @@
|
||||||
|
const NotificationProvider = require("./notification-provider");
|
||||||
|
const axios = require("axios");
|
||||||
|
const qs = require("qs");
|
||||||
|
const { DOWN, UP } = require("../../src/util");
|
||||||
|
|
||||||
|
class LineNotify extends NotificationProvider {
|
||||||
|
|
||||||
|
name = "LineNotify";
|
||||||
|
|
||||||
|
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
|
||||||
|
let okMsg = "Sent Successfully.";
|
||||||
|
try {
|
||||||
|
let lineAPIUrl = "https://notify-api.line.me/api/notify";
|
||||||
|
let config = {
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/x-www-form-urlencoded",
|
||||||
|
"Authorization": "Bearer " + notification.lineNotifyAccessToken
|
||||||
|
}
|
||||||
|
};
|
||||||
|
if (heartbeatJSON == null) {
|
||||||
|
let testMessage = {
|
||||||
|
"message": msg,
|
||||||
|
};
|
||||||
|
await axios.post(lineAPIUrl, qs.stringify(testMessage), config);
|
||||||
|
} else if (heartbeatJSON["status"] === DOWN) {
|
||||||
|
let downMessage = {
|
||||||
|
"message": "\n[🔴 Down]\n" + "Name: " + monitorJSON["name"] + " \n" + heartbeatJSON["msg"] + "\nTime (UTC): " + heartbeatJSON["time"]
|
||||||
|
};
|
||||||
|
await axios.post(lineAPIUrl, qs.stringify(downMessage), config);
|
||||||
|
} else if (heartbeatJSON["status"] === UP) {
|
||||||
|
let upMessage = {
|
||||||
|
"message": "\n[✅ Up]\n" + "Name: " + monitorJSON["name"] + " \n" + heartbeatJSON["msg"] + "\nTime (UTC): " + heartbeatJSON["time"]
|
||||||
|
};
|
||||||
|
await axios.post(lineAPIUrl, qs.stringify(upMessage), config);
|
||||||
|
}
|
||||||
|
return okMsg;
|
||||||
|
} catch (error) {
|
||||||
|
this.throwGeneralAxiosError(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = LineNotify;
|
|
@ -14,7 +14,7 @@ class LunaSea extends NotificationProvider {
|
||||||
if (heartbeatJSON == null) {
|
if (heartbeatJSON == null) {
|
||||||
let testdata = {
|
let testdata = {
|
||||||
"title": "Uptime Kuma Alert",
|
"title": "Uptime Kuma Alert",
|
||||||
"body": "Testing Successful.",
|
"body": msg,
|
||||||
};
|
};
|
||||||
await axios.post(lunaseadevice, testdata);
|
await axios.post(lunaseadevice, testdata);
|
||||||
return okMsg;
|
return okMsg;
|
||||||
|
|
26
server/notification-providers/ntfy.js
Normal file
26
server/notification-providers/ntfy.js
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
const NotificationProvider = require("./notification-provider");
|
||||||
|
const axios = require("axios");
|
||||||
|
|
||||||
|
class Ntfy extends NotificationProvider {
|
||||||
|
|
||||||
|
name = "ntfy";
|
||||||
|
|
||||||
|
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
|
||||||
|
let okMsg = "Sent Successfully.";
|
||||||
|
try {
|
||||||
|
await axios.post(`${notification.ntfyserverurl}`, {
|
||||||
|
"topic": notification.ntfytopic,
|
||||||
|
"message": msg,
|
||||||
|
"priority": notification.ntfyPriority || 4,
|
||||||
|
"title": "Uptime-Kuma",
|
||||||
|
});
|
||||||
|
|
||||||
|
return okMsg;
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
this.throwGeneralAxiosError(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = Ntfy;
|
113
server/notification-providers/pagerduty.js
Normal file
113
server/notification-providers/pagerduty.js
Normal file
|
@ -0,0 +1,113 @@
|
||||||
|
const NotificationProvider = require("./notification-provider");
|
||||||
|
const axios = require("axios");
|
||||||
|
const { UP, DOWN, getMonitorRelativeURL } = require("../../src/util");
|
||||||
|
const { setting } = require("../util-server");
|
||||||
|
let successMessage = "Sent Successfully.";
|
||||||
|
|
||||||
|
class PagerDuty extends NotificationProvider {
|
||||||
|
name = "PagerDuty";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @inheritdoc
|
||||||
|
*/
|
||||||
|
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
|
||||||
|
try {
|
||||||
|
if (heartbeatJSON == null) {
|
||||||
|
const title = "Uptime Kuma Alert";
|
||||||
|
const monitor = {
|
||||||
|
type: "ping",
|
||||||
|
url: "Uptime Kuma Test Button",
|
||||||
|
};
|
||||||
|
return this.postNotification(notification, title, msg, monitor);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (heartbeatJSON.status === UP) {
|
||||||
|
const title = "Uptime Kuma Monitor ✅ Up";
|
||||||
|
const eventAction = notification.pagerdutyAutoResolve || null;
|
||||||
|
|
||||||
|
return this.postNotification(notification, title, heartbeatJSON.msg, monitorJSON, eventAction);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (heartbeatJSON.status === DOWN) {
|
||||||
|
const title = "Uptime Kuma Monitor 🔴 Down";
|
||||||
|
return this.postNotification(notification, title, heartbeatJSON.msg, monitorJSON, "trigger");
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
this.throwGeneralAxiosError(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if result is successful, result code should be in range 2xx
|
||||||
|
* @param {Object} result Axios response object
|
||||||
|
* @throws {Error} The status code is not in range 2xx
|
||||||
|
*/
|
||||||
|
checkResult(result) {
|
||||||
|
if (result.status == null) {
|
||||||
|
throw new Error("PagerDuty notification failed with invalid response!");
|
||||||
|
}
|
||||||
|
if (result.status < 200 || result.status >= 300) {
|
||||||
|
throw new Error("PagerDuty notification failed with status code " + result.status);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send the message
|
||||||
|
* @param {BeanModel} notification Message title
|
||||||
|
* @param {string} title Message title
|
||||||
|
* @param {string} body Message
|
||||||
|
* @param {Object} monitorInfo Monitor details (For Up/Down only)
|
||||||
|
* @param {?string} eventAction Action event for PagerDuty (trigger, acknowledge, resolve)
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
async postNotification(notification, title, body, monitorInfo, eventAction = "trigger") {
|
||||||
|
|
||||||
|
if (eventAction == null) {
|
||||||
|
return "No action required";
|
||||||
|
}
|
||||||
|
|
||||||
|
let monitorUrl;
|
||||||
|
if (monitorInfo.type === "port") {
|
||||||
|
monitorUrl = monitorInfo.hostname;
|
||||||
|
if (monitorInfo.port) {
|
||||||
|
monitorUrl += ":" + monitorInfo.port;
|
||||||
|
}
|
||||||
|
} else if (monitorInfo.hostname != null) {
|
||||||
|
monitorUrl = monitorInfo.hostname;
|
||||||
|
} else {
|
||||||
|
monitorUrl = monitorInfo.url;
|
||||||
|
}
|
||||||
|
|
||||||
|
const options = {
|
||||||
|
method: "POST",
|
||||||
|
url: notification.pagerdutyIntegrationUrl,
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
data: {
|
||||||
|
payload: {
|
||||||
|
summary: `[${title}] [${monitorInfo.name}] ${body}`,
|
||||||
|
severity: notification.pagerdutyPriority || "warning",
|
||||||
|
source: monitorUrl,
|
||||||
|
},
|
||||||
|
routing_key: notification.pagerdutyIntegrationKey,
|
||||||
|
event_action: eventAction,
|
||||||
|
dedup_key: "Uptime Kuma/" + monitorInfo.id,
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const baseURL = await setting("primaryBaseURL");
|
||||||
|
if (baseURL && monitorInfo) {
|
||||||
|
options.client = "Uptime Kuma";
|
||||||
|
options.client_url = baseURL + getMonitorRelativeURL(monitorInfo.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
let result = await axios.request(options);
|
||||||
|
this.checkResult(result);
|
||||||
|
if (result.statusText != null) {
|
||||||
|
return "PagerDuty notification succeed: " + result.statusText;
|
||||||
|
}
|
||||||
|
|
||||||
|
return successMessage;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = PagerDuty;
|
25
server/notification-providers/smsmanager.js
Normal file
25
server/notification-providers/smsmanager.js
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
const NotificationProvider = require("./notification-provider");
|
||||||
|
const axios = require("axios");
|
||||||
|
|
||||||
|
class SMSManager extends NotificationProvider {
|
||||||
|
|
||||||
|
name = "SMSManager";
|
||||||
|
|
||||||
|
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
|
||||||
|
try {
|
||||||
|
let data = {
|
||||||
|
apikey: notification.smsmanagerApiKey,
|
||||||
|
endpoint: "https://http-api.smsmanager.cz/Send",
|
||||||
|
message: msg.replace(/[^\x00-\x7F]/g, ""),
|
||||||
|
to: notification.numbers,
|
||||||
|
messageType: notification.messageType,
|
||||||
|
};
|
||||||
|
await axios.get(`${data.endpoint}?apikey=${data.apikey}&message=${data.message}&number=${data.to}&gateway=${data.messageType}`);
|
||||||
|
return "SMS sent sucessfully.";
|
||||||
|
} catch (error) {
|
||||||
|
this.throwGeneralAxiosError(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = SMSManager;
|
|
@ -1,38 +1,45 @@
|
||||||
const { R } = require("redbean-node");
|
const { R } = require("redbean-node");
|
||||||
|
const { log } = require("../src/util");
|
||||||
|
const Alerta = require("./notification-providers/alerta");
|
||||||
|
const AlertNow = require("./notification-providers/alertnow");
|
||||||
|
const AliyunSms = require("./notification-providers/aliyun-sms");
|
||||||
const Apprise = require("./notification-providers/apprise");
|
const Apprise = require("./notification-providers/apprise");
|
||||||
const Discord = require("./notification-providers/discord");
|
const Bark = require("./notification-providers/bark");
|
||||||
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 ClickSendSMS = require("./notification-providers/clicksendsms");
|
const ClickSendSMS = require("./notification-providers/clicksendsms");
|
||||||
|
const DingDing = require("./notification-providers/dingding");
|
||||||
|
const Discord = require("./notification-providers/discord");
|
||||||
|
const Feishu = require("./notification-providers/feishu");
|
||||||
|
const GoogleChat = require("./notification-providers/google-chat");
|
||||||
|
const Gorush = require("./notification-providers/gorush");
|
||||||
|
const Gotify = require("./notification-providers/gotify");
|
||||||
|
const HomeAssistant = require("./notification-providers/home-assistant");
|
||||||
|
const Line = require("./notification-providers/line");
|
||||||
|
const LineNotify = require("./notification-providers/linenotify");
|
||||||
|
const LunaSea = require("./notification-providers/lunasea");
|
||||||
|
const Matrix = require("./notification-providers/matrix");
|
||||||
|
const Mattermost = require("./notification-providers/mattermost");
|
||||||
|
const Ntfy = require("./notification-providers/ntfy");
|
||||||
|
const Octopush = require("./notification-providers/octopush");
|
||||||
|
const OneBot = require("./notification-providers/onebot");
|
||||||
|
const PagerDuty = require("./notification-providers/pagerduty");
|
||||||
|
const PromoSMS = require("./notification-providers/promosms");
|
||||||
const Pushbullet = require("./notification-providers/pushbullet");
|
const Pushbullet = require("./notification-providers/pushbullet");
|
||||||
|
const PushDeer = require("./notification-providers/pushdeer");
|
||||||
const Pushover = require("./notification-providers/pushover");
|
const Pushover = require("./notification-providers/pushover");
|
||||||
const Pushy = require("./notification-providers/pushy");
|
const Pushy = require("./notification-providers/pushy");
|
||||||
const TechulusPush = require("./notification-providers/techulus-push");
|
|
||||||
const RocketChat = require("./notification-providers/rocket-chat");
|
const RocketChat = require("./notification-providers/rocket-chat");
|
||||||
|
const SerwerSMS = require("./notification-providers/serwersms");
|
||||||
const Signal = require("./notification-providers/signal");
|
const Signal = require("./notification-providers/signal");
|
||||||
const Slack = require("./notification-providers/slack");
|
const Slack = require("./notification-providers/slack");
|
||||||
const SMTP = require("./notification-providers/smtp");
|
const SMTP = require("./notification-providers/smtp");
|
||||||
|
const Stackfield = require("./notification-providers/stackfield");
|
||||||
const Teams = require("./notification-providers/teams");
|
const Teams = require("./notification-providers/teams");
|
||||||
|
const TechulusPush = require("./notification-providers/techulus-push");
|
||||||
const Telegram = require("./notification-providers/telegram");
|
const Telegram = require("./notification-providers/telegram");
|
||||||
const Webhook = require("./notification-providers/webhook");
|
const Webhook = require("./notification-providers/webhook");
|
||||||
const Feishu = require("./notification-providers/feishu");
|
|
||||||
const AliyunSms = require("./notification-providers/aliyun-sms");
|
|
||||||
const DingDing = require("./notification-providers/dingding");
|
|
||||||
const Bark = require("./notification-providers/bark");
|
|
||||||
const { log } = require("../src/util");
|
|
||||||
const SerwerSMS = require("./notification-providers/serwersms");
|
|
||||||
const Stackfield = require("./notification-providers/stackfield");
|
|
||||||
const WeCom = require("./notification-providers/wecom");
|
const WeCom = require("./notification-providers/wecom");
|
||||||
const GoogleChat = require("./notification-providers/google-chat");
|
const GoAlert = require("./notification-providers/goalert");
|
||||||
const Gorush = require("./notification-providers/gorush");
|
const SMSManager = require("./notification-providers/smsmanager");
|
||||||
const Alerta = require("./notification-providers/alerta");
|
|
||||||
const OneBot = require("./notification-providers/onebot");
|
|
||||||
const PushDeer = require("./notification-providers/pushdeer");
|
|
||||||
|
|
||||||
class Notification {
|
class Notification {
|
||||||
|
|
||||||
|
@ -45,39 +52,46 @@ class Notification {
|
||||||
this.providerList = {};
|
this.providerList = {};
|
||||||
|
|
||||||
const list = [
|
const list = [
|
||||||
new Apprise(),
|
new Alerta(),
|
||||||
|
new AlertNow(),
|
||||||
new AliyunSms(),
|
new AliyunSms(),
|
||||||
|
new Apprise(),
|
||||||
|
new Bark(),
|
||||||
|
new ClickSendSMS(),
|
||||||
new DingDing(),
|
new DingDing(),
|
||||||
new Discord(),
|
new Discord(),
|
||||||
new Teams(),
|
|
||||||
new Gotify(),
|
|
||||||
new Line(),
|
|
||||||
new LunaSea(),
|
|
||||||
new Feishu(),
|
new Feishu(),
|
||||||
new Mattermost(),
|
|
||||||
new Matrix(),
|
|
||||||
new Octopush(),
|
|
||||||
new PromoSMS(),
|
|
||||||
new ClickSendSMS(),
|
|
||||||
new Pushbullet(),
|
|
||||||
new Pushover(),
|
|
||||||
new Pushy(),
|
|
||||||
new TechulusPush(),
|
|
||||||
new RocketChat(),
|
|
||||||
new Signal(),
|
|
||||||
new Slack(),
|
|
||||||
new SMTP(),
|
|
||||||
new Telegram(),
|
|
||||||
new Webhook(),
|
|
||||||
new Bark(),
|
|
||||||
new SerwerSMS(),
|
|
||||||
new Stackfield(),
|
|
||||||
new WeCom(),
|
|
||||||
new GoogleChat(),
|
new GoogleChat(),
|
||||||
new Gorush(),
|
new Gorush(),
|
||||||
new Alerta(),
|
new Gotify(),
|
||||||
|
new HomeAssistant(),
|
||||||
|
new Line(),
|
||||||
|
new LineNotify(),
|
||||||
|
new LunaSea(),
|
||||||
|
new Matrix(),
|
||||||
|
new Mattermost(),
|
||||||
|
new Ntfy(),
|
||||||
|
new Octopush(),
|
||||||
new OneBot(),
|
new OneBot(),
|
||||||
|
new PagerDuty(),
|
||||||
|
new PromoSMS(),
|
||||||
|
new Pushbullet(),
|
||||||
new PushDeer(),
|
new PushDeer(),
|
||||||
|
new Pushover(),
|
||||||
|
new Pushy(),
|
||||||
|
new RocketChat(),
|
||||||
|
new SerwerSMS(),
|
||||||
|
new Signal(),
|
||||||
|
new SMSManager(),
|
||||||
|
new Slack(),
|
||||||
|
new SMTP(),
|
||||||
|
new Stackfield(),
|
||||||
|
new Teams(),
|
||||||
|
new TechulusPush(),
|
||||||
|
new Telegram(),
|
||||||
|
new Webhook(),
|
||||||
|
new WeCom(),
|
||||||
|
new GoAlert(),
|
||||||
];
|
];
|
||||||
|
|
||||||
for (let item of list) {
|
for (let item of list) {
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
let express = require("express");
|
let express = require("express");
|
||||||
const { allowDevAllOrigin, allowAllOrigin, percentageToColor, filterAndJoin } = require("../util-server");
|
const { allowDevAllOrigin, allowAllOrigin, percentageToColor, filterAndJoin, send403 } = require("../util-server");
|
||||||
const { R } = require("redbean-node");
|
const { R } = require("redbean-node");
|
||||||
const apicache = require("../modules/apicache");
|
const apicache = require("../modules/apicache");
|
||||||
const Monitor = require("../model/monitor");
|
const Monitor = require("../model/monitor");
|
||||||
|
@ -59,7 +59,7 @@ router.get("/api/push/:pushToken", async (request, response) => {
|
||||||
let duration = 0;
|
let duration = 0;
|
||||||
|
|
||||||
let bean = R.dispense("heartbeat");
|
let bean = R.dispense("heartbeat");
|
||||||
bean.time = R.isoDateTime(dayjs.utc());
|
bean.time = R.isoDateTimeMillis(dayjs.utc());
|
||||||
|
|
||||||
if (previousHeartbeat) {
|
if (previousHeartbeat) {
|
||||||
isFirstBeat = false;
|
isFirstBeat = false;
|
||||||
|
@ -72,6 +72,7 @@ router.get("/api/push/:pushToken", async (request, response) => {
|
||||||
status = MAINTENANCE;
|
status = MAINTENANCE;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
log.debug("router", `/api/push/ called at ${dayjs().format("YYYY-MM-DD HH:mm:ss.SSS")}`);
|
||||||
log.debug("router", "PreviousStatus: " + previousStatus);
|
log.debug("router", "PreviousStatus: " + previousStatus);
|
||||||
log.debug("router", "Current Status: " + status);
|
log.debug("router", "Current Status: " + status);
|
||||||
|
|
||||||
|
@ -96,149 +97,13 @@ router.get("/api/push/:pushToken", async (request, response) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
response.json({
|
response.status(404).json({
|
||||||
ok: false,
|
ok: false,
|
||||||
msg: e.message
|
msg: e.message
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Status page config, incident, monitor list
|
|
||||||
router.get("/api/status-page/:slug", cache("5 minutes"), async (request, response) => {
|
|
||||||
allowDevAllOrigin(response);
|
|
||||||
let slug = request.params.slug;
|
|
||||||
|
|
||||||
// Get Status Page
|
|
||||||
let statusPage = await R.findOne("status_page", " slug = ? ", [
|
|
||||||
slug
|
|
||||||
]);
|
|
||||||
|
|
||||||
if (!statusPage) {
|
|
||||||
response.statusCode = 404;
|
|
||||||
response.json({
|
|
||||||
msg: "Not Found"
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Incident
|
|
||||||
let incident = await R.findOne("incident", " pin = 1 AND active = 1 AND status_page_id = ? ", [
|
|
||||||
statusPage.id,
|
|
||||||
]);
|
|
||||||
|
|
||||||
if (incident) {
|
|
||||||
incident = incident.toPublicJSON();
|
|
||||||
}
|
|
||||||
|
|
||||||
let maintenance = await getMaintenanceList(statusPage.id);
|
|
||||||
|
|
||||||
// Public Group List
|
|
||||||
const publicGroupList = [];
|
|
||||||
const showTags = !!statusPage.show_tags;
|
|
||||||
|
|
||||||
const list = await R.find("group", " public = 1 AND status_page_id = ? ORDER BY weight ", [
|
|
||||||
statusPage.id
|
|
||||||
]);
|
|
||||||
|
|
||||||
for (let groupBean of list) {
|
|
||||||
let monitorGroup = await groupBean.toPublicJSON(showTags);
|
|
||||||
publicGroupList.push(monitorGroup);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Response
|
|
||||||
response.json({
|
|
||||||
config: await statusPage.toPublicJSON(),
|
|
||||||
incident,
|
|
||||||
maintenance,
|
|
||||||
publicGroupList
|
|
||||||
});
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
send403(response, error.message);
|
|
||||||
}
|
|
||||||
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get list of maintenances
|
|
||||||
* @param {number} statusPageId ID of status page to get maintenance for
|
|
||||||
* @returns {Object} Object representing maintenances sanitized for public
|
|
||||||
*/
|
|
||||||
async function getMaintenanceList(statusPageId) {
|
|
||||||
try {
|
|
||||||
const publicMaintenanceList = [];
|
|
||||||
|
|
||||||
let maintenanceBeanList = R.convertToBeans("maintenance", await R.getAll(`
|
|
||||||
SELECT m.*
|
|
||||||
FROM maintenance m
|
|
||||||
JOIN maintenance_status_page msp
|
|
||||||
ON msp.maintenance_id = m.id
|
|
||||||
WHERE datetime(m.start_date) <= datetime('now')
|
|
||||||
AND datetime(m.end_date) >= datetime('now')
|
|
||||||
AND msp.status_page_id = ?
|
|
||||||
ORDER BY m.end_date
|
|
||||||
`, [ statusPageId ]));
|
|
||||||
|
|
||||||
for (const bean of maintenanceBeanList) {
|
|
||||||
publicMaintenanceList.push(await bean.toPublicJSON());
|
|
||||||
}
|
|
||||||
|
|
||||||
return publicMaintenanceList;
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Status Page Polling Data
|
|
||||||
// Can fetch only if published
|
|
||||||
router.get("/api/status-page/heartbeat/:slug", cache("1 minutes"), async (request, response) => {
|
|
||||||
allowDevAllOrigin(response);
|
|
||||||
|
|
||||||
try {
|
|
||||||
let heartbeatList = {};
|
|
||||||
let uptimeList = {};
|
|
||||||
|
|
||||||
let slug = request.params.slug;
|
|
||||||
let statusPageID = await StatusPage.slugToID(slug);
|
|
||||||
|
|
||||||
let monitorIDList = await R.getCol(`
|
|
||||||
SELECT monitor_group.monitor_id FROM monitor_group, \`group\`
|
|
||||||
WHERE monitor_group.group_id = \`group\`.id
|
|
||||||
AND public = 1
|
|
||||||
AND \`group\`.status_page_id = ?
|
|
||||||
`, [
|
|
||||||
statusPageID
|
|
||||||
]);
|
|
||||||
|
|
||||||
for (let monitorID of monitorIDList) {
|
|
||||||
let list = await R.getAll(`
|
|
||||||
SELECT * FROM heartbeat
|
|
||||||
WHERE monitor_id = ?
|
|
||||||
ORDER BY time DESC
|
|
||||||
LIMIT 50
|
|
||||||
`, [
|
|
||||||
monitorID,
|
|
||||||
]);
|
|
||||||
|
|
||||||
list = R.convertToBeans("heartbeat", list);
|
|
||||||
heartbeatList[monitorID] = list.reverse().map(row => row.toPublicJSON());
|
|
||||||
|
|
||||||
const type = 24;
|
|
||||||
uptimeList[`${monitorID}_${type}`] = await Monitor.calcUptime(type, monitorID);
|
|
||||||
}
|
|
||||||
|
|
||||||
response.json({
|
|
||||||
heartbeatList,
|
|
||||||
uptimeList
|
|
||||||
});
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
send403(response, error.message);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
router.get("/api/badge/:id/status", cache("5 minutes"), async (request, response) => {
|
router.get("/api/badge/:id/status", cache("5 minutes"), async (request, response) => {
|
||||||
allowAllOrigin(response);
|
allowAllOrigin(response);
|
||||||
|
|
||||||
|
@ -276,6 +141,7 @@ router.get("/api/badge/:id/status", cache("5 minutes"), async (request, response
|
||||||
const heartbeat = await Monitor.getPreviousHeartbeat(requestedMonitorId);
|
const heartbeat = await Monitor.getPreviousHeartbeat(requestedMonitorId);
|
||||||
const state = overrideValue !== undefined ? overrideValue : heartbeat.status === 1;
|
const state = overrideValue !== undefined ? overrideValue : heartbeat.status === 1;
|
||||||
|
|
||||||
|
badgeValues.label = label ? label : "";
|
||||||
badgeValues.color = state ? upColor : downColor;
|
badgeValues.color = state ? upColor : downColor;
|
||||||
badgeValues.message = label ?? state ? upLabel : downLabel;
|
badgeValues.message = label ?? state ? upLabel : downLabel;
|
||||||
}
|
}
|
||||||
|
@ -415,16 +281,4 @@ router.get("/api/badge/:id/ping/:duration?", cache("5 minutes"), async (request,
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
|
||||||
* Send a 403 response
|
|
||||||
* @param {Object} res Express response object
|
|
||||||
* @param {string} [msg=""] Message to send
|
|
||||||
*/
|
|
||||||
function send403(res, msg = "") {
|
|
||||||
res.status(403).json({
|
|
||||||
"status": "fail",
|
|
||||||
"msg": msg,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = router;
|
module.exports = router;
|
||||||
|
|
148
server/routers/status-page-router.js
Normal file
148
server/routers/status-page-router.js
Normal file
|
@ -0,0 +1,148 @@
|
||||||
|
let express = require("express");
|
||||||
|
const apicache = require("../modules/apicache");
|
||||||
|
const { UptimeKumaServer } = require("../uptime-kuma-server");
|
||||||
|
const StatusPage = require("../model/status_page");
|
||||||
|
const { allowDevAllOrigin, send403 } = require("../util-server");
|
||||||
|
const { R } = require("redbean-node");
|
||||||
|
const Monitor = require("../model/monitor");
|
||||||
|
|
||||||
|
let router = express.Router();
|
||||||
|
|
||||||
|
let cache = apicache.middleware;
|
||||||
|
const server = UptimeKumaServer.getInstance();
|
||||||
|
|
||||||
|
router.get("/status/:slug", cache("5 minutes"), async (request, response) => {
|
||||||
|
let slug = request.params.slug;
|
||||||
|
await StatusPage.handleStatusPageResponse(response, server.indexHTML, slug);
|
||||||
|
});
|
||||||
|
|
||||||
|
router.get("/status", cache("5 minutes"), async (request, response) => {
|
||||||
|
let slug = "default";
|
||||||
|
await StatusPage.handleStatusPageResponse(response, server.indexHTML, slug);
|
||||||
|
});
|
||||||
|
|
||||||
|
router.get("/status-page", cache("5 minutes"), async (request, response) => {
|
||||||
|
let slug = "default";
|
||||||
|
await StatusPage.handleStatusPageResponse(response, server.indexHTML, slug);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Status page config, incident, monitor list
|
||||||
|
router.get("/api/status-page/:slug", cache("5 minutes"), async (request, response) => {
|
||||||
|
allowDevAllOrigin(response);
|
||||||
|
let slug = request.params.slug;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Get Status Page
|
||||||
|
let statusPage = await R.findOne("status_page", " slug = ? ", [
|
||||||
|
slug
|
||||||
|
]);
|
||||||
|
|
||||||
|
if (!statusPage) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
let statusPageData = await StatusPage.getStatusPageData(statusPage);
|
||||||
|
|
||||||
|
if (!statusPageData) {
|
||||||
|
response.statusCode = 404;
|
||||||
|
response.json({
|
||||||
|
msg: "Not Found"
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Response
|
||||||
|
response.json(statusPageData);
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
send403(response, error.message);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Status Page Polling Data
|
||||||
|
// Can fetch only if published
|
||||||
|
router.get("/api/status-page/heartbeat/:slug", cache("1 minutes"), async (request, response) => {
|
||||||
|
allowDevAllOrigin(response);
|
||||||
|
|
||||||
|
try {
|
||||||
|
let heartbeatList = {};
|
||||||
|
let uptimeList = {};
|
||||||
|
|
||||||
|
let slug = request.params.slug;
|
||||||
|
let statusPageID = await StatusPage.slugToID(slug);
|
||||||
|
|
||||||
|
let monitorIDList = await R.getCol(`
|
||||||
|
SELECT monitor_group.monitor_id FROM monitor_group, \`group\`
|
||||||
|
WHERE monitor_group.group_id = \`group\`.id
|
||||||
|
AND public = 1
|
||||||
|
AND \`group\`.status_page_id = ?
|
||||||
|
`, [
|
||||||
|
statusPageID
|
||||||
|
]);
|
||||||
|
|
||||||
|
for (let monitorID of monitorIDList) {
|
||||||
|
let list = await R.getAll(`
|
||||||
|
SELECT * FROM heartbeat
|
||||||
|
WHERE monitor_id = ?
|
||||||
|
ORDER BY time DESC
|
||||||
|
LIMIT 50
|
||||||
|
`, [
|
||||||
|
monitorID,
|
||||||
|
]);
|
||||||
|
|
||||||
|
list = R.convertToBeans("heartbeat", list);
|
||||||
|
heartbeatList[monitorID] = list.reverse().map(row => row.toPublicJSON());
|
||||||
|
|
||||||
|
const type = 24;
|
||||||
|
uptimeList[`${monitorID}_${type}`] = await Monitor.calcUptime(type, monitorID);
|
||||||
|
}
|
||||||
|
|
||||||
|
response.json({
|
||||||
|
heartbeatList,
|
||||||
|
uptimeList
|
||||||
|
});
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
send403(response, error.message);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Status page's manifest.json
|
||||||
|
router.get("/api/status-page/:slug/manifest.json", cache("1440 minutes"), async (request, response) => {
|
||||||
|
allowDevAllOrigin(response);
|
||||||
|
let slug = request.params.slug;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Get Status Page
|
||||||
|
let statusPage = await R.findOne("status_page", " slug = ? ", [
|
||||||
|
slug
|
||||||
|
]);
|
||||||
|
|
||||||
|
if (!statusPage) {
|
||||||
|
response.statusCode = 404;
|
||||||
|
response.json({
|
||||||
|
msg: "Not Found"
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Response
|
||||||
|
response.json({
|
||||||
|
"name": statusPage.title,
|
||||||
|
"start_url": "/status/" + statusPage.slug,
|
||||||
|
"display": "standalone",
|
||||||
|
"icons": [
|
||||||
|
{
|
||||||
|
"src": statusPage.icon,
|
||||||
|
"sizes": "128x128",
|
||||||
|
"type": "image/png"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
});
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
send403(response, error.message);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
module.exports = router;
|
122
server/server.js
122
server/server.js
|
@ -16,7 +16,7 @@ if (nodeVersion < requiredVersion) {
|
||||||
}
|
}
|
||||||
|
|
||||||
const args = require("args-parser")(process.argv);
|
const args = require("args-parser")(process.argv);
|
||||||
const { sleep, log, getRandomInt, genSecret, debug, isDev } = require("../src/util");
|
const { sleep, log, getRandomInt, genSecret, isDev } = require("../src/util");
|
||||||
const config = require("./config");
|
const config = require("./config");
|
||||||
|
|
||||||
log.info("server", "Welcome to Uptime Kuma");
|
log.info("server", "Welcome to Uptime Kuma");
|
||||||
|
@ -35,6 +35,7 @@ const fs = require("fs");
|
||||||
log.info("server", "Importing 3rd-party libraries");
|
log.info("server", "Importing 3rd-party libraries");
|
||||||
log.debug("server", "Importing express");
|
log.debug("server", "Importing express");
|
||||||
const express = require("express");
|
const express = require("express");
|
||||||
|
const expressStaticGzip = require("express-static-gzip");
|
||||||
log.debug("server", "Importing redbean-node");
|
log.debug("server", "Importing redbean-node");
|
||||||
const { R } = require("redbean-node");
|
const { R } = require("redbean-node");
|
||||||
log.debug("server", "Importing jsonwebtoken");
|
log.debug("server", "Importing jsonwebtoken");
|
||||||
|
@ -60,7 +61,7 @@ log.info("server", "Importing this project modules");
|
||||||
log.debug("server", "Importing Monitor");
|
log.debug("server", "Importing Monitor");
|
||||||
const Monitor = require("./model/monitor");
|
const Monitor = require("./model/monitor");
|
||||||
log.debug("server", "Importing Settings");
|
log.debug("server", "Importing Settings");
|
||||||
const { getSettings, setSettings, setting, initJWTSecret, checkLogin, startUnitTest, FBSD, doubleCheckPassword } = require("./util-server");
|
const { getSettings, setSettings, setting, initJWTSecret, checkLogin, startUnitTest, FBSD, doubleCheckPassword, startE2eTests } = require("./util-server");
|
||||||
|
|
||||||
log.debug("server", "Importing Notification");
|
log.debug("server", "Importing Notification");
|
||||||
const { Notification } = require("./notification");
|
const { Notification } = require("./notification");
|
||||||
|
@ -111,19 +112,21 @@ const twoFAVerifyOptions = {
|
||||||
* @type {boolean}
|
* @type {boolean}
|
||||||
*/
|
*/
|
||||||
const testMode = !!args["test"] || false;
|
const testMode = !!args["test"] || false;
|
||||||
|
const e2eTestMode = !!args["e2e"] || false;
|
||||||
|
|
||||||
if (config.demoMode) {
|
if (config.demoMode) {
|
||||||
log.info("server", "==== Demo Mode ====");
|
log.info("server", "==== Demo Mode ====");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Must be after io instantiation
|
// Must be after io instantiation
|
||||||
const { sendNotificationList, sendHeartbeatList, sendImportantHeartbeatList, sendInfo, sendProxyList } = require("./client");
|
const { sendNotificationList, sendHeartbeatList, sendImportantHeartbeatList, sendInfo, sendProxyList, sendDockerHostList } = require("./client");
|
||||||
const { statusPageSocketHandler } = require("./socket-handlers/status-page-socket-handler");
|
const { statusPageSocketHandler } = require("./socket-handlers/status-page-socket-handler");
|
||||||
const databaseSocketHandler = require("./socket-handlers/database-socket-handler");
|
const databaseSocketHandler = require("./socket-handlers/database-socket-handler");
|
||||||
const TwoFA = require("./2fa");
|
const TwoFA = require("./2fa");
|
||||||
const StatusPage = require("./model/status_page");
|
const StatusPage = require("./model/status_page");
|
||||||
const { cloudflaredSocketHandler, autoStart: cloudflaredAutoStart, stop: cloudflaredStop } = require("./socket-handlers/cloudflared-socket-handler");
|
const { cloudflaredSocketHandler, autoStart: cloudflaredAutoStart, stop: cloudflaredStop } = require("./socket-handlers/cloudflared-socket-handler");
|
||||||
const { proxySocketHandler } = require("./socket-handlers/proxy-socket-handler");
|
const { proxySocketHandler } = require("./socket-handlers/proxy-socket-handler");
|
||||||
|
const { dockerSocketHandler } = require("./socket-handlers/docker-socket-handler");
|
||||||
const apicache = require("./modules/apicache");
|
const apicache = require("./modules/apicache");
|
||||||
|
|
||||||
app.use(express.json());
|
app.use(express.json());
|
||||||
|
@ -155,22 +158,6 @@ let maintenanceList = {};
|
||||||
*/
|
*/
|
||||||
let needSetup = false;
|
let needSetup = false;
|
||||||
|
|
||||||
/**
|
|
||||||
* Cache Index HTML
|
|
||||||
* @type {string}
|
|
||||||
*/
|
|
||||||
let indexHTML = "";
|
|
||||||
|
|
||||||
try {
|
|
||||||
indexHTML = fs.readFileSync("./dist/index.html").toString();
|
|
||||||
} catch (e) {
|
|
||||||
// "dist/index.html" is not necessary for development
|
|
||||||
if (process.env.NODE_ENV !== "development") {
|
|
||||||
log.error("server", "Error: Cannot find 'dist/index.html', did you install correctly?");
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
(async () => {
|
(async () => {
|
||||||
Database.init(args);
|
Database.init(args);
|
||||||
await initDatabase(testMode);
|
await initDatabase(testMode);
|
||||||
|
@ -186,13 +173,25 @@ try {
|
||||||
|
|
||||||
// Entry Page
|
// Entry Page
|
||||||
app.get("/", async (request, response) => {
|
app.get("/", async (request, response) => {
|
||||||
debug(`Request Domain: ${request.hostname}`);
|
let hostname = request.hostname;
|
||||||
|
if (await setting("trustProxy")) {
|
||||||
|
const proxy = request.headers["x-forwarded-host"];
|
||||||
|
if (proxy) {
|
||||||
|
hostname = proxy;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
log.debug("entry", `Request Domain: ${hostname}`);
|
||||||
|
|
||||||
|
if (hostname in StatusPage.domainMappingList) {
|
||||||
|
log.debug("entry", "This is a status page domain");
|
||||||
|
|
||||||
|
let slug = StatusPage.domainMappingList[hostname];
|
||||||
|
await StatusPage.handleStatusPageResponse(response, server.indexHTML, slug);
|
||||||
|
|
||||||
if (request.hostname in StatusPage.domainMappingList) {
|
|
||||||
debug("This is a status page domain");
|
|
||||||
response.send(indexHTML);
|
|
||||||
} else if (exports.entryPage && exports.entryPage.startsWith("statusPage-")) {
|
} else if (exports.entryPage && exports.entryPage.startsWith("statusPage-")) {
|
||||||
response.redirect("/status/" + exports.entryPage.replace("statusPage-", ""));
|
response.redirect("/status/" + exports.entryPage.replace("statusPage-", ""));
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
response.redirect("/dashboard");
|
response.redirect("/dashboard");
|
||||||
}
|
}
|
||||||
|
@ -221,7 +220,9 @@ try {
|
||||||
// With Basic Auth using the first user's username/password
|
// With Basic Auth using the first user's username/password
|
||||||
app.get("/metrics", basicAuth, prometheusAPIMetrics());
|
app.get("/metrics", basicAuth, prometheusAPIMetrics());
|
||||||
|
|
||||||
app.use("/", express.static("dist"));
|
app.use("/", expressStaticGzip("dist", {
|
||||||
|
enableBrotli: true,
|
||||||
|
}));
|
||||||
|
|
||||||
// ./data/upload
|
// ./data/upload
|
||||||
app.use("/upload", express.static(Database.uploadDir));
|
app.use("/upload", express.static(Database.uploadDir));
|
||||||
|
@ -234,12 +235,16 @@ try {
|
||||||
const apiRouter = require("./routers/api-router");
|
const apiRouter = require("./routers/api-router");
|
||||||
app.use(apiRouter);
|
app.use(apiRouter);
|
||||||
|
|
||||||
|
// Status Page Router
|
||||||
|
const statusPageRouter = require("./routers/status-page-router");
|
||||||
|
app.use(statusPageRouter);
|
||||||
|
|
||||||
// Universal Route Handler, must be at the end of all express routes.
|
// Universal Route Handler, must be at the end of all express routes.
|
||||||
app.get("*", async (_request, response) => {
|
app.get("*", async (_request, response) => {
|
||||||
if (_request.originalUrl.startsWith("/upload/")) {
|
if (_request.originalUrl.startsWith("/upload/")) {
|
||||||
response.status(404).send("File not found.");
|
response.status(404).send("File not found.");
|
||||||
} else {
|
} else {
|
||||||
response.send(indexHTML);
|
response.send(server.indexHTML);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -258,7 +263,9 @@ try {
|
||||||
// ***************************
|
// ***************************
|
||||||
|
|
||||||
socket.on("loginByToken", async (token, callback) => {
|
socket.on("loginByToken", async (token, callback) => {
|
||||||
log.info("auth", `Login by token. IP=${getClientIp(socket)}`);
|
const clientIP = await server.getClientIP(socket);
|
||||||
|
|
||||||
|
log.info("auth", `Login by token. IP=${clientIP}`);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
let decoded = jwt.verify(token, jwtSecret);
|
let decoded = jwt.verify(token, jwtSecret);
|
||||||
|
@ -274,14 +281,14 @@ try {
|
||||||
afterLogin(socket, user);
|
afterLogin(socket, user);
|
||||||
log.debug("auth", "afterLogin ok");
|
log.debug("auth", "afterLogin ok");
|
||||||
|
|
||||||
log.info("auth", `Successfully logged in user ${decoded.username}. IP=${getClientIp(socket)}`);
|
log.info("auth", `Successfully logged in user ${decoded.username}. IP=${clientIP}`);
|
||||||
|
|
||||||
callback({
|
callback({
|
||||||
ok: true,
|
ok: true,
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
|
|
||||||
log.info("auth", `Inactive or deleted user ${decoded.username}. IP=${getClientIp(socket)}`);
|
log.info("auth", `Inactive or deleted user ${decoded.username}. IP=${clientIP}`);
|
||||||
|
|
||||||
callback({
|
callback({
|
||||||
ok: false,
|
ok: false,
|
||||||
|
@ -290,7 +297,7 @@ try {
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|
||||||
log.error("auth", `Invalid token. IP=${getClientIp(socket)}`);
|
log.error("auth", `Invalid token. IP=${clientIP}`);
|
||||||
|
|
||||||
callback({
|
callback({
|
||||||
ok: false,
|
ok: false,
|
||||||
|
@ -301,7 +308,9 @@ try {
|
||||||
});
|
});
|
||||||
|
|
||||||
socket.on("login", async (data, callback) => {
|
socket.on("login", async (data, callback) => {
|
||||||
log.info("auth", `Login by username + password. IP=${getClientIp(socket)}`);
|
const clientIP = await server.getClientIP(socket);
|
||||||
|
|
||||||
|
log.info("auth", `Login by username + password. IP=${clientIP}`);
|
||||||
|
|
||||||
// Checking
|
// Checking
|
||||||
if (typeof callback !== "function") {
|
if (typeof callback !== "function") {
|
||||||
|
@ -314,7 +323,7 @@ try {
|
||||||
|
|
||||||
// Login Rate Limit
|
// Login Rate Limit
|
||||||
if (! await loginRateLimiter.pass(callback)) {
|
if (! await loginRateLimiter.pass(callback)) {
|
||||||
log.info("auth", `Too many failed requests for user ${data.username}. IP=${getClientIp(socket)}`);
|
log.info("auth", `Too many failed requests for user ${data.username}. IP=${clientIP}`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -324,7 +333,7 @@ try {
|
||||||
if (user.twofa_status === 0) {
|
if (user.twofa_status === 0) {
|
||||||
afterLogin(socket, user);
|
afterLogin(socket, user);
|
||||||
|
|
||||||
log.info("auth", `Successfully logged in user ${data.username}. IP=${getClientIp(socket)}`);
|
log.info("auth", `Successfully logged in user ${data.username}. IP=${clientIP}`);
|
||||||
|
|
||||||
callback({
|
callback({
|
||||||
ok: true,
|
ok: true,
|
||||||
|
@ -336,7 +345,7 @@ try {
|
||||||
|
|
||||||
if (user.twofa_status === 1 && !data.token) {
|
if (user.twofa_status === 1 && !data.token) {
|
||||||
|
|
||||||
log.info("auth", `2FA token required for user ${data.username}. IP=${getClientIp(socket)}`);
|
log.info("auth", `2FA token required for user ${data.username}. IP=${clientIP}`);
|
||||||
|
|
||||||
callback({
|
callback({
|
||||||
tokenRequired: true,
|
tokenRequired: true,
|
||||||
|
@ -354,7 +363,7 @@ try {
|
||||||
socket.userID,
|
socket.userID,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
log.info("auth", `Successfully logged in user ${data.username}. IP=${getClientIp(socket)}`);
|
log.info("auth", `Successfully logged in user ${data.username}. IP=${clientIP}`);
|
||||||
|
|
||||||
callback({
|
callback({
|
||||||
ok: true,
|
ok: true,
|
||||||
|
@ -364,7 +373,7 @@ try {
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
|
|
||||||
log.warn("auth", `Invalid token provided for user ${data.username}. IP=${getClientIp(socket)}`);
|
log.warn("auth", `Invalid token provided for user ${data.username}. IP=${clientIP}`);
|
||||||
|
|
||||||
callback({
|
callback({
|
||||||
ok: false,
|
ok: false,
|
||||||
|
@ -374,7 +383,7 @@ try {
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
|
||||||
log.warn("auth", `Incorrect username or password for user ${data.username}. IP=${getClientIp(socket)}`);
|
log.warn("auth", `Incorrect username or password for user ${data.username}. IP=${clientIP}`);
|
||||||
|
|
||||||
callback({
|
callback({
|
||||||
ok: false,
|
ok: false,
|
||||||
|
@ -446,6 +455,8 @@ try {
|
||||||
});
|
});
|
||||||
|
|
||||||
socket.on("save2FA", async (currentPassword, callback) => {
|
socket.on("save2FA", async (currentPassword, callback) => {
|
||||||
|
const clientIP = await server.getClientIP(socket);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (! await twoFaRateLimiter.pass(callback)) {
|
if (! await twoFaRateLimiter.pass(callback)) {
|
||||||
return;
|
return;
|
||||||
|
@ -458,7 +469,7 @@ try {
|
||||||
socket.userID,
|
socket.userID,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
log.info("auth", `Saved 2FA token. IP=${getClientIp(socket)}`);
|
log.info("auth", `Saved 2FA token. IP=${clientIP}`);
|
||||||
|
|
||||||
callback({
|
callback({
|
||||||
ok: true,
|
ok: true,
|
||||||
|
@ -466,7 +477,7 @@ try {
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|
||||||
log.error("auth", `Error changing 2FA token. IP=${getClientIp(socket)}`);
|
log.error("auth", `Error changing 2FA token. IP=${clientIP}`);
|
||||||
|
|
||||||
callback({
|
callback({
|
||||||
ok: false,
|
ok: false,
|
||||||
|
@ -476,6 +487,8 @@ try {
|
||||||
});
|
});
|
||||||
|
|
||||||
socket.on("disable2FA", async (currentPassword, callback) => {
|
socket.on("disable2FA", async (currentPassword, callback) => {
|
||||||
|
const clientIP = await server.getClientIP(socket);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (! await twoFaRateLimiter.pass(callback)) {
|
if (! await twoFaRateLimiter.pass(callback)) {
|
||||||
return;
|
return;
|
||||||
|
@ -485,7 +498,7 @@ try {
|
||||||
await doubleCheckPassword(socket, currentPassword);
|
await doubleCheckPassword(socket, currentPassword);
|
||||||
await TwoFA.disable2FA(socket.userID);
|
await TwoFA.disable2FA(socket.userID);
|
||||||
|
|
||||||
log.info("auth", `Disabled 2FA token. IP=${getClientIp(socket)}`);
|
log.info("auth", `Disabled 2FA token. IP=${clientIP}`);
|
||||||
|
|
||||||
callback({
|
callback({
|
||||||
ok: true,
|
ok: true,
|
||||||
|
@ -493,7 +506,7 @@ try {
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|
||||||
log.error("auth", `Error disabling 2FA token. IP=${getClientIp(socket)}`);
|
log.error("auth", `Error disabling 2FA token. IP=${clientIP}`);
|
||||||
|
|
||||||
callback({
|
callback({
|
||||||
ok: false,
|
ok: false,
|
||||||
|
@ -664,9 +677,10 @@ try {
|
||||||
bean.basic_auth_pass = monitor.basic_auth_pass;
|
bean.basic_auth_pass = monitor.basic_auth_pass;
|
||||||
bean.interval = monitor.interval;
|
bean.interval = monitor.interval;
|
||||||
bean.retryInterval = monitor.retryInterval;
|
bean.retryInterval = monitor.retryInterval;
|
||||||
|
bean.resendInterval = monitor.resendInterval;
|
||||||
bean.hostname = monitor.hostname;
|
bean.hostname = monitor.hostname;
|
||||||
bean.maxretries = monitor.maxretries;
|
bean.maxretries = monitor.maxretries;
|
||||||
bean.port = monitor.port;
|
bean.port = parseInt(monitor.port);
|
||||||
bean.keyword = monitor.keyword;
|
bean.keyword = monitor.keyword;
|
||||||
bean.ignoreTls = monitor.ignoreTls;
|
bean.ignoreTls = monitor.ignoreTls;
|
||||||
bean.expiryNotification = monitor.expiryNotification;
|
bean.expiryNotification = monitor.expiryNotification;
|
||||||
|
@ -676,11 +690,23 @@ try {
|
||||||
bean.dns_resolve_type = monitor.dns_resolve_type;
|
bean.dns_resolve_type = monitor.dns_resolve_type;
|
||||||
bean.dns_resolve_server = monitor.dns_resolve_server;
|
bean.dns_resolve_server = monitor.dns_resolve_server;
|
||||||
bean.pushToken = monitor.pushToken;
|
bean.pushToken = monitor.pushToken;
|
||||||
|
bean.docker_container = monitor.docker_container;
|
||||||
|
bean.docker_host = monitor.docker_host;
|
||||||
bean.proxyId = Number.isInteger(monitor.proxyId) ? monitor.proxyId : null;
|
bean.proxyId = Number.isInteger(monitor.proxyId) ? monitor.proxyId : null;
|
||||||
bean.mqttUsername = monitor.mqttUsername;
|
bean.mqttUsername = monitor.mqttUsername;
|
||||||
bean.mqttPassword = monitor.mqttPassword;
|
bean.mqttPassword = monitor.mqttPassword;
|
||||||
bean.mqttTopic = monitor.mqttTopic;
|
bean.mqttTopic = monitor.mqttTopic;
|
||||||
bean.mqttSuccessMessage = monitor.mqttSuccessMessage;
|
bean.mqttSuccessMessage = monitor.mqttSuccessMessage;
|
||||||
|
bean.databaseConnectionString = monitor.databaseConnectionString;
|
||||||
|
bean.databaseQuery = monitor.databaseQuery;
|
||||||
|
bean.authMethod = monitor.authMethod;
|
||||||
|
bean.authWorkstation = monitor.authWorkstation;
|
||||||
|
bean.authDomain = monitor.authDomain;
|
||||||
|
bean.radiusUsername = monitor.radiusUsername;
|
||||||
|
bean.radiusPassword = monitor.radiusPassword;
|
||||||
|
bean.radiusCalledStationId = monitor.radiusCalledStationId;
|
||||||
|
bean.radiusCallingStationId = monitor.radiusCallingStationId;
|
||||||
|
bean.radiusSecret = monitor.radiusSecret;
|
||||||
|
|
||||||
await R.store(bean);
|
await R.store(bean);
|
||||||
|
|
||||||
|
@ -1501,10 +1527,14 @@ try {
|
||||||
method: monitorListData[i].method || "GET",
|
method: monitorListData[i].method || "GET",
|
||||||
body: monitorListData[i].body,
|
body: monitorListData[i].body,
|
||||||
headers: monitorListData[i].headers,
|
headers: monitorListData[i].headers,
|
||||||
|
authMethod: monitorListData[i].authMethod,
|
||||||
basic_auth_user: monitorListData[i].basic_auth_user,
|
basic_auth_user: monitorListData[i].basic_auth_user,
|
||||||
basic_auth_pass: monitorListData[i].basic_auth_pass,
|
basic_auth_pass: monitorListData[i].basic_auth_pass,
|
||||||
|
authWorkstation: monitorListData[i].authWorkstation,
|
||||||
|
authDomain: monitorListData[i].authDomain,
|
||||||
interval: monitorListData[i].interval,
|
interval: monitorListData[i].interval,
|
||||||
retryInterval: retryInterval,
|
retryInterval: retryInterval,
|
||||||
|
resendInterval: monitorListData[i].resendInterval || 0,
|
||||||
hostname: monitorListData[i].hostname,
|
hostname: monitorListData[i].hostname,
|
||||||
maxretries: monitorListData[i].maxretries,
|
maxretries: monitorListData[i].maxretries,
|
||||||
port: monitorListData[i].port,
|
port: monitorListData[i].port,
|
||||||
|
@ -1673,6 +1703,7 @@ try {
|
||||||
cloudflaredSocketHandler(socket);
|
cloudflaredSocketHandler(socket);
|
||||||
databaseSocketHandler(socket);
|
databaseSocketHandler(socket);
|
||||||
proxySocketHandler(socket);
|
proxySocketHandler(socket);
|
||||||
|
dockerSocketHandler(socket);
|
||||||
|
|
||||||
log.debug("server", "added all socket handlers");
|
log.debug("server", "added all socket handlers");
|
||||||
|
|
||||||
|
@ -1710,6 +1741,10 @@ try {
|
||||||
if (testMode) {
|
if (testMode) {
|
||||||
startUnitTest();
|
startUnitTest();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (e2eTestMode) {
|
||||||
|
startE2eTests();
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
initBackgroundJobs(args);
|
initBackgroundJobs(args);
|
||||||
|
@ -1785,6 +1820,7 @@ async function afterLogin(socket, user) {
|
||||||
sendMaintenanceList(socket);
|
sendMaintenanceList(socket);
|
||||||
sendNotificationList(socket);
|
sendNotificationList(socket);
|
||||||
sendProxyList(socket);
|
sendProxyList(socket);
|
||||||
|
sendDockerHostList(socket);
|
||||||
|
|
||||||
await sleep(500);
|
await sleep(500);
|
||||||
|
|
||||||
|
@ -1958,10 +1994,6 @@ async function shutdownFunction(signal) {
|
||||||
await cloudflaredStop();
|
await cloudflaredStop();
|
||||||
}
|
}
|
||||||
|
|
||||||
function getClientIp(socket) {
|
|
||||||
return socket.client.conn.remoteAddress.replace(/^.*:/, "");
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Final function called before application exits */
|
/** Final function called before application exits */
|
||||||
function finalFunction() {
|
function finalFunction() {
|
||||||
log.info("server", "Graceful shutdown successful!");
|
log.info("server", "Graceful shutdown successful!");
|
||||||
|
|
165
server/settings.js
Normal file
165
server/settings.js
Normal file
|
@ -0,0 +1,165 @@
|
||||||
|
const { R } = require("redbean-node");
|
||||||
|
const { log } = require("../src/util");
|
||||||
|
|
||||||
|
class Settings {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Example:
|
||||||
|
* {
|
||||||
|
* key1: {
|
||||||
|
* value: "value2",
|
||||||
|
* timestamp: 12345678
|
||||||
|
* },
|
||||||
|
* key2: {
|
||||||
|
* value: 2,
|
||||||
|
* timestamp: 12345678
|
||||||
|
* },
|
||||||
|
* }
|
||||||
|
* @type {{}}
|
||||||
|
*/
|
||||||
|
static cacheList = {
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
static cacheCleaner = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieve value of setting based on key
|
||||||
|
* @param {string} key Key of setting to retrieve
|
||||||
|
* @returns {Promise<any>} Value
|
||||||
|
*/
|
||||||
|
static async get(key) {
|
||||||
|
|
||||||
|
// Start cache clear if not started yet
|
||||||
|
if (!Settings.cacheCleaner) {
|
||||||
|
Settings.cacheCleaner = setInterval(() => {
|
||||||
|
log.debug("settings", "Cache Cleaner is just started.");
|
||||||
|
for (key in Settings.cacheList) {
|
||||||
|
if (Date.now() - Settings.cacheList[key].timestamp > 60 * 1000) {
|
||||||
|
log.debug("settings", "Cache Cleaner deleted: " + key);
|
||||||
|
delete Settings.cacheList[key];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}, 60 * 1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Query from cache
|
||||||
|
if (key in Settings.cacheList) {
|
||||||
|
const v = Settings.cacheList[key].value;
|
||||||
|
log.debug("settings", `Get Setting (cache): ${key}: ${v}`);
|
||||||
|
return v;
|
||||||
|
}
|
||||||
|
|
||||||
|
let value = await R.getCell("SELECT `value` FROM setting WHERE `key` = ? ", [
|
||||||
|
key,
|
||||||
|
]);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const v = JSON.parse(value);
|
||||||
|
log.debug("settings", `Get Setting: ${key}: ${v}`);
|
||||||
|
|
||||||
|
Settings.cacheList[key] = {
|
||||||
|
value: v,
|
||||||
|
timestamp: Date.now()
|
||||||
|
};
|
||||||
|
|
||||||
|
return v;
|
||||||
|
} catch (e) {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the specified setting to specified value
|
||||||
|
* @param {string} key Key of setting to set
|
||||||
|
* @param {any} value Value to set to
|
||||||
|
* @param {?string} type Type of setting
|
||||||
|
* @returns {Promise<void>}
|
||||||
|
*/
|
||||||
|
static async set(key, value, type = null) {
|
||||||
|
|
||||||
|
let bean = await R.findOne("setting", " `key` = ? ", [
|
||||||
|
key,
|
||||||
|
]);
|
||||||
|
if (!bean) {
|
||||||
|
bean = R.dispense("setting");
|
||||||
|
bean.key = key;
|
||||||
|
}
|
||||||
|
bean.type = type;
|
||||||
|
bean.value = JSON.stringify(value);
|
||||||
|
await R.store(bean);
|
||||||
|
|
||||||
|
Settings.deleteCache([ key ]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get settings based on type
|
||||||
|
* @param {string} type The type of setting
|
||||||
|
* @returns {Promise<Bean>}
|
||||||
|
*/
|
||||||
|
static async getSettings(type) {
|
||||||
|
let list = await R.getAll("SELECT `key`, `value` FROM setting WHERE `type` = ? ", [
|
||||||
|
type,
|
||||||
|
]);
|
||||||
|
|
||||||
|
let result = {};
|
||||||
|
|
||||||
|
for (let row of list) {
|
||||||
|
try {
|
||||||
|
result[row.key] = JSON.parse(row.value);
|
||||||
|
} catch (e) {
|
||||||
|
result[row.key] = row.value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set settings based on type
|
||||||
|
* @param {string} type Type of settings to set
|
||||||
|
* @param {Object} data Values of settings
|
||||||
|
* @returns {Promise<void>}
|
||||||
|
*/
|
||||||
|
static async setSettings(type, data) {
|
||||||
|
let keyList = Object.keys(data);
|
||||||
|
|
||||||
|
let promiseList = [];
|
||||||
|
|
||||||
|
for (let key of keyList) {
|
||||||
|
let bean = await R.findOne("setting", " `key` = ? ", [
|
||||||
|
key
|
||||||
|
]);
|
||||||
|
|
||||||
|
if (bean == null) {
|
||||||
|
bean = R.dispense("setting");
|
||||||
|
bean.type = type;
|
||||||
|
bean.key = key;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (bean.type === type) {
|
||||||
|
bean.value = JSON.stringify(data[key]);
|
||||||
|
promiseList.push(R.store(bean));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await Promise.all(promiseList);
|
||||||
|
|
||||||
|
Settings.deleteCache(keyList);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param {string[]} keyList
|
||||||
|
*/
|
||||||
|
static deleteCache(keyList) {
|
||||||
|
for (let key of keyList) {
|
||||||
|
delete Settings.cacheList[key];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
Settings,
|
||||||
|
};
|
|
@ -63,7 +63,10 @@ module.exports.cloudflaredSocketHandler = (socket) => {
|
||||||
socket.on(prefix + "stop", async (currentPassword, callback) => {
|
socket.on(prefix + "stop", async (currentPassword, callback) => {
|
||||||
try {
|
try {
|
||||||
checkLogin(socket);
|
checkLogin(socket);
|
||||||
|
const disabledAuth = await setting("disableAuth");
|
||||||
|
if (!disabledAuth) {
|
||||||
await doubleCheckPassword(socket, currentPassword);
|
await doubleCheckPassword(socket, currentPassword);
|
||||||
|
}
|
||||||
cloudflared.stop();
|
cloudflared.stop();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
callback({
|
callback({
|
||||||
|
|
79
server/socket-handlers/docker-socket-handler.js
Normal file
79
server/socket-handlers/docker-socket-handler.js
Normal file
|
@ -0,0 +1,79 @@
|
||||||
|
const { sendDockerHostList } = require("../client");
|
||||||
|
const { checkLogin } = require("../util-server");
|
||||||
|
const { DockerHost } = require("../docker");
|
||||||
|
const { log } = require("../../src/util");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handlers for docker hosts
|
||||||
|
* @param {Socket} socket Socket.io instance
|
||||||
|
*/
|
||||||
|
module.exports.dockerSocketHandler = (socket) => {
|
||||||
|
socket.on("addDockerHost", async (dockerHost, dockerHostID, callback) => {
|
||||||
|
try {
|
||||||
|
checkLogin(socket);
|
||||||
|
|
||||||
|
let dockerHostBean = await DockerHost.save(dockerHost, dockerHostID, socket.userID);
|
||||||
|
await sendDockerHostList(socket);
|
||||||
|
|
||||||
|
callback({
|
||||||
|
ok: true,
|
||||||
|
msg: "Saved",
|
||||||
|
id: dockerHostBean.id,
|
||||||
|
});
|
||||||
|
|
||||||
|
} catch (e) {
|
||||||
|
callback({
|
||||||
|
ok: false,
|
||||||
|
msg: e.message,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.on("deleteDockerHost", async (dockerHostID, callback) => {
|
||||||
|
try {
|
||||||
|
checkLogin(socket);
|
||||||
|
|
||||||
|
await DockerHost.delete(dockerHostID, socket.userID);
|
||||||
|
await sendDockerHostList(socket);
|
||||||
|
|
||||||
|
callback({
|
||||||
|
ok: true,
|
||||||
|
msg: "Deleted",
|
||||||
|
});
|
||||||
|
|
||||||
|
} catch (e) {
|
||||||
|
callback({
|
||||||
|
ok: false,
|
||||||
|
msg: e.message,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.on("testDockerHost", async (dockerHost, callback) => {
|
||||||
|
try {
|
||||||
|
checkLogin(socket);
|
||||||
|
|
||||||
|
let amount = await DockerHost.testDockerHost(dockerHost);
|
||||||
|
let msg;
|
||||||
|
|
||||||
|
if (amount > 1) {
|
||||||
|
msg = "Connected Successfully. Amount of containers: " + amount;
|
||||||
|
} else {
|
||||||
|
msg = "Connected Successfully, but there are no containers?";
|
||||||
|
}
|
||||||
|
|
||||||
|
callback({
|
||||||
|
ok: true,
|
||||||
|
msg,
|
||||||
|
});
|
||||||
|
|
||||||
|
} catch (e) {
|
||||||
|
log.error("docker", e);
|
||||||
|
|
||||||
|
callback({
|
||||||
|
ok: false,
|
||||||
|
msg: e.message,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
|
@ -202,6 +202,11 @@ module.exports.statusPageSocketHandler = (socket) => {
|
||||||
relationBean.weight = monitorOrder++;
|
relationBean.weight = monitorOrder++;
|
||||||
relationBean.group_id = groupBean.id;
|
relationBean.group_id = groupBean.id;
|
||||||
relationBean.monitor_id = monitor.id;
|
relationBean.monitor_id = monitor.id;
|
||||||
|
|
||||||
|
if (monitor.sendUrl !== undefined) {
|
||||||
|
relationBean.send_url = monitor.sendUrl;
|
||||||
|
}
|
||||||
|
|
||||||
await R.store(relationBean);
|
await R.store(relationBean);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -7,6 +7,8 @@ const { R } = require("redbean-node");
|
||||||
const { log } = require("../src/util");
|
const { log } = require("../src/util");
|
||||||
const Database = require("./database");
|
const Database = require("./database");
|
||||||
const util = require("util");
|
const util = require("util");
|
||||||
|
const { CacheableDnsHttpAgent } = require("./cacheable-dns-http-agent");
|
||||||
|
const { Settings } = require("./settings");
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* `module.exports` (alias: `server`) should be inside this class, in order to avoid circular dependency issue.
|
* `module.exports` (alias: `server`) should be inside this class, in order to avoid circular dependency issue.
|
||||||
|
@ -29,6 +31,12 @@ class UptimeKumaServer {
|
||||||
httpServer = undefined;
|
httpServer = undefined;
|
||||||
io = undefined;
|
io = undefined;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cache Index HTML
|
||||||
|
* @type {string}
|
||||||
|
*/
|
||||||
|
indexHTML = "";
|
||||||
|
|
||||||
static getInstance(args) {
|
static getInstance(args) {
|
||||||
if (UptimeKumaServer.instance == null) {
|
if (UptimeKumaServer.instance == null) {
|
||||||
UptimeKumaServer.instance = new UptimeKumaServer(args);
|
UptimeKumaServer.instance = new UptimeKumaServer(args);
|
||||||
|
@ -43,7 +51,6 @@ class UptimeKumaServer {
|
||||||
|
|
||||||
log.info("server", "Creating express and socket.io instance");
|
log.info("server", "Creating express and socket.io instance");
|
||||||
this.app = express();
|
this.app = express();
|
||||||
|
|
||||||
if (sslKey && sslCert) {
|
if (sslKey && sslCert) {
|
||||||
log.info("server", "Server Type: HTTPS");
|
log.info("server", "Server Type: HTTPS");
|
||||||
this.httpServer = https.createServer({
|
this.httpServer = https.createServer({
|
||||||
|
@ -55,6 +62,18 @@ class UptimeKumaServer {
|
||||||
this.httpServer = http.createServer(this.app);
|
this.httpServer = http.createServer(this.app);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
this.indexHTML = fs.readFileSync("./dist/index.html").toString();
|
||||||
|
} catch (e) {
|
||||||
|
// "dist/index.html" is not necessary for development
|
||||||
|
if (process.env.NODE_ENV !== "development") {
|
||||||
|
log.error("server", "Error: Cannot find 'dist/index.html', did you install correctly?");
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
CacheableDnsHttpAgent.registerGlobalAgent();
|
||||||
|
|
||||||
this.io = new Server(this.httpServer);
|
this.io = new Server(this.httpServer);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -110,6 +129,22 @@ class UptimeKumaServer {
|
||||||
|
|
||||||
errorLogStream.end();
|
errorLogStream.end();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async getClientIP(socket) {
|
||||||
|
let clientIP = socket.client.conn.remoteAddress;
|
||||||
|
|
||||||
|
if (clientIP === undefined) {
|
||||||
|
clientIP = "";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (await Settings.get("trustProxy")) {
|
||||||
|
return socket.client.conn.request.headers["x-forwarded-for"]
|
||||||
|
|| socket.client.conn.request.headers["x-real-ip"]
|
||||||
|
|| clientIP.replace(/^.*:/, "");
|
||||||
|
} else {
|
||||||
|
return clientIP.replace(/^.*:/, "");
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
|
|
|
@ -10,6 +10,17 @@ const chardet = require("chardet");
|
||||||
const mqtt = require("mqtt");
|
const mqtt = require("mqtt");
|
||||||
const chroma = require("chroma-js");
|
const chroma = require("chroma-js");
|
||||||
const { badgeConstants } = require("./config");
|
const { badgeConstants } = require("./config");
|
||||||
|
const mssql = require("mssql");
|
||||||
|
const { Client } = require("pg");
|
||||||
|
const postgresConParse = require("pg-connection-string").parse;
|
||||||
|
const { NtlmClient } = require("axios-ntlm");
|
||||||
|
const { Settings } = require("./settings");
|
||||||
|
const radiusClient = require("node-radius-client");
|
||||||
|
const {
|
||||||
|
dictionaries: {
|
||||||
|
rfc2865: { file, attributes },
|
||||||
|
},
|
||||||
|
} = require("node-radius-utils");
|
||||||
|
|
||||||
// From ping-lite
|
// From ping-lite
|
||||||
exports.WIN = /^win/.test(process.platform);
|
exports.WIN = /^win/.test(process.platform);
|
||||||
|
@ -172,16 +183,40 @@ exports.mqttAsync = function (hostname, topic, okMessage, options = {}) {
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Use NTLM Auth for a http request.
|
||||||
|
* @param {Object} options The http request options
|
||||||
|
* @param {Object} ntlmOptions The auth options
|
||||||
|
* @returns {Promise<(string[]|Object[]|Object)>}
|
||||||
|
*/
|
||||||
|
exports.httpNtlm = function (options, ntlmOptions) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
let client = NtlmClient(ntlmOptions);
|
||||||
|
|
||||||
|
client(options)
|
||||||
|
.then((resp) => {
|
||||||
|
resolve(resp);
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
reject(err);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Resolves a given record using the specified DNS server
|
* Resolves a given record using the specified DNS server
|
||||||
* @param {string} hostname The hostname of the record to lookup
|
* @param {string} hostname The hostname of the record to lookup
|
||||||
* @param {string} resolverServer The DNS server to use
|
* @param {string} resolverServer The DNS server to use
|
||||||
|
* @param {string} resolverPort Port the DNS server is listening on
|
||||||
* @param {string} rrtype The type of record to request
|
* @param {string} rrtype The type of record to request
|
||||||
* @returns {Promise<(string[]|Object[]|Object)>}
|
* @returns {Promise<(string[]|Object[]|Object)>}
|
||||||
*/
|
*/
|
||||||
exports.dnsResolve = function (hostname, resolverServer, rrtype) {
|
exports.dnsResolve = function (hostname, resolverServer, resolverPort, rrtype) {
|
||||||
const resolver = new Resolver();
|
const resolver = new Resolver();
|
||||||
resolver.setServers([ resolverServer ]);
|
// Remove brackets from IPv6 addresses so we can re-add them to
|
||||||
|
// prevent issues with ::1:5300 (::1 port 5300)
|
||||||
|
resolverServer = resolverServer.replace("[", "").replace("]", "");
|
||||||
|
resolver.setServers([ `[${resolverServer}]:${resolverPort}` ]);
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
if (rrtype === "PTR") {
|
if (rrtype === "PTR") {
|
||||||
resolver.reverse(hostname, (err, records) => {
|
resolver.reverse(hostname, (err, records) => {
|
||||||
|
@ -203,23 +238,91 @@ exports.dnsResolve = function (hostname, resolverServer, rrtype) {
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Run a query on SQL Server
|
||||||
|
* @param {string} connectionString The database connection string
|
||||||
|
* @param {string} query The query to validate the database with
|
||||||
|
* @returns {Promise<(string[]|Object[]|Object)>}
|
||||||
|
*/
|
||||||
|
exports.mssqlQuery = function (connectionString, query) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
mssql.connect(connectionString).then(pool => {
|
||||||
|
return pool.request()
|
||||||
|
.query(query);
|
||||||
|
}).then(result => {
|
||||||
|
resolve(result);
|
||||||
|
}).catch(err => {
|
||||||
|
reject(err);
|
||||||
|
}).finally(() => {
|
||||||
|
mssql.close();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Run a query on Postgres
|
||||||
|
* @param {string} connectionString The database connection string
|
||||||
|
* @param {string} query The query to validate the database with
|
||||||
|
* @returns {Promise<(string[]|Object[]|Object)>}
|
||||||
|
*/
|
||||||
|
exports.postgresQuery = function (connectionString, query) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const config = postgresConParse(connectionString);
|
||||||
|
|
||||||
|
if (config.password === "") {
|
||||||
|
// See https://github.com/brianc/node-postgres/issues/1927
|
||||||
|
return reject(new Error("Password is undefined."));
|
||||||
|
}
|
||||||
|
|
||||||
|
const client = new Client({ connectionString });
|
||||||
|
|
||||||
|
client.connect();
|
||||||
|
|
||||||
|
return client.query(query)
|
||||||
|
.then(res => {
|
||||||
|
resolve(res);
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
reject(err);
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
client.end();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
exports.radius = function (
|
||||||
|
hostname,
|
||||||
|
username,
|
||||||
|
password,
|
||||||
|
calledStationId,
|
||||||
|
callingStationId,
|
||||||
|
secret,
|
||||||
|
) {
|
||||||
|
const client = new radiusClient({
|
||||||
|
host: hostname,
|
||||||
|
dictionaries: [ file ],
|
||||||
|
});
|
||||||
|
|
||||||
|
return client.accessRequest({
|
||||||
|
secret: secret,
|
||||||
|
attributes: [
|
||||||
|
[ attributes.USER_NAME, username ],
|
||||||
|
[ attributes.USER_PASSWORD, password ],
|
||||||
|
[ attributes.CALLING_STATION_ID, callingStationId ],
|
||||||
|
[ attributes.CALLED_STATION_ID, calledStationId ],
|
||||||
|
],
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Retrieve value of setting based on key
|
* Retrieve value of setting based on key
|
||||||
* @param {string} key Key of setting to retrieve
|
* @param {string} key Key of setting to retrieve
|
||||||
* @returns {Promise<any>} Value
|
* @returns {Promise<any>} Value
|
||||||
|
* @deprecated Use await Settings.get(key)
|
||||||
*/
|
*/
|
||||||
exports.setting = async function (key) {
|
exports.setting = async function (key) {
|
||||||
let value = await R.getCell("SELECT `value` FROM setting WHERE `key` = ? ", [
|
return await Settings.get(key);
|
||||||
key,
|
|
||||||
]);
|
|
||||||
|
|
||||||
try {
|
|
||||||
const v = JSON.parse(value);
|
|
||||||
log.debug("util", `Get Setting: ${key}: ${v}`);
|
|
||||||
return v;
|
|
||||||
} catch (e) {
|
|
||||||
return value;
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -230,70 +333,26 @@ exports.setting = async function (key) {
|
||||||
* @returns {Promise<void>}
|
* @returns {Promise<void>}
|
||||||
*/
|
*/
|
||||||
exports.setSetting = async function (key, value, type = null) {
|
exports.setSetting = async function (key, value, type = null) {
|
||||||
let bean = await R.findOne("setting", " `key` = ? ", [
|
await Settings.set(key, value, type);
|
||||||
key,
|
|
||||||
]);
|
|
||||||
if (!bean) {
|
|
||||||
bean = R.dispense("setting");
|
|
||||||
bean.key = key;
|
|
||||||
}
|
|
||||||
bean.type = type;
|
|
||||||
bean.value = JSON.stringify(value);
|
|
||||||
await R.store(bean);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get settings based on type
|
* Get settings based on type
|
||||||
* @param {?string} type The type of setting
|
* @param {string} type The type of setting
|
||||||
* @returns {Promise<Bean>}
|
* @returns {Promise<Bean>}
|
||||||
*/
|
*/
|
||||||
exports.getSettings = async function (type) {
|
exports.getSettings = async function (type) {
|
||||||
let list = await R.getAll("SELECT `key`, `value` FROM setting WHERE `type` = ? ", [
|
return await Settings.getSettings(type);
|
||||||
type,
|
|
||||||
]);
|
|
||||||
|
|
||||||
let result = {};
|
|
||||||
|
|
||||||
for (let row of list) {
|
|
||||||
try {
|
|
||||||
result[row.key] = JSON.parse(row.value);
|
|
||||||
} catch (e) {
|
|
||||||
result[row.key] = row.value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Set settings based on type
|
* Set settings based on type
|
||||||
* @param {?string} type Type of settings to set
|
* @param {string} type Type of settings to set
|
||||||
* @param {Object} data Values of settings
|
* @param {Object} data Values of settings
|
||||||
* @returns {Promise<void>}
|
* @returns {Promise<void>}
|
||||||
*/
|
*/
|
||||||
exports.setSettings = async function (type, data) {
|
exports.setSettings = async function (type, data) {
|
||||||
let keyList = Object.keys(data);
|
await Settings.setSettings(type, data);
|
||||||
|
|
||||||
let promiseList = [];
|
|
||||||
|
|
||||||
for (let key of keyList) {
|
|
||||||
let bean = await R.findOne("setting", " `key` = ? ", [
|
|
||||||
key
|
|
||||||
]);
|
|
||||||
|
|
||||||
if (bean == null) {
|
|
||||||
bean = R.dispense("setting");
|
|
||||||
bean.type = type;
|
|
||||||
bean.key = key;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (bean.type === type) {
|
|
||||||
bean.value = JSON.stringify(data[key]);
|
|
||||||
promiseList.push(R.store(bean));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
await Promise.all(promiseList);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// ssl-checker by @dyaa
|
// ssl-checker by @dyaa
|
||||||
|
@ -386,7 +445,7 @@ exports.checkCertificate = function (res) {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check if the provided status code is within the accepted ranges
|
* Check if the provided status code is within the accepted ranges
|
||||||
* @param {string} status The status code to check
|
* @param {number} status The status code to check
|
||||||
* @param {string[]} acceptedCodes An array of accepted status codes
|
* @param {string[]} acceptedCodes An array of accepted status codes
|
||||||
* @returns {boolean} True if status code within range, false otherwise
|
* @returns {boolean} True if status code within range, false otherwise
|
||||||
* @throws {Error} Will throw an error if the provided status code is not a valid range string or code string
|
* @throws {Error} Will throw an error if the provided status code is not a valid range string or code string
|
||||||
|
@ -514,6 +573,26 @@ exports.startUnitTest = async () => {
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/** Start end-to-end tests */
|
||||||
|
exports.startE2eTests = async () => {
|
||||||
|
console.log("Starting unit test...");
|
||||||
|
const npm = /^win/.test(process.platform) ? "npm.cmd" : "npm";
|
||||||
|
const child = childProcess.spawn(npm, [ "run", "cy:run" ]);
|
||||||
|
|
||||||
|
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);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Convert unknown string to UTF8
|
* Convert unknown string to UTF8
|
||||||
* @param {Uint8Array} body Buffer
|
* @param {Uint8Array} body Buffer
|
||||||
|
@ -554,3 +633,15 @@ exports.percentageToColor = (percentage, maxHue = 90, minHue = 10) => {
|
||||||
exports.filterAndJoin = (parts, connector = "") => {
|
exports.filterAndJoin = (parts, connector = "") => {
|
||||||
return parts.filter((part) => !!part && part !== "").join(connector);
|
return parts.filter((part) => !!part && part !== "").join(connector);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send a 403 response
|
||||||
|
* @param {Object} res Express response object
|
||||||
|
* @param {string} [msg=""] Message to send
|
||||||
|
*/
|
||||||
|
module.exports.send403 = (res, msg = "") => {
|
||||||
|
res.status(403).json({
|
||||||
|
"status": "fail",
|
||||||
|
"msg": msg,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
|
@ -34,6 +34,25 @@ textarea.form-control {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// optgroup
|
||||||
|
optgroup {
|
||||||
|
color: #b1b1b1;
|
||||||
|
option {
|
||||||
|
color: #212529;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark {
|
||||||
|
optgroup {
|
||||||
|
color: #535864;
|
||||||
|
option {
|
||||||
|
color: $dark-font-color;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Scrollbar
|
||||||
::-webkit-scrollbar-thumb {
|
::-webkit-scrollbar-thumb {
|
||||||
background: #ccc;
|
background: #ccc;
|
||||||
border-radius: 20px;
|
border-radius: 20px;
|
||||||
|
@ -365,10 +384,16 @@ textarea.form-control {
|
||||||
height: calc(100% - 65px);
|
height: calc(100% - 65px);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@media (max-width: 770px) {
|
||||||
|
&.scrollbar {
|
||||||
|
height: calc(100% - 40px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.item {
|
.item {
|
||||||
display: block;
|
display: block;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
padding: 15px;
|
padding: 13px 15px 10px 15px;
|
||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
transition: all ease-in-out 0.15s;
|
transition: all ease-in-out 0.15s;
|
||||||
|
|
||||||
|
@ -379,7 +404,6 @@ textarea.form-control {
|
||||||
.info {
|
.info {
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
|
@ -474,6 +498,14 @@ textarea.form-control {
|
||||||
outline: none !important;
|
outline: none !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
h5.settings-subheading::after {
|
||||||
|
content: "";
|
||||||
|
display: block;
|
||||||
|
width: 50%;
|
||||||
|
padding-top: 8px;
|
||||||
|
border-bottom: 1px solid $dark-border-color;
|
||||||
|
}
|
||||||
|
|
||||||
// Localization
|
// Localization
|
||||||
|
|
||||||
@import "localization.scss";
|
@import "localization.scss";
|
||||||
|
|
86
src/components/ActionInput.vue
Normal file
86
src/components/ActionInput.vue
Normal file
|
@ -0,0 +1,86 @@
|
||||||
|
<template>
|
||||||
|
<div class="input-group mb-3">
|
||||||
|
<input
|
||||||
|
ref="input"
|
||||||
|
v-model="model"
|
||||||
|
class="form-control"
|
||||||
|
:type="type"
|
||||||
|
:placeholder="placeholder"
|
||||||
|
:disabled="!enabled"
|
||||||
|
>
|
||||||
|
<a class="btn btn-outline-primary" @click="action()">
|
||||||
|
<font-awesome-icon :icon="icon" />
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
/**
|
||||||
|
* Generic input field with a customizable action on the right.
|
||||||
|
* Action is passed in as a function.
|
||||||
|
*/
|
||||||
|
export default {
|
||||||
|
props: {
|
||||||
|
/**
|
||||||
|
* The value of the input field.
|
||||||
|
*/
|
||||||
|
modelValue: {
|
||||||
|
type: String,
|
||||||
|
default: ""
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* Whether the input field is enabled / disabled.
|
||||||
|
*/
|
||||||
|
enabled: {
|
||||||
|
type: Boolean,
|
||||||
|
default: true
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* Placeholder text for the input field.
|
||||||
|
*/
|
||||||
|
placeholder: {
|
||||||
|
type: String,
|
||||||
|
default: ""
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* The icon displayed in the right button of the input field.
|
||||||
|
* Accepts a Font Awesome icon string identifier.
|
||||||
|
* @example "plus"
|
||||||
|
*/
|
||||||
|
icon: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* The input type of the input field.
|
||||||
|
* @example "email"
|
||||||
|
*/
|
||||||
|
type: {
|
||||||
|
type: String,
|
||||||
|
default: "text",
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* The action to be performed when the button is clicked.
|
||||||
|
* Action is passed in as a function.
|
||||||
|
*/
|
||||||
|
action: {
|
||||||
|
type: Function,
|
||||||
|
default: () => {},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
emits: [ "update:modelValue" ],
|
||||||
|
computed: {
|
||||||
|
/**
|
||||||
|
* Send value update to parent on change.
|
||||||
|
*/
|
||||||
|
model: {
|
||||||
|
get() {
|
||||||
|
return this.modelValue;
|
||||||
|
},
|
||||||
|
set(value) {
|
||||||
|
this.$emit("update:modelValue", value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
|
@ -25,10 +25,12 @@ export default {
|
||||||
CertificateInfoRow,
|
CertificateInfoRow,
|
||||||
},
|
},
|
||||||
props: {
|
props: {
|
||||||
|
/** Object representing certificate */
|
||||||
certInfo: {
|
certInfo: {
|
||||||
type: Object,
|
type: Object,
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
|
/** Is the TLS certificate valid? */
|
||||||
valid: {
|
valid: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
required: true,
|
required: true,
|
||||||
|
|
|
@ -56,12 +56,19 @@ export default {
|
||||||
Datetime,
|
Datetime,
|
||||||
},
|
},
|
||||||
props: {
|
props: {
|
||||||
|
/** Object representing certificate */
|
||||||
cert: {
|
cert: {
|
||||||
type: Object,
|
type: Object,
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
/**
|
||||||
|
* Format the subject of the certificate
|
||||||
|
* @param {Object} subject Object representing the certificates
|
||||||
|
* subject
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
formatSubject(subject) {
|
formatSubject(subject) {
|
||||||
if (subject.O && subject.CN && subject.C) {
|
if (subject.O && subject.CN && subject.C) {
|
||||||
return `${subject.CN} - ${subject.O} (${subject.C})`;
|
return `${subject.CN} - ${subject.O} (${subject.C})`;
|
||||||
|
|
|
@ -29,14 +29,17 @@ import { Modal } from "bootstrap";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
props: {
|
props: {
|
||||||
|
/** Style of button */
|
||||||
btnStyle: {
|
btnStyle: {
|
||||||
type: String,
|
type: String,
|
||||||
default: "btn-primary",
|
default: "btn-primary",
|
||||||
},
|
},
|
||||||
|
/** Text to use as yes */
|
||||||
yesText: {
|
yesText: {
|
||||||
type: String,
|
type: String,
|
||||||
default: "Yes", // TODO: No idea what to translate this
|
default: "Yes", // TODO: No idea what to translate this
|
||||||
},
|
},
|
||||||
|
/** Text to use as no */
|
||||||
noText: {
|
noText: {
|
||||||
type: String,
|
type: String,
|
||||||
default: "No",
|
default: "No",
|
||||||
|
@ -50,9 +53,13 @@ export default {
|
||||||
this.modal = new Modal(this.$refs.modal);
|
this.modal = new Modal(this.$refs.modal);
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
/** Show the confirm dialog */
|
||||||
show() {
|
show() {
|
||||||
this.modal.show();
|
this.modal.show();
|
||||||
},
|
},
|
||||||
|
/**
|
||||||
|
* @emits string "yes" Notify the parent when Yes is pressed
|
||||||
|
*/
|
||||||
yes() {
|
yes() {
|
||||||
this.$emit("yes");
|
this.$emit("yes");
|
||||||
},
|
},
|
||||||
|
|
|
@ -25,33 +25,41 @@ let timeout;
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
props: {
|
props: {
|
||||||
|
/** ID of this input */
|
||||||
id: {
|
id: {
|
||||||
type: String,
|
type: String,
|
||||||
default: ""
|
default: ""
|
||||||
},
|
},
|
||||||
|
/** Type of input */
|
||||||
type: {
|
type: {
|
||||||
type: String,
|
type: String,
|
||||||
default: "text"
|
default: "text"
|
||||||
},
|
},
|
||||||
|
/** The value of the input */
|
||||||
modelValue: {
|
modelValue: {
|
||||||
type: String,
|
type: String,
|
||||||
default: ""
|
default: ""
|
||||||
},
|
},
|
||||||
|
/** A placeholder to use */
|
||||||
placeholder: {
|
placeholder: {
|
||||||
type: String,
|
type: String,
|
||||||
default: ""
|
default: ""
|
||||||
},
|
},
|
||||||
|
/** Should the field auto complete */
|
||||||
autocomplete: {
|
autocomplete: {
|
||||||
type: String,
|
type: String,
|
||||||
default: undefined,
|
default: undefined,
|
||||||
},
|
},
|
||||||
|
/** Is the input required? */
|
||||||
required: {
|
required: {
|
||||||
type: Boolean
|
type: Boolean
|
||||||
},
|
},
|
||||||
|
/** Should the input be read only? */
|
||||||
readonly: {
|
readonly: {
|
||||||
type: String,
|
type: String,
|
||||||
default: undefined,
|
default: undefined,
|
||||||
},
|
},
|
||||||
|
/** Is the input disabled? */
|
||||||
disabled: {
|
disabled: {
|
||||||
type: String,
|
type: String,
|
||||||
default: undefined,
|
default: undefined,
|
||||||
|
@ -79,14 +87,21 @@ export default {
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
|
||||||
|
/** Show the input */
|
||||||
showInput() {
|
showInput() {
|
||||||
this.visibility = "text";
|
this.visibility = "text";
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/** Hide the input */
|
||||||
hideInput() {
|
hideInput() {
|
||||||
this.visibility = "password";
|
this.visibility = "password";
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Copy the provided text to the users clipboard
|
||||||
|
* @param {string} textToCopy
|
||||||
|
* @returns {Promise<void>}
|
||||||
|
*/
|
||||||
copyToClipboard(textToCopy) {
|
copyToClipboard(textToCopy) {
|
||||||
this.icon = "check";
|
this.icon = "check";
|
||||||
|
|
||||||
|
|
|
@ -10,6 +10,7 @@ import { sleep } from "../util.ts";
|
||||||
export default {
|
export default {
|
||||||
|
|
||||||
props: {
|
props: {
|
||||||
|
/** Value to count */
|
||||||
value: {
|
value: {
|
||||||
type: [ String, Number ],
|
type: [ String, Number ],
|
||||||
default: 0,
|
default: 0,
|
||||||
|
@ -18,6 +19,7 @@ export default {
|
||||||
type: Number,
|
type: Number,
|
||||||
default: 0.3,
|
default: 0.3,
|
||||||
},
|
},
|
||||||
|
/** Unit of the value */
|
||||||
unit: {
|
unit: {
|
||||||
type: String,
|
type: String,
|
||||||
default: "ms",
|
default: "ms",
|
||||||
|
@ -43,9 +45,7 @@ export default {
|
||||||
let frames = 12;
|
let frames = 12;
|
||||||
let step = Math.floor(diff / frames);
|
let step = Math.floor(diff / frames);
|
||||||
|
|
||||||
if (isNaN(step) || ! this.isNum || (diff > 0 && step < 1) || (diff < 0 && step > 1) || diff === 0) {
|
if (! (isNaN(step) || ! this.isNum || (diff > 0 && step < 1) || (diff < 0 && step > 1) || diff === 0)) {
|
||||||
// Lazy to NOT this condition, hahaha.
|
|
||||||
} else {
|
|
||||||
for (let i = 1; i < frames; i++) {
|
for (let i = 1; i < frames; i++) {
|
||||||
this.output += step;
|
this.output += step;
|
||||||
await sleep(15);
|
await sleep(15);
|
||||||
|
|
|
@ -13,10 +13,12 @@ dayjs.extend(relativeTime);
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
props: {
|
props: {
|
||||||
|
/** Value of date time */
|
||||||
value: {
|
value: {
|
||||||
type: String,
|
type: String,
|
||||||
default: null,
|
default: null,
|
||||||
},
|
},
|
||||||
|
/** Should only the date be displayed? */
|
||||||
dateOnly: {
|
dateOnly: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: false,
|
default: false,
|
||||||
|
|
177
src/components/DockerHostDialog.vue
Normal file
177
src/components/DockerHostDialog.vue
Normal file
|
@ -0,0 +1,177 @@
|
||||||
|
<template>
|
||||||
|
<form @submit.prevent="submit">
|
||||||
|
<div ref="modal" class="modal fade" tabindex="-1" data-bs-backdrop="static">
|
||||||
|
<div class="modal-dialog">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h5 id="exampleModalLabel" class="modal-title">
|
||||||
|
{{ $t("Setup Docker Host") }}
|
||||||
|
</h5>
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close" />
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="docker-name" class="form-label">{{ $t("Friendly Name") }}</label>
|
||||||
|
<input id="docker-name" v-model="dockerHost.name" type="text" class="form-control" required>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="docker-type" class="form-label">{{ $t("Connection Type") }}</label>
|
||||||
|
<select id="docker-type" v-model="dockerHost.dockerType" class="form-select">
|
||||||
|
<option v-for="type in connectionTypes" :key="type" :value="type">{{ $t(type) }}</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="docker-daemon" class="form-label">{{ $t("Docker Daemon") }}</label>
|
||||||
|
<input id="docker-daemon" v-model="dockerHost.dockerDaemon" type="text" class="form-control" required>
|
||||||
|
|
||||||
|
<div class="form-text">
|
||||||
|
{{ $t("Examples") }}:
|
||||||
|
<ul>
|
||||||
|
<li>/var/run/docker.sock</li>
|
||||||
|
<li>tcp://localhost:2375</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button v-if="id" type="button" class="btn btn-danger" :disabled="processing" @click="deleteConfirm">
|
||||||
|
{{ $t("Delete") }}
|
||||||
|
</button>
|
||||||
|
<button type="button" class="btn btn-warning" :disabled="processing" @click="test">
|
||||||
|
{{ $t("Test") }}
|
||||||
|
</button>
|
||||||
|
<button type="submit" class="btn btn-primary" :disabled="processing">
|
||||||
|
<div v-if="processing" class="spinner-border spinner-border-sm me-1"></div>
|
||||||
|
{{ $t("Save") }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<Confirm ref="confirmDelete" btn-style="btn-danger" :yes-text="$t('Yes')" :no-text="$t('No')" @yes="deleteDockerHost">
|
||||||
|
{{ $t("deleteDockerHostMsg") }}
|
||||||
|
</Confirm>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import { Modal } from "bootstrap";
|
||||||
|
import Confirm from "./Confirm.vue";
|
||||||
|
import { useToast } from "vue-toastification";
|
||||||
|
const toast = useToast();
|
||||||
|
|
||||||
|
export default {
|
||||||
|
components: {
|
||||||
|
Confirm,
|
||||||
|
},
|
||||||
|
props: {},
|
||||||
|
emits: [ "added" ],
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
model: null,
|
||||||
|
processing: false,
|
||||||
|
id: null,
|
||||||
|
connectionTypes: [ "socket", "tcp" ],
|
||||||
|
dockerHost: {
|
||||||
|
name: "",
|
||||||
|
dockerDaemon: "",
|
||||||
|
dockerType: "",
|
||||||
|
// Do not set default value here, please scroll to show()
|
||||||
|
}
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
mounted() {
|
||||||
|
this.modal = new Modal(this.$refs.modal);
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
|
||||||
|
deleteConfirm() {
|
||||||
|
this.modal.hide();
|
||||||
|
this.$refs.confirmDelete.show();
|
||||||
|
},
|
||||||
|
|
||||||
|
show(dockerHostID) {
|
||||||
|
if (dockerHostID) {
|
||||||
|
let found = false;
|
||||||
|
|
||||||
|
this.id = dockerHostID;
|
||||||
|
|
||||||
|
for (let n of this.$root.dockerHostList) {
|
||||||
|
if (n.id === dockerHostID) {
|
||||||
|
this.dockerHost = n;
|
||||||
|
found = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!found) {
|
||||||
|
toast.error("Docker Host not found!");
|
||||||
|
}
|
||||||
|
|
||||||
|
} else {
|
||||||
|
this.id = null;
|
||||||
|
this.dockerHost = {
|
||||||
|
name: "",
|
||||||
|
dockerType: "socket",
|
||||||
|
dockerDaemon: "/var/run/docker.sock",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
this.modal.show();
|
||||||
|
},
|
||||||
|
|
||||||
|
submit() {
|
||||||
|
this.processing = true;
|
||||||
|
this.$root.getSocket().emit("addDockerHost", this.dockerHost, this.id, (res) => {
|
||||||
|
this.$root.toastRes(res);
|
||||||
|
this.processing = false;
|
||||||
|
|
||||||
|
if (res.ok) {
|
||||||
|
this.modal.hide();
|
||||||
|
|
||||||
|
// Emit added event, doesn't emit edit.
|
||||||
|
if (! this.id) {
|
||||||
|
this.$emit("added", res.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
test() {
|
||||||
|
this.processing = true;
|
||||||
|
this.$root.getSocket().emit("testDockerHost", this.dockerHost, (res) => {
|
||||||
|
this.$root.toastRes(res);
|
||||||
|
this.processing = false;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
deleteDockerHost() {
|
||||||
|
this.processing = true;
|
||||||
|
this.$root.getSocket().emit("deleteDockerHost", this.id, (res) => {
|
||||||
|
this.$root.toastRes(res);
|
||||||
|
this.processing = false;
|
||||||
|
|
||||||
|
if (res.ok) {
|
||||||
|
this.modal.hide();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
@import "../assets/vars.scss";
|
||||||
|
|
||||||
|
.dark {
|
||||||
|
.modal-dialog .form-text, .modal-dialog p {
|
||||||
|
color: $dark-font-color;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -1,6 +1,6 @@
|
||||||
<template>
|
<template>
|
||||||
<div ref="wrap" class="wrap" :style="wrapStyle">
|
<div ref="wrap" class="wrap" :style="wrapStyle">
|
||||||
<div class="hp-bar-big d-flex" :style="barStyle">
|
<div class="hp-bar-big" :style="barStyle">
|
||||||
<div
|
<div
|
||||||
v-for="(beat, index) in shortBeatList"
|
v-for="(beat, index) in shortBeatList"
|
||||||
:key="index"
|
:key="index"
|
||||||
|
@ -8,11 +8,7 @@
|
||||||
:class="{ 'empty' : (beat === 0), 'down' : (beat.status === 0), 'pending' : (beat.status === 2), 'maintenance' : (beat.status === 3) }"
|
:class="{ 'empty' : (beat === 0), 'down' : (beat.status === 0), 'pending' : (beat.status === 2), 'maintenance' : (beat.status === 3) }"
|
||||||
:style="beatStyle"
|
:style="beatStyle"
|
||||||
:title="getBeatTitle(beat)"
|
:title="getBeatTitle(beat)"
|
||||||
@mouseenter="toggleActivateSibling"
|
/>
|
||||||
@mouseleave="toggleActivateSibling"
|
|
||||||
>
|
|
||||||
<div class="beat-inner" />
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
@ -21,14 +17,17 @@
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
props: {
|
props: {
|
||||||
|
/** Size of the heartbeat bar */
|
||||||
size: {
|
size: {
|
||||||
type: String,
|
type: String,
|
||||||
default: "big",
|
default: "big",
|
||||||
},
|
},
|
||||||
|
/** ID of the monitor */
|
||||||
monitorId: {
|
monitorId: {
|
||||||
type: Number,
|
type: Number,
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
|
/** Array of the monitors heartbeats */
|
||||||
heartbeatList: {
|
heartbeatList: {
|
||||||
type: Array,
|
type: Array,
|
||||||
default: null,
|
default: null,
|
||||||
|
@ -164,38 +163,23 @@ export default {
|
||||||
this.resize();
|
this.resize();
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
/** Resize the heartbeat bar */
|
||||||
resize() {
|
resize() {
|
||||||
if (this.$refs.wrap) {
|
if (this.$refs.wrap) {
|
||||||
this.maxBeat = Math.floor(this.$refs.wrap.clientWidth / (this.beatWidth + this.beatMargin * 2));
|
this.maxBeat = Math.floor(this.$refs.wrap.clientWidth / (this.beatWidth + this.beatMargin * 2));
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the title of the beat.
|
||||||
|
* Used as the hover tooltip on the heartbeat bar.
|
||||||
|
* @param {Object} beat Beat to get title from
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
getBeatTitle(beat) {
|
getBeatTitle(beat) {
|
||||||
return `${this.$root.datetime(beat.time)}` + ((beat.msg) ? ` - ${beat.msg}` : "");
|
return `${this.$root.datetime(beat.time)}` + ((beat.msg) ? ` - ${beat.msg}` : "");
|
||||||
},
|
},
|
||||||
|
|
||||||
// Toggling the activeSibling class on hover over the current hover item
|
|
||||||
toggleActivateSibling(e) {
|
|
||||||
// Variable definition
|
|
||||||
const element = e.target;
|
|
||||||
const previous = element.previousSibling;
|
|
||||||
const next = element.nextSibling;
|
|
||||||
|
|
||||||
// Return if the hovered element has empty class
|
|
||||||
if (element.classList.contains("empty")) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if Previous Sibling is heartbar element and doesn't have the empty class
|
|
||||||
if (previous.children && !previous.classList.contains("empty")) {
|
|
||||||
previous.classList.toggle("active-sibling");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if Next Sibling is heartbar element and doesn't have the empty class
|
|
||||||
if (next.children && !next.classList.contains("empty")) {
|
|
||||||
next.classList.toggle("active-sibling");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
@ -211,10 +195,9 @@ export default {
|
||||||
|
|
||||||
.hp-bar-big {
|
.hp-bar-big {
|
||||||
.beat {
|
.beat {
|
||||||
|
display: inline-block;
|
||||||
background-color: $primary;
|
background-color: $primary;
|
||||||
border-radius: $border-radius;
|
border-radius: $border-radius;
|
||||||
display: inline-block;
|
|
||||||
transition: all ease 0.6s;
|
|
||||||
|
|
||||||
&.empty {
|
&.empty {
|
||||||
background-color: aliceblue;
|
background-color: aliceblue;
|
||||||
|
@ -228,27 +211,15 @@ export default {
|
||||||
background-color: $warning;
|
background-color: $warning;
|
||||||
}
|
}
|
||||||
|
|
||||||
.beat-inner {
|
|
||||||
border-radius: $border-radius;
|
|
||||||
display: inline-block;
|
|
||||||
height: 100%;
|
|
||||||
width: 5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.maintenance {
|
&.maintenance {
|
||||||
background-color: $maintenance;
|
background-color: $maintenance;
|
||||||
}
|
}
|
||||||
|
|
||||||
&:not(.empty):hover {
|
&:not(.empty):hover {
|
||||||
transition: all ease 0.15s;
|
transition: all ease-in-out 0.15s;
|
||||||
opacity: 0.8;
|
opacity: 0.8;
|
||||||
transform: scale(var(--hover-scale));
|
transform: scale(var(--hover-scale));
|
||||||
}
|
}
|
||||||
|
|
||||||
&.active-sibling {
|
|
||||||
transform: scale(1.15);
|
|
||||||
transition: all ease 0.15s;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -24,25 +24,31 @@
|
||||||
<script>
|
<script>
|
||||||
export default {
|
export default {
|
||||||
props: {
|
props: {
|
||||||
|
/** The value of the input */
|
||||||
modelValue: {
|
modelValue: {
|
||||||
type: String,
|
type: String,
|
||||||
default: ""
|
default: ""
|
||||||
},
|
},
|
||||||
|
/** A placeholder to use */
|
||||||
placeholder: {
|
placeholder: {
|
||||||
type: String,
|
type: String,
|
||||||
default: ""
|
default: ""
|
||||||
},
|
},
|
||||||
|
/** Maximum length of the input */
|
||||||
maxlength: {
|
maxlength: {
|
||||||
type: Number,
|
type: Number,
|
||||||
default: 255
|
default: 255
|
||||||
},
|
},
|
||||||
|
/** Should the field auto complete */
|
||||||
autocomplete: {
|
autocomplete: {
|
||||||
type: String,
|
type: String,
|
||||||
default: undefined,
|
default: undefined,
|
||||||
},
|
},
|
||||||
|
/** Is the input required? */
|
||||||
required: {
|
required: {
|
||||||
type: Boolean
|
type: Boolean
|
||||||
},
|
},
|
||||||
|
/** Should the input be read only? */
|
||||||
readonly: {
|
readonly: {
|
||||||
type: String,
|
type: String,
|
||||||
default: undefined,
|
default: undefined,
|
||||||
|
@ -68,9 +74,11 @@ export default {
|
||||||
|
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
/** Show users input in plain text */
|
||||||
showInput() {
|
showInput() {
|
||||||
this.visibility = "text";
|
this.visibility = "text";
|
||||||
},
|
},
|
||||||
|
/** Censor users input */
|
||||||
hideInput() {
|
hideInput() {
|
||||||
this.visibility = "password";
|
this.visibility = "password";
|
||||||
},
|
},
|
||||||
|
|
|
@ -54,7 +54,17 @@ export default {
|
||||||
tokenRequired: false,
|
tokenRequired: false,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
|
mounted() {
|
||||||
|
document.title += " - Login";
|
||||||
|
},
|
||||||
|
|
||||||
|
unmounted() {
|
||||||
|
document.title = document.title.replace(" - Login", "");
|
||||||
|
},
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
|
/** Submit the user details and attempt to log in */
|
||||||
submit() {
|
submit() {
|
||||||
this.processing = true;
|
this.processing = true;
|
||||||
|
|
||||||
|
|
|
@ -94,6 +94,7 @@ export default {
|
||||||
Tag,
|
Tag,
|
||||||
},
|
},
|
||||||
props: {
|
props: {
|
||||||
|
/** Should the scrollbar be shown */
|
||||||
scrollbar: {
|
scrollbar: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
},
|
},
|
||||||
|
@ -106,10 +107,22 @@ export default {
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
|
/**
|
||||||
|
* Improve the sticky appearance of the list by increasing its
|
||||||
|
* height as user scrolls down.
|
||||||
|
* Not used on mobile.
|
||||||
|
*/
|
||||||
boxStyle() {
|
boxStyle() {
|
||||||
|
if (window.innerWidth > 550) {
|
||||||
return {
|
return {
|
||||||
height: `calc(100vh - 160px + ${this.windowTop}px)`,
|
height: `calc(100vh - 160px + ${this.windowTop}px)`,
|
||||||
};
|
};
|
||||||
|
} else {
|
||||||
|
return {
|
||||||
|
height: "calc(100vh - 160px)",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
},
|
},
|
||||||
|
|
||||||
sortedMaintenanceList() {
|
sortedMaintenanceList() {
|
||||||
|
@ -211,6 +224,7 @@ export default {
|
||||||
window.removeEventListener("scroll", this.onScroll);
|
window.removeEventListener("scroll", this.onScroll);
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
/** Handle user scroll */
|
||||||
onScroll() {
|
onScroll() {
|
||||||
if (window.top.scrollY <= 133) {
|
if (window.top.scrollY <= 133) {
|
||||||
this.windowTop = window.top.scrollY;
|
this.windowTop = window.top.scrollY;
|
||||||
|
@ -218,12 +232,18 @@ export default {
|
||||||
this.windowTop = 133;
|
this.windowTop = 133;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
/**
|
||||||
|
* Get URL of monitor
|
||||||
|
* @param {number} id ID of monitor
|
||||||
|
* @returns {string} Relative URL of monitor
|
||||||
|
*/
|
||||||
monitorURL(id) {
|
monitorURL(id) {
|
||||||
return getMonitorRelativeURL(id);
|
return getMonitorRelativeURL(id);
|
||||||
},
|
},
|
||||||
maintenanceURL(id) {
|
maintenanceURL(id) {
|
||||||
return getMaintenanceRelativeURL(id);
|
return getMaintenanceRelativeURL(id);
|
||||||
},
|
},
|
||||||
|
/** Clear the search bar */
|
||||||
clearSearchText() {
|
clearSearchText() {
|
||||||
this.searchText = "";
|
this.searchText = "";
|
||||||
}
|
}
|
||||||
|
|
|
@ -125,11 +125,16 @@ export default {
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
|
||||||
|
/** Show dialog to confirm deletion */
|
||||||
deleteConfirm() {
|
deleteConfirm() {
|
||||||
this.modal.hide();
|
this.modal.hide();
|
||||||
this.$refs.confirmDelete.show();
|
this.$refs.confirmDelete.show();
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show settings for specified notification
|
||||||
|
* @param {number} notificationID ID of notification to show
|
||||||
|
*/
|
||||||
show(notificationID) {
|
show(notificationID) {
|
||||||
if (notificationID) {
|
if (notificationID) {
|
||||||
this.id = notificationID;
|
this.id = notificationID;
|
||||||
|
@ -152,6 +157,7 @@ export default {
|
||||||
this.modal.show();
|
this.modal.show();
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/** Submit the form to the server */
|
||||||
submit() {
|
submit() {
|
||||||
this.processing = true;
|
this.processing = true;
|
||||||
this.$root.getSocket().emit("addNotification", this.notification, this.id, (res) => {
|
this.$root.getSocket().emit("addNotification", this.notification, this.id, (res) => {
|
||||||
|
@ -170,6 +176,7 @@ export default {
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/** Test the notification endpoint */
|
||||||
test() {
|
test() {
|
||||||
this.processing = true;
|
this.processing = true;
|
||||||
this.$root.getSocket().emit("testNotification", this.notification, (res) => {
|
this.$root.getSocket().emit("testNotification", this.notification, (res) => {
|
||||||
|
@ -178,6 +185,7 @@ export default {
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/** Delete the notification endpoint */
|
||||||
deleteNotification() {
|
deleteNotification() {
|
||||||
this.processing = true;
|
this.processing = true;
|
||||||
this.$root.getSocket().emit("deleteNotification", this.id, (res) => {
|
this.$root.getSocket().emit("deleteNotification", this.id, (res) => {
|
||||||
|
@ -190,6 +198,7 @@ export default {
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
/**
|
/**
|
||||||
|
* Get a unique default name for the notification
|
||||||
* @param {keyof NotificationFormList} notificationKey
|
* @param {keyof NotificationFormList} notificationKey
|
||||||
* @return {string}
|
* @return {string}
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -35,6 +35,7 @@ Chart.register(LineController, BarController, LineElement, PointElement, TimeSca
|
||||||
export default {
|
export default {
|
||||||
components: { LineChart },
|
components: { LineChart },
|
||||||
props: {
|
props: {
|
||||||
|
/** ID of monitor */
|
||||||
monitorId: {
|
monitorId: {
|
||||||
type: Number,
|
type: Number,
|
||||||
required: true,
|
required: true,
|
||||||
|
|
|
@ -130,11 +130,16 @@ export default {
|
||||||
},
|
},
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
|
/** Show dialog to confirm deletion */
|
||||||
deleteConfirm() {
|
deleteConfirm() {
|
||||||
this.modal.hide();
|
this.modal.hide();
|
||||||
this.$refs.confirmDelete.show();
|
this.$refs.confirmDelete.show();
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show settings for specified proxy
|
||||||
|
* @param {number} proxyID ID of proxy to show
|
||||||
|
*/
|
||||||
show(proxyID) {
|
show(proxyID) {
|
||||||
if (proxyID) {
|
if (proxyID) {
|
||||||
this.id = proxyID;
|
this.id = proxyID;
|
||||||
|
@ -163,6 +168,7 @@ export default {
|
||||||
this.modal.show();
|
this.modal.show();
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/** Submit form data for saving */
|
||||||
submit() {
|
submit() {
|
||||||
this.processing = true;
|
this.processing = true;
|
||||||
this.$root.getSocket().emit("addProxy", this.proxy, this.id, (res) => {
|
this.$root.getSocket().emit("addProxy", this.proxy, this.id, (res) => {
|
||||||
|
@ -180,6 +186,7 @@ export default {
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/** Delete this proxy */
|
||||||
deleteProxy() {
|
deleteProxy() {
|
||||||
this.processing = true;
|
this.processing = true;
|
||||||
this.$root.getSocket().emit("deleteProxy", this.id, (res) => {
|
this.$root.getSocket().emit("deleteProxy", this.id, (res) => {
|
||||||
|
|
|
@ -33,19 +33,40 @@
|
||||||
<template #item="monitor">
|
<template #item="monitor">
|
||||||
<div class="item">
|
<div class="item">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-9 col-md-8 small-padding d-flex align-items-center flex-wrap">
|
<div class="col-9 col-md-8 small-padding">
|
||||||
<div class="info d-flex align-items-center gap-3 w-100">
|
<div class="info">
|
||||||
<font-awesome-icon v-if="editMode" icon="arrows-alt-v" class="action drag me-3" />
|
<font-awesome-icon v-if="editMode" icon="arrows-alt-v" class="action drag me-3" />
|
||||||
<font-awesome-icon v-if="editMode" icon="times" class="action remove me-3" @click="removeMonitor(group.index, monitor.index)" />
|
<font-awesome-icon v-if="editMode" icon="times" class="action remove me-3" @click="removeMonitor(group.index, monitor.index)" />
|
||||||
|
|
||||||
<Uptime :monitor="monitor.element" type="24" :pill="true" />
|
<Uptime :monitor="monitor.element" type="24" :pill="true" />
|
||||||
|
<a
|
||||||
|
v-if="showLink(monitor)"
|
||||||
|
:href="monitor.element.url"
|
||||||
|
class="item-name"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
>
|
||||||
{{ monitor.element.name }}
|
{{ monitor.element.name }}
|
||||||
|
</a>
|
||||||
|
<p v-else class="item-name"> {{ monitor.element.name }} </p>
|
||||||
|
<span
|
||||||
|
v-if="showLink(monitor, true)"
|
||||||
|
title="Toggle Clickable Link"
|
||||||
|
>
|
||||||
|
<font-awesome-icon
|
||||||
|
v-if="editMode"
|
||||||
|
:class="{'link-active': monitor.element.sendUrl, 'btn-link': true}"
|
||||||
|
icon="link" class="action me-3"
|
||||||
|
|
||||||
|
@click="toggleLink(group.index, monitor.index)"
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="showTags && monitor.element.tags.length > 0" class="tags">
|
<div v-if="showTags" class="tags">
|
||||||
<Tag v-for="tag in monitor.element.tags" :key="tag" :item="tag" :size="'sm'" />
|
<Tag v-for="tag in monitor.element.tags" :key="tag" :item="tag" :size="'sm'" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div :key="$root.userHeartbeatBar" class="col-3 col-md-4 d-flex align-items-center">
|
<div :key="$root.userHeartbeatBar" class="col-3 col-md-4">
|
||||||
<HeartbeatBar size="small" :monitor-id="monitor.element.id" />
|
<HeartbeatBar size="small" :monitor-id="monitor.element.id" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -72,10 +93,12 @@ export default {
|
||||||
Tag,
|
Tag,
|
||||||
},
|
},
|
||||||
props: {
|
props: {
|
||||||
|
/** Are we in edit mode? */
|
||||||
editMode: {
|
editMode: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
|
/** Should tags be shown? */
|
||||||
showTags: {
|
showTags: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
}
|
}
|
||||||
|
@ -94,13 +117,50 @@ export default {
|
||||||
|
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
/**
|
||||||
|
* Remove the specified group
|
||||||
|
* @param {number} index Index of group to remove
|
||||||
|
*/
|
||||||
removeGroup(index) {
|
removeGroup(index) {
|
||||||
this.$root.publicGroupList.splice(index, 1);
|
this.$root.publicGroupList.splice(index, 1);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove a monitor from a group
|
||||||
|
* @param {number} groupIndex Index of group to remove monitor
|
||||||
|
* from
|
||||||
|
* @param {number} index Index of monitor to remove
|
||||||
|
*/
|
||||||
removeMonitor(groupIndex, index) {
|
removeMonitor(groupIndex, index) {
|
||||||
this.$root.publicGroupList[groupIndex].monitorList.splice(index, 1);
|
this.$root.publicGroupList[groupIndex].monitorList.splice(index, 1);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Toggle the value of sendUrl
|
||||||
|
* @param {number} groupIndex Index of group monitor is member of
|
||||||
|
* @param {number} index Index of monitor within group
|
||||||
|
*/
|
||||||
|
toggleLink(groupIndex, index) {
|
||||||
|
this.$root.publicGroupList[groupIndex].monitorList[index].sendUrl = !this.$root.publicGroupList[groupIndex].monitorList[index].sendUrl;
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Should a link to the monitor be shown?
|
||||||
|
* Attempts to guess if a link should be shown based upon if
|
||||||
|
* sendUrl is set and if the URL is default or not.
|
||||||
|
* @param {Object} monitor Monitor to check
|
||||||
|
* @param {boolean} [ignoreSendUrl=false] Should the presence of the sendUrl
|
||||||
|
* property be ignored. This will only work in edit mode.
|
||||||
|
* @returns {boolean}
|
||||||
|
*/
|
||||||
|
showLink(monitor, ignoreSendUrl = false) {
|
||||||
|
// We must check if there are any elements in monitorList to
|
||||||
|
// prevent undefined errors if it hasn't been loaded yet
|
||||||
|
if (this.$parent.editMode && ignoreSendUrl && Object.keys(this.$root.monitorList).length) {
|
||||||
|
return this.$root.monitorList[monitor.element.id].type === "http" || this.$root.monitorList[monitor.element.id].type === "keyword";
|
||||||
|
}
|
||||||
|
return monitor.element.sendUrl && monitor.element.url && monitor.element.url !== "https://" && !this.editMode;
|
||||||
|
},
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
@ -119,6 +179,22 @@ export default {
|
||||||
min-height: 46px;
|
min-height: 46px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.item-name {
|
||||||
|
padding-left: 5px;
|
||||||
|
padding-right: 5px;
|
||||||
|
margin: 0;
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-link {
|
||||||
|
color: #bbbbbb;
|
||||||
|
margin-left: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.link-active {
|
||||||
|
color: $primary;
|
||||||
|
}
|
||||||
|
|
||||||
.flip-list-move {
|
.flip-list-move {
|
||||||
transition: transform 0.5s;
|
transition: transform 0.5s;
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,6 +5,7 @@
|
||||||
<script>
|
<script>
|
||||||
export default {
|
export default {
|
||||||
props: {
|
props: {
|
||||||
|
/** Current status of monitor */
|
||||||
status: {
|
status: {
|
||||||
type: Number,
|
type: Number,
|
||||||
default: 0,
|
default: 0,
|
||||||
|
|
|
@ -20,14 +20,20 @@
|
||||||
<script>
|
<script>
|
||||||
export default {
|
export default {
|
||||||
props: {
|
props: {
|
||||||
|
/** Object representing tag */
|
||||||
item: {
|
item: {
|
||||||
type: Object,
|
type: Object,
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
|
/** Function to remove tag */
|
||||||
remove: {
|
remove: {
|
||||||
type: Function,
|
type: Function,
|
||||||
default: null,
|
default: null,
|
||||||
},
|
},
|
||||||
|
/**
|
||||||
|
* Size of tag
|
||||||
|
* @values normal, small
|
||||||
|
*/
|
||||||
size: {
|
size: {
|
||||||
type: String,
|
type: String,
|
||||||
default: "normal",
|
default: "normal",
|
||||||
|
|
|
@ -139,6 +139,7 @@ export default {
|
||||||
VueMultiselect,
|
VueMultiselect,
|
||||||
},
|
},
|
||||||
props: {
|
props: {
|
||||||
|
/** Array of tags to be pre-selected */
|
||||||
preSelectedTags: {
|
preSelectedTags: {
|
||||||
type: Array,
|
type: Array,
|
||||||
default: () => [],
|
default: () => [],
|
||||||
|
@ -241,9 +242,11 @@ export default {
|
||||||
this.getExistingTags();
|
this.getExistingTags();
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
/** Show the add tag dialog */
|
||||||
showAddDialog() {
|
showAddDialog() {
|
||||||
this.modal.show();
|
this.modal.show();
|
||||||
},
|
},
|
||||||
|
/** Get all existing tags */
|
||||||
getExistingTags() {
|
getExistingTags() {
|
||||||
this.$root.getSocket().emit("getTags", (res) => {
|
this.$root.getSocket().emit("getTags", (res) => {
|
||||||
if (res.ok) {
|
if (res.ok) {
|
||||||
|
@ -253,6 +256,10 @@ export default {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
/**
|
||||||
|
* Delete the specified tag
|
||||||
|
* @param {Object} tag Object representing tag to delete
|
||||||
|
*/
|
||||||
deleteTag(item) {
|
deleteTag(item) {
|
||||||
if (item.new) {
|
if (item.new) {
|
||||||
// Undo Adding a new Tag
|
// Undo Adding a new Tag
|
||||||
|
@ -262,6 +269,13 @@ export default {
|
||||||
this.deleteTags.push(item);
|
this.deleteTags.push(item);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
/**
|
||||||
|
* Get colour of text inside the tag
|
||||||
|
* @param {Object} option The tag that needs to be displayed.
|
||||||
|
* Defaults to "white" unless the tag has no color, which will
|
||||||
|
* then return the body color (based on application theme)
|
||||||
|
* @returns string
|
||||||
|
*/
|
||||||
textColor(option) {
|
textColor(option) {
|
||||||
if (option.color) {
|
if (option.color) {
|
||||||
return "white";
|
return "white";
|
||||||
|
@ -269,6 +283,7 @@ export default {
|
||||||
return this.$root.theme === "light" ? "var(--bs-body-color)" : "inherit";
|
return this.$root.theme === "light" ? "var(--bs-body-color)" : "inherit";
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
/** Add a draft tag */
|
||||||
addDraftTag() {
|
addDraftTag() {
|
||||||
console.log("Adding Draft Tag: ", this.newDraftTag);
|
console.log("Adding Draft Tag: ", this.newDraftTag);
|
||||||
if (this.newDraftTag.select != null) {
|
if (this.newDraftTag.select != null) {
|
||||||
|
@ -296,6 +311,7 @@ export default {
|
||||||
}
|
}
|
||||||
this.clearDraftTag();
|
this.clearDraftTag();
|
||||||
},
|
},
|
||||||
|
/** Remove a draft tag */
|
||||||
clearDraftTag() {
|
clearDraftTag() {
|
||||||
this.newDraftTag = {
|
this.newDraftTag = {
|
||||||
name: null,
|
name: null,
|
||||||
|
@ -307,26 +323,51 @@ export default {
|
||||||
};
|
};
|
||||||
this.modal.hide();
|
this.modal.hide();
|
||||||
},
|
},
|
||||||
|
/**
|
||||||
|
* Add a tag asynchronously
|
||||||
|
* @param {Object} newTag Object representing new tag to add
|
||||||
|
* @returns {Promise<void>}
|
||||||
|
*/
|
||||||
addTagAsync(newTag) {
|
addTagAsync(newTag) {
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
this.$root.getSocket().emit("addTag", newTag, resolve);
|
this.$root.getSocket().emit("addTag", newTag, resolve);
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
/**
|
||||||
|
* Add a tag to a monitor asynchronously
|
||||||
|
* @param {number} tagId ID of tag to add
|
||||||
|
* @param {number} monitorId ID of monitor to add tag to
|
||||||
|
* @param {string} value Value of tag
|
||||||
|
* @returns {Promise<void>}
|
||||||
|
*/
|
||||||
addMonitorTagAsync(tagId, monitorId, value) {
|
addMonitorTagAsync(tagId, monitorId, value) {
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
this.$root.getSocket().emit("addMonitorTag", tagId, monitorId, value, resolve);
|
this.$root.getSocket().emit("addMonitorTag", tagId, monitorId, value, resolve);
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
/**
|
||||||
|
* Delete a tag from a monitor asynchronously
|
||||||
|
* @param {number} tagId ID of tag to remove
|
||||||
|
* @param {number} monitorId ID of monitor to remove tag from
|
||||||
|
* @param {string} value Value of tag
|
||||||
|
* @returns {Promise<void>}
|
||||||
|
*/
|
||||||
deleteMonitorTagAsync(tagId, monitorId, value) {
|
deleteMonitorTagAsync(tagId, monitorId, value) {
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
this.$root.getSocket().emit("deleteMonitorTag", tagId, monitorId, value, resolve);
|
this.$root.getSocket().emit("deleteMonitorTag", tagId, monitorId, value, resolve);
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
/** Handle pressing Enter key when inside the modal */
|
||||||
onEnter() {
|
onEnter() {
|
||||||
if (!this.validateDraftTag.invalid) {
|
if (!this.validateDraftTag.invalid) {
|
||||||
this.addDraftTag();
|
this.addDraftTag();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
/**
|
||||||
|
* Submit the form data
|
||||||
|
* @param {number} monitorId ID of monitor this change affects
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
async submit(monitorId) {
|
async submit(monitorId) {
|
||||||
console.log(`Submitting tag changes for monitor ${monitorId}...`);
|
console.log(`Submitting tag changes for monitor ${monitorId}...`);
|
||||||
this.processing = true;
|
this.processing = true;
|
||||||
|
|
|
@ -29,10 +29,12 @@
|
||||||
<script>
|
<script>
|
||||||
export default {
|
export default {
|
||||||
props: {
|
props: {
|
||||||
|
/** Heading of the section */
|
||||||
heading: {
|
heading: {
|
||||||
type: String,
|
type: String,
|
||||||
default: "",
|
default: "",
|
||||||
},
|
},
|
||||||
|
/** Should the section be open by default? */
|
||||||
defaultOpen: {
|
defaultOpen: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: false,
|
default: false,
|
||||||
|
|
|
@ -100,18 +100,22 @@ export default {
|
||||||
this.getStatus();
|
this.getStatus();
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
/** Show the dialog */
|
||||||
show() {
|
show() {
|
||||||
this.modal.show();
|
this.modal.show();
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/** Show dialog to confirm enabling 2FA */
|
||||||
confirmEnableTwoFA() {
|
confirmEnableTwoFA() {
|
||||||
this.$refs.confirmEnableTwoFA.show();
|
this.$refs.confirmEnableTwoFA.show();
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/** Show dialog to confirm disabling 2FA */
|
||||||
confirmDisableTwoFA() {
|
confirmDisableTwoFA() {
|
||||||
this.$refs.confirmDisableTwoFA.show();
|
this.$refs.confirmDisableTwoFA.show();
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/** Prepare 2FA configuration */
|
||||||
prepare2FA() {
|
prepare2FA() {
|
||||||
this.processing = true;
|
this.processing = true;
|
||||||
|
|
||||||
|
@ -126,6 +130,7 @@ export default {
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/** Save the current 2FA configuration */
|
||||||
save2FA() {
|
save2FA() {
|
||||||
this.processing = true;
|
this.processing = true;
|
||||||
|
|
||||||
|
@ -143,6 +148,7 @@ export default {
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/** Disable 2FA for this user */
|
||||||
disable2FA() {
|
disable2FA() {
|
||||||
this.processing = true;
|
this.processing = true;
|
||||||
|
|
||||||
|
@ -160,6 +166,7 @@ export default {
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/** Verify the token generated by the user */
|
||||||
verifyToken() {
|
verifyToken() {
|
||||||
this.$root.getSocket().emit("verifyToken", this.token, this.currentPassword, (res) => {
|
this.$root.getSocket().emit("verifyToken", this.token, this.currentPassword, (res) => {
|
||||||
if (res.ok) {
|
if (res.ok) {
|
||||||
|
@ -170,6 +177,7 @@ export default {
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/** Get current status of 2FA */
|
||||||
getStatus() {
|
getStatus() {
|
||||||
this.$root.getSocket().emit("twoFAStatus", (res) => {
|
this.$root.getSocket().emit("twoFAStatus", (res) => {
|
||||||
if (res.ok) {
|
if (res.ok) {
|
||||||
|
|
|
@ -5,14 +5,17 @@
|
||||||
<script>
|
<script>
|
||||||
export default {
|
export default {
|
||||||
props: {
|
props: {
|
||||||
|
/** Monitor this represents */
|
||||||
monitor: {
|
monitor: {
|
||||||
type: Object,
|
type: Object,
|
||||||
default: null,
|
default: null,
|
||||||
},
|
},
|
||||||
|
/** Type of monitor */
|
||||||
type: {
|
type: {
|
||||||
type: String,
|
type: String,
|
||||||
default: null,
|
default: null,
|
||||||
},
|
},
|
||||||
|
/** Is this a pill? */
|
||||||
pill: {
|
pill: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: false,
|
default: false,
|
||||||
|
|
13
src/components/notifications/AlertNow.vue
Normal file
13
src/components/notifications/AlertNow.vue
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
<template>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="alertnow-webhook-url" class="form-label">{{ $t("Webhook URL") }}<span style="color: red;"><sup>*</sup></span></label>
|
||||||
|
<input id="alertnow-webhook-url" v-model="$parent.notification.alertNowWebhookURL" type="text" class="form-control" required>
|
||||||
|
|
||||||
|
<div class="form-text">
|
||||||
|
<span style="color: red;"><sup>*</sup></span>{{ $t("Required") }}
|
||||||
|
<i18n-t tag="p" keypath="aboutWebhooks" style="margin-top: 8px;">
|
||||||
|
<a href="https://service.opsnow.com/docs/alertnow/en/user-guide-alertnow-en.html#standard" target="_blank">{{ $t("here") }}</a>
|
||||||
|
</i18n-t>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
|
@ -2,9 +2,6 @@
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label for="Bark Endpoint" class="form-label">{{ $t("Bark Endpoint") }}<span style="color: red;"><sup>*</sup></span></label>
|
<label for="Bark Endpoint" class="form-label">{{ $t("Bark Endpoint") }}<span style="color: red;"><sup>*</sup></span></label>
|
||||||
<input id="Bark Endpoint" v-model="$parent.notification.barkEndpoint" type="text" class="form-control" required>
|
<input id="Bark Endpoint" v-model="$parent.notification.barkEndpoint" type="text" class="form-control" required>
|
||||||
<div class="form-text">
|
|
||||||
<p><span style="color: red;"><sup>*</sup></span>{{ $t("Required") }}</p>
|
|
||||||
</div>
|
|
||||||
<i18n-t tag="div" keypath="wayToGetTeamsURL" class="form-text">
|
<i18n-t tag="div" keypath="wayToGetTeamsURL" class="form-text">
|
||||||
<a
|
<a
|
||||||
href="https://github.com/Finb/Bark"
|
href="https://github.com/Finb/Bark"
|
||||||
|
@ -12,4 +9,45 @@
|
||||||
>{{ $t("here") }}</a>
|
>{{ $t("here") }}</a>
|
||||||
</i18n-t>
|
</i18n-t>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="Bark Group" class="form-label">{{ $t("Bark Group") }}</label>
|
||||||
|
<input id="Bark Group" v-model="$parent.notification.barkGroup" type="text" class="form-control" required>
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="Bark Sound" class="form-label">{{ $t("Bark Sound") }}</label>
|
||||||
|
<select id="Bark Sound" v-model="$parent.notification.barkSound" class="form-select" required>
|
||||||
|
<option value="alarm">alarm</option>
|
||||||
|
<option value="anticipate">anticipate</option>
|
||||||
|
<option value="bell">bell</option>
|
||||||
|
<option value="birdsong">birdsong</option>
|
||||||
|
<option value="bloom">bloom</option>
|
||||||
|
<option value="calypso">calypso</option>
|
||||||
|
<option value="chime">chime</option>
|
||||||
|
<option value="choo">choo</option>
|
||||||
|
<option value="descent">descent</option>
|
||||||
|
<option value="electronic">electronic</option>
|
||||||
|
<option value="fanfare">fanfare</option>
|
||||||
|
<option value="glass">glass</option>
|
||||||
|
<option value="gotosleep">gotosleep</option>
|
||||||
|
<option value="healthnotification">healthnotification</option>
|
||||||
|
<option value="horn">horn</option>
|
||||||
|
<option value="ladder">ladder</option>
|
||||||
|
<option value="mailsent">mailsent</option>
|
||||||
|
<option value="minuet">minuet</option>
|
||||||
|
<option value="multiwayinvitation">multiwayinvitation</option>
|
||||||
|
<option value="newmail">newmail</option>
|
||||||
|
<option value="newsflash">newsflash</option>
|
||||||
|
<option value="noir">noir</option>
|
||||||
|
<option value="paymentsuccess">paymentsuccess</option>
|
||||||
|
<option value="shake">shake</option>
|
||||||
|
<option value="sherwoodforest">sherwoodforest</option>
|
||||||
|
<option value="silence">silence</option>
|
||||||
|
<option value="spell">spell</option>
|
||||||
|
<option value="suspense">suspense</option>
|
||||||
|
<option value="telegraph">telegraph</option>
|
||||||
|
<option value="tiptoes">tiptoes</option>
|
||||||
|
<option value="typewriters">typewriters</option>
|
||||||
|
<option value="update">update</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
30
src/components/notifications/GoAlert.vue
Normal file
30
src/components/notifications/GoAlert.vue
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
<template>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="goalert-base-url" class="form-label">{{ $t("Base URL") }}</label>
|
||||||
|
<div class="input-group mb-3">
|
||||||
|
<input id="goalert-base-url" v-model="$parent.notification.goAlertBaseURL" type="text" class="form-control" required>
|
||||||
|
</div>
|
||||||
|
<i18n-t tag="div" keypath="goAlertInfo" class="form-text">
|
||||||
|
<a href="https://goalert.me" target="_blank">https://goalert.me</a>
|
||||||
|
</i18n-t>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="goalert-token" class="form-label">{{ $t("Token") }}</label>
|
||||||
|
<HiddenInput id="goalert-token" v-model="$parent.notification.goAlertToken" autocomplete="one-time-code" :required="true"></HiddenInput>
|
||||||
|
|
||||||
|
<div class="form-text">
|
||||||
|
{{ $t("goAlertIntegrationKeyInfo") }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import HiddenInput from "../HiddenInput.vue";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
components: {
|
||||||
|
HiddenInput,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
40
src/components/notifications/HomeAssistant.vue
Normal file
40
src/components/notifications/HomeAssistant.vue
Normal file
|
@ -0,0 +1,40 @@
|
||||||
|
<template>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="homeAssistantUrl" class="form-label">{{ $t("Home Assistant URL") }}<span style="color: red;"><sup>*</sup></span></label>
|
||||||
|
<input id="homeAssistantUrl" v-model="$parent.notification.homeAssistantUrl" type="url" class="form-control" required>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="longLivedAccessToken" class="form-label">{{ $t("Long-Lived Access Token") }}<span style="color: red;"><sup>*</sup></span></label>
|
||||||
|
<input id="longLivedAccessToken" v-model="$parent.notification.longLivedAccessToken" type="text" class="form-control" required>
|
||||||
|
|
||||||
|
<div class="form-text">
|
||||||
|
<p>{{ $t("Long-Lived Access Token can be created by clicking on your profile name (bottom left) and scrolling to the bottom then click Create Token. ") }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="notificationService" class="form-label">{{ $t("Notification Service") }}</label>
|
||||||
|
<input id="notificationService" v-model="$parent.notification.notificationService" type="text" :placeholder="$t('default: notify all devices')" class="form-control">
|
||||||
|
|
||||||
|
<div class="form-text">
|
||||||
|
<p>{{ $t("A list of Notification Services can be found in Home Assistant under \"Developer Tools > Services\" search for \"notification\" to find your device/phone name.") }}</p>
|
||||||
|
<p>{{ $t("Automations can optionally be triggered in Home Assistant:") }}</p>
|
||||||
|
<p>
|
||||||
|
{{ $t("Trigger type:") }} <code>Event</code><br />
|
||||||
|
{{ $t("Event type:") }} <code>call_service</code><br />
|
||||||
|
{{ $t("Event data:") }}
|
||||||
|
</p>
|
||||||
|
<pre>domain: notify
|
||||||
|
service: mobile_app_my_phone # change to your device name
|
||||||
|
service_data:
|
||||||
|
title: Uptime Kuma
|
||||||
|
data:
|
||||||
|
status: 0 # 0=down 1=up
|
||||||
|
# name: Optional Uptime Kuma Monitor Name to filter by</pre>
|
||||||
|
<p>
|
||||||
|
{{ $t("Then choose an action, for example switch the scene to where an RGB light is red.") }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
9
src/components/notifications/LineNotify.vue
Normal file
9
src/components/notifications/LineNotify.vue
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
<template>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="line-notify-access-token" class="form-label">{{ $t("Access Token") }}</label>
|
||||||
|
<input id="line-notify-access-token" v-model="$parent.notification.lineNotifyAccessToken" type="text" class="form-control" :required="true">
|
||||||
|
</div>
|
||||||
|
<i18n-t tag="div" keypath="wayToGetLineNotifyToken" class="form-text" style="margin-top: 8px;">
|
||||||
|
<a href="https://notify-bot.line.me/" target="_blank">https://notify-bot.line.me/</a>
|
||||||
|
</i18n-t>
|
||||||
|
</template>
|
30
src/components/notifications/Ntfy.vue
Normal file
30
src/components/notifications/Ntfy.vue
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
<template>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="ntfy-ntfytopic" class="form-label">{{ $t("ntfy Topic") }}</label>
|
||||||
|
<div class="input-group mb-3">
|
||||||
|
<input id="ntfy-ntfytopic" v-model="$parent.notification.ntfytopic" type="text" class="form-control" required>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="ntfy-server-url" class="form-label">{{ $t("Server URL") }}</label>
|
||||||
|
<div class="input-group mb-3">
|
||||||
|
<input id="ntfy-server-url" v-model="$parent.notification.ntfyserverurl" type="text" class="form-control" required>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="ntfy-priority" class="form-label">{{ $t("Priority") }}</label>
|
||||||
|
<input id="ntfy-priority" v-model="$parent.notification.ntfyPriority" type="number" class="form-control" required min="1" max="5" step="1">
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
mounted() {
|
||||||
|
if (typeof this.$parent.notification.ntfyPriority === "undefined") {
|
||||||
|
this.$parent.notification.ntfyserverurl = "https://ntfy.sh";
|
||||||
|
this.$parent.notification.ntfyPriority = 5;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
45
src/components/notifications/PagerDuty.vue
Normal file
45
src/components/notifications/PagerDuty.vue
Normal file
|
@ -0,0 +1,45 @@
|
||||||
|
<template>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="pagerduty-integration-key" class="form-label">{{ $t("Integration Key") }}</label>
|
||||||
|
<HiddenInput id="pagerduty-integration-key" v-model="$parent.notification.pagerdutyIntegrationKey" :required="true" autocomplete="false"></HiddenInput>
|
||||||
|
<i18n-t tag="div" keypath="wayToGetPagerDutyKey" class="form-text">
|
||||||
|
<a href="https://support.pagerduty.com/docs/services-and-integrations" target="_blank">{{ $t("here") }}</a>
|
||||||
|
</i18n-t>
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="pagerduty-integration-url" class="form-label">{{ $t("Integration URL") }}</label>
|
||||||
|
<input id="pagerduty-integration-url" v-model="$parent.notification.pagerdutyIntegrationUrl" type="text" class="form-control" autocomplete="false">
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="pagerduty-priority" class="form-label">{{ $t("Priority") }}</label>
|
||||||
|
<select id="pagerduty-priority" v-model="$parent.notification.pagerdutyPriority" class="form-select">
|
||||||
|
<option value="info">{{ $t("info") }}</option>
|
||||||
|
<option value="warning" selected="selected">{{ $t("warning") }}</option>
|
||||||
|
<option value="error">{{ $t("error") }}</option>
|
||||||
|
<option value="critical">{{ $t("critical") }}</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="pagerduty-resolve" class="form-label">{{ $t("Auto resolve or acknowledged") }}</label>
|
||||||
|
<select id="pagerduty-resolve" v-model="$parent.notification.pagerdutyAutoResolve" class="form-select">
|
||||||
|
<option value="0" selected="selected">{{ $t("do nothing") }}</option>
|
||||||
|
<option value="acknowledge">{{ $t("auto acknowledged") }}</option>
|
||||||
|
<option value="resolve">{{ $t("auto resolve") }}</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import HiddenInput from "../HiddenInput.vue";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
components: {
|
||||||
|
HiddenInput,
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
if (typeof this.$parent.notification.pagerdutyIntegrationUrl === "undefined") {
|
||||||
|
this.$parent.notification.pagerdutyIntegrationUrl = "https://events.pagerduty.com/v2/enqueue";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
|
@ -1,8 +1,8 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label for="promosms-login" class="form-label">{{ $("promosmsLogin") }}</label>
|
<label for="promosms-login" class="form-label">{{ $t("promosmsLogin") }}</label>
|
||||||
<input id="promosms-login" v-model="$parent.notification.promosmsLogin" type="text" class="form-control" required>
|
<input id="promosms-login" v-model="$parent.notification.promosmsLogin" type="text" class="form-control" required>
|
||||||
<label for="promosms-key" class="form-label">{{ $("promosmsPassword") }}</label>
|
<label for="promosms-key" class="form-label">{{ $t("promosmsPassword") }}</label>
|
||||||
<HiddenInput id="promosms-key" v-model="$parent.notification.promosmsPassword" :required="true" autocomplete="one-time-code"></HiddenInput>
|
<HiddenInput id="promosms-key" v-model="$parent.notification.promosmsPassword" :required="true" autocomplete="one-time-code"></HiddenInput>
|
||||||
</div>
|
</div>
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
|
|
31
src/components/notifications/SMSManager.vue
Normal file
31
src/components/notifications/SMSManager.vue
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
<template>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="smsmanager-key" class="form-label">API Key</label>
|
||||||
|
<div class="form-text">
|
||||||
|
{{ $t("SMSManager API Docs ") }}
|
||||||
|
<a href="https://smsmanager.cz/api/http#send" target="_blank">{{ $t("here") }}</a>
|
||||||
|
</div>
|
||||||
|
<input id="smsmanager-key" v-model="$parent.notification.smsmanagerApiKey" type="text" class="form-control">
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="smsmanager-numbers" class="form-label"> {{ $t("Recipients") }}</label>
|
||||||
|
<div class="form-text">
|
||||||
|
{{ $t("You can divide numbers with") }} <b>,</b> {{ $t("or") }} <b>;</b>
|
||||||
|
</div>
|
||||||
|
<input id="smsmanager-numbers" v-model="$parent.notification.numbers" type="text" class="form-control">
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="smsmanager-messageType" class="form-label">{{ $t("Gateway Type") }}</label>
|
||||||
|
<select id="smsmanager-messageType" v-model="$parent.notification.messageType" class="form-select">
|
||||||
|
<option value="economy">Economy</option>
|
||||||
|
<option value="lowcost">Lowcost</option>
|
||||||
|
<option value="high" selected>High</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<div class="form-text">
|
||||||
|
{{ $t("checkPrice", [$t("SMSManager")]) }}
|
||||||
|
<a href="https://smsmanager.cz/rozesilani-sms/ceny/ceska-republika/" target="_blank">{{ $t("here") }}</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
|
@ -1,36 +1,43 @@
|
||||||
import STMP from "./SMTP.vue";
|
import Alerta from "./Alerta.vue";
|
||||||
import Telegram from "./Telegram.vue";
|
import AlertNow from "./AlertNow.vue";
|
||||||
import Discord from "./Discord.vue";
|
|
||||||
import Webhook from "./Webhook.vue";
|
|
||||||
import Signal from "./Signal.vue";
|
|
||||||
import Gotify from "./Gotify.vue";
|
|
||||||
import Slack from "./Slack.vue";
|
|
||||||
import RocketChat from "./RocketChat.vue";
|
|
||||||
import Teams from "./Teams.vue";
|
|
||||||
import Pushover from "./Pushover.vue";
|
|
||||||
import Pushy from "./Pushy.vue";
|
|
||||||
import TechulusPush from "./TechulusPush.vue";
|
|
||||||
import Octopush from "./Octopush.vue";
|
|
||||||
import PromoSMS from "./PromoSMS.vue";
|
|
||||||
import ClickSendSMS from "./ClickSendSMS.vue";
|
|
||||||
import LunaSea from "./LunaSea.vue";
|
|
||||||
import Feishu from "./Feishu.vue";
|
|
||||||
import Apprise from "./Apprise.vue";
|
|
||||||
import Pushbullet from "./Pushbullet.vue";
|
|
||||||
import Line from "./Line.vue";
|
|
||||||
import Mattermost from "./Mattermost.vue";
|
|
||||||
import Matrix from "./Matrix.vue";
|
|
||||||
import AliyunSMS from "./AliyunSms.vue";
|
import AliyunSMS from "./AliyunSms.vue";
|
||||||
import DingDing from "./DingDing.vue";
|
import Apprise from "./Apprise.vue";
|
||||||
import Bark from "./Bark.vue";
|
import Bark from "./Bark.vue";
|
||||||
import SerwerSMS from "./SerwerSMS.vue";
|
import ClickSendSMS from "./ClickSendSMS.vue";
|
||||||
import Stackfield from "./Stackfield.vue";
|
import DingDing from "./DingDing.vue";
|
||||||
import WeCom from "./WeCom.vue";
|
import Discord from "./Discord.vue";
|
||||||
|
import Feishu from "./Feishu.vue";
|
||||||
import GoogleChat from "./GoogleChat.vue";
|
import GoogleChat from "./GoogleChat.vue";
|
||||||
import Gorush from "./Gorush.vue";
|
import Gorush from "./Gorush.vue";
|
||||||
import Alerta from "./Alerta.vue";
|
import Gotify from "./Gotify.vue";
|
||||||
|
import HomeAssistant from "./HomeAssistant.vue";
|
||||||
|
import Line from "./Line.vue";
|
||||||
|
import LineNotify from "./LineNotify.vue";
|
||||||
|
import LunaSea from "./LunaSea.vue";
|
||||||
|
import Matrix from "./Matrix.vue";
|
||||||
|
import Mattermost from "./Mattermost.vue";
|
||||||
|
import Ntfy from "./Ntfy.vue";
|
||||||
|
import Octopush from "./Octopush.vue";
|
||||||
import OneBot from "./OneBot.vue";
|
import OneBot from "./OneBot.vue";
|
||||||
|
import PagerDuty from "./PagerDuty.vue";
|
||||||
|
import PromoSMS from "./PromoSMS.vue";
|
||||||
|
import Pushbullet from "./Pushbullet.vue";
|
||||||
import PushDeer from "./PushDeer.vue";
|
import PushDeer from "./PushDeer.vue";
|
||||||
|
import Pushover from "./Pushover.vue";
|
||||||
|
import Pushy from "./Pushy.vue";
|
||||||
|
import RocketChat from "./RocketChat.vue";
|
||||||
|
import SerwerSMS from "./SerwerSMS.vue";
|
||||||
|
import Signal from "./Signal.vue";
|
||||||
|
import SMSManager from "./SMSManager.vue";
|
||||||
|
import Slack from "./Slack.vue";
|
||||||
|
import Stackfield from "./Stackfield.vue";
|
||||||
|
import STMP from "./SMTP.vue";
|
||||||
|
import Teams from "./Teams.vue";
|
||||||
|
import TechulusPush from "./TechulusPush.vue";
|
||||||
|
import Telegram from "./Telegram.vue";
|
||||||
|
import Webhook from "./Webhook.vue";
|
||||||
|
import WeCom from "./WeCom.vue";
|
||||||
|
import GoAlert from "./GoAlert.vue";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Manage all notification form.
|
* Manage all notification form.
|
||||||
|
@ -38,39 +45,46 @@ import PushDeer from "./PushDeer.vue";
|
||||||
* @type { Record<string, any> }
|
* @type { Record<string, any> }
|
||||||
*/
|
*/
|
||||||
const NotificationFormList = {
|
const NotificationFormList = {
|
||||||
"telegram": Telegram,
|
"alerta": Alerta,
|
||||||
"webhook": Webhook,
|
"AlertNow": AlertNow,
|
||||||
"smtp": STMP,
|
|
||||||
"discord": Discord,
|
|
||||||
"teams": Teams,
|
|
||||||
"signal": Signal,
|
|
||||||
"gotify": Gotify,
|
|
||||||
"slack": Slack,
|
|
||||||
"rocket.chat": RocketChat,
|
|
||||||
"pushover": Pushover,
|
|
||||||
"pushy": Pushy,
|
|
||||||
"PushByTechulus": TechulusPush,
|
|
||||||
"octopush": Octopush,
|
|
||||||
"promosms": PromoSMS,
|
|
||||||
"clicksendsms": ClickSendSMS,
|
|
||||||
"lunasea": LunaSea,
|
|
||||||
"Feishu": Feishu,
|
|
||||||
"AliyunSMS": AliyunSMS,
|
"AliyunSMS": AliyunSMS,
|
||||||
"apprise": Apprise,
|
"apprise": Apprise,
|
||||||
"pushbullet": Pushbullet,
|
|
||||||
"line": Line,
|
|
||||||
"mattermost": Mattermost,
|
|
||||||
"matrix": Matrix,
|
|
||||||
"DingDing": DingDing,
|
|
||||||
"Bark": Bark,
|
"Bark": Bark,
|
||||||
"serwersms": SerwerSMS,
|
"clicksendsms": ClickSendSMS,
|
||||||
"stackfield": Stackfield,
|
"DingDing": DingDing,
|
||||||
"WeCom": WeCom,
|
"discord": Discord,
|
||||||
|
"Feishu": Feishu,
|
||||||
"GoogleChat": GoogleChat,
|
"GoogleChat": GoogleChat,
|
||||||
"gorush": Gorush,
|
"gorush": Gorush,
|
||||||
"alerta": Alerta,
|
"gotify": Gotify,
|
||||||
|
"HomeAssistant": HomeAssistant,
|
||||||
|
"line": Line,
|
||||||
|
"LineNotify": LineNotify,
|
||||||
|
"lunasea": LunaSea,
|
||||||
|
"matrix": Matrix,
|
||||||
|
"mattermost": Mattermost,
|
||||||
|
"ntfy": Ntfy,
|
||||||
|
"octopush": Octopush,
|
||||||
"OneBot": OneBot,
|
"OneBot": OneBot,
|
||||||
|
"PagerDuty": PagerDuty,
|
||||||
|
"promosms": PromoSMS,
|
||||||
|
"pushbullet": Pushbullet,
|
||||||
|
"PushByTechulus": TechulusPush,
|
||||||
"PushDeer": PushDeer,
|
"PushDeer": PushDeer,
|
||||||
|
"pushover": Pushover,
|
||||||
|
"pushy": Pushy,
|
||||||
|
"rocket.chat": RocketChat,
|
||||||
|
"serwersms": SerwerSMS,
|
||||||
|
"signal": Signal,
|
||||||
|
"SMSManager": SMSManager,
|
||||||
|
"slack": Slack,
|
||||||
|
"smtp": STMP,
|
||||||
|
"stackfield": Stackfield,
|
||||||
|
"teams": Teams,
|
||||||
|
"telegram": Telegram,
|
||||||
|
"webhook": Webhook,
|
||||||
|
"WeCom": WeCom,
|
||||||
|
"GoAlert": GoAlert,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default NotificationFormList;
|
export default NotificationFormList;
|
||||||
|
|
|
@ -4,6 +4,11 @@
|
||||||
<object class="my-4" width="200" height="200" data="/icon.svg" />
|
<object class="my-4" width="200" height="200" data="/icon.svg" />
|
||||||
<div class="fs-4 fw-bold">Uptime Kuma</div>
|
<div class="fs-4 fw-bold">Uptime Kuma</div>
|
||||||
<div>{{ $t("Version") }}: {{ $root.info.version }}</div>
|
<div>{{ $t("Version") }}: {{ $root.info.version }}</div>
|
||||||
|
<div class="frontend-version">{{ $t("Frontend Version") }}: {{ $root.frontendVersion }}</div>
|
||||||
|
|
||||||
|
<div v-if="!$root.isFrontendBackendVersionMatched" class="alert alert-warning mt-4" role="alert">
|
||||||
|
⚠️ {{ $t("Frontend Version do not match backend version!") }}
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="my-3 update-link"><a href="https://github.com/louislam/uptime-kuma/releases" target="_blank" rel="noopener">{{ $t("Check Update On GitHub") }}</a></div>
|
<div class="my-3 update-link"><a href="https://github.com/louislam/uptime-kuma/releases" target="_blank" rel="noopener">{{ $t("Check Update On GitHub") }}</a></div>
|
||||||
|
|
||||||
|
@ -46,6 +51,16 @@ export default {
|
||||||
}
|
}
|
||||||
|
|
||||||
.update-link {
|
.update-link {
|
||||||
font-size: 0.9em;
|
font-size: 0.8em;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.frontend-version {
|
||||||
|
font-size: 0.9em;
|
||||||
|
color: #cccccc;
|
||||||
|
|
||||||
|
.dark & {
|
||||||
|
color: #333333;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -1,6 +1,12 @@
|
||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<div class="my-4">
|
<div class="my-4">
|
||||||
|
<div class="alert alert-warning" role="alert" style="border-radius: 15px;">
|
||||||
|
{{ $t("backupOutdatedWarning") }}<br />
|
||||||
|
<br />
|
||||||
|
{{ $t("backupRecommend") }}
|
||||||
|
</div>
|
||||||
|
|
||||||
<h4 class="mt-4 mb-2">{{ $t("Export Backup") }}</h4>
|
<h4 class="mt-4 mb-2">{{ $t("Export Backup") }}</h4>
|
||||||
|
|
||||||
<p>
|
<p>
|
||||||
|
@ -133,10 +139,15 @@ export default {
|
||||||
},
|
},
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
|
/**
|
||||||
|
* Show the confimation dialog confirming the configuration
|
||||||
|
* be imported
|
||||||
|
*/
|
||||||
confirmImport() {
|
confirmImport() {
|
||||||
this.$refs.confirmImport.show();
|
this.$refs.confirmImport.show();
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/** Download a backup of the configuration */
|
||||||
downloadBackup() {
|
downloadBackup() {
|
||||||
let time = dayjs().format("YYYY_MM_DD-hh_mm_ss");
|
let time = dayjs().format("YYYY_MM_DD-hh_mm_ss");
|
||||||
let fileName = `Uptime_Kuma_Backup_${time}.json`;
|
let fileName = `Uptime_Kuma_Backup_${time}.json`;
|
||||||
|
@ -157,6 +168,10 @@ export default {
|
||||||
downloadItem.click();
|
downloadItem.click();
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Import the specified backup file
|
||||||
|
* @returns {?string}
|
||||||
|
*/
|
||||||
importBackup() {
|
importBackup() {
|
||||||
this.processing = true;
|
this.processing = true;
|
||||||
let uploadItem = document.getElementById("import-backend").files;
|
let uploadItem = document.getElementById("import-backend").files;
|
||||||
|
|
48
src/components/settings/Docker.vue
Normal file
48
src/components/settings/Docker.vue
Normal file
|
@ -0,0 +1,48 @@
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<div class="dockerHost-list my-4">
|
||||||
|
<p v-if="$root.dockerHostList.length === 0">
|
||||||
|
{{ $t("Not available, please setup.") }}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<ul class="list-group mb-3" style="border-radius: 1rem;">
|
||||||
|
<li v-for="(dockerHost, index) in $root.dockerHostList" :key="index" class="list-group-item">
|
||||||
|
{{ dockerHost.name }}<br>
|
||||||
|
<a href="#" @click="$refs.dockerHostDialog.show(dockerHost.id)">{{ $t("Edit") }}</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<button class="btn btn-primary me-2" type="button" @click="$refs.dockerHostDialog.show()">
|
||||||
|
{{ $t("Setup Docker Host") }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<DockerHostDialog ref="dockerHostDialog" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import DockerHostDialog from "../../components/DockerHostDialog.vue";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
components: {
|
||||||
|
DockerHostDialog,
|
||||||
|
},
|
||||||
|
|
||||||
|
data() {
|
||||||
|
return {};
|
||||||
|
},
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
settings() {
|
||||||
|
return this.$parent.$parent.$parent.settings;
|
||||||
|
},
|
||||||
|
saveSettings() {
|
||||||
|
return this.$parent.$parent.$parent.saveSettings;
|
||||||
|
},
|
||||||
|
settingsLoaded() {
|
||||||
|
return this.$parent.$parent.$parent.settingsLoaded;
|
||||||
|
},
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
|
@ -178,10 +178,12 @@ export default {
|
||||||
},
|
},
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
|
/** Save the settings */
|
||||||
saveGeneral() {
|
saveGeneral() {
|
||||||
localStorage.timezone = this.$root.userTimezone;
|
localStorage.timezone = this.$root.userTimezone;
|
||||||
this.saveSettings();
|
this.saveSettings();
|
||||||
},
|
},
|
||||||
|
/** Get the base URL of the application */
|
||||||
autoGetPrimaryBaseURL() {
|
autoGetPrimaryBaseURL() {
|
||||||
this.settings.primaryBaseURL = location.protocol + "//" + location.host;
|
this.settings.primaryBaseURL = location.protocol + "//" + location.host;
|
||||||
},
|
},
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue