mirror of
https://github.com/louislam/uptime-kuma.git
synced 2025-09-12 22:47:00 +08:00
Compare commits
439 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
25ea99a436 | ||
|
c2c3f981bc | ||
|
2c237e9c03 | ||
|
3e85893bdd | ||
|
543a74ecab | ||
|
62ad2f9bb4 | ||
|
7672057319 | ||
|
0f99d49a27 | ||
|
d93f7b33be | ||
|
894aeaea0a | ||
|
97dc8eba13 | ||
|
d39a4770e0 | ||
|
f6ac09b751 | ||
|
9c1ad4f8c6 | ||
|
8595824b5d | ||
|
da34685019 | ||
|
0cf28c2025 | ||
|
ed7bc0e6d1 | ||
|
6a3eccf6a6 | ||
|
97de3959cd | ||
|
63e408f4f2 | ||
|
a3b1123e82 | ||
|
2e54dee817 | ||
|
ceeb47bf82 | ||
|
929d238106 | ||
|
bef9cb6a5f | ||
|
4157c7d546 | ||
|
c3eef28443 | ||
|
443235b20b | ||
|
35810e299d | ||
|
b03624b7e3 | ||
|
dcbd9c12cf | ||
|
83ca74eba7 | ||
|
c6cf600722 | ||
|
22ef8ff751 | ||
|
565e9233fe | ||
|
9a7c2d562a | ||
|
c4cb825fef | ||
|
3193533a60 | ||
|
1f1825dbff | ||
|
e4e47c3976 | ||
|
1c4e97439c | ||
|
d23085cddc | ||
|
f96bad1629 | ||
|
38c45a3fe3 | ||
|
e815e51608 | ||
|
bec3b0d2dc | ||
|
2d5096317f | ||
|
1c3da995e3 | ||
|
db6fdf5e26 | ||
|
fce175cad6 | ||
|
b673cfbe94 | ||
|
0ae8010156 | ||
|
527e479f2d | ||
|
d63022676a | ||
|
08fdbeaa75 | ||
|
0dd858d516 | ||
|
104d521633 | ||
|
839183aa85 | ||
|
e83ff0d679 | ||
|
80c1054877 | ||
|
f503488618 | ||
|
7577477ae8 | ||
|
3ea57600ba | ||
|
b22176d218 | ||
|
7f9e291206 | ||
|
197d44981f | ||
|
97e9bc7705 | ||
|
87b72191e5 | ||
|
8827176390 | ||
|
42969d11ee | ||
|
7b8f9c7655 | ||
|
e90a4f1f34 | ||
|
1e5376d80b | ||
|
dad2ec1164 | ||
|
676e64c77d | ||
|
0244507a07 | ||
|
6601e9bbba | ||
|
9589fcfdef | ||
|
ce3fe9f0a6 | ||
|
995276badc | ||
|
9e62a6ec7d | ||
|
75deab2cc5 | ||
|
50f7b39672 | ||
|
6a802bf68c | ||
|
be8caa0d1e | ||
|
d1aa9cfbcc | ||
|
808efb267f | ||
|
252d6ea9c9 | ||
|
7d12cd0d42 | ||
|
cf10e26aff | ||
|
53135641f3 | ||
|
c0fe2d54f9 | ||
|
d8303f1f4d | ||
|
ee14ab6751 | ||
|
fd2df562b1 | ||
|
87e45b21fa | ||
|
626accedee | ||
|
b890812411 | ||
|
cbc0b9c553 | ||
|
c2472bf750 | ||
|
e0cdc3e7c5 | ||
|
84fad93555 | ||
|
b9b00050dd | ||
|
064fe50e38 | ||
|
a8ea76e8a1 | ||
|
2975204a0a | ||
|
31150642cd | ||
|
3c5c49c16d | ||
|
584d52517a | ||
|
82dd9a7c16 | ||
|
d44663c57c | ||
|
e557545c97 | ||
|
055948d1b9 | ||
|
4ac80cfc02 | ||
|
af89c4d8ae | ||
|
40b9d9ed17 | ||
|
65e6921a41 | ||
|
04fc124928 | ||
|
5c25354682 | ||
|
2aad2510b7 | ||
|
fac2f1cbc6 | ||
|
8bc3651a7d | ||
|
684d0a7eb8 | ||
|
b3712ee1cc | ||
|
af94424283 | ||
|
728e811969 | ||
|
a6007adce3 | ||
|
30b72d81cf | ||
|
de6e1e7ddd | ||
|
2af754b5e8 | ||
|
3b3763351b | ||
|
9a488d6968 | ||
|
aca395cea1 | ||
|
a49faf09b9 | ||
|
d0d1e0de28 | ||
|
70aa8fe453 | ||
|
19d8761305 | ||
|
d6a113396a | ||
|
fb3fe17c28 | ||
|
71d62ee151 | ||
|
82b9bfc5a0 | ||
|
f016caa513 | ||
|
6e29feffd3 | ||
|
2389b604fe | ||
|
a3b1612938 | ||
|
a07f54f35b | ||
|
b777c0c3e4 | ||
|
bea8679788 | ||
|
f091e92c70 | ||
|
a0843745f9 | ||
|
16d6885a88 | ||
|
4d975a5bd5 | ||
|
96ec46765b | ||
|
96971f6776 | ||
|
ffb1a948fe | ||
|
4e4156285a | ||
|
1223b56205 | ||
|
8ced61697a | ||
|
f3322398e5 | ||
|
b76ca59dfe | ||
|
554b0d2bc3 | ||
|
4575f31094 | ||
|
df7f0b078d | ||
|
d8253405b4 | ||
|
56c6c0c6f1 | ||
|
b16cb6a337 | ||
|
694b4cadb3 | ||
|
75f6ff8b58 | ||
|
1062e629c5 | ||
|
13f7db655b | ||
|
60e7824ff0 | ||
|
fb3b407577 | ||
|
d54c652d26 | ||
|
f1bcecb0c6 | ||
|
88afd662db | ||
|
e356d5f623 | ||
|
0d098b0958 | ||
|
239611a016 | ||
|
2ccf1fe41b | ||
|
77340cf0d2 | ||
|
c412c66aeb | ||
|
c4a2ce4e78 | ||
|
a382f811f4 | ||
|
986c03aecd | ||
|
31c388a6e3 | ||
|
9a8b484ee8 | ||
|
17ed051401 | ||
|
8f7b7e74c9 | ||
|
9cd060c6c3 | ||
|
1999541802 | ||
|
65d71e5db0 | ||
|
2073f0c284 | ||
|
25d711e683 | ||
|
77a7801992 | ||
|
525607f49e | ||
|
8613d3ffa9 | ||
|
d44d984a46 | ||
|
d362372b05 | ||
|
3fa5dfc873 | ||
|
6ce012c9a1 | ||
|
f33b6de157 | ||
|
e0a2ed2523 | ||
|
a78cb7ab42 | ||
|
278d9f5689 | ||
|
2ad79a68b9 | ||
|
d29955f3ba | ||
|
c4125a8334 | ||
|
0a368ff553 | ||
|
27dbc021b4 | ||
|
1b120f8a6f | ||
|
6f57c4195a | ||
|
baa592bce3 | ||
|
42e30de209 | ||
|
624678826d | ||
|
e8c3807594 | ||
|
4d8e755400 | ||
|
9b92a02968 | ||
|
6418f99f1a | ||
|
93ac4e1b96 | ||
|
f65bef686c | ||
|
2f26864892 | ||
|
a802f7ebed | ||
|
e5e8db6c38 | ||
|
303738b7c2 | ||
|
dddd2c0042 | ||
|
515095ecfb | ||
|
83284b6d2c | ||
|
1af6d33fcd | ||
|
e36b65c2df | ||
|
8542e6cbb9 | ||
|
5dd197374d | ||
|
f84ae82983 | ||
|
9650418ef7 | ||
|
b546c846ae | ||
|
b5cbc6f5f6 | ||
|
f8def5aa6f | ||
|
6f01a448ad | ||
|
5dd3d32d77 | ||
|
ff8bba6863 | ||
|
ed1f88a852 | ||
|
0ecaa2cbd7 | ||
|
3c3dc05621 | ||
|
1f5466a3e8 | ||
|
d5da5af174 | ||
|
c36d9a4b8b | ||
|
a7063b8aca | ||
|
0a8046c98e | ||
|
7ba717ee55 | ||
|
ea400ac35f | ||
|
15db2c060d | ||
|
89717495dc | ||
|
7b710af12c | ||
|
5b278ca500 | ||
|
f1d24782f8 | ||
|
b97019eea8 | ||
|
d65abe5b8c | ||
|
c4e2d67d17 | ||
|
bcd616a4d0 | ||
|
7533041696 | ||
|
7b0deb5e20 | ||
|
b4a4171178 | ||
|
012be23509 | ||
|
dd09351c8e | ||
|
ffad990ca4 | ||
|
47e82ed83a | ||
|
e1f766756f | ||
|
4b2a465c94 | ||
|
edcdedcaae | ||
|
f7afe121e3 | ||
|
945288f0c0 | ||
|
ac27e6e2af | ||
|
869a040011 | ||
|
42848bcd2e | ||
|
a3b94aa532 | ||
|
fdbdf83a0d | ||
|
81d5360520 | ||
|
8f1e193de3 | ||
|
ac449ec1c2 | ||
|
da91317760 | ||
|
bef0febede | ||
|
7d63b700e1 | ||
|
0223f86a2a | ||
|
c170b1edd0 | ||
|
5943514a92 | ||
|
62acd2edb1 | ||
|
483cbfb636 | ||
|
660005b143 | ||
|
98f3c126e5 | ||
|
6995a29980 | ||
|
cf2ca71dee | ||
|
0bd1c42080 | ||
|
9b21b86e70 | ||
|
f723930d11 | ||
|
e425e408a2 | ||
|
c690d1c3a1 | ||
|
8abbc9fd15 | ||
|
af7c905b44 | ||
|
0e8f6d2f85 | ||
|
d16be6fb7d | ||
|
f25ca96308 | ||
|
6682839ec8 | ||
|
817f6db4fd | ||
|
dc2302244f | ||
|
7a27d3752a | ||
|
f0e8f34aeb | ||
|
54b9698a05 | ||
|
69273a6c41 | ||
|
6424fe77ab | ||
|
fa60672cce | ||
|
6e43ef1dd3 | ||
|
f4f2b8ddb8 | ||
|
436bc13aeb | ||
|
b72a279361 | ||
|
a28ef56553 | ||
|
7f432bd916 | ||
|
f570d41142 | ||
|
1c4ddaeddf | ||
|
d4485fe62f | ||
|
e1681ce370 | ||
|
69d6633e6d | ||
|
55a6e5af42 | ||
|
252709ff49 | ||
|
774fe58ddc | ||
|
5f347b10ba | ||
|
f442507cab | ||
|
a23ab9d1de | ||
|
404923b7c8 | ||
|
a41023ca2a | ||
|
817c941489 | ||
|
5f6347d277 | ||
|
fbfa5a33ed | ||
|
04e22f17a9 | ||
|
aa398948da | ||
|
11243a6ca1 | ||
|
54548e34ed | ||
|
87428231ad | ||
|
a68d945cdc | ||
|
2c0180f323 | ||
|
4fdaa1abb6 | ||
|
6ee7b3696a | ||
|
cc258dce14 | ||
|
fb420fa1b1 | ||
|
a707b51053 | ||
|
a927f5cd15 | ||
|
0e28707307 | ||
|
c94dcf1533 | ||
|
b0476cfb5b | ||
|
2170229031 | ||
|
213aca4fc3 | ||
|
2b42c3c828 | ||
|
d939d03690 | ||
|
07888e43f1 | ||
|
c6c1bb5b5c | ||
|
3210264e28 | ||
|
54e948c2ca | ||
|
80094ec4e1 | ||
|
091158cfe7 | ||
|
abb6ce2366 | ||
|
e4ad8cbfc8 | ||
|
a674caa520 | ||
|
179e3569b5 | ||
|
43527f2f40 | ||
|
26ff6f45a0 | ||
|
c095767f4a | ||
|
ffb7ba176c | ||
|
e9e78c26e5 | ||
|
cfa5b551a5 | ||
|
46ee149b70 | ||
|
54184350a4 | ||
|
14dbe7c334 | ||
|
122e6a842b | ||
|
77ef22bdb4 | ||
|
59f983d506 | ||
|
71f031c14e | ||
|
73b965c867 | ||
|
32cfd411f8 | ||
|
a9f3142cee | ||
|
b7ba6330db | ||
|
da99a57560 | ||
|
42d68edab0 | ||
|
019d638767 | ||
|
ef73af391f | ||
|
44f6fca945 | ||
|
23ce7c6623 | ||
|
c346ea7864 | ||
|
f0ad32a252 | ||
|
5720017fb4 | ||
|
b7dc8e3ef8 | ||
|
5bba19f866 | ||
|
e198f2f1ab | ||
|
f91e5b98f9 | ||
|
87f933df4f | ||
|
332b9ab248 | ||
|
7cc89979f0 | ||
|
398ecb7666 | ||
|
668e97c5a9 | ||
|
fdd781b081 | ||
|
373bd9b962 | ||
|
59be9bb971 | ||
|
201a25c659 | ||
|
93050208bb | ||
|
98ee9caf2c | ||
|
8e99cbf426 | ||
|
cbfecab850 | ||
|
25cc54bf72 | ||
|
3700b16c5b | ||
|
d0546afe71 | ||
|
f4515ad8c5 | ||
|
7ed8ae9f7c | ||
|
c7ec9a07e2 | ||
|
052fde5a24 | ||
|
d6b591a513 | ||
|
19933bbd99 | ||
|
60f8ab7285 | ||
|
b7e2489d22 | ||
|
361e44ad6a | ||
|
af44b0beab | ||
|
84a0b24448 | ||
|
a4be651118 | ||
|
d8013f31e8 | ||
|
91366ff565 | ||
|
244a7b3671 | ||
|
ee90d2713f | ||
|
d446a57d42 | ||
|
855b12f435 | ||
|
f390a8caf1 | ||
|
30ce53f57c | ||
|
8c4ab9d652 | ||
|
f931e709e6 | ||
|
11e9eee09d | ||
|
65fc71e485 | ||
|
b69a8b8493 | ||
|
1ac904d6d6 | ||
|
29df70949d | ||
|
4818bb67d6 | ||
|
9619d31a05 | ||
|
c5cc42272f | ||
|
b0259b5592 |
@@ -1,6 +1,7 @@
|
|||||||
/.idea
|
/.idea
|
||||||
/node_modules
|
/node_modules
|
||||||
/data
|
/data
|
||||||
|
/cypress
|
||||||
/out
|
/out
|
||||||
/test
|
/test
|
||||||
/kubernetes
|
/kubernetes
|
||||||
|
6
.github/PULL_REQUEST_TEMPLATE.md
vendored
6
.github/PULL_REQUEST_TEMPLATE.md
vendored
@@ -1,3 +1,9 @@
|
|||||||
|
⚠️⚠️⚠️ Since we do not accept all types of pull requests and do not want to waste your time. Please be sure that you have read pull request rules:
|
||||||
|
https://github.com/louislam/uptime-kuma/blob/master/CONTRIBUTING.md#can-i-create-a-pull-request-for-uptime-kuma
|
||||||
|
|
||||||
|
Tick the checkbox if you understand [x]:
|
||||||
|
- [ ] I have read and understand the pull request rules.
|
||||||
|
|
||||||
# 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
|
||||||
|
24
.github/workflows/stale-bot.yml
vendored
Normal file
24
.github/workflows/stale-bot.yml
vendored
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
name: 'Automatically close stale issues and PRs'
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
schedule:
|
||||||
|
- cron: '0 */6 * * *'
|
||||||
|
#Run every 6 hours
|
||||||
|
|
||||||
|
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 2 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 2 days.'
|
||||||
|
close-issue-message: 'This issue was closed because it has been stalled for 2 days with no activity.'
|
||||||
|
close-pr-message: 'This PR was closed because it has been stalled for 2 days with no activity.'
|
||||||
|
days-before-stale: 90
|
||||||
|
days-before-close: 2
|
||||||
|
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'
|
||||||
|
operations-per-run: 200
|
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,34 @@ 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 or no, it depends on what you will try to do. Since I don't want to waste your time, be sure to **create an empty draft pull request or open an issue, so we can discuss first**. Especially for a large pull request or you don't know it will be merged or not.
|
||||||
|
|
||||||
✅ Accept:
|
Here are some references:
|
||||||
|
|
||||||
|
✅ Usually 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
|
||||||
|
- UI/UX is not close to Uptime Kuma
|
||||||
|
- Existing logic is completely modified or deleted for no reason
|
||||||
|
- A function that is completely out of scope
|
||||||
|
- Unnecessary large code changes (Hard to review, causes code conflicts to other 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.
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
|
||||||
### 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 +70,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 +90,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
|
||||||
|
|
||||||
@@ -171,7 +181,18 @@ npm test
|
|||||||
|
|
||||||
By default, the Chromium window will be shown up during the test. Specifying `HEADLESS_TEST=1` for terminal environments.
|
By default, the Chromium window will be shown up during the test. Specifying `HEADLESS_TEST=1` for terminal environments.
|
||||||
|
|
||||||
## Update Dependencies
|
## Dependencies
|
||||||
|
|
||||||
|
Both frontend and backend share the same package.json. However, the frontend dependencies are eventually not used in the production environment, because it is usually also baked into dist files. So:
|
||||||
|
|
||||||
|
- Frontend dependencies = "devDependencies"
|
||||||
|
- Examples: vue, chart.js
|
||||||
|
- Backend dependencies = "dependencies"
|
||||||
|
- Examples: socket.io, sqlite3
|
||||||
|
- Development dependencies = "devDependencies"
|
||||||
|
- Examples: eslint, sass
|
||||||
|
|
||||||
|
### Update Dependencies
|
||||||
|
|
||||||
Install `ncu`
|
Install `ncu`
|
||||||
https://github.com/raineorshine/npm-check-updates
|
https://github.com/raineorshine/npm-check-updates
|
||||||
|
21
README.md
21
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, please read this guide and follow the rules here: 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,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
14977
package-lock.json
generated
14977
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
112
package.json
112
package.json
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "uptime-kuma",
|
"name": "uptime-kuma",
|
||||||
"version": "1.16.1",
|
"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.16.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",
|
||||||
|
"compression": "^1.7.4",
|
||||||
"dayjs": "^1.11.0",
|
"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.4",
|
||||||
"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,11 +53,17 @@ 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-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,
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -144,6 +150,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
|
||||||
@@ -175,7 +184,13 @@ class Database {
|
|||||||
} else {
|
} else {
|
||||||
log.info("db", "Database patch is needed");
|
log.info("db", "Database patch is needed");
|
||||||
|
|
||||||
this.backup(version);
|
try {
|
||||||
|
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 {
|
||||||
@@ -443,6 +458,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, flipStatus, TimeLogger } = require("../../src/util");
|
const { log, UP, DOWN, PENDING, 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:
|
||||||
@@ -34,7 +35,13 @@ class Monitor extends BeanModel {
|
|||||||
let obj = {
|
let obj = {
|
||||||
id: this.id,
|
id: this.id,
|
||||||
name: this.name,
|
name: this.name,
|
||||||
|
sendUrl: this.sendUrl,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (this.sendUrl) {
|
||||||
|
obj.url = this.url;
|
||||||
|
}
|
||||||
|
|
||||||
if (showTags) {
|
if (showTags) {
|
||||||
obj.tags = await this.getTags();
|
obj.tags = await this.getTags();
|
||||||
}
|
}
|
||||||
@@ -72,6 +79,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(),
|
||||||
@@ -81,13 +89,26 @@ 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,
|
||||||
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) {
|
||||||
@@ -194,6 +215,7 @@ class Monitor extends BeanModel {
|
|||||||
bean.monitor_id = this.id;
|
bean.monitor_id = this.id;
|
||||||
bean.time = R.isoDateTimeMillis(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);
|
||||||
@@ -213,7 +235,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),
|
||||||
};
|
};
|
||||||
@@ -264,7 +286,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;
|
||||||
|
|
||||||
@@ -312,7 +348,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 + "]");
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -377,7 +417,7 @@ class Monitor extends BeanModel {
|
|||||||
|
|
||||||
// If the previous beat was down or pending we use the regular
|
// If the previous beat was down or pending we use the regular
|
||||||
// beatInterval/retryInterval in the setTimeout further below
|
// beatInterval/retryInterval in the setTimeout further below
|
||||||
if (previousBeat.status !== UP || msSinceLastBeat > beatInterval * 1000 + bufferTime) {
|
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;
|
let timeout = beatInterval * 1000 - msSinceLastBeat;
|
||||||
@@ -411,10 +451,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());
|
||||||
@@ -435,6 +478,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,
|
||||||
@@ -443,6 +515,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;
|
||||||
@@ -484,23 +596,38 @@ class Monitor extends BeanModel {
|
|||||||
log.debug("monitor", `[${this.name}] sendNotification`);
|
log.debug("monitor", `[${this.name}] sendNotification`);
|
||||||
await Monitor.sendNotification(isFirstBeat, this, bean);
|
await Monitor.sendNotification(isFirstBeat, this, bean);
|
||||||
|
|
||||||
|
// 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;
|
||||||
}
|
}
|
||||||
log.warn("monitor", `Monitor #${this.id} '${this.name}': Pending: ${bean.msg} | Max retries: ${this.maxretries} | Retry: ${retries} | Retry Interval: ${beatInterval} seconds | Type: ${this.type}`);
|
log.warn("monitor", `Monitor #${this.id} '${this.name}': Pending: ${bean.msg} | Max retries: ${this.maxretries} | Retry: ${retries} | Retry Interval: ${beatInterval} seconds | 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`);
|
||||||
@@ -837,10 +964,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) {
|
||||||
|
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;
|
||||||
|
33
server/notification-providers/ntfy.js
Normal file
33
server/notification-providers/ntfy.js
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
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 {
|
||||||
|
let headers = {};
|
||||||
|
if (notification.ntfyusername.length > 0) {
|
||||||
|
headers = {
|
||||||
|
"Authorization": "Basic " + Buffer.from(notification.ntfyusername + ":" + notification.ntfypassword).toString("base64"),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
let data = {
|
||||||
|
"topic": notification.ntfytopic,
|
||||||
|
"message": msg,
|
||||||
|
"priority": notification.ntfyPriority || 4,
|
||||||
|
"title": "Uptime-Kuma",
|
||||||
|
};
|
||||||
|
await axios.post(`${notification.ntfyserverurl}`, data, { headers: headers });
|
||||||
|
|
||||||
|
return okMsg;
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
this.throwGeneralAxiosError(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = Ntfy;
|
@@ -10,7 +10,7 @@ class Octopush extends NotificationProvider {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
// Default - V2
|
// Default - V2
|
||||||
if (notification.octopushVersion === 2 || !notification.octopushVersion) {
|
if (notification.octopushVersion === "2" || !notification.octopushVersion) {
|
||||||
let config = {
|
let config = {
|
||||||
headers: {
|
headers: {
|
||||||
"api-key": notification.octopushAPIKey,
|
"api-key": notification.octopushAPIKey,
|
||||||
@@ -31,7 +31,7 @@ class Octopush extends NotificationProvider {
|
|||||||
"sender": notification.octopushSenderName
|
"sender": notification.octopushSenderName
|
||||||
};
|
};
|
||||||
await axios.post("https://api.octopush.com/v1/public/sms-campaign/send", data, config);
|
await axios.post("https://api.octopush.com/v1/public/sms-campaign/send", data, config);
|
||||||
} else if (notification.octopushVersion === 1) {
|
} else if (notification.octopushVersion === "1") {
|
||||||
let data = {
|
let data = {
|
||||||
"user_login": notification.octopushDMLogin,
|
"user_login": notification.octopushDMLogin,
|
||||||
"api_key": notification.octopushDMAPIKey,
|
"api_key": notification.octopushDMAPIKey,
|
||||||
@@ -49,7 +49,15 @@ class Octopush extends NotificationProvider {
|
|||||||
},
|
},
|
||||||
params: data
|
params: data
|
||||||
};
|
};
|
||||||
await axios.post("https://www.octopush-dm.com/api/sms/json", {}, config);
|
|
||||||
|
// V1 API returns 200 even on error so we must check
|
||||||
|
// response data
|
||||||
|
let response = await axios.post("https://www.octopush-dm.com/api/sms/json", {}, config);
|
||||||
|
if ("error_code" in response.data) {
|
||||||
|
if (response.data.error_code !== "000") {
|
||||||
|
this.throwGeneralAxiosError(`Octopush error ${JSON.stringify(response.data)}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
throw new Error("Unknown Octopush version!");
|
throw new Error("Unknown Octopush version!");
|
||||||
}
|
}
|
||||||
|
36
server/notification-providers/serverchan.js
Normal file
36
server/notification-providers/serverchan.js
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
const NotificationProvider = require("./notification-provider");
|
||||||
|
const axios = require("axios");
|
||||||
|
const { DOWN, UP } = require("../../src/util");
|
||||||
|
|
||||||
|
class ServerChan extends NotificationProvider {
|
||||||
|
|
||||||
|
name = "ServerChan";
|
||||||
|
|
||||||
|
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
|
||||||
|
let okMsg = "Sent Successfully.";
|
||||||
|
try {
|
||||||
|
await axios.post(`https://sctapi.ftqq.com/${notification.serverChanSendKey}.send`, {
|
||||||
|
"title": this.checkStatus(heartbeatJSON, monitorJSON),
|
||||||
|
"desp": msg,
|
||||||
|
});
|
||||||
|
|
||||||
|
return okMsg;
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
this.throwGeneralAxiosError(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
checkStatus(heartbeatJSON, monitorJSON) {
|
||||||
|
let title = "UptimeKuma Message";
|
||||||
|
if (heartbeatJSON != null && heartbeatJSON["status"] === UP) {
|
||||||
|
title = "UptimeKuma Monitor Up " + monitorJSON["name"];
|
||||||
|
}
|
||||||
|
if (heartbeatJSON != null && heartbeatJSON["status"] === DOWN) {
|
||||||
|
title = "UptimeKuma Monitor Down " + monitorJSON["name"];
|
||||||
|
}
|
||||||
|
return title;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = ServerChan;
|
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;
|
76
server/notification-providers/squadcast.js
Normal file
76
server/notification-providers/squadcast.js
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
const NotificationProvider = require("./notification-provider");
|
||||||
|
const axios = require("axios");
|
||||||
|
const { DOWN } = require("../../src/util");
|
||||||
|
|
||||||
|
class Squadcast extends NotificationProvider {
|
||||||
|
|
||||||
|
name = "squadcast";
|
||||||
|
|
||||||
|
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
|
||||||
|
let okMsg = "Sent Successfully.";
|
||||||
|
|
||||||
|
try {
|
||||||
|
|
||||||
|
let config = {};
|
||||||
|
let data = {
|
||||||
|
message: msg,
|
||||||
|
description: "",
|
||||||
|
tags: {},
|
||||||
|
heartbeat: heartbeatJSON,
|
||||||
|
source: "uptime-kuma"
|
||||||
|
};
|
||||||
|
|
||||||
|
if (heartbeatJSON !== null) {
|
||||||
|
data.description = heartbeatJSON["msg"];
|
||||||
|
data.event_id = heartbeatJSON["monitorID"];
|
||||||
|
|
||||||
|
if (heartbeatJSON["status"] === DOWN) {
|
||||||
|
data.message = `${monitorJSON["name"]} is DOWN`;
|
||||||
|
data.status = "trigger";
|
||||||
|
} else {
|
||||||
|
data.message = `${monitorJSON["name"]} is UP`;
|
||||||
|
data.status = "resolve";
|
||||||
|
}
|
||||||
|
|
||||||
|
let address;
|
||||||
|
switch (monitorJSON["type"]) {
|
||||||
|
case "ping":
|
||||||
|
address = monitorJSON["hostname"];
|
||||||
|
break;
|
||||||
|
case "port":
|
||||||
|
case "dns":
|
||||||
|
case "steam":
|
||||||
|
address = monitorJSON["hostname"];
|
||||||
|
if (monitorJSON["port"]) {
|
||||||
|
address += ":" + monitorJSON["port"];
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
address = monitorJSON["url"];
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
data.tags["AlertAddress"] = address;
|
||||||
|
|
||||||
|
monitorJSON["tags"].forEach(tag => {
|
||||||
|
data.tags[tag["name"]] = {
|
||||||
|
value: tag["value"]
|
||||||
|
};
|
||||||
|
if (tag["color"] !== null) {
|
||||||
|
data.tags[tag["name"]]["color"] = tag["color"];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
await axios.post(notification.squadcastWebhookURL, data, config);
|
||||||
|
return okMsg;
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
this.throwGeneralAxiosError(error);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = Squadcast;
|
@@ -63,7 +63,7 @@ class Teams extends NotificationProvider {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (monitorUrl) {
|
if (monitorUrl && monitorUrl !== "https://") {
|
||||||
facts.push({
|
facts.push({
|
||||||
name: "URL",
|
name: "URL",
|
||||||
value: monitorUrl,
|
value: monitorUrl,
|
||||||
@@ -127,13 +127,17 @@ class Teams extends NotificationProvider {
|
|||||||
|
|
||||||
let url;
|
let url;
|
||||||
|
|
||||||
if (monitorJSON["type"] === "port") {
|
switch (monitorJSON["type"]) {
|
||||||
url = monitorJSON["hostname"];
|
case "http":
|
||||||
if (monitorJSON["port"]) {
|
case "keywork":
|
||||||
url += ":" + monitorJSON["port"];
|
url = monitorJSON["url"];
|
||||||
}
|
break;
|
||||||
} else {
|
case "docker":
|
||||||
url = monitorJSON["url"];
|
url = monitorJSON["docker_host"];
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
url = monitorJSON["hostname"];
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
const payload = this._notificationPayloadFactory({
|
const payload = this._notificationPayloadFactory({
|
||||||
|
@@ -1,39 +1,47 @@
|
|||||||
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 Squadcast = require("./notification-providers/squadcast");
|
||||||
|
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 PagerDuty = require("./notification-providers/pagerduty");
|
const SMSManager = require("./notification-providers/smsmanager");
|
||||||
const Gorush = require("./notification-providers/gorush");
|
const ServerChan = require("./notification-providers/serverchan");
|
||||||
const Alerta = require("./notification-providers/alerta");
|
|
||||||
const OneBot = require("./notification-providers/onebot");
|
|
||||||
const PushDeer = require("./notification-providers/pushdeer");
|
|
||||||
|
|
||||||
class Notification {
|
class Notification {
|
||||||
|
|
||||||
@@ -46,40 +54,48 @@ 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 GoogleChat(),
|
||||||
|
new Gorush(),
|
||||||
|
new Gotify(),
|
||||||
|
new HomeAssistant(),
|
||||||
|
new Line(),
|
||||||
|
new LineNotify(),
|
||||||
|
new LunaSea(),
|
||||||
new Matrix(),
|
new Matrix(),
|
||||||
|
new Mattermost(),
|
||||||
|
new Ntfy(),
|
||||||
new Octopush(),
|
new Octopush(),
|
||||||
|
new OneBot(),
|
||||||
|
new PagerDuty(),
|
||||||
new PromoSMS(),
|
new PromoSMS(),
|
||||||
new ClickSendSMS(),
|
|
||||||
new Pushbullet(),
|
new Pushbullet(),
|
||||||
|
new PushDeer(),
|
||||||
new Pushover(),
|
new Pushover(),
|
||||||
new Pushy(),
|
new Pushy(),
|
||||||
new TechulusPush(),
|
|
||||||
new RocketChat(),
|
new RocketChat(),
|
||||||
|
new ServerChan(),
|
||||||
|
new SerwerSMS(),
|
||||||
new Signal(),
|
new Signal(),
|
||||||
|
new SMSManager(),
|
||||||
new Slack(),
|
new Slack(),
|
||||||
new SMTP(),
|
new SMTP(),
|
||||||
|
new Squadcast(),
|
||||||
|
new Stackfield(),
|
||||||
|
new Teams(),
|
||||||
|
new TechulusPush(),
|
||||||
new Telegram(),
|
new Telegram(),
|
||||||
new Webhook(),
|
new Webhook(),
|
||||||
new Bark(),
|
|
||||||
new SerwerSMS(),
|
|
||||||
new Stackfield(),
|
|
||||||
new WeCom(),
|
new WeCom(),
|
||||||
new GoogleChat(),
|
new GoAlert(),
|
||||||
new PagerDuty(),
|
|
||||||
new Gorush(),
|
|
||||||
new Alerta(),
|
|
||||||
new OneBot(),
|
|
||||||
new PushDeer(),
|
|
||||||
];
|
];
|
||||||
|
|
||||||
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");
|
||||||
@@ -92,115 +92,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();
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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,
|
|
||||||
publicGroupList
|
|
||||||
});
|
|
||||||
|
|
||||||
} 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);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
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);
|
||||||
|
|
||||||
@@ -238,6 +136,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;
|
||||||
}
|
}
|
||||||
@@ -377,16 +276,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");
|
||||||
|
|
||||||
app.use(express.json());
|
app.use(express.json());
|
||||||
|
|
||||||
@@ -148,22 +151,6 @@ let jwtSecret = null;
|
|||||||
*/
|
*/
|
||||||
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);
|
||||||
@@ -179,13 +166,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");
|
||||||
}
|
}
|
||||||
@@ -214,7 +213,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));
|
||||||
@@ -227,12 +228,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);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -251,7 +256,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);
|
||||||
@@ -267,14 +274,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,
|
||||||
@@ -283,7 +290,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,
|
||||||
@@ -294,7 +301,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") {
|
||||||
@@ -307,7 +316,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;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -317,7 +326,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,
|
||||||
@@ -329,7 +338,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,
|
||||||
@@ -347,7 +356,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,
|
||||||
@@ -357,7 +366,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,
|
||||||
@@ -367,7 +376,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,
|
||||||
@@ -439,6 +448,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;
|
||||||
@@ -451,7 +462,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,
|
||||||
@@ -459,7 +470,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,
|
||||||
@@ -469,6 +480,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;
|
||||||
@@ -478,7 +491,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,
|
||||||
@@ -486,7 +499,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,
|
||||||
@@ -657,9 +670,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;
|
||||||
@@ -669,11 +683,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);
|
||||||
|
|
||||||
@@ -1247,10 +1273,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,
|
||||||
@@ -1419,6 +1449,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");
|
||||||
|
|
||||||
@@ -1456,6 +1487,10 @@ try {
|
|||||||
if (testMode) {
|
if (testMode) {
|
||||||
startUnitTest();
|
startUnitTest();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (e2eTestMode) {
|
||||||
|
startE2eTests();
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
initBackgroundJobs(args);
|
initBackgroundJobs(args);
|
||||||
@@ -1519,6 +1554,7 @@ async function afterLogin(socket, user) {
|
|||||||
let monitorList = await server.sendMonitorList(socket);
|
let monitorList = await server.sendMonitorList(socket);
|
||||||
sendNotificationList(socket);
|
sendNotificationList(socket);
|
||||||
sendProxyList(socket);
|
sendProxyList(socket);
|
||||||
|
sendDockerHostList(socket);
|
||||||
|
|
||||||
await sleep(500);
|
await sleep(500);
|
||||||
|
|
||||||
@@ -1673,10 +1709,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);
|
||||||
await doubleCheckPassword(socket, currentPassword);
|
const disabledAuth = await setting("disableAuth");
|
||||||
|
if (!disabledAuth) {
|
||||||
|
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,6 +183,26 @@ 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
|
||||||
@@ -185,7 +216,7 @@ exports.dnsResolve = function (hostname, resolverServer, resolverPort, rrtype) {
|
|||||||
// Remove brackets from IPv6 addresses so we can re-add them to
|
// Remove brackets from IPv6 addresses so we can re-add them to
|
||||||
// prevent issues with ::1:5300 (::1 port 5300)
|
// prevent issues with ::1:5300 (::1 port 5300)
|
||||||
resolverServer = resolverServer.replace("[", "").replace("]", "");
|
resolverServer = resolverServer.replace("[", "").replace("]", "");
|
||||||
resolver.setServers([`[${resolverServer}]:${resolverPort}`]);
|
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) => {
|
||||||
@@ -207,23 +238,91 @@ exports.dnsResolve = function (hostname, resolverServer, resolverPort, 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;
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -234,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
|
||||||
@@ -390,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
|
||||||
@@ -518,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
|
||||||
@@ -558,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;
|
||||||
@@ -364,6 +383,12 @@ 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;
|
||||||
@@ -378,7 +403,6 @@ textarea.form-control {
|
|||||||
.info {
|
.info {
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
@@ -473,6 +497,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>
|
@@ -17,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,
|
||||||
@@ -160,12 +163,19 @@ 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}` : "");
|
||||||
},
|
},
|
||||||
|
@@ -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;
|
||||||
|
|
||||||
|
@@ -58,6 +58,7 @@ export default {
|
|||||||
Tag,
|
Tag,
|
||||||
},
|
},
|
||||||
props: {
|
props: {
|
||||||
|
/** Should the scrollbar be shown */
|
||||||
scrollbar: {
|
scrollbar: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
},
|
},
|
||||||
@@ -69,10 +70,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() {
|
||||||
return {
|
if (window.innerWidth > 550) {
|
||||||
height: `calc(100vh - 160px + ${this.windowTop}px)`,
|
return {
|
||||||
};
|
height: `calc(100vh - 160px + ${this.windowTop}px)`,
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
return {
|
||||||
|
height: "calc(100vh - 160px)",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
},
|
},
|
||||||
|
|
||||||
sortedMonitorList() {
|
sortedMonitorList() {
|
||||||
@@ -124,6 +137,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;
|
||||||
@@ -131,9 +145,15 @@ 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);
|
||||||
},
|
},
|
||||||
|
/** 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) => {
|
||||||
|
@@ -39,7 +39,28 @@
|
|||||||
<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" />
|
||||||
{{ monitor.element.name }}
|
<a
|
||||||
|
v-if="showLink(monitor)"
|
||||||
|
:href="monitor.element.url"
|
||||||
|
class="item-name"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
>
|
||||||
|
{{ 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" 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'" />
|
||||||
@@ -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>
|
46
src/components/notifications/Ntfy.vue
Normal file
46
src/components/notifications/Ntfy.vue
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
<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>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="ntfy-username" class="form-label">{{ $t("Username") }} ({{ $t("Optional") }})</label>
|
||||||
|
<div class="input-group mb-3">
|
||||||
|
<input id="ntfy-username" v-model="$parent.notification.ntfyusername" type="text" class="form-control" required>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="ntfy-password" class="form-label">{{ $t("Password") }} ({{ $t("Optional") }})</label>
|
||||||
|
<div class="input-group mb-3">
|
||||||
|
<HiddenInput id="ntfy-password" v-model="$parent.notification.ntfypassword" autocomplete="new-password"></HiddenInput>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import HiddenInput from "../HiddenInput.vue";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
components: {
|
||||||
|
HiddenInput,
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
if (typeof this.$parent.notification.ntfyPriority === "undefined") {
|
||||||
|
this.$parent.notification.ntfyserverurl = "https://ntfy.sh";
|
||||||
|
this.$parent.notification.ntfyPriority = 5;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</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>
|
16
src/components/notifications/ServerChan.vue
Normal file
16
src/components/notifications/ServerChan.vue
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
<template>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="serverchan-sendkey" class="form-label">{{ $t("SendKey") }}</label>
|
||||||
|
<HiddenInput id="serverchan-sendkey" v-model="$parent.notification.serverChanSendKey" :required="true" autocomplete="one-time-code"></HiddenInput>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import HiddenInput from "../HiddenInput.vue";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
components: {
|
||||||
|
HiddenInput,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
6
src/components/notifications/Squadcast.vue
Normal file
6
src/components/notifications/Squadcast.vue
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
<template>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="webhook-url" class="form-label">{{ $t("Post URL") }}</label>
|
||||||
|
<input id="webhook-url" v-model="$parent.notification.squadcastWebhookURL" type="url" pattern="https?://.+" class="form-control" required>
|
||||||
|
</div>
|
||||||
|
</template>
|
@@ -1,37 +1,45 @@
|
|||||||
import STMP from "./SMTP.vue";
|
import Alerta from "./Alerta.vue";
|
||||||
import Telegram from "./Telegram.vue";
|
import AlertNow from "./AlertNow.vue";
|
||||||
|
import AliyunSMS from "./AliyunSms.vue";
|
||||||
|
import Apprise from "./Apprise.vue";
|
||||||
|
import Bark from "./Bark.vue";
|
||||||
|
import ClickSendSMS from "./ClickSendSMS.vue";
|
||||||
|
import DingDing from "./DingDing.vue";
|
||||||
import Discord from "./Discord.vue";
|
import Discord from "./Discord.vue";
|
||||||
import Webhook from "./Webhook.vue";
|
import Feishu from "./Feishu.vue";
|
||||||
import Signal from "./Signal.vue";
|
import GoogleChat from "./GoogleChat.vue";
|
||||||
|
import Gorush from "./Gorush.vue";
|
||||||
import Gotify from "./Gotify.vue";
|
import Gotify from "./Gotify.vue";
|
||||||
import Slack from "./Slack.vue";
|
import HomeAssistant from "./HomeAssistant.vue";
|
||||||
import RocketChat from "./RocketChat.vue";
|
import Line from "./Line.vue";
|
||||||
import Teams from "./Teams.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 PagerDuty from "./PagerDuty.vue";
|
||||||
|
import PromoSMS from "./PromoSMS.vue";
|
||||||
|
import Pushbullet from "./Pushbullet.vue";
|
||||||
|
import PushDeer from "./PushDeer.vue";
|
||||||
import Pushover from "./Pushover.vue";
|
import Pushover from "./Pushover.vue";
|
||||||
import Pushy from "./Pushy.vue";
|
import Pushy from "./Pushy.vue";
|
||||||
import TechulusPush from "./TechulusPush.vue";
|
import RocketChat from "./RocketChat.vue";
|
||||||
import Octopush from "./Octopush.vue";
|
import ServerChan from "./ServerChan.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 DingDing from "./DingDing.vue";
|
|
||||||
import Bark from "./Bark.vue";
|
|
||||||
import SerwerSMS from "./SerwerSMS.vue";
|
import SerwerSMS from "./SerwerSMS.vue";
|
||||||
|
import Signal from "./Signal.vue";
|
||||||
|
import SMSManager from "./SMSManager.vue";
|
||||||
|
import Slack from "./Slack.vue";
|
||||||
|
import Squadcast from "./Squadcast.vue";
|
||||||
import Stackfield from "./Stackfield.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 WeCom from "./WeCom.vue";
|
||||||
import GoogleChat from "./GoogleChat.vue";
|
import GoAlert from "./GoAlert.vue";
|
||||||
import PagerDuty from "./PagerDuty.vue";
|
|
||||||
import Gorush from "./Gorush.vue";
|
|
||||||
import Alerta from "./Alerta.vue";
|
|
||||||
import OneBot from "./OneBot.vue";
|
|
||||||
import PushDeer from "./PushDeer.vue";
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Manage all notification form.
|
* Manage all notification form.
|
||||||
@@ -39,40 +47,48 @@ 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,
|
||||||
"PagerDuty": PagerDuty,
|
|
||||||
"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,
|
||||||
|
"squadcast": Squadcast,
|
||||||
|
"smtp": STMP,
|
||||||
|
"stackfield": Stackfield,
|
||||||
|
"teams": Teams,
|
||||||
|
"telegram": Telegram,
|
||||||
|
"webhook": Webhook,
|
||||||
|
"WeCom": WeCom,
|
||||||
|
"GoAlert": GoAlert,
|
||||||
|
"ServerChan": ServerChan,
|
||||||
};
|
};
|
||||||
|
|
||||||
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>
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user