mirror of
https://github.com/louislam/uptime-kuma.git
synced 2025-09-11 13:36:55 +08:00
Compare commits
359 Commits
1.15.0-bet
...
1.17.1
Author | SHA1 | Date | |
---|---|---|---|
|
0ecaa2cbd7 | ||
|
3c3dc05621 | ||
|
1f5466a3e8 | ||
|
d5da5af174 | ||
|
c36d9a4b8b | ||
|
a7063b8aca | ||
|
0a8046c98e | ||
|
7ba717ee55 | ||
|
ea400ac35f | ||
|
15db2c060d | ||
|
89717495dc | ||
|
7b710af12c | ||
|
5b278ca500 | ||
|
b97019eea8 | ||
|
d65abe5b8c | ||
|
bcd616a4d0 | ||
|
7533041696 | ||
|
7b0deb5e20 | ||
|
b4a4171178 | ||
|
012be23509 | ||
|
dd09351c8e | ||
|
ffad990ca4 | ||
|
f7afe121e3 | ||
|
42848bcd2e | ||
|
a3b94aa532 | ||
|
fdbdf83a0d | ||
|
81d5360520 | ||
|
8f1e193de3 | ||
|
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 | ||
|
69273a6c41 | ||
|
6424fe77ab | ||
|
fa60672cce | ||
|
6e43ef1dd3 | ||
|
f4f2b8ddb8 | ||
|
436bc13aeb | ||
|
b72a279361 | ||
|
a28ef56553 | ||
|
7f432bd916 | ||
|
f570d41142 | ||
|
d4485fe62f | ||
|
e1681ce370 | ||
|
69d6633e6d | ||
|
04e22f17a9 | ||
|
11243a6ca1 | ||
|
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 | ||
|
857e88b27e | ||
|
90fe25e8ad | ||
|
46a593534b | ||
|
7a4b54f4ee | ||
|
ea10d89f51 | ||
|
7f46223d68 | ||
|
df4ce811d9 | ||
|
30858ab038 | ||
|
e25d406fa5 | ||
|
10e16782b1 | ||
|
107a44885c | ||
|
ef9f66fad9 | ||
|
46dae99695 | ||
|
edd9bf3887 | ||
|
ab4edf2092 | ||
|
334cb57fed | ||
|
cfa5b551a5 | ||
|
46ee149b70 | ||
|
0a8c922abf | ||
|
058e5442af | ||
|
ea1725737f | ||
|
5566b038c8 | ||
|
5830f1e0b5 | ||
|
35b8e89457 | ||
|
d892b2c549 | ||
|
f23baf9c22 | ||
|
54184350a4 | ||
|
14dbe7c334 | ||
|
122e6a842b | ||
|
77ef22bdb4 | ||
|
59f983d506 | ||
|
71f031c14e | ||
|
6ae2a48584 | ||
|
7373747906 | ||
|
9d87f8d390 | ||
|
73b965c867 | ||
|
751e5ac477 | ||
|
93e5023ead | ||
|
b7ba6330db | ||
|
4c3aa20eb0 | ||
|
f779c6286a | ||
|
9fc5a3329f | ||
|
23c4ece2a5 | ||
|
175556f9fc | ||
|
398219f847 | ||
|
7a50f0e3f3 | ||
|
4178b003a2 | ||
|
8ede6d888f | ||
|
cec0521834 | ||
|
73b603dd10 | ||
|
92a43e1f30 | ||
|
0cf395dfc3 | ||
|
749ca6f4a8 | ||
|
ef73af391f | ||
|
44f6fca945 | ||
|
23ce7c6623 | ||
|
c346ea7864 | ||
|
f0ad32a252 | ||
|
5720017fb4 | ||
|
b7dc8e3ef8 | ||
|
5bba19f866 | ||
|
e198f2f1ab | ||
|
f91e5b98f9 | ||
|
87f933df4f | ||
|
332b9ab248 | ||
|
7cc89979f0 | ||
|
668e97c5a9 | ||
|
90473e7924 | ||
|
fdd781b081 | ||
|
373bd9b962 | ||
|
66971deaf4 | ||
|
59be9bb971 | ||
|
8077744c60 | ||
|
c5faf709b8 | ||
|
7da9f139c1 | ||
|
7acbfd2474 | ||
|
9f493bbec7 | ||
|
5bf58cc6c4 | ||
|
d344914ca0 | ||
|
201a25c659 | ||
|
b680371746 | ||
|
e488e2dc0a | ||
|
4e3258579d | ||
|
aa8ea6d398 | ||
|
cd3fbc80b4 | ||
|
bb7d67f717 | ||
|
8b0813ceff | ||
|
91178ce6a5 | ||
|
429ad384d0 | ||
|
24e52726b2 | ||
|
e0a0a5db4c | ||
|
cbfecab850 | ||
|
25cc54bf72 | ||
|
3700b16c5b | ||
|
4b9dc2890d | ||
|
f9004bcbed | ||
|
bc174c3325 | ||
|
4c2753af46 | ||
|
c6ba5b621c | ||
|
96536ae391 | ||
|
ba46544772 | ||
|
5c852db1cf | ||
|
069d3765f0 | ||
|
15820c6937 | ||
|
000cbeb0ce | ||
|
e118d59ac8 | ||
|
39aa0a7f07 | ||
|
a12dffd1bc | ||
|
410805052e | ||
|
02a8147f22 | ||
|
d962ab7a1c | ||
|
63c8d24d6f | ||
|
254a6bfd36 | ||
|
29f3cbe8c6 | ||
|
53b98ad3e4 | ||
|
dbd7c087e0 | ||
|
d0546afe71 | ||
|
272956025c | ||
|
db50ba91cc | ||
|
42ea3fb412 | ||
|
9f8b3151d8 | ||
|
73e38a13d2 | ||
|
f4515ad8c5 | ||
|
369477b4b9 | ||
|
2347a01f7c | ||
|
c114c053d6 | ||
|
ae2c49a729 | ||
|
b9e72b9645 | ||
|
5a069b278d | ||
|
65ea2e6aeb | ||
|
e82fc1df61 | ||
|
7dd5f5ea0d | ||
|
45da7c5431 | ||
|
26230a3d3a | ||
|
82aa52b330 | ||
|
fa7d15cf64 | ||
|
d7f16908d8 | ||
|
bddd5de22b | ||
|
6333231f1b | ||
|
60538036c6 | ||
|
0ba5d031d0 | ||
|
66e4c89897 | ||
|
d210548ae8 | ||
|
023db1450d | ||
|
824c16a07c | ||
|
09fdef9bdc | ||
|
7078b06272 | ||
|
d3bd2976c5 | ||
|
db646aa40b | ||
|
3c01e8732c | ||
|
b50f1bb7e8 | ||
|
a3baa3c149 | ||
|
2adb142ae2 | ||
|
752415dae6 | ||
|
1687de163c | ||
|
245b13d3c8 | ||
|
d6c3fdb6fb | ||
|
372bf57e9f | ||
|
39df4eea92 | ||
|
03e6f0a6c8 | ||
|
dcec53a755 | ||
|
ce17ed163e | ||
|
3019d5dd64 | ||
|
dcdbb7be8b | ||
|
b874ea8b28 | ||
|
1e595eaa76 | ||
|
5fbfacf5ce | ||
|
d39dc94496 | ||
|
94ada36dfa | ||
|
4114f43b48 | ||
|
4c8da89c36 | ||
|
db3ef3805b | ||
|
751924b335 | ||
|
edec1024b5 | ||
|
5f9f29f527 | ||
|
13a3dd91bb | ||
|
d1a3cd047a | ||
|
d9c5a7812c | ||
|
484d4a20ab | ||
|
3582e99770 | ||
|
2197b98444 | ||
|
b641c8a878 | ||
|
9130b3762c | ||
|
587faecf87 | ||
|
46da5e51be | ||
|
1eecdec2d9 | ||
|
e6a1719ab4 | ||
|
7d5e7a577d | ||
|
64a33d7455 | ||
|
09e61d9d63 | ||
|
9996ba1636 | ||
|
c2f6c5b42e | ||
|
0083485d4c | ||
|
630bb03d9c | ||
|
4ddbf71920 | ||
|
068b920553 | ||
|
f1c83bb838 | ||
|
303a226ab7 | ||
|
3d04befc1f | ||
|
d3f0bdb440 | ||
|
6d22ebedca | ||
|
e56ac7b03b | ||
|
aafcbaf098 | ||
|
4d4d04adbd | ||
|
03b2d8d521 | ||
|
f8c9472ea2 | ||
|
4e28ad4ac2 | ||
|
06f326e49e | ||
|
07c0801ad5 | ||
|
8cefc96c78 | ||
|
b326a69838 | ||
|
e103ac8335 | ||
|
a391576285 | ||
|
e0966e55c8 | ||
|
59d9891105 | ||
|
f8f19d8dc5 | ||
|
a3d79a93e9 | ||
|
bdc23a3f57 | ||
|
18925293fb | ||
|
45f44b183d | ||
|
5a209c74e1 | ||
|
60c63cc18e | ||
|
6fb66728e6 | ||
|
a680331dd7 | ||
|
288ed1e3ca | ||
|
8c8eeaf627 | ||
|
b893d50e45 | ||
|
0765f05090 | ||
|
2638d68c97 | ||
|
e38742a2d0 | ||
|
1b1e0f6dd9 | ||
|
0961c6d9b3 | ||
|
ce7d8c38c5 | ||
|
a4be651118 | ||
|
454c1687cf | ||
|
244a7b3671 | ||
|
28be32fc68 | ||
|
ee90d2713f | ||
|
dd3992063e | ||
|
0313acd4c5 | ||
|
cd19b9fc49 | ||
|
c57b2c4d28 | ||
|
3dda5938f2 | ||
|
f00ec4dfef | ||
|
43f8fc701c | ||
|
499042504f | ||
|
faf6719e7c | ||
|
a9d264ccfc | ||
|
df8f93f0c2 | ||
|
28c0e16a0c | ||
|
6acc9546a0 | ||
|
f455e3a454 | ||
|
7abbf421d0 | ||
|
3625915a85 | ||
|
d74404e106 | ||
|
1c5bce8afa | ||
|
8b5997691e | ||
|
35360e2069 | ||
|
3d002b3ce9 |
@@ -40,14 +40,15 @@ module.exports = {
|
|||||||
SwitchCase: 1,
|
SwitchCase: 1,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
quotes: [ "warn", "double" ],
|
quotes: [ "error", "double" ],
|
||||||
semi: "error",
|
semi: "error",
|
||||||
"vue/html-indent": [ "warn", 4 ], // default: 2
|
"vue/html-indent": [ "error", 4 ], // default: 2
|
||||||
"vue/max-attributes-per-line": "off",
|
"vue/max-attributes-per-line": "off",
|
||||||
"vue/singleline-html-element-content-newline": "off",
|
"vue/singleline-html-element-content-newline": "off",
|
||||||
"vue/html-self-closing": "off",
|
"vue/html-self-closing": "off",
|
||||||
"vue/require-component-is": "off", // not allow is="style" https://github.com/vuejs/eslint-plugin-vue/issues/462#issuecomment-430234675
|
"vue/require-component-is": "off", // not allow is="style" https://github.com/vuejs/eslint-plugin-vue/issues/462#issuecomment-430234675
|
||||||
"vue/attribute-hyphenation": "off", // This change noNL to "no-n-l" unexpectedly
|
"vue/attribute-hyphenation": "off", // This change noNL to "no-n-l" unexpectedly
|
||||||
|
"vue/multi-word-component-names": "off",
|
||||||
"no-multi-spaces": [ "error", {
|
"no-multi-spaces": [ "error", {
|
||||||
ignoreEOLComments: true,
|
ignoreEOLComments: true,
|
||||||
}],
|
}],
|
||||||
@@ -72,7 +73,7 @@ module.exports = {
|
|||||||
"keyword-spacing": "warn",
|
"keyword-spacing": "warn",
|
||||||
"space-infix-ops": "warn",
|
"space-infix-ops": "warn",
|
||||||
"arrow-spacing": "warn",
|
"arrow-spacing": "warn",
|
||||||
"no-trailing-spaces": "warn",
|
"no-trailing-spaces": "error",
|
||||||
"no-constant-condition": [ "error", {
|
"no-constant-condition": [ "error", {
|
||||||
"checkLoops": false,
|
"checkLoops": false,
|
||||||
}],
|
}],
|
||||||
|
3
.github/PULL_REQUEST_TEMPLATE.md
vendored
3
.github/PULL_REQUEST_TEMPLATE.md
vendored
@@ -1,3 +1,5 @@
|
|||||||
|
👉 Delete this line if you have read and agree our pull request rules and guidelines: https://github.com/louislam/uptime-kuma/blob/master/CONTRIBUTING.md#can-i-create-a-pull-request-for-uptime-kuma
|
||||||
|
|
||||||
# Description
|
# Description
|
||||||
|
|
||||||
Fixes #(issue)
|
Fixes #(issue)
|
||||||
@@ -20,6 +22,7 @@ Please delete any options that are not relevant.
|
|||||||
- [ ] I ran ESLint and other linters for modified files
|
- [ ] I ran ESLint and other linters for modified files
|
||||||
- [ ] I have performed a self-review of my own code and tested it
|
- [ ] I have performed a self-review of my own code and tested it
|
||||||
- [ ] I have commented my code, particularly in hard-to-understand areas
|
- [ ] I have commented my code, particularly in hard-to-understand areas
|
||||||
|
(including JSDoc for methods)
|
||||||
- [ ] My changes generate no new warnings
|
- [ ] My changes generate no new warnings
|
||||||
- [ ] My code needed automated testing. I have added them (this is optional task)
|
- [ ] My code needed automated testing. I have added them (this is optional task)
|
||||||
|
|
||||||
|
28
.github/workflows/auto-test.yml
vendored
28
.github/workflows/auto-test.yml
vendored
@@ -11,26 +11,42 @@ on:
|
|||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
auto-test:
|
auto-test:
|
||||||
|
needs: [ check-linters ]
|
||||||
runs-on: ${{ matrix.os }}
|
runs-on: ${{ matrix.os }}
|
||||||
|
timeout-minutes: 15
|
||||||
|
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
os: [macos-latest, ubuntu-latest, windows-latest]
|
os: [macos-latest, ubuntu-latest, windows-latest]
|
||||||
node-version: [14.x, 16.x, 17.x]
|
node: [ 14, 16, 17, 18 ]
|
||||||
# See supported Node.js release schedule at https://nodejs.org/en/about/releases/
|
# See supported Node.js release schedule at https://nodejs.org/en/about/releases/
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- run: git config --global core.autocrlf false # Mainly for Windows
|
- run: git config --global core.autocrlf false # Mainly for Windows
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v3
|
||||||
|
|
||||||
- name: Use Node.js ${{ matrix.node-version }}
|
- name: Use Node.js ${{ matrix.node }}
|
||||||
uses: actions/setup-node@v2
|
uses: actions/setup-node@v3
|
||||||
with:
|
with:
|
||||||
node-version: ${{ matrix.node-version }}
|
node-version: ${{ matrix.node }}
|
||||||
cache: 'npm'
|
cache: 'npm'
|
||||||
- run: npm run install-legacy
|
- run: npm install
|
||||||
- run: npm run build
|
- run: npm run build
|
||||||
- run: npm test
|
- run: npm test
|
||||||
env:
|
env:
|
||||||
HEADLESS_TEST: 1
|
HEADLESS_TEST: 1
|
||||||
JUST_FOR_TEST: ${{ secrets.JUST_FOR_TEST }}
|
JUST_FOR_TEST: ${{ secrets.JUST_FOR_TEST }}
|
||||||
|
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 lint
|
||||||
|
@@ -27,24 +27,33 @@ 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?
|
||||||
|
|
||||||
⚠️ 2022-03-02 Update:
|
Yes, you can. However, since I don't want to waste your time, be sure to **create empty draft pull request, so we can discuss first** if it is a large pull request or you don't know it will be merged or not.
|
||||||
|
|
||||||
Since I found that merging pull requests is a pretty heavy task for me, I try to rearrange 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.
|
||||||
|
|
||||||
|
I will mark your pull request in the [milestones](https://github.com/louislam/uptime-kuma/milestones), if I am plan to review and merge it.
|
||||||
|
|
||||||
✅ Accept:
|
✅ Accept:
|
||||||
- Bug/Security fix
|
- Bug/Security fix
|
||||||
- Translations
|
- Translations
|
||||||
- Adding notification providers
|
- Adding notification providers
|
||||||
|
|
||||||
❌ Avoid:
|
⚠️ Discussion First
|
||||||
- Large pull requests
|
- Large pull requests
|
||||||
- New big features
|
- New features
|
||||||
|
|
||||||
|
❌ Won't Merge
|
||||||
|
- Do not pass auto test
|
||||||
|
- Any breaking changes
|
||||||
|
- Duplicated pull request
|
||||||
|
- Buggy
|
||||||
|
- Existing logic is completely modified or deleted for no reason
|
||||||
|
- A function that is completely out of scope
|
||||||
|
|
||||||
My long story here: https://www.reddit.com/r/UptimeKuma/comments/t1t6or/comment/hynyijx/
|
|
||||||
|
|
||||||
### Recommended Pull Request Guideline
|
### Recommended Pull Request Guideline
|
||||||
|
|
||||||
Before deep into coding, disscussion first is preferred. Creating an empty pull request for disscussion would be recommended.
|
Before deep into coding, discussion first is preferred. Creating an empty pull request for discussion would be recommended.
|
||||||
|
|
||||||
1. Fork the project
|
1. Fork the project
|
||||||
1. Clone your fork repo to local
|
1. Clone your fork repo to local
|
||||||
@@ -57,41 +66,36 @@ Before deep into coding, disscussion first is preferred. Creating an empty pull
|
|||||||
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
|
||||||
|
|
||||||
- 4 spaces indentation
|
- 4 spaces indentation
|
||||||
- Follow `.editorconfig`
|
- Follow `.editorconfig`
|
||||||
- Follow ESLint
|
- Follow ESLint
|
||||||
|
- Methods and functions should be documented with JSDoc
|
||||||
|
|
||||||
## 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
|
||||||
|
|
||||||
- Node.js >= 14
|
- Node.js >= 14
|
||||||
|
- NPM >= 8.5
|
||||||
- Git
|
- Git
|
||||||
- IDE that supports ESLint and EditorConfig (I am using IntelliJ IDEA)
|
- IDE that supports ESLint and EditorConfig (I am using IntelliJ IDEA)
|
||||||
- A SQLite tool (SQLite Expert Personal is suggested)
|
- A SQLite GUI tool (SQLite Expert Personal is suggested)
|
||||||
|
|
||||||
## Install dependencies
|
## Install dependencies
|
||||||
|
|
||||||
@@ -99,39 +103,45 @@ I personally do not like something need to learn so much and need to config so m
|
|||||||
npm ci
|
npm ci
|
||||||
```
|
```
|
||||||
|
|
||||||
## How to start the Backend Dev Server
|
## Dev Server
|
||||||
|
|
||||||
(2021-09-23 Update)
|
(2022-04-26 Update)
|
||||||
|
|
||||||
|
We can start the frontend dev server and the backend dev server in one command.
|
||||||
|
|
||||||
|
Port `3000` and port `3001` will be used.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
npm run start-server-dev
|
npm run dev
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Backend Server
|
||||||
|
|
||||||
It binds to `0.0.0.0:3001` by default.
|
It binds to `0.0.0.0:3001` by default.
|
||||||
|
|
||||||
### Backend Details
|
|
||||||
|
|
||||||
It is mainly a socket.io app + express.js.
|
It is mainly a socket.io app + express.js.
|
||||||
|
|
||||||
express.js is just used for serving the frontend built files (index.html, .js and .css etc.)
|
express.js is used for:
|
||||||
|
- entry point such as redirecting to a status page or the dashboard
|
||||||
|
- serving the frontend built files (index.html, .js and .css etc.)
|
||||||
|
- serving internal APIs of status page
|
||||||
|
|
||||||
|
|
||||||
|
### Structure in /server/
|
||||||
|
|
||||||
- model/ (Object model, auto mapping to the database table name)
|
- model/ (Object model, auto mapping to the database table name)
|
||||||
- modules/ (Modified 3rd-party modules)
|
- modules/ (Modified 3rd-party modules)
|
||||||
- notification-providers/ (individual notification logic)
|
- notification-providers/ (individual notification logic)
|
||||||
- routers/ (Express Routers)
|
- routers/ (Express Routers)
|
||||||
- socket-handler (Socket.io Handlers)
|
- socket-handler (Socket.io Handlers)
|
||||||
- server.js (Server main logic)
|
- server.js (Server entry point and main logic)
|
||||||
|
|
||||||
## How to start the Frontend Dev Server
|
## Frontend Dev Server
|
||||||
|
|
||||||
1. Set the env var `NODE_ENV` to "development".
|
It binds to `0.0.0.0:3000` by default. Frontend dev server is used for development only.
|
||||||
2. Start the frontend dev server by the following command.
|
|
||||||
|
|
||||||
```bash
|
For production, it is not used. It will be compiled to `dist` directory instead.
|
||||||
npm run dev
|
|
||||||
```
|
|
||||||
|
|
||||||
It binds to `0.0.0.0:3000` by default.
|
|
||||||
|
|
||||||
You can use Vue.js devtools Chrome extension for debugging.
|
You can use Vue.js devtools Chrome extension for debugging.
|
||||||
|
|
||||||
|
15
README.md
15
README.md
@@ -25,12 +25,15 @@ VPS is sponsored by Uptime Kuma sponsors on [Open Collective](https://opencollec
|
|||||||
|
|
||||||
* 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.
|
||||||
* Fancy, Reactive, Fast UI/UX.
|
* Fancy, Reactive, Fast UI/UX.
|
||||||
* Notifications via Telegram, Discord, Gotify, Slack, Pushover, Email (SMTP), and [70+ notification services, click here for the full list](https://github.com/louislam/uptime-kuma/tree/master/src/components/notifications).
|
* Notifications via Telegram, Discord, Gotify, Slack, Pushover, Email (SMTP), and [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.
|
||||||
* [Multi Languages](https://github.com/louislam/uptime-kuma/tree/master/src/languages)
|
* [Multi Languages](https://github.com/louislam/uptime-kuma/tree/master/src/languages)
|
||||||
* Simple Status Page
|
* Multiple Status Pages
|
||||||
|
* Map Status Page to Domain
|
||||||
* Ping Chart
|
* Ping Chart
|
||||||
* Certificate Info
|
* Certificate Info
|
||||||
|
* Proxy Support
|
||||||
|
* 2FA available
|
||||||
|
|
||||||
## 🔧 How to Install
|
## 🔧 How to Install
|
||||||
|
|
||||||
@@ -154,17 +157,17 @@ https://www.reddit.com/r/UptimeKuma/
|
|||||||
|
|
||||||
## Contribute
|
## Contribute
|
||||||
|
|
||||||
### Test Beta Version
|
### 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
|
||||||
|
|
||||||
### Bug Reports / Feature Requests
|
### Bug Reports / Feature Requests
|
||||||
If you want to report a bug or request a new feature. Free feel to open a [new issue](https://github.com/louislam/uptime-kuma/issues).
|
If you want to report a bug or request a new feature, feel free to open a [new issue](https://github.com/louislam/uptime-kuma/issues).
|
||||||
|
|
||||||
## Translations
|
### Translations
|
||||||
If you want to translate Uptime Kuma into your language, please read: https://github.com/louislam/uptime-kuma/tree/master/src/languages
|
If you want to translate Uptime Kuma into your language, please read: https://github.com/louislam/uptime-kuma/tree/master/src/languages
|
||||||
|
|
||||||
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
|
### Pull Requests
|
||||||
If you want to modify Uptime Kuma, this guideline may be useful for you: https://github.com/louislam/uptime-kuma/blob/master/CONTRIBUTING.md
|
If you want to modify Uptime Kuma, this guideline may be useful for you: https://github.com/louislam/uptime-kuma/blob/master/CONTRIBUTING.md
|
||||||
|
12
SECURITY.md
12
SECURITY.md
@@ -8,15 +8,9 @@ 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
|
||||||
|
|
||||||
| Version | Supported |
|
You should use or upgrade to the latest version of Uptime Kuma. All `1.X.X` versions are upgradable to the lastest version.
|
||||||
| ------- | ------------------ |
|
|
||||||
| 1.9.X | :white_check_mark: |
|
|
||||||
| <= 1.8.X | ❌ |
|
|
||||||
|
|
||||||
### Upgradable Docker Tags
|
### Upgradable Docker Tags
|
||||||
|
|
||||||
@@ -24,8 +18,8 @@ currently being supported with security updates.
|
|||||||
| ------- | ------------------ |
|
| ------- | ------------------ |
|
||||||
| 1 | :white_check_mark: |
|
| 1 | :white_check_mark: |
|
||||||
| 1-debian | :white_check_mark: |
|
| 1-debian | :white_check_mark: |
|
||||||
| 1-alpine | :white_check_mark: |
|
|
||||||
| latest | :white_check_mark: |
|
| latest | :white_check_mark: |
|
||||||
| debian | :white_check_mark: |
|
| debian | :white_check_mark: |
|
||||||
| alpine | :white_check_mark: |
|
| 1-alpine | ⚠️ Deprecated |
|
||||||
|
| alpine | ⚠️ Deprecated |
|
||||||
| All other tags | ❌ |
|
| All other tags | ❌ |
|
||||||
|
@@ -1,18 +1,32 @@
|
|||||||
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({
|
||||||
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 +35,13 @@ export default defineConfig({
|
|||||||
"plugins": [ postcssRTLCSS ]
|
"plugins": [ postcssRTLCSS ]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
build: {
|
||||||
|
rollupOptions: {
|
||||||
|
output: {
|
||||||
|
manualChunks(id, { getModuleInfo, getModuleIds }) {
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
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;
|
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
|
@@ -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.7 && \
|
pip3 --no-cache-dir install apprise==0.9.8.3 && \
|
||||||
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.7 && \
|
pip3 --no-cache-dir install apprise==0.9.8.3 && \
|
||||||
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
|
||||||
|
|
||||||
|
@@ -8,7 +8,7 @@ services:
|
|||||||
image: louislam/uptime-kuma:1
|
image: louislam/uptime-kuma:1
|
||||||
container_name: uptime-kuma
|
container_name: uptime-kuma
|
||||||
volumes:
|
volumes:
|
||||||
- ./uptime-kuma:/app/data
|
- ./uptime-kuma-data:/app/data
|
||||||
ports:
|
ports:
|
||||||
- 3001:3001
|
- 3001:3001 # <Host Port>:<Container Port>
|
||||||
restart: always
|
restart: always
|
||||||
|
@@ -20,6 +20,10 @@ if (! exists) {
|
|||||||
// Process package.json
|
// Process package.json
|
||||||
pkg.version = version;
|
pkg.version = version;
|
||||||
fs.writeFileSync("package.json", JSON.stringify(pkg, null, 4) + "\n");
|
fs.writeFileSync("package.json", JSON.stringify(pkg, null, 4) + "\n");
|
||||||
|
|
||||||
|
// Also update package-lock.json
|
||||||
|
childProcess.spawnSync("npm", [ "install" ]);
|
||||||
|
|
||||||
commit(version);
|
commit(version);
|
||||||
tag(version);
|
tag(version);
|
||||||
|
|
||||||
|
@@ -25,6 +25,9 @@ if (! exists) {
|
|||||||
pkg.scripts.setup = pkg.scripts.setup.replace(/(git checkout )([^\s]+)/, `$1${newVersion}`);
|
pkg.scripts.setup = pkg.scripts.setup.replace(/(git checkout )([^\s]+)/, `$1${newVersion}`);
|
||||||
fs.writeFileSync("package.json", JSON.stringify(pkg, null, 4) + "\n");
|
fs.writeFileSync("package.json", JSON.stringify(pkg, null, 4) + "\n");
|
||||||
|
|
||||||
|
// Also update package-lock.json
|
||||||
|
childProcess.spawnSync("npm", [ "install" ]);
|
||||||
|
|
||||||
commit(newVersion);
|
commit(newVersion);
|
||||||
tag(newVersion);
|
tag(newVersion);
|
||||||
|
|
||||||
|
5167
package-lock.json
generated
5167
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
54
package.json
54
package.json
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "uptime-kuma",
|
"name": "uptime-kuma",
|
||||||
"version": "1.15.0-beta.1",
|
"version": "1.17.1",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
@@ -10,18 +10,20 @@
|
|||||||
"node": "14.* || >=16.*"
|
"node": "14.* || >=16.*"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"install-legacy": "npm install --legacy-peer-deps",
|
"install-legacy": "npm install",
|
||||||
"update-legacy": "npm update --legacy-peer-deps",
|
"update-legacy": "npm update",
|
||||||
"lint:js": "eslint --ext \".js,.vue\" --ignore-path .gitignore .",
|
"lint:js": "eslint --ext \".js,.vue\" --ignore-path .gitignore .",
|
||||||
"lint-fix:js": "eslint --ext \".js,.vue\" --fix --ignore-path .gitignore .",
|
"lint-fix:js": "eslint --ext \".js,.vue\" --fix --ignore-path .gitignore .",
|
||||||
"lint:style": "stylelint \"**/*.{vue,css,scss}\" --ignore-path .gitignore",
|
"lint:style": "stylelint \"**/*.{vue,css,scss}\" --ignore-path .gitignore",
|
||||||
|
"lint-fix:style": "stylelint \"**/*.{vue,css,scss}\" --fix --ignore-path .gitignore",
|
||||||
"lint": "npm run lint:js && npm run lint:style",
|
"lint": "npm run lint:js && npm run lint:style",
|
||||||
"dev": "vite --host --config ./config/vite.config.js",
|
"dev": "concurrently -k -r \"wait-on tcp:3000 && npm run start-server-dev \" \"npm run start-frontend-dev\"",
|
||||||
|
"start-frontend-dev": "cross-env NODE_ENV=development vite --host --config ./config/vite.config.js",
|
||||||
"start": "npm run start-server",
|
"start": "npm run start-server",
|
||||||
"start-server": "node server/server.js",
|
"start-server": "node server/server.js",
|
||||||
"start-server-dev": "cross-env NODE_ENV=development node server/server.js",
|
"start-server-dev": "cross-env NODE_ENV=development node server/server.js",
|
||||||
"build": "vite build --config ./config/vite.config.js",
|
"build": "vite build --config ./config/vite.config.js",
|
||||||
"test": "npm run lint && node test/prepare-test-server.js && node server/server.js --port=3002 --data-dir=./data/test/ --test",
|
"test": "node test/prepare-test-server.js && node server/server.js --port=3002 --data-dir=./data/test/ --test",
|
||||||
"test-with-build": "npm run build && npm test",
|
"test-with-build": "npm run build && npm test",
|
||||||
"jest": "node test/prepare-jest.js && npm run jest-frontend && npm run jest-backend",
|
"jest": "node test/prepare-jest.js && npm run jest-frontend && npm run jest-backend",
|
||||||
"jest-frontend": "cross-env TEST_FRONTEND=1 jest --config=./config/jest-frontend.config.js",
|
"jest-frontend": "cross-env TEST_FRONTEND=1 jest --config=./config/jest-frontend.config.js",
|
||||||
@@ -37,7 +39,7 @@
|
|||||||
"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",
|
||||||
"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.14.1 && npm ci --production && npm run download-dist",
|
"setup": "git checkout 1.17.1 && 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",
|
||||||
@@ -55,17 +57,20 @@
|
|||||||
"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"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@fortawesome/fontawesome-svg-core": "~1.2.36",
|
"@fortawesome/fontawesome-svg-core": "~1.2.36",
|
||||||
"@fortawesome/free-regular-svg-icons": "~5.15.4",
|
"@fortawesome/free-regular-svg-icons": "~5.15.4",
|
||||||
"@fortawesome/free-solid-svg-icons": "~5.15.4",
|
"@fortawesome/free-solid-svg-icons": "~5.15.4",
|
||||||
"@fortawesome/vue-fontawesome": "~3.0.0-5",
|
"@fortawesome/vue-fontawesome": "~3.0.0-5",
|
||||||
"@louislam/sqlite3": "~15.0.3",
|
"@louislam/sqlite3": "~15.0.6",
|
||||||
"@popperjs/core": "~2.10.2",
|
"@popperjs/core": "~2.10.2",
|
||||||
"args-parser": "~1.3.0",
|
"args-parser": "~1.3.0",
|
||||||
"axios": "~0.26.1",
|
"axios": "~0.26.1",
|
||||||
|
"axios-ntlm": "^1.3.0",
|
||||||
|
"badge-maker": "^3.3.1",
|
||||||
"bcryptjs": "~2.4.3",
|
"bcryptjs": "~2.4.3",
|
||||||
"bootstrap": "5.1.3",
|
"bootstrap": "5.1.3",
|
||||||
"bree": "~7.1.5",
|
"bree": "~7.1.5",
|
||||||
@@ -73,11 +78,15 @@
|
|||||||
"chart.js": "~3.6.2",
|
"chart.js": "~3.6.2",
|
||||||
"chartjs-adapter-dayjs": "~1.0.0",
|
"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",
|
||||||
"command-exists": "~1.2.9",
|
"command-exists": "~1.2.9",
|
||||||
"compare-versions": "~3.6.0",
|
"compare-versions": "~3.6.0",
|
||||||
"dayjs": "~1.10.8",
|
"compression": "^1.7.4",
|
||||||
|
"dayjs": "^1.11.0",
|
||||||
"express": "~4.17.3",
|
"express": "~4.17.3",
|
||||||
"express-basic-auth": "~1.2.1",
|
"express-basic-auth": "~1.2.1",
|
||||||
|
"express-static-gzip": "^2.1.7",
|
||||||
"favico.js": "^0.3.10",
|
"favico.js": "^0.3.10",
|
||||||
"form-data": "~4.0.0",
|
"form-data": "~4.0.0",
|
||||||
"http-graceful-shutdown": "~3.1.7",
|
"http-graceful-shutdown": "~3.1.7",
|
||||||
@@ -88,6 +97,7 @@
|
|||||||
"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",
|
||||||
"nodemailer": "~6.6.5",
|
"nodemailer": "~6.6.5",
|
||||||
"notp": "~2.0.3",
|
"notp": "~2.0.3",
|
||||||
@@ -98,7 +108,7 @@
|
|||||||
"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",
|
"qrcode": "~1.5.0",
|
||||||
"redbean-node": "0.1.3",
|
"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",
|
||||||
@@ -122,28 +132,32 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@actions/github": "~5.0.1",
|
"@actions/github": "~5.0.1",
|
||||||
"@babel/eslint-parser": "~7.15.8",
|
"@babel/eslint-parser": "~7.17.0",
|
||||||
"@babel/preset-env": "^7.15.8",
|
"@babel/preset-env": "^7.15.8",
|
||||||
"@types/bootstrap": "~5.1.9",
|
"@types/bootstrap": "~5.1.9",
|
||||||
"@vitejs/plugin-legacy": "~1.6.4",
|
"@vitejs/plugin-legacy": "~1.8.2",
|
||||||
"@vitejs/plugin-vue": "~1.9.4",
|
"@vitejs/plugin-vue": "~2.3.3",
|
||||||
"@vue/compiler-sfc": "~3.2.31",
|
"@vue/compiler-sfc": "~3.2.36",
|
||||||
"aedes": "^0.46.3",
|
"aedes": "^0.46.3",
|
||||||
"babel-plugin-rewire": "~1.2.0",
|
"babel-plugin-rewire": "~1.2.0",
|
||||||
|
"concurrently": "^7.1.0",
|
||||||
"core-js": "~3.18.3",
|
"core-js": "~3.18.3",
|
||||||
"cross-env": "~7.0.3",
|
"cross-env": "~7.0.3",
|
||||||
"dns2": "~2.0.1",
|
"dns2": "~2.0.1",
|
||||||
"eslint": "~7.32.0",
|
"eslint": "~8.14.0",
|
||||||
"eslint-plugin-vue": "~7.18.0",
|
"eslint-plugin-vue": "~8.7.1",
|
||||||
"jest": "~27.2.5",
|
"jest": "~27.2.5",
|
||||||
"jest-puppeteer": "~6.0.3",
|
"jest-puppeteer": "~6.0.3",
|
||||||
"npm-check-updates": "^12.5.5",
|
"npm-check-updates": "^12.5.9",
|
||||||
"postcss-html": "^1.3.1",
|
"postcss-html": "^1.3.1",
|
||||||
"puppeteer": "~13.1.3",
|
"puppeteer": "~13.1.3",
|
||||||
|
"rollup-plugin-visualizer": "^5.6.0",
|
||||||
"sass": "~1.42.1",
|
"sass": "~1.42.1",
|
||||||
"stylelint": "~14.2.0",
|
"stylelint": "~14.7.1",
|
||||||
"stylelint-config-standard": "~24.0.0",
|
"stylelint-config-standard": "~25.0.0",
|
||||||
"typescript": "~4.4.4",
|
"typescript": "~4.4.4",
|
||||||
"vite": "~2.6.14"
|
"vite": "~2.9.9",
|
||||||
|
"vite-plugin-compression": "^0.5.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: 6.4 KiB |
@@ -2,6 +2,11 @@ const { R } = require("redbean-node");
|
|||||||
|
|
||||||
class TwoFA {
|
class TwoFA {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Disable 2FA for specified user
|
||||||
|
* @param {number} userID ID of user to disable
|
||||||
|
* @returns {Promise<void>}
|
||||||
|
*/
|
||||||
static async disable2FA(userID) {
|
static async disable2FA(userID) {
|
||||||
return await R.exec("UPDATE `user` SET twofa_status = 0 WHERE id = ? ", [
|
return await R.exec("UPDATE `user` SET twofa_status = 0 WHERE id = ? ", [
|
||||||
userID,
|
userID,
|
||||||
|
@@ -5,10 +5,10 @@ const { setting } = require("./util-server");
|
|||||||
const { loginRateLimiter } = require("./rate-limiter");
|
const { loginRateLimiter } = require("./rate-limiter");
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
* Login to web app
|
||||||
* @param username : string
|
* @param {string} username
|
||||||
* @param password : string
|
* @param {string} password
|
||||||
* @returns {Promise<Bean|null>}
|
* @returns {Promise<(Bean|null)>}
|
||||||
*/
|
*/
|
||||||
exports.login = async function (username, password) {
|
exports.login = async function (username, password) {
|
||||||
if (typeof username !== "string" || typeof password !== "string") {
|
if (typeof username !== "string" || typeof password !== "string") {
|
||||||
@@ -34,11 +34,17 @@ exports.login = async function (username, password) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A function that checks if a user is logged in.
|
* Callback for myAuthorizer
|
||||||
* @param {string} username The username of the user to check for.
|
* @callback myAuthorizerCB
|
||||||
* @param {function} callback The callback to call when done, with an error and result parameter.
|
* @param {any} err Any error encountered
|
||||||
*
|
* @param {boolean} authorized Is the client authorized?
|
||||||
* Generated by Trelent
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Custom authorizer for express-basic-auth
|
||||||
|
* @param {string} username
|
||||||
|
* @param {string} password
|
||||||
|
* @param {myAuthorizerCB} callback
|
||||||
*/
|
*/
|
||||||
function myAuthorizer(username, password, callback) {
|
function myAuthorizer(username, password, callback) {
|
||||||
// Login Rate Limit
|
// Login Rate Limit
|
||||||
|
@@ -7,6 +7,7 @@ exports.latestVersion = null;
|
|||||||
|
|
||||||
let interval;
|
let interval;
|
||||||
|
|
||||||
|
/** Start 48 hour check interval */
|
||||||
exports.startInterval = () => {
|
exports.startInterval = () => {
|
||||||
let check = async () => {
|
let check = async () => {
|
||||||
try {
|
try {
|
||||||
@@ -42,6 +43,11 @@ exports.startInterval = () => {
|
|||||||
interval = setInterval(check, 3600 * 1000 * 48);
|
interval = setInterval(check, 3600 * 1000 * 48);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Enable the check update feature
|
||||||
|
* @param {boolean} value Should the check update feature be enabled?
|
||||||
|
* @returns {Promise<void>}
|
||||||
|
*/
|
||||||
exports.enableCheckUpdate = async (value) => {
|
exports.enableCheckUpdate = async (value) => {
|
||||||
await setSetting("checkUpdate", value);
|
await setSetting("checkUpdate", value);
|
||||||
|
|
||||||
|
@@ -9,10 +9,9 @@ const { setting } = require("./util-server");
|
|||||||
const checkVersion = require("./check-version");
|
const checkVersion = require("./check-version");
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Send a list of notifications to the user.
|
* Send list of notification providers to client
|
||||||
* @param {Socket} socket The socket object that is connected to the client.
|
* @param {Socket} socket Socket.io socket instance
|
||||||
*
|
* @returns {Promise<Bean[]>}
|
||||||
* Generated by Trelent
|
|
||||||
*/
|
*/
|
||||||
async function sendNotificationList(socket) {
|
async function sendNotificationList(socket) {
|
||||||
const timeLogger = new TimeLogger();
|
const timeLogger = new TimeLogger();
|
||||||
@@ -23,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);
|
||||||
@@ -35,8 +37,11 @@ async function sendNotificationList(socket) {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Send Heartbeat History list to socket
|
* Send Heartbeat History list to socket
|
||||||
* @param toUser True = send to all browsers with the same user id, False = send to the current browser only
|
* @param {Socket} socket Socket.io instance
|
||||||
* @param overwrite Overwrite client-side's heartbeat list
|
* @param {number} monitorID ID of monitor to send heartbeat history
|
||||||
|
* @param {boolean} [toUser=false] True = send to all browsers with the same user id, False = send to the current browser only
|
||||||
|
* @param {boolean} [overwrite=false] Overwrite client-side's heartbeat list
|
||||||
|
* @returns {Promise<void>}
|
||||||
*/
|
*/
|
||||||
async function sendHeartbeatList(socket, monitorID, toUser = false, overwrite = false) {
|
async function sendHeartbeatList(socket, monitorID, toUser = false, overwrite = false) {
|
||||||
const timeLogger = new TimeLogger();
|
const timeLogger = new TimeLogger();
|
||||||
@@ -62,11 +67,12 @@ async function sendHeartbeatList(socket, monitorID, toUser = false, overwrite =
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Important Heart beat list (aka event list)
|
* Important Heart beat list (aka event list)
|
||||||
* @param socket
|
* @param {Socket} socket Socket.io instance
|
||||||
* @param monitorID
|
* @param {number} monitorID ID of monitor to send heartbeat history
|
||||||
* @param toUser True = send to all browsers with the same user id, False = send to the current browser only
|
* @param {boolean} [toUser=false] True = send to all browsers with the same user id, False = send to the current browser only
|
||||||
* @param overwrite Overwrite client-side's heartbeat list
|
* @param {boolean} [overwrite=false] Overwrite client-side's heartbeat list
|
||||||
|
* @returns {Promise<void>}
|
||||||
*/
|
*/
|
||||||
async function sendImportantHeartbeatList(socket, monitorID, toUser = false, overwrite = false) {
|
async function sendImportantHeartbeatList(socket, monitorID, toUser = false, overwrite = false) {
|
||||||
const timeLogger = new TimeLogger();
|
const timeLogger = new TimeLogger();
|
||||||
@@ -91,9 +97,8 @@ async function sendImportantHeartbeatList(socket, monitorID, toUser = false, ove
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Delivers proxy list
|
* Emit proxy list to client
|
||||||
*
|
* @param {Socket} socket Socket.io socket instance
|
||||||
* @param socket
|
|
||||||
* @return {Promise<Bean[]>}
|
* @return {Promise<Bean[]>}
|
||||||
*/
|
*/
|
||||||
async function sendProxyList(socket) {
|
async function sendProxyList(socket) {
|
||||||
@@ -109,9 +114,8 @@ async function sendProxyList(socket) {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Emits the version information to the client.
|
* Emits the version information to the client.
|
||||||
* @param {Socket} socket The socket object that is connected to the client.
|
* @param {Socket} socket Socket.io socket instance
|
||||||
*
|
* @returns {Promise<void>}
|
||||||
* Generated by Trelent
|
|
||||||
*/
|
*/
|
||||||
async function sendInfo(socket) {
|
async function sendInfo(socket) {
|
||||||
socket.emit("info", {
|
socket.emit("info", {
|
||||||
|
@@ -1,7 +1,20 @@
|
|||||||
const args = require("args-parser")(process.argv);
|
const args = require("args-parser")(process.argv);
|
||||||
const demoMode = args["demo"] || false;
|
const demoMode = args["demo"] || false;
|
||||||
|
|
||||||
|
const badgeConstants = {
|
||||||
|
naColor: "#999",
|
||||||
|
defaultUpColor: "#66c20a",
|
||||||
|
defaultDownColor: "#c2290a",
|
||||||
|
defaultPingColor: "blue", // as defined by badge-maker / shields.io
|
||||||
|
defaultStyle: "flat",
|
||||||
|
defaultPingValueSuffix: "ms",
|
||||||
|
defaultPingLabelSuffix: "h",
|
||||||
|
defaultUptimeValueSuffix: "%",
|
||||||
|
defaultUptimeLabelSuffix: "h",
|
||||||
|
};
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
args,
|
args,
|
||||||
demoMode
|
demoMode,
|
||||||
|
badgeConstants,
|
||||||
};
|
};
|
||||||
|
@@ -58,7 +58,9 @@ class Database {
|
|||||||
"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-sqlserver-monitor.sql": true,
|
||||||
|
"patch-add-other-auth.sql": { parents: [ "patch-monitor-basic-auth.sql" ] },
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The final version should be 10 after merged tag feature
|
* The final version should be 10 after merged tag feature
|
||||||
@@ -68,6 +70,10 @@ class Database {
|
|||||||
|
|
||||||
static noReject = true;
|
static noReject = true;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize the database
|
||||||
|
* @param {Object} args Arguments to initialize DB with
|
||||||
|
*/
|
||||||
static init(args) {
|
static init(args) {
|
||||||
// Data Directory (must be end with "/")
|
// Data Directory (must be end with "/")
|
||||||
Database.dataDir = process.env.DATA_DIR || args["data-dir"] || "./data/";
|
Database.dataDir = process.env.DATA_DIR || args["data-dir"] || "./data/";
|
||||||
@@ -85,6 +91,15 @@ class Database {
|
|||||||
log.info("db", `Data Dir: ${Database.dataDir}`);
|
log.info("db", `Data Dir: ${Database.dataDir}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Connect to the database
|
||||||
|
* @param {boolean} [testMode=false] Should the connection be
|
||||||
|
* started in test mode?
|
||||||
|
* @param {boolean} [autoloadModels=true] Should models be
|
||||||
|
* automatically loaded?
|
||||||
|
* @param {boolean} [noLog=false] Should logs not be output?
|
||||||
|
* @returns {Promise<void>}
|
||||||
|
*/
|
||||||
static async connect(testMode = false, autoloadModels = true, noLog = false) {
|
static async connect(testMode = false, autoloadModels = true, noLog = false) {
|
||||||
const acquireConnectionTimeout = 120 * 1000;
|
const acquireConnectionTimeout = 120 * 1000;
|
||||||
|
|
||||||
@@ -144,6 +159,7 @@ class Database {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Patch the database */
|
||||||
static async patch() {
|
static async patch() {
|
||||||
let version = parseInt(await setting("database_version"));
|
let version = parseInt(await setting("database_version"));
|
||||||
|
|
||||||
@@ -189,7 +205,9 @@ class Database {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
* Patch DB using new process
|
||||||
* Call it from patch() only
|
* Call it from patch() only
|
||||||
|
* @private
|
||||||
* @returns {Promise<void>}
|
* @returns {Promise<void>}
|
||||||
*/
|
*/
|
||||||
static async patch2() {
|
static async patch2() {
|
||||||
@@ -296,9 +314,12 @@ class Database {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
* Patch database using new patching process
|
||||||
* Used it patch2() only
|
* Used it patch2() only
|
||||||
|
* @private
|
||||||
* @param sqlFilename
|
* @param sqlFilename
|
||||||
* @param databasePatchedFiles
|
* @param databasePatchedFiles
|
||||||
|
* @returns {Promise<void>}
|
||||||
*/
|
*/
|
||||||
static async patch2Recursion(sqlFilename, databasePatchedFiles) {
|
static async patch2Recursion(sqlFilename, databasePatchedFiles) {
|
||||||
let value = this.patchList[sqlFilename];
|
let value = this.patchList[sqlFilename];
|
||||||
@@ -333,12 +354,12 @@ class Database {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sadly, multi sql statements is not supported by many sqlite libraries, I have to implement it myself
|
* Load an SQL file and execute it
|
||||||
* @param filename
|
* @param filename Filename of SQL file to import
|
||||||
* @returns {Promise<void>}
|
* @returns {Promise<void>}
|
||||||
*/
|
*/
|
||||||
static async importSQLFile(filename) {
|
static async importSQLFile(filename) {
|
||||||
|
// Sadly, multi sql statements is not supported by many sqlite libraries, I have to implement it myself
|
||||||
await R.getCell("SELECT 1");
|
await R.getCell("SELECT 1");
|
||||||
|
|
||||||
let text = fs.readFileSync(filename).toString();
|
let text = fs.readFileSync(filename).toString();
|
||||||
@@ -366,6 +387,10 @@ class Database {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Aquire a direct connection to database
|
||||||
|
* @returns {any}
|
||||||
|
*/
|
||||||
static getBetterSQLite3Database() {
|
static getBetterSQLite3Database() {
|
||||||
return R.knex.client.acquireConnection();
|
return R.knex.client.acquireConnection();
|
||||||
}
|
}
|
||||||
@@ -401,7 +426,7 @@ class Database {
|
|||||||
/**
|
/**
|
||||||
* One backup one time in this process.
|
* One backup one time in this process.
|
||||||
* Reset this.backupPath if you want to backup again
|
* Reset this.backupPath if you want to backup again
|
||||||
* @param version
|
* @param {string} version Version code of backup
|
||||||
*/
|
*/
|
||||||
static backup(version) {
|
static backup(version) {
|
||||||
if (! this.backupPath) {
|
if (! this.backupPath) {
|
||||||
@@ -423,9 +448,7 @@ class Database {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/** Restore from most recent backup */
|
||||||
*
|
|
||||||
*/
|
|
||||||
static restore() {
|
static restore() {
|
||||||
if (this.backupPath) {
|
if (this.backupPath) {
|
||||||
log.error("db", "Patching the database failed!!! Restoring the backup");
|
log.error("db", "Patching the database failed!!! Restoring the backup");
|
||||||
@@ -467,6 +490,7 @@ class Database {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Get the size of the database */
|
||||||
static getSize() {
|
static getSize() {
|
||||||
log.debug("db", "Database.getSize()");
|
log.debug("db", "Database.getSize()");
|
||||||
let stats = fs.statSync(Database.path);
|
let stats = fs.statSync(Database.path);
|
||||||
@@ -474,6 +498,10 @@ class Database {
|
|||||||
return stats.size;
|
return stats.size;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Shrink the database
|
||||||
|
* @returns {Promise<void>}
|
||||||
|
*/
|
||||||
static async shrink() {
|
static async shrink() {
|
||||||
await R.exec("VACUUM");
|
await R.exec("VACUUM");
|
||||||
}
|
}
|
||||||
|
@@ -8,10 +8,12 @@ const { log } = require("../src/util");
|
|||||||
let ImageDataURI = (() => {
|
let ImageDataURI = (() => {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {string} dataURI - A string that is a valid Data URI.
|
* Decode the data:image/ URI
|
||||||
* @returns {?Object} An object with properties "imageType" and "dataBase64". The former is the image type, e.g., "png", and the latter is a base64 encoded string of the image's binary data. If it fails to parse, returns null instead of an object.
|
* @param {string} dataURI data:image/ URI to decode
|
||||||
*
|
* @returns {?Object} An object with properties "imageType" and "dataBase64".
|
||||||
* Generated by Trelent
|
* The former is the image type, e.g., "png", and the latter is a base64
|
||||||
|
* encoded string of the image's binary data. If it fails to parse, returns
|
||||||
|
* null instead of an object.
|
||||||
*/
|
*/
|
||||||
function decode(dataURI) {
|
function decode(dataURI) {
|
||||||
if (!/data:image\//.test(dataURI)) {
|
if (!/data:image\//.test(dataURI)) {
|
||||||
@@ -28,11 +30,11 @@ let ImageDataURI = (() => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {Buffer} data - The image data to be encoded.
|
* Endcode an image into data:image/ URI
|
||||||
* @param {String} mediaType - The type of the image, e.g., "image/png".
|
* @param {(Buffer|string)} data Data to encode
|
||||||
* @returns {String|null} A string representing the base64-encoded version of the given Buffer object or null if an error occurred.
|
* @param {string} mediaType Media type of data
|
||||||
*
|
* @returns {(string|null)} A string representing the base64-encoded
|
||||||
* Generated by Trelent
|
* version of the given Buffer object or null if an error occurred.
|
||||||
*/
|
*/
|
||||||
function encode(data, mediaType) {
|
function encode(data, mediaType) {
|
||||||
if (!data || !mediaType) {
|
if (!data || !mediaType) {
|
||||||
@@ -48,11 +50,10 @@ let ImageDataURI = (() => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Converts a data URI to a file path.
|
* Write data URI to file
|
||||||
* @param {string} dataURI The Data URI of the image.
|
* @param {string} dataURI data:image/ URI
|
||||||
* @param {string} [filePath] The path where the image will be saved, defaults to "./".
|
* @param {string} [filePath] Path to write file to
|
||||||
*
|
* @returns {Promise<string>}
|
||||||
* Generated by Trelent
|
|
||||||
*/
|
*/
|
||||||
function outputFile(dataURI, filePath) {
|
function outputFile(dataURI, filePath) {
|
||||||
filePath = filePath || "./";
|
filePath = filePath || "./";
|
||||||
|
@@ -10,6 +10,11 @@ const jobs = [
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize background jobs
|
||||||
|
* @param {Object} args Arguments to pass to workers
|
||||||
|
* @returns {Bree}
|
||||||
|
*/
|
||||||
const initBackgroundJobs = function (args) {
|
const initBackgroundJobs = function (args) {
|
||||||
bree = new Bree({
|
bree = new Bree({
|
||||||
root: path.resolve("server", "jobs"),
|
root: path.resolve("server", "jobs"),
|
||||||
|
@@ -2,12 +2,22 @@ const { parentPort, workerData } = require("worker_threads");
|
|||||||
const Database = require("../database");
|
const Database = require("../database");
|
||||||
const path = require("path");
|
const path = require("path");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send message to parent process for logging
|
||||||
|
* since worker_thread does not have access to stdout, this is used
|
||||||
|
* instead of console.log()
|
||||||
|
* @param {any} any The message to log
|
||||||
|
*/
|
||||||
const log = function (any) {
|
const log = function (any) {
|
||||||
if (parentPort) {
|
if (parentPort) {
|
||||||
parentPort.postMessage(any);
|
parentPort.postMessage(any);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Exit the worker process
|
||||||
|
* @param {number} error The status code to exit
|
||||||
|
*/
|
||||||
const exit = function (error) {
|
const exit = function (error) {
|
||||||
if (error && error !== 0) {
|
if (error && error !== 0) {
|
||||||
process.exit(error);
|
process.exit(error);
|
||||||
@@ -20,6 +30,7 @@ const exit = function (error) {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/** Connects to the database */
|
||||||
const connectDb = async function () {
|
const connectDb = async function () {
|
||||||
const dbPath = path.join(
|
const dbPath = path.join(
|
||||||
process.env.DATA_DIR || workerData["data-dir"] || "./data/"
|
process.env.DATA_DIR || workerData["data-dir"] || "./data/"
|
||||||
|
@@ -3,6 +3,12 @@ const { R } = require("redbean-node");
|
|||||||
|
|
||||||
class Group extends BeanModel {
|
class Group extends BeanModel {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return an object that ready to parse to JSON for public
|
||||||
|
* Only show necessary data to public
|
||||||
|
* @param {boolean} [showTags=false] Should the JSON include monitor tags
|
||||||
|
* @returns {Object}
|
||||||
|
*/
|
||||||
async toPublicJSON(showTags = false) {
|
async toPublicJSON(showTags = false) {
|
||||||
let monitorBeanList = await this.getMonitorList();
|
let monitorBeanList = await this.getMonitorList();
|
||||||
let monitorList = [];
|
let monitorList = [];
|
||||||
@@ -19,6 +25,10 @@ class Group extends BeanModel {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all monitors
|
||||||
|
* @returns {Bean[]}
|
||||||
|
*/
|
||||||
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.* FROM monitor, monitor_group
|
||||||
|
@@ -13,6 +13,11 @@ const { BeanModel } = require("redbean-node/dist/bean-model");
|
|||||||
*/
|
*/
|
||||||
class Heartbeat extends BeanModel {
|
class Heartbeat extends BeanModel {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return an object that ready to parse to JSON for public
|
||||||
|
* Only show necessary data to public
|
||||||
|
* @returns {Object}
|
||||||
|
*/
|
||||||
toPublicJSON() {
|
toPublicJSON() {
|
||||||
return {
|
return {
|
||||||
status: this.status,
|
status: this.status,
|
||||||
@@ -22,6 +27,10 @@ class Heartbeat extends BeanModel {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return an object that ready to parse to JSON
|
||||||
|
* @returns {Object}
|
||||||
|
*/
|
||||||
toJSON() {
|
toJSON() {
|
||||||
return {
|
return {
|
||||||
monitorID: this.monitor_id,
|
monitorID: this.monitor_id,
|
||||||
|
@@ -2,6 +2,11 @@ const { BeanModel } = require("redbean-node/dist/bean-model");
|
|||||||
|
|
||||||
class Incident extends BeanModel {
|
class Incident extends BeanModel {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return an object that ready to parse to JSON for public
|
||||||
|
* Only show necessary data to public
|
||||||
|
* @returns {Object}
|
||||||
|
*/
|
||||||
toPublicJSON() {
|
toPublicJSON() {
|
||||||
return {
|
return {
|
||||||
id: this.id,
|
id: this.id,
|
||||||
|
@@ -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, errorLog, mqttAsync } = require("../util-server");
|
const { tcping, ping, dnsResolve, checkCertificate, checkStatusCode, getTotalClientInRoom, setting, mssqlQuery, mqttAsync, setSetting, httpNtlm } = 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");
|
||||||
@@ -15,6 +15,7 @@ const { Proxy } = require("../proxy");
|
|||||||
const { demoMode } = require("../config");
|
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");
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* status:
|
* status:
|
||||||
@@ -27,6 +28,7 @@ class Monitor extends BeanModel {
|
|||||||
/**
|
/**
|
||||||
* Return an object that ready to parse to JSON for public
|
* Return an object that ready to parse to JSON for public
|
||||||
* Only show necessary data to public
|
* Only show necessary data to public
|
||||||
|
* @returns {Object}
|
||||||
*/
|
*/
|
||||||
async toPublicJSON(showTags = false) {
|
async toPublicJSON(showTags = false) {
|
||||||
let obj = {
|
let obj = {
|
||||||
@@ -41,6 +43,7 @@ class Monitor extends BeanModel {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Return an object that ready to parse to JSON
|
* Return an object that ready to parse to JSON
|
||||||
|
* @returns {Object}
|
||||||
*/
|
*/
|
||||||
async toJSON(includeSensitiveData = true) {
|
async toJSON(includeSensitiveData = true) {
|
||||||
|
|
||||||
@@ -84,7 +87,12 @@ class Monitor extends BeanModel {
|
|||||||
mqttUsername: this.mqttUsername,
|
mqttUsername: this.mqttUsername,
|
||||||
mqttPassword: this.mqttPassword,
|
mqttPassword: this.mqttPassword,
|
||||||
mqttTopic: this.mqttTopic,
|
mqttTopic: this.mqttTopic,
|
||||||
mqttSuccessMessage: this.mqttSuccessMessage
|
mqttSuccessMessage: this.mqttSuccessMessage,
|
||||||
|
databaseConnectionString: this.databaseConnectionString,
|
||||||
|
databaseQuery: this.databaseQuery,
|
||||||
|
authMethod: this.authMethod,
|
||||||
|
authWorkstation: this.authWorkstation,
|
||||||
|
authDomain: this.authDomain,
|
||||||
};
|
};
|
||||||
|
|
||||||
if (includeSensitiveData) {
|
if (includeSensitiveData) {
|
||||||
@@ -101,6 +109,10 @@ class Monitor extends BeanModel {
|
|||||||
return data;
|
return data;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all tags applied to this monitor
|
||||||
|
* @returns {Promise<LooseObject<any>[]>}
|
||||||
|
*/
|
||||||
async getTags() {
|
async getTags() {
|
||||||
return await R.getAll("SELECT mt.*, tag.name, tag.color FROM monitor_tag mt JOIN tag ON mt.tag_id = tag.id WHERE mt.monitor_id = ?", [ this.id ]);
|
return await R.getAll("SELECT mt.*, tag.name, tag.color FROM monitor_tag mt JOIN tag ON mt.tag_id = tag.id WHERE mt.monitor_id = ?", [ this.id ]);
|
||||||
}
|
}
|
||||||
@@ -114,6 +126,10 @@ class Monitor extends BeanModel {
|
|||||||
return Buffer.from(user + ":" + pass).toString("base64");
|
return Buffer.from(user + ":" + pass).toString("base64");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Is the TLS expiry notification enabled?
|
||||||
|
* @returns {boolean}
|
||||||
|
*/
|
||||||
isEnabledExpiryNotification() {
|
isEnabledExpiryNotification() {
|
||||||
return Boolean(this.expiryNotification);
|
return Boolean(this.expiryNotification);
|
||||||
}
|
}
|
||||||
@@ -134,10 +150,18 @@ class Monitor extends BeanModel {
|
|||||||
return Boolean(this.upsideDown);
|
return Boolean(this.upsideDown);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get accepted status codes
|
||||||
|
* @returns {Object}
|
||||||
|
*/
|
||||||
getAcceptedStatuscodes() {
|
getAcceptedStatuscodes() {
|
||||||
return JSON.parse(this.accepted_statuscodes_json);
|
return JSON.parse(this.accepted_statuscodes_json);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Start monitor
|
||||||
|
* @param {Server} io Socket server instance
|
||||||
|
*/
|
||||||
start(io) {
|
start(io) {
|
||||||
let previousBeat = null;
|
let previousBeat = null;
|
||||||
let retries = 0;
|
let retries = 0;
|
||||||
@@ -163,7 +187,7 @@ class Monitor extends BeanModel {
|
|||||||
// undefined if not https
|
// undefined if not https
|
||||||
let tlsInfo = undefined;
|
let tlsInfo = undefined;
|
||||||
|
|
||||||
if (!previousBeat) {
|
if (!previousBeat || this.type === "push") {
|
||||||
previousBeat = await R.findOne("heartbeat", " monitor_id = ? ORDER BY time DESC", [
|
previousBeat = await R.findOne("heartbeat", " monitor_id = ? ORDER BY time DESC", [
|
||||||
this.id,
|
this.id,
|
||||||
]);
|
]);
|
||||||
@@ -173,7 +197,7 @@ class Monitor extends BeanModel {
|
|||||||
|
|
||||||
let bean = R.dispense("heartbeat");
|
let bean = R.dispense("heartbeat");
|
||||||
bean.monitor_id = this.id;
|
bean.monitor_id = this.id;
|
||||||
bean.time = R.isoDateTime(dayjs.utc());
|
bean.time = R.isoDateTimeMillis(dayjs.utc());
|
||||||
bean.status = DOWN;
|
bean.status = DOWN;
|
||||||
|
|
||||||
if (this.isUpsideDown()) {
|
if (this.isUpsideDown()) {
|
||||||
@@ -194,7 +218,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),
|
||||||
};
|
};
|
||||||
@@ -245,7 +269,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;
|
||||||
|
|
||||||
@@ -293,7 +331,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 + "]");
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -311,7 +353,7 @@ class Monitor extends BeanModel {
|
|||||||
let startTime = dayjs().valueOf();
|
let startTime = dayjs().valueOf();
|
||||||
let dnsMessage = "";
|
let dnsMessage = "";
|
||||||
|
|
||||||
let dnsRes = await dnsResolve(this.hostname, this.dns_resolve_server, this.dns_resolve_type);
|
let dnsRes = await dnsResolve(this.hostname, this.dns_resolve_server, this.port, this.dns_resolve_type);
|
||||||
bean.ping = dayjs().valueOf() - startTime;
|
bean.ping = dayjs().valueOf() - startTime;
|
||||||
|
|
||||||
if (this.dns_resolve_type === "A" || this.dns_resolve_type === "AAAA" || this.dns_resolve_type === "TXT") {
|
if (this.dns_resolve_type === "A" || this.dns_resolve_type === "AAAA" || this.dns_resolve_type === "TXT") {
|
||||||
@@ -348,25 +390,33 @@ class Monitor extends BeanModel {
|
|||||||
bean.msg = dnsMessage;
|
bean.msg = dnsMessage;
|
||||||
bean.status = UP;
|
bean.status = UP;
|
||||||
} else if (this.type === "push") { // Type: Push
|
} else if (this.type === "push") { // Type: Push
|
||||||
const time = R.isoDateTime(dayjs.utc().subtract(this.interval, "second"));
|
log.debug("monitor", `[${this.name}] Checking monitor at ${dayjs().format("YYYY-MM-DD HH:mm:ss.SSS")}`);
|
||||||
|
const bufferTime = 1000; // 1s buffer to accommodate clock differences
|
||||||
|
|
||||||
let heartbeatCount = await R.count("heartbeat", " monitor_id = ? AND time > ? ", [
|
if (previousBeat) {
|
||||||
this.id,
|
const msSinceLastBeat = dayjs.utc().valueOf() - dayjs.utc(previousBeat.time).valueOf();
|
||||||
time
|
|
||||||
]);
|
|
||||||
|
|
||||||
log.debug("monitor", "heartbeatCount" + heartbeatCount + " " + time);
|
log.debug("monitor", `[${this.name}] msSinceLastBeat = ${msSinceLastBeat}`);
|
||||||
|
|
||||||
if (heartbeatCount <= 0) {
|
// If the previous beat was down or pending we use the regular
|
||||||
// Fix #922, since previous heartbeat could be inserted by api, it should get from database
|
// beatInterval/retryInterval in the setTimeout further below
|
||||||
previousBeat = await Monitor.getPreviousHeartbeat(this.id);
|
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 {
|
||||||
|
let timeout = beatInterval * 1000 - msSinceLastBeat;
|
||||||
|
if (timeout < 0) {
|
||||||
|
timeout = bufferTime;
|
||||||
|
} else {
|
||||||
|
timeout += bufferTime;
|
||||||
|
}
|
||||||
|
// No need to insert successful heartbeat for push type, so end here
|
||||||
|
retries = 0;
|
||||||
|
log.debug("monitor", `[${this.name}] timeout = ${timeout}`);
|
||||||
|
this.heartbeatInterval = setTimeout(beat, timeout);
|
||||||
|
return;
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
// No need to insert successful heartbeat for push type, so end here
|
throw new Error("No heartbeat in the time window");
|
||||||
retries = 0;
|
|
||||||
this.heartbeatInterval = setTimeout(beat, beatInterval * 1000);
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
} else if (this.type === "steam") {
|
} else if (this.type === "steam") {
|
||||||
@@ -416,6 +466,14 @@ 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 {
|
} else {
|
||||||
bean.msg = "Unknown Monitor Type";
|
bean.msg = "Unknown Monitor Type";
|
||||||
bean.status = PENDING;
|
bean.status = PENDING;
|
||||||
@@ -466,7 +524,7 @@ class Monitor extends BeanModel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
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;
|
||||||
@@ -497,12 +555,13 @@ class Monitor extends BeanModel {
|
|||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/** Get a heartbeat and handle errors */
|
||||||
const safeBeat = async () => {
|
const safeBeat = async () => {
|
||||||
try {
|
try {
|
||||||
await beat();
|
await beat();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.trace(e);
|
console.trace(e);
|
||||||
errorLog(e, false);
|
UptimeKumaServer.errorLog(e, false);
|
||||||
log.error("monitor", "Please report to https://github.com/louislam/uptime-kuma/issues");
|
log.error("monitor", "Please report to https://github.com/louislam/uptime-kuma/issues");
|
||||||
|
|
||||||
if (! this.isStop) {
|
if (! this.isStop) {
|
||||||
@@ -522,6 +581,7 @@ class Monitor extends BeanModel {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Stop monitor */
|
||||||
stop() {
|
stop() {
|
||||||
clearTimeout(this.heartbeatInterval);
|
clearTimeout(this.heartbeatInterval);
|
||||||
this.isStop = true;
|
this.isStop = true;
|
||||||
@@ -529,6 +589,10 @@ class Monitor extends BeanModel {
|
|||||||
this.prometheus().remove();
|
this.prometheus().remove();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a new prometheus instance
|
||||||
|
* @returns {Prometheus}
|
||||||
|
*/
|
||||||
prometheus() {
|
prometheus() {
|
||||||
return new Prometheus(this);
|
return new Prometheus(this);
|
||||||
}
|
}
|
||||||
@@ -537,7 +601,7 @@ class Monitor extends BeanModel {
|
|||||||
* Helper Method:
|
* Helper Method:
|
||||||
* returns URL object for further usage
|
* returns URL object for further usage
|
||||||
* returns null if url is invalid
|
* returns null if url is invalid
|
||||||
* @returns {null|URL}
|
* @returns {(null|URL)}
|
||||||
*/
|
*/
|
||||||
getUrl() {
|
getUrl() {
|
||||||
try {
|
try {
|
||||||
@@ -550,7 +614,7 @@ class Monitor extends BeanModel {
|
|||||||
/**
|
/**
|
||||||
* Store TLS info to database
|
* Store TLS info to database
|
||||||
* @param checkCertificateResult
|
* @param checkCertificateResult
|
||||||
* @returns {Promise<object>}
|
* @returns {Promise<Object>}
|
||||||
*/
|
*/
|
||||||
async updateTlsInfo(checkCertificateResult) {
|
async updateTlsInfo(checkCertificateResult) {
|
||||||
let tlsInfoBean = await R.findOne("monitor_tls_info", "monitor_id = ?", [
|
let tlsInfoBean = await R.findOne("monitor_tls_info", "monitor_id = ?", [
|
||||||
@@ -592,6 +656,12 @@ class Monitor extends BeanModel {
|
|||||||
return checkCertificateResult;
|
return checkCertificateResult;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send statistics to clients
|
||||||
|
* @param {Server} io Socket server instance
|
||||||
|
* @param {number} monitorID ID of monitor to send
|
||||||
|
* @param {number} userID ID of user to send to
|
||||||
|
*/
|
||||||
static async sendStats(io, monitorID, userID) {
|
static async sendStats(io, monitorID, userID) {
|
||||||
const hasClients = getTotalClientInRoom(io, userID) > 0;
|
const hasClients = getTotalClientInRoom(io, userID) > 0;
|
||||||
|
|
||||||
@@ -606,8 +676,8 @@ class Monitor extends BeanModel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
* Send the average ping to user
|
||||||
* @param duration : int Hours
|
* @param {number} duration Hours
|
||||||
*/
|
*/
|
||||||
static async sendAvgPing(duration, io, monitorID, userID) {
|
static async sendAvgPing(duration, io, monitorID, userID) {
|
||||||
const timeLogger = new TimeLogger();
|
const timeLogger = new TimeLogger();
|
||||||
@@ -627,6 +697,12 @@ class Monitor extends BeanModel {
|
|||||||
io.to(userID).emit("avgPing", monitorID, avgPing);
|
io.to(userID).emit("avgPing", monitorID, avgPing);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send certificate information to client
|
||||||
|
* @param {Server} io Socket server instance
|
||||||
|
* @param {number} monitorID ID of monitor to send
|
||||||
|
* @param {number} userID ID of user to send to
|
||||||
|
*/
|
||||||
static async sendCertInfo(io, monitorID, userID) {
|
static async sendCertInfo(io, monitorID, userID) {
|
||||||
let tlsInfo = await R.findOne("monitor_tls_info", "monitor_id = ?", [
|
let tlsInfo = await R.findOne("monitor_tls_info", "monitor_id = ?", [
|
||||||
monitorID,
|
monitorID,
|
||||||
@@ -640,7 +716,8 @@ class Monitor extends BeanModel {
|
|||||||
* Uptime with calculation
|
* Uptime with calculation
|
||||||
* Calculation based on:
|
* Calculation based on:
|
||||||
* https://www.uptrends.com/support/kb/reporting/calculation-of-uptime-and-downtime
|
* https://www.uptrends.com/support/kb/reporting/calculation-of-uptime-and-downtime
|
||||||
* @param duration : int Hours
|
* @param {number} duration Hours
|
||||||
|
* @param {number} monitorID ID of monitor to calculate
|
||||||
*/
|
*/
|
||||||
static async calcUptime(duration, monitorID) {
|
static async calcUptime(duration, monitorID) {
|
||||||
const timeLogger = new TimeLogger();
|
const timeLogger = new TimeLogger();
|
||||||
@@ -694,7 +771,7 @@ class Monitor extends BeanModel {
|
|||||||
|
|
||||||
} else {
|
} else {
|
||||||
// Handle new monitor with only one beat, because the beat's duration = 0
|
// Handle new monitor with only one beat, because the beat's duration = 0
|
||||||
let status = parseInt(await R.getCell("SELECT `status` FROM heartbeat WHERE monitor_id = ?", [monitorID]));
|
let status = parseInt(await R.getCell("SELECT `status` FROM heartbeat WHERE monitor_id = ?", [ monitorID ]));
|
||||||
|
|
||||||
if (status === UP) {
|
if (status === UP) {
|
||||||
uptime = 1;
|
uptime = 1;
|
||||||
@@ -706,13 +783,23 @@ class Monitor extends BeanModel {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Send Uptime
|
* Send Uptime
|
||||||
* @param duration : int Hours
|
* @param {number} duration Hours
|
||||||
|
* @param {Server} io Socket server instance
|
||||||
|
* @param {number} monitorID ID of monitor to send
|
||||||
|
* @param {number} userID ID of user to send to
|
||||||
*/
|
*/
|
||||||
static async sendUptime(duration, io, monitorID, userID) {
|
static async sendUptime(duration, io, monitorID, userID) {
|
||||||
const uptime = await this.calcUptime(duration, monitorID);
|
const uptime = await this.calcUptime(duration, monitorID);
|
||||||
io.to(userID).emit("uptime", monitorID, duration, uptime);
|
io.to(userID).emit("uptime", monitorID, duration, uptime);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Has status of monitor changed since last beat?
|
||||||
|
* @param {boolean} isFirstBeat Is this the first beat of this monitor?
|
||||||
|
* @param {const} previousBeatStatus Status of the previous beat
|
||||||
|
* @param {const} currentBeatStatus Status of the current beat
|
||||||
|
* @returns {boolean} True if is an important beat else false
|
||||||
|
*/
|
||||||
static isImportantBeat(isFirstBeat, previousBeatStatus, currentBeatStatus) {
|
static isImportantBeat(isFirstBeat, previousBeatStatus, currentBeatStatus) {
|
||||||
// * ? -> ANY STATUS = important [isFirstBeat]
|
// * ? -> ANY STATUS = important [isFirstBeat]
|
||||||
// UP -> PENDING = not important
|
// UP -> PENDING = not important
|
||||||
@@ -731,6 +818,12 @@ class Monitor extends BeanModel {
|
|||||||
return isImportant;
|
return isImportant;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send a notification about a monitor
|
||||||
|
* @param {boolean} isFirstBeat Is this beat the first of this monitor?
|
||||||
|
* @param {Monitor} monitor The monitor to send a notificaton about
|
||||||
|
* @param {Bean} bean Status information about monitor
|
||||||
|
*/
|
||||||
static async sendNotification(isFirstBeat, monitor, bean) {
|
static async sendNotification(isFirstBeat, monitor, bean) {
|
||||||
if (!isFirstBeat || bean.status === DOWN) {
|
if (!isFirstBeat || bean.status === DOWN) {
|
||||||
const notificationList = await Monitor.getNotificationList(monitor);
|
const notificationList = await Monitor.getNotificationList(monitor);
|
||||||
@@ -755,6 +848,11 @@ class Monitor extends BeanModel {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get list of notification providers for a given monitor
|
||||||
|
* @param {Monitor} monitor Monitor to get notification providers for
|
||||||
|
* @returns {Promise<LooseObject<any>[]>}
|
||||||
|
*/
|
||||||
static async getNotificationList(monitor) {
|
static async getNotificationList(monitor) {
|
||||||
let notificationList = await R.getAll("SELECT notification.* FROM notification, monitor_notification WHERE monitor_id = ? AND monitor_notification.notification_id = notification.id ", [
|
let notificationList = await R.getAll("SELECT notification.* FROM notification, monitor_notification WHERE monitor_id = ? AND monitor_notification.notification_id = notification.id ", [
|
||||||
monitor.id,
|
monitor.id,
|
||||||
@@ -762,17 +860,38 @@ class Monitor extends BeanModel {
|
|||||||
return notificationList;
|
return notificationList;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send notification about a certificate
|
||||||
|
* @param {Object} tlsInfoObject Information about certificate
|
||||||
|
*/
|
||||||
async sendCertNotification(tlsInfoObject) {
|
async sendCertNotification(tlsInfoObject) {
|
||||||
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send a certificate notification when certificate expires in less
|
||||||
|
* than target days
|
||||||
|
* @param {number} daysRemaining Number of days remaining on certifcate
|
||||||
|
* @param {number} targetDays Number of days to alert after
|
||||||
|
* @param {LooseObject<any>[]} notificationList List of notification providers
|
||||||
|
* @returns {Promise<void>}
|
||||||
|
*/
|
||||||
async sendCertNotificationByTargetDays(daysRemaining, targetDays, notificationList) {
|
async sendCertNotificationByTargetDays(daysRemaining, targetDays, notificationList) {
|
||||||
|
|
||||||
if (daysRemaining > targetDays) {
|
if (daysRemaining > targetDays) {
|
||||||
@@ -820,6 +939,11 @@ class Monitor extends BeanModel {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the status of the previous heartbeat
|
||||||
|
* @param {number} monitorID ID of monitor to check
|
||||||
|
* @returns {Promise<LooseObject<any>>}
|
||||||
|
*/
|
||||||
static async getPreviousHeartbeat(monitorID) {
|
static async getPreviousHeartbeat(monitorID) {
|
||||||
return await R.getRow(`
|
return await R.getRow(`
|
||||||
SELECT status, time FROM heartbeat
|
SELECT status, time FROM heartbeat
|
||||||
|
@@ -1,6 +1,10 @@
|
|||||||
const { BeanModel } = require("redbean-node/dist/bean-model");
|
const { BeanModel } = require("redbean-node/dist/bean-model");
|
||||||
|
|
||||||
class Proxy extends BeanModel {
|
class Proxy extends BeanModel {
|
||||||
|
/**
|
||||||
|
* Return an object that ready to parse to JSON
|
||||||
|
* @returns {Object}
|
||||||
|
*/
|
||||||
toJSON() {
|
toJSON() {
|
||||||
return {
|
return {
|
||||||
id: this._id,
|
id: this._id,
|
||||||
|
@@ -1,11 +1,106 @@
|
|||||||
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");
|
||||||
|
}
|
||||||
|
|
||||||
|
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>
|
||||||
|
`);
|
||||||
|
|
||||||
|
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
|
||||||
* Return object like this: { "test-uptime.kuma.pet": "default" }
|
* Return object like this: { "test-uptime.kuma.pet": "default" }
|
||||||
* @returns {Promise<void>}
|
* @returns {Promise<void>}
|
||||||
*/
|
*/
|
||||||
@@ -17,6 +112,12 @@ class StatusPage extends BeanModel {
|
|||||||
`);
|
`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send status page list to client
|
||||||
|
* @param {Server} io io Socket server instance
|
||||||
|
* @param {Socket} socket Socket.io instance
|
||||||
|
* @returns {Promise<Bean[]>}
|
||||||
|
*/
|
||||||
static async sendStatusPageList(io, socket) {
|
static async sendStatusPageList(io, socket) {
|
||||||
let result = {};
|
let result = {};
|
||||||
|
|
||||||
@@ -30,6 +131,11 @@ class StatusPage extends BeanModel {
|
|||||||
return list;
|
return list;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update list of domain names
|
||||||
|
* @param {string[]} domainNameList
|
||||||
|
* @returns {Promise<void>}
|
||||||
|
*/
|
||||||
async updateDomainNameList(domainNameList) {
|
async updateDomainNameList(domainNameList) {
|
||||||
|
|
||||||
if (!Array.isArray(domainNameList)) {
|
if (!Array.isArray(domainNameList)) {
|
||||||
@@ -69,6 +175,10 @@ class StatusPage extends BeanModel {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get list of domain names
|
||||||
|
* @returns {Object[]}
|
||||||
|
*/
|
||||||
getDomainNameList() {
|
getDomainNameList() {
|
||||||
let domainList = [];
|
let domainList = [];
|
||||||
for (let domain in StatusPage.domainMappingList) {
|
for (let domain in StatusPage.domainMappingList) {
|
||||||
@@ -81,6 +191,10 @@ class StatusPage extends BeanModel {
|
|||||||
return domainList;
|
return domainList;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return an object that ready to parse to JSON
|
||||||
|
* @returns {Object}
|
||||||
|
*/
|
||||||
async toJSON() {
|
async toJSON() {
|
||||||
return {
|
return {
|
||||||
id: this.id,
|
id: this.id,
|
||||||
@@ -98,6 +212,11 @@ class StatusPage extends BeanModel {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return an object that ready to parse to JSON for public
|
||||||
|
* Only show necessary data to public
|
||||||
|
* @returns {Object}
|
||||||
|
*/
|
||||||
async toPublicJSON() {
|
async toPublicJSON() {
|
||||||
return {
|
return {
|
||||||
slug: this.slug,
|
slug: this.slug,
|
||||||
@@ -113,12 +232,20 @@ class StatusPage extends BeanModel {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert slug to status page ID
|
||||||
|
* @param {string} slug
|
||||||
|
*/
|
||||||
static async slugToID(slug) {
|
static async slugToID(slug) {
|
||||||
return await R.getCell("SELECT id FROM status_page WHERE slug = ? ", [
|
return await R.getCell("SELECT id FROM status_page WHERE slug = ? ", [
|
||||||
slug
|
slug
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get path to the icon for the page
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
getIcon() {
|
getIcon() {
|
||||||
if (!this.icon) {
|
if (!this.icon) {
|
||||||
return "/icon.svg";
|
return "/icon.svg";
|
||||||
|
@@ -1,6 +1,11 @@
|
|||||||
const { BeanModel } = require("redbean-node/dist/bean-model");
|
const { BeanModel } = require("redbean-node/dist/bean-model");
|
||||||
|
|
||||||
class Tag extends BeanModel {
|
class Tag extends BeanModel {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return an object that ready to parse to JSON
|
||||||
|
* @returns {Object}
|
||||||
|
*/
|
||||||
toJSON() {
|
toJSON() {
|
||||||
return {
|
return {
|
||||||
id: this._id,
|
id: this._id,
|
||||||
|
@@ -3,12 +3,11 @@ const passwordHash = require("../password-hash");
|
|||||||
const { R } = require("redbean-node");
|
const { R } = require("redbean-node");
|
||||||
|
|
||||||
class User extends BeanModel {
|
class User extends BeanModel {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
* Reset user password
|
||||||
* Fix #1510, as in the context reset-password.js, there is no auto model mapping. Call this static function instead.
|
* Fix #1510, as in the context reset-password.js, there is no auto model mapping. Call this static function instead.
|
||||||
* @param userID
|
* @param {number} userID ID of user to update
|
||||||
* @param newPassword
|
* @param {string} newPassword
|
||||||
* @returns {Promise<void>}
|
* @returns {Promise<void>}
|
||||||
*/
|
*/
|
||||||
static async resetPassword(userID, newPassword) {
|
static async resetPassword(userID, newPassword) {
|
||||||
@@ -19,8 +18,8 @@ class User extends BeanModel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
* Reset this users password
|
||||||
* @param newPassword
|
* @param {string} newPassword
|
||||||
* @returns {Promise<void>}
|
* @returns {Promise<void>}
|
||||||
*/
|
*/
|
||||||
async resetPassword(newPassword) {
|
async resetPassword(newPassword) {
|
||||||
|
@@ -13,27 +13,49 @@ let t = {
|
|||||||
|
|
||||||
let instances = [];
|
let instances = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Does a === b
|
||||||
|
* @param {any} a
|
||||||
|
* @returns {function(any): boolean}
|
||||||
|
*/
|
||||||
let matches = function (a) {
|
let matches = function (a) {
|
||||||
return function (b) {
|
return function (b) {
|
||||||
return a === b;
|
return a === b;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Does a!==b
|
||||||
|
* @param {any} a
|
||||||
|
* @returns {function(any): boolean}
|
||||||
|
*/
|
||||||
let doesntMatch = function (a) {
|
let doesntMatch = function (a) {
|
||||||
return function (b) {
|
return function (b) {
|
||||||
return !matches(a)(b);
|
return !matches(a)(b);
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get log duration
|
||||||
|
* @param {number} d Time in ms
|
||||||
|
* @param {string} prefix Prefix for log
|
||||||
|
* @returns {string} Coloured log string
|
||||||
|
*/
|
||||||
let logDuration = function (d, prefix) {
|
let logDuration = function (d, prefix) {
|
||||||
let str = d > 1000 ? (d / 1000).toFixed(2) + "sec" : d + "ms";
|
let str = d > 1000 ? (d / 1000).toFixed(2) + "sec" : d + "ms";
|
||||||
return "\x1b[33m- " + (prefix ? prefix + " " : "") + str + "\x1b[0m";
|
return "\x1b[33m- " + (prefix ? prefix + " " : "") + str + "\x1b[0m";
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get safe headers
|
||||||
|
* @param {Object} res Express response object
|
||||||
|
* @returns {Object}
|
||||||
|
*/
|
||||||
function getSafeHeaders(res) {
|
function getSafeHeaders(res) {
|
||||||
return res.getHeaders ? res.getHeaders() : res._headers;
|
return res.getHeaders ? res.getHeaders() : res._headers;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Constructor for ApiCache instance */
|
||||||
function ApiCache() {
|
function ApiCache() {
|
||||||
let memCache = new MemoryCache();
|
let memCache = new MemoryCache();
|
||||||
|
|
||||||
@@ -70,10 +92,10 @@ function ApiCache() {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Logs a message to the console if the `DEBUG` environment variable is set.
|
* Logs a message to the console if the `DEBUG` environment variable is set.
|
||||||
* @param {string} a - The first argument to log.
|
* @param {string} a The first argument to log.
|
||||||
* @param {string} b - The second argument to log.
|
* @param {string} b The second argument to log.
|
||||||
* @param {string} c - The third argument to log.
|
* @param {string} c The third argument to log.
|
||||||
* @param {string} d - The fourth argument to log, and so on... (optional)
|
* @param {string} d The fourth argument to log, and so on... (optional)
|
||||||
*
|
*
|
||||||
* Generated by Trelent
|
* Generated by Trelent
|
||||||
*/
|
*/
|
||||||
@@ -90,8 +112,8 @@ function ApiCache() {
|
|||||||
* Returns true if the given request and response should be logged.
|
* Returns true if the given request and response should be logged.
|
||||||
* @param {Object} request The HTTP request object.
|
* @param {Object} request The HTTP request object.
|
||||||
* @param {Object} response The HTTP response object.
|
* @param {Object} response The HTTP response object.
|
||||||
*
|
* @param {function(Object, Object):boolean} toggle
|
||||||
* Generated by Trelent
|
* @returns {boolean}
|
||||||
*/
|
*/
|
||||||
function shouldCacheResponse(request, response, toggle) {
|
function shouldCacheResponse(request, response, toggle) {
|
||||||
let opt = globalOptions;
|
let opt = globalOptions;
|
||||||
@@ -116,10 +138,9 @@ function ApiCache() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Adds a key to the index.
|
* Add key to index array
|
||||||
* @param {string} key The key to add.
|
* @param {string} key Key to add
|
||||||
*
|
* @param {Object} req Express request object
|
||||||
* Generated by Trelent
|
|
||||||
*/
|
*/
|
||||||
function addIndexEntries(key, req) {
|
function addIndexEntries(key, req) {
|
||||||
let groupName = req.apicacheGroup;
|
let groupName = req.apicacheGroup;
|
||||||
@@ -135,8 +156,11 @@ function ApiCache() {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns a new object containing only the whitelisted headers.
|
* Returns a new object containing only the whitelisted headers.
|
||||||
* @param {Object} headers The original object of header names and values.
|
* @param {Object} headers The original object of header names and
|
||||||
* @param {Array.<string>} globalOptions.headerWhitelist An array of strings representing the whitelisted header names to keep in the output object.
|
* values.
|
||||||
|
* @param {string[]} globalOptions.headerWhitelist An array of
|
||||||
|
* strings representing the whitelisted header names to keep in the
|
||||||
|
* output object.
|
||||||
*
|
*
|
||||||
* Generated by Trelent
|
* Generated by Trelent
|
||||||
*/
|
*/
|
||||||
@@ -152,8 +176,10 @@ function ApiCache() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
* Create a cache object
|
||||||
* @param {Object} headers The response headers to filter.
|
* @param {Object} headers The response headers to filter.
|
||||||
* @returns {Object} A new object containing only the whitelisted response headers.
|
* @returns {Object} A new object containing only the whitelisted
|
||||||
|
* response headers.
|
||||||
*
|
*
|
||||||
* Generated by Trelent
|
* Generated by Trelent
|
||||||
*/
|
*/
|
||||||
@@ -170,8 +196,9 @@ function ApiCache() {
|
|||||||
/**
|
/**
|
||||||
* Sets a cache value for the given key.
|
* Sets a cache value for the given key.
|
||||||
* @param {string} key The cache key to set.
|
* @param {string} key The cache key to set.
|
||||||
* @param {*} value The cache value to set.
|
* @param {any} value The cache value to set.
|
||||||
* @param {number} duration How long in milliseconds the cached response should be valid for (defaults to 1 hour).
|
* @param {number} duration How long in milliseconds the cached
|
||||||
|
* response should be valid for (defaults to 1 hour).
|
||||||
*
|
*
|
||||||
* Generated by Trelent
|
* Generated by Trelent
|
||||||
*/
|
*/
|
||||||
@@ -199,7 +226,8 @@ function ApiCache() {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Appends content to the response.
|
* Appends content to the response.
|
||||||
* @param {string|Buffer} content The content to append.
|
* @param {Object} res Express response object
|
||||||
|
* @param {(string|Buffer)} content The content to append.
|
||||||
*
|
*
|
||||||
* Generated by Trelent
|
* Generated by Trelent
|
||||||
*/
|
*/
|
||||||
@@ -229,11 +257,15 @@ function ApiCache() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Monkeypatches the response object to add cache control headers and create a cache object.
|
* Monkeypatches the response object to add cache control headers
|
||||||
* @param {Object} req - The request object.
|
* and create a cache object.
|
||||||
* @param {Object} res - The response object.
|
* @param {Object} req Express request object
|
||||||
*
|
* @param {Object} res Express response object
|
||||||
* Generated by Trelent
|
* @param {function} next Function to call next
|
||||||
|
* @param {string} key Key to add response as
|
||||||
|
* @param {number} duration Time to cache response for
|
||||||
|
* @param {string} strDuration Duration in string form
|
||||||
|
* @param {function(Object, Object):boolean} toggle
|
||||||
*/
|
*/
|
||||||
function makeResponseCacheable(req, res, next, key, duration, strDuration, toggle) {
|
function makeResponseCacheable(req, res, next, key, duration, strDuration, toggle) {
|
||||||
// monkeypatch res.end to create cache object
|
// monkeypatch res.end to create cache object
|
||||||
@@ -302,11 +334,15 @@ function ApiCache() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {Request} request
|
* Send a cached response to client
|
||||||
* @param {Response} response
|
* @param {Request} request Express request object
|
||||||
* @returns {boolean|undefined} true if the request should be cached, false otherwise. If undefined, defaults to true.
|
* @param {Response} response Express response object
|
||||||
*
|
* @param {object} cacheObject Cache object to send
|
||||||
* Generated by Trelent
|
* @param {function(Object, Object):boolean} toggle
|
||||||
|
* @param {function} next Function to call next
|
||||||
|
* @param {number} duration Not used
|
||||||
|
* @returns {boolean|undefined} true if the request should be
|
||||||
|
* cached, false otherwise. If undefined, defaults to true.
|
||||||
*/
|
*/
|
||||||
function sendCachedResponse(request, response, cacheObject, toggle, next, duration) {
|
function sendCachedResponse(request, response, cacheObject, toggle, next, duration) {
|
||||||
if (toggle && !toggle(request, response)) {
|
if (toggle && !toggle(request, response)) {
|
||||||
@@ -348,12 +384,19 @@ function ApiCache() {
|
|||||||
return response.end(data, cacheObject.encoding);
|
return response.end(data, cacheObject.encoding);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Sync caching options */
|
||||||
function syncOptions() {
|
function syncOptions() {
|
||||||
for (let i in middlewareOptions) {
|
for (let i in middlewareOptions) {
|
||||||
Object.assign(middlewareOptions[i].options, globalOptions, middlewareOptions[i].localOptions);
|
Object.assign(middlewareOptions[i].options, globalOptions, middlewareOptions[i].localOptions);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clear key from cache
|
||||||
|
* @param {string} target Key to clear
|
||||||
|
* @param {boolean} isAutomatic Is the key being cleared automatically
|
||||||
|
* @returns {number}
|
||||||
|
*/
|
||||||
this.clear = function (target, isAutomatic) {
|
this.clear = function (target, isAutomatic) {
|
||||||
let group = index.groups[target];
|
let group = index.groups[target];
|
||||||
let redis = globalOptions.redisClient;
|
let redis = globalOptions.redisClient;
|
||||||
@@ -430,10 +473,11 @@ function ApiCache() {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Converts a duration string to an integer number of milliseconds.
|
* Converts a duration string to an integer number of milliseconds.
|
||||||
* @param {string} duration - The string to convert.
|
* @param {(string|number)} duration The string to convert.
|
||||||
* @returns {number} The converted value in milliseconds, or the defaultDuration if it can't be parsed.
|
* @param {number} defaultDuration The default duration to return if
|
||||||
*
|
* can't parse duration
|
||||||
* Generated by Trelent
|
* @returns {number} The converted value in milliseconds, or the
|
||||||
|
* defaultDuration if it can't be parsed.
|
||||||
*/
|
*/
|
||||||
function parseDuration(duration, defaultDuration) {
|
function parseDuration(duration, defaultDuration) {
|
||||||
if (typeof duration === "number") {
|
if (typeof duration === "number") {
|
||||||
@@ -457,17 +501,24 @@ function ApiCache() {
|
|||||||
return defaultDuration;
|
return defaultDuration;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse duration
|
||||||
|
* @param {(number|string)} duration
|
||||||
|
* @returns {number} Duration parsed to a number
|
||||||
|
*/
|
||||||
this.getDuration = function (duration) {
|
this.getDuration = function (duration) {
|
||||||
return parseDuration(duration, globalOptions.defaultDuration);
|
return parseDuration(duration, globalOptions.defaultDuration);
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Return cache performance statistics (hit rate). Suitable for putting into a route:
|
* Return cache performance statistics (hit rate). Suitable for
|
||||||
|
* putting into a route:
|
||||||
* <code>
|
* <code>
|
||||||
* app.get('/api/cache/performance', (req, res) => {
|
* app.get('/api/cache/performance', (req, res) => {
|
||||||
* res.json(apicache.getPerformance())
|
* res.json(apicache.getPerformance())
|
||||||
* })
|
* })
|
||||||
* </code>
|
* </code>
|
||||||
|
* @returns {any[]}
|
||||||
*/
|
*/
|
||||||
this.getPerformance = function () {
|
this.getPerformance = function () {
|
||||||
return performanceArray.map(function (p) {
|
return performanceArray.map(function (p) {
|
||||||
@@ -475,6 +526,11 @@ function ApiCache() {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get index of a group
|
||||||
|
* @param {string} group
|
||||||
|
* @returns {number}
|
||||||
|
*/
|
||||||
this.getIndex = function (group) {
|
this.getIndex = function (group) {
|
||||||
if (group) {
|
if (group) {
|
||||||
return index.groups[group];
|
return index.groups[group];
|
||||||
@@ -483,6 +539,14 @@ function ApiCache() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Express middleware
|
||||||
|
* @param {(string|number)} strDuration Duration to cache responses
|
||||||
|
* for.
|
||||||
|
* @param {function(Object, Object):boolean} middlewareToggle
|
||||||
|
* @param {Object} localOptions Options for APICache
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
this.middleware = function cache(strDuration, middlewareToggle, localOptions) {
|
this.middleware = function cache(strDuration, middlewareToggle, localOptions) {
|
||||||
let duration = instance.getDuration(strDuration);
|
let duration = instance.getDuration(strDuration);
|
||||||
let opt = {};
|
let opt = {};
|
||||||
@@ -506,63 +570,72 @@ function ApiCache() {
|
|||||||
options(localOptions);
|
options(localOptions);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A Function for non tracking performance
|
* A Function for non tracking performance
|
||||||
*/
|
*/
|
||||||
function NOOPCachePerformance() {
|
function NOOPCachePerformance() {
|
||||||
this.report = this.hit = this.miss = function () {}; // noop;
|
this.report = this.hit = this.miss = function () {}; // noop;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A function for tracking and reporting hit rate. These statistics are returned by the getPerformance() call above.
|
* A function for tracking and reporting hit rate. These
|
||||||
*/
|
* statistics are returned by the getPerformance() call above.
|
||||||
|
*/
|
||||||
function CachePerformance() {
|
function CachePerformance() {
|
||||||
/**
|
/**
|
||||||
* Tracks the hit rate for the last 100 requests.
|
* Tracks the hit rate for the last 100 requests. If there
|
||||||
* If there have been fewer than 100 requests, the hit rate just considers the requests that have happened.
|
* have been fewer than 100 requests, the hit rate just
|
||||||
*/
|
* considers the requests that have happened.
|
||||||
|
*/
|
||||||
this.hitsLast100 = new Uint8Array(100 / 4); // each hit is 2 bits
|
this.hitsLast100 = new Uint8Array(100 / 4); // each hit is 2 bits
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Tracks the hit rate for the last 1000 requests.
|
* Tracks the hit rate for the last 1000 requests. If there
|
||||||
* If there have been fewer than 1000 requests, the hit rate just considers the requests that have happened.
|
* have been fewer than 1000 requests, the hit rate just
|
||||||
*/
|
* considers the requests that have happened.
|
||||||
|
*/
|
||||||
this.hitsLast1000 = new Uint8Array(1000 / 4); // each hit is 2 bits
|
this.hitsLast1000 = new Uint8Array(1000 / 4); // each hit is 2 bits
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Tracks the hit rate for the last 10000 requests.
|
* Tracks the hit rate for the last 10000 requests. If there
|
||||||
* If there have been fewer than 10000 requests, the hit rate just considers the requests that have happened.
|
* have been fewer than 10000 requests, the hit rate just
|
||||||
*/
|
* considers the requests that have happened.
|
||||||
|
*/
|
||||||
this.hitsLast10000 = new Uint8Array(10000 / 4); // each hit is 2 bits
|
this.hitsLast10000 = new Uint8Array(10000 / 4); // each hit is 2 bits
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Tracks the hit rate for the last 100000 requests.
|
* Tracks the hit rate for the last 100000 requests. If
|
||||||
* If there have been fewer than 100000 requests, the hit rate just considers the requests that have happened.
|
* there have been fewer than 100000 requests, the hit rate
|
||||||
*/
|
* just considers the requests that have happened.
|
||||||
|
*/
|
||||||
this.hitsLast100000 = new Uint8Array(100000 / 4); // each hit is 2 bits
|
this.hitsLast100000 = new Uint8Array(100000 / 4); // each hit is 2 bits
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The number of calls that have passed through the middleware since the server started.
|
* The number of calls that have passed through the
|
||||||
*/
|
* middleware since the server started.
|
||||||
|
*/
|
||||||
this.callCount = 0;
|
this.callCount = 0;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The total number of hits since the server started
|
* The total number of hits since the server started
|
||||||
*/
|
*/
|
||||||
this.hitCount = 0;
|
this.hitCount = 0;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The key from the last cache hit. This is useful in identifying which route these statistics apply to.
|
* The key from the last cache hit. This is useful in
|
||||||
*/
|
* identifying which route these statistics apply to.
|
||||||
|
*/
|
||||||
this.lastCacheHit = null;
|
this.lastCacheHit = null;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The key from the last cache miss. This is useful in identifying which route these statistics apply to.
|
* The key from the last cache miss. This is useful in
|
||||||
*/
|
* identifying which route these statistics apply to.
|
||||||
|
*/
|
||||||
this.lastCacheMiss = null;
|
this.lastCacheMiss = null;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Return performance statistics
|
* Return performance statistics
|
||||||
*/
|
* @returns {Object}
|
||||||
|
*/
|
||||||
this.report = function () {
|
this.report = function () {
|
||||||
return {
|
return {
|
||||||
lastCacheHit: this.lastCacheHit,
|
lastCacheHit: this.lastCacheHit,
|
||||||
@@ -579,10 +652,13 @@ function ApiCache() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Computes a cache hit rate from an array of hits and misses.
|
* Computes a cache hit rate from an array of hits and
|
||||||
* @param {Uint8Array} array An array representing hits and misses.
|
* misses.
|
||||||
* @returns a number between 0 and 1, or null if the array has no hits or misses
|
* @param {Uint8Array} array An array representing hits and
|
||||||
*/
|
* misses.
|
||||||
|
* @returns {?number} a number between 0 and 1, or null if
|
||||||
|
* the array has no hits or misses
|
||||||
|
*/
|
||||||
this.hitRate = function (array) {
|
this.hitRate = function (array) {
|
||||||
let hits = 0;
|
let hits = 0;
|
||||||
let misses = 0;
|
let misses = 0;
|
||||||
@@ -608,16 +684,17 @@ function ApiCache() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Record a hit or miss in the given array. It will be recorded at a position determined
|
* Record a hit or miss in the given array. It will be
|
||||||
* by the current value of the callCount variable.
|
* recorded at a position determined by the current value of
|
||||||
* @param {Uint8Array} array An array representing hits and misses.
|
* the callCount variable.
|
||||||
* @param {boolean} hit true for a hit, false for a miss
|
* @param {Uint8Array} array An array representing hits and
|
||||||
* Each element in the array is 8 bits, and encodes 4 hit/miss records.
|
* misses.
|
||||||
* Each hit or miss is encoded as to bits as follows:
|
* @param {boolean} hit true for a hit, false for a miss
|
||||||
* 00 means no hit or miss has been recorded in these bits
|
* Each element in the array is 8 bits, and encodes 4
|
||||||
* 01 encodes a hit
|
* hit/miss records. Each hit or miss is encoded as to bits
|
||||||
* 10 encodes a miss
|
* as follows: 00 means no hit or miss has been recorded in
|
||||||
*/
|
* these bits 01 encodes a hit 10 encodes a miss
|
||||||
|
*/
|
||||||
this.recordHitInArray = function (array, hit) {
|
this.recordHitInArray = function (array, hit) {
|
||||||
let arrayIndex = ~~(this.callCount / 4) % array.length;
|
let arrayIndex = ~~(this.callCount / 4) % array.length;
|
||||||
let bitOffset = (this.callCount % 4) * 2; // 2 bits per record, 4 records per uint8 array element
|
let bitOffset = (this.callCount % 4) * 2; // 2 bits per record, 4 records per uint8 array element
|
||||||
@@ -627,9 +704,11 @@ function ApiCache() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Records the hit or miss in the tracking arrays and increments the call count.
|
* Records the hit or miss in the tracking arrays and
|
||||||
* @param {boolean} hit true records a hit, false records a miss
|
* increments the call count.
|
||||||
*/
|
* @param {boolean} hit true records a hit, false records a
|
||||||
|
* miss
|
||||||
|
*/
|
||||||
this.recordHit = function (hit) {
|
this.recordHit = function (hit) {
|
||||||
this.recordHitInArray(this.hitsLast100, hit);
|
this.recordHitInArray(this.hitsLast100, hit);
|
||||||
this.recordHitInArray(this.hitsLast1000, hit);
|
this.recordHitInArray(this.hitsLast1000, hit);
|
||||||
@@ -642,18 +721,18 @@ function ApiCache() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Records a hit event, setting lastCacheMiss to the given key
|
* Records a hit event, setting lastCacheMiss to the given key
|
||||||
* @param {string} key The key that had the cache hit
|
* @param {string} key The key that had the cache hit
|
||||||
*/
|
*/
|
||||||
this.hit = function (key) {
|
this.hit = function (key) {
|
||||||
this.recordHit(true);
|
this.recordHit(true);
|
||||||
this.lastCacheHit = key;
|
this.lastCacheHit = key;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Records a miss event, setting lastCacheMiss to the given key
|
* Records a miss event, setting lastCacheMiss to the given key
|
||||||
* @param {string} key The key that had the cache miss
|
* @param {string} key The key that had the cache miss
|
||||||
*/
|
*/
|
||||||
this.miss = function (key) {
|
this.miss = function (key) {
|
||||||
this.recordHit(false);
|
this.recordHit(false);
|
||||||
this.lastCacheMiss = key;
|
this.lastCacheMiss = key;
|
||||||
@@ -664,6 +743,13 @@ function ApiCache() {
|
|||||||
|
|
||||||
performanceArray.push(perf);
|
performanceArray.push(perf);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cache a request
|
||||||
|
* @param {Object} req Express request object
|
||||||
|
* @param {Object} res Express response object
|
||||||
|
* @param {function} next Function to call next
|
||||||
|
* @returns {any}
|
||||||
|
*/
|
||||||
let cache = function (req, res, next) {
|
let cache = function (req, res, next) {
|
||||||
function bypass() {
|
function bypass() {
|
||||||
debug("bypass detected, skipping cache.");
|
debug("bypass detected, skipping cache.");
|
||||||
@@ -771,6 +857,11 @@ function ApiCache() {
|
|||||||
return cache;
|
return cache;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Process options
|
||||||
|
* @param {Object} options
|
||||||
|
* @returns {Object}
|
||||||
|
*/
|
||||||
this.options = function (options) {
|
this.options = function (options) {
|
||||||
if (options) {
|
if (options) {
|
||||||
Object.assign(globalOptions, options);
|
Object.assign(globalOptions, options);
|
||||||
@@ -791,6 +882,7 @@ function ApiCache() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/** Reset the index */
|
||||||
this.resetIndex = function () {
|
this.resetIndex = function () {
|
||||||
index = {
|
index = {
|
||||||
all: [],
|
all: [],
|
||||||
@@ -798,6 +890,11 @@ function ApiCache() {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new instance of ApiCache
|
||||||
|
* @param {Object} config Config to pass
|
||||||
|
* @returns {ApiCache}
|
||||||
|
*/
|
||||||
this.newInstance = function (config) {
|
this.newInstance = function (config) {
|
||||||
let instance = new ApiCache();
|
let instance = new ApiCache();
|
||||||
|
|
||||||
@@ -808,6 +905,7 @@ function ApiCache() {
|
|||||||
return instance;
|
return instance;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/** Clone this instance */
|
||||||
this.clone = function () {
|
this.clone = function () {
|
||||||
return this.newInstance(this.options());
|
return this.newInstance(this.options());
|
||||||
};
|
};
|
||||||
|
@@ -3,6 +3,15 @@ function MemoryCache() {
|
|||||||
this.size = 0;
|
this.size = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param {string} key Key to store cache as
|
||||||
|
* @param {any} value Value to store
|
||||||
|
* @param {number} time Time to store for
|
||||||
|
* @param {function(any, string)} timeoutCallback Callback to call in
|
||||||
|
* case of timeout
|
||||||
|
* @returns {Object}
|
||||||
|
*/
|
||||||
MemoryCache.prototype.add = function (key, value, time, timeoutCallback) {
|
MemoryCache.prototype.add = function (key, value, time, timeoutCallback) {
|
||||||
let old = this.cache[key];
|
let old = this.cache[key];
|
||||||
let instance = this;
|
let instance = this;
|
||||||
@@ -22,6 +31,11 @@ MemoryCache.prototype.add = function (key, value, time, timeoutCallback) {
|
|||||||
return entry;
|
return entry;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete a cache entry
|
||||||
|
* @param {string} key Key to delete
|
||||||
|
* @returns {null}
|
||||||
|
*/
|
||||||
MemoryCache.prototype.delete = function (key) {
|
MemoryCache.prototype.delete = function (key) {
|
||||||
let entry = this.cache[key];
|
let entry = this.cache[key];
|
||||||
|
|
||||||
@@ -36,18 +50,32 @@ MemoryCache.prototype.delete = function (key) {
|
|||||||
return null;
|
return null;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get value of key
|
||||||
|
* @param {string} key
|
||||||
|
* @returns {Object}
|
||||||
|
*/
|
||||||
MemoryCache.prototype.get = function (key) {
|
MemoryCache.prototype.get = function (key) {
|
||||||
let entry = this.cache[key];
|
let entry = this.cache[key];
|
||||||
|
|
||||||
return entry;
|
return entry;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get value of cache entry
|
||||||
|
* @param {string} key
|
||||||
|
* @returns {any}
|
||||||
|
*/
|
||||||
MemoryCache.prototype.getValue = function (key) {
|
MemoryCache.prototype.getValue = function (key) {
|
||||||
let entry = this.get(key);
|
let entry = this.get(key);
|
||||||
|
|
||||||
return entry && entry.value;
|
return entry && entry.value;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clear cache
|
||||||
|
* @returns {boolean}
|
||||||
|
*/
|
||||||
MemoryCache.prototype.clear = function () {
|
MemoryCache.prototype.clear = function () {
|
||||||
Object.keys(this.cache).forEach(function (key) {
|
Object.keys(this.cache).forEach(function (key) {
|
||||||
this.delete(key);
|
this.delete(key);
|
||||||
|
@@ -37,6 +37,12 @@ class AliyunSMS extends NotificationProvider {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send the SMS notification
|
||||||
|
* @param {BeanModel} notification Notification details
|
||||||
|
* @param {string} msgbody Message template
|
||||||
|
* @returns {boolean} True if successful else false
|
||||||
|
*/
|
||||||
async sendSms(notification, msgbody) {
|
async sendSms(notification, msgbody) {
|
||||||
let params = {
|
let params = {
|
||||||
PhoneNumbers: notification.phonenumber,
|
PhoneNumbers: notification.phonenumber,
|
||||||
@@ -70,7 +76,12 @@ class AliyunSMS extends NotificationProvider {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Aliyun request sign */
|
/**
|
||||||
|
* Aliyun request sign
|
||||||
|
* @param {Object} param Parameters object to sign
|
||||||
|
* @param {string} AccessKeySecret Secret key to sign parameters with
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
sign(param, AccessKeySecret) {
|
sign(param, AccessKeySecret) {
|
||||||
let param2 = {};
|
let param2 = {};
|
||||||
let data = [];
|
let data = [];
|
||||||
@@ -82,8 +93,23 @@ class AliyunSMS extends NotificationProvider {
|
|||||||
param2[key] = param[key];
|
param2[key] = param[key];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Escape more characters than encodeURIComponent does.
|
||||||
|
// For generating Aliyun signature, all characters except A-Za-z0-9~-._ are encoded.
|
||||||
|
// See https://help.aliyun.com/document_detail/315526.html
|
||||||
|
// This encoding methods as known as RFC 3986 (https://tools.ietf.org/html/rfc3986)
|
||||||
|
let moreEscapesTable = function (m) {
|
||||||
|
return {
|
||||||
|
"!": "%21",
|
||||||
|
"*": "%2A",
|
||||||
|
"'": "%27",
|
||||||
|
"(": "%28",
|
||||||
|
")": "%29"
|
||||||
|
}[m];
|
||||||
|
};
|
||||||
|
|
||||||
for (let key in param2) {
|
for (let key in param2) {
|
||||||
data.push(`${encodeURIComponent(key)}=${encodeURIComponent(param2[key])}`);
|
let value = encodeURIComponent(param2[key]).replace(/[!*'()]/g, moreEscapesTable);
|
||||||
|
data.push(`${encodeURIComponent(key)}=${value}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
let StringToSign = `POST&${encodeURIComponent("/")}&${encodeURIComponent(data.join("&"))}`;
|
let StringToSign = `POST&${encodeURIComponent("/")}&${encodeURIComponent(data.join("&"))}`;
|
||||||
@@ -93,6 +119,11 @@ class AliyunSMS extends NotificationProvider {
|
|||||||
.digest("base64");
|
.digest("base64");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert status constant to string
|
||||||
|
* @param {const} status The status constant
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
statusToString(status) {
|
statusToString(status) {
|
||||||
switch (status) {
|
switch (status) {
|
||||||
case DOWN:
|
case DOWN:
|
||||||
|
@@ -6,9 +6,14 @@ class Apprise extends NotificationProvider {
|
|||||||
name = "apprise";
|
name = "apprise";
|
||||||
|
|
||||||
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
|
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
|
||||||
let s = childProcess.spawnSync("apprise", [ "-vv", "-b", msg, notification.appriseURL ]);
|
const args = [ "-vv", "-b", msg, notification.appriseURL ];
|
||||||
|
if (notification.title) {
|
||||||
|
args.push("-t");
|
||||||
|
args.push(notification.title);
|
||||||
|
}
|
||||||
|
const s = childProcess.spawnSync("apprise", args);
|
||||||
|
|
||||||
let output = (s.stdout) ? s.stdout.toString() : "ERROR: maybe apprise not found";
|
const output = (s.stdout) ? s.stdout.toString() : "ERROR: maybe apprise not found";
|
||||||
|
|
||||||
if (output) {
|
if (output) {
|
||||||
|
|
||||||
|
@@ -44,7 +44,12 @@ class Bark extends NotificationProvider {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// add additional parameter for better on device styles (iOS 15 optimized)
|
/**
|
||||||
|
* Add additional parameter for better on device styles (iOS 15
|
||||||
|
* optimized)
|
||||||
|
* @param {string} postUrl URL to append parameters to
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
appendAdditionalParameters(postUrl) {
|
appendAdditionalParameters(postUrl) {
|
||||||
// grouping all our notifications
|
// grouping all our notifications
|
||||||
postUrl += "?group=" + barkNotificationGroup;
|
postUrl += "?group=" + barkNotificationGroup;
|
||||||
@@ -55,7 +60,11 @@ class Bark extends NotificationProvider {
|
|||||||
return postUrl;
|
return postUrl;
|
||||||
}
|
}
|
||||||
|
|
||||||
// thrown if failed to check result, result code should be in range 2xx
|
/**
|
||||||
|
* Check if result is successful
|
||||||
|
* @param {Object} result Axios response object
|
||||||
|
* @throws {Error} The status code is not in range 2xx
|
||||||
|
*/
|
||||||
checkResult(result) {
|
checkResult(result) {
|
||||||
if (result.status == null) {
|
if (result.status == null) {
|
||||||
throw new Error("Bark notification failed with invalid response!");
|
throw new Error("Bark notification failed with invalid response!");
|
||||||
@@ -65,6 +74,13 @@ class Bark extends NotificationProvider {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send the message
|
||||||
|
* @param {string} title Message title
|
||||||
|
* @param {string} subtitle Message
|
||||||
|
* @param {string} endpoint Endpoint to send request to
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
async postNotification(title, subtitle, endpoint) {
|
async postNotification(title, subtitle, endpoint) {
|
||||||
// url encode title and subtitle
|
// url encode title and subtitle
|
||||||
title = encodeURIComponent(title);
|
title = encodeURIComponent(title);
|
||||||
|
@@ -37,6 +37,12 @@ class DingDing extends NotificationProvider {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send message to DingDing
|
||||||
|
* @param {BeanModel} notification
|
||||||
|
* @param {Object} params Parameters of message
|
||||||
|
* @returns {boolean} True if successful else false
|
||||||
|
*/
|
||||||
async sendToDingDing(notification, params) {
|
async sendToDingDing(notification, params) {
|
||||||
let timestamp = Date.now();
|
let timestamp = Date.now();
|
||||||
|
|
||||||
@@ -56,7 +62,12 @@ class DingDing extends NotificationProvider {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** DingDing sign */
|
/**
|
||||||
|
* DingDing sign
|
||||||
|
* @param {Date} timestamp Timestamp of message
|
||||||
|
* @param {string} secretKey Secret key to sign data with
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
sign(timestamp, secretKey) {
|
sign(timestamp, secretKey) {
|
||||||
return Crypto
|
return Crypto
|
||||||
.createHmac("sha256", Buffer.from(secretKey, "utf8"))
|
.createHmac("sha256", Buffer.from(secretKey, "utf8"))
|
||||||
@@ -64,7 +75,13 @@ class DingDing extends NotificationProvider {
|
|||||||
.digest("base64");
|
.digest("base64");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert status constant to string
|
||||||
|
* @param {const} status The status constant
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
statusToString(status) {
|
statusToString(status) {
|
||||||
|
// TODO: Move to notification-provider.js to avoid repetition in classes
|
||||||
switch (status) {
|
switch (status) {
|
||||||
case DOWN:
|
case DOWN:
|
||||||
return "DOWN";
|
return "DOWN";
|
||||||
|
@@ -22,16 +22,23 @@ class Discord extends NotificationProvider {
|
|||||||
return okMsg;
|
return okMsg;
|
||||||
}
|
}
|
||||||
|
|
||||||
let url;
|
let address;
|
||||||
|
|
||||||
if (monitorJSON["type"] === "port") {
|
switch (monitorJSON["type"]) {
|
||||||
url = monitorJSON["hostname"];
|
case "ping":
|
||||||
if (monitorJSON["port"]) {
|
address = monitorJSON["hostname"];
|
||||||
url += ":" + monitorJSON["port"];
|
break;
|
||||||
}
|
case "port":
|
||||||
|
case "dns":
|
||||||
} else {
|
case "steam":
|
||||||
url = monitorJSON["url"];
|
address = monitorJSON["hostname"];
|
||||||
|
if (monitorJSON["port"]) {
|
||||||
|
address += ":" + monitorJSON["port"];
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
address = monitorJSON["url"];
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
// If heartbeatJSON is not null, we go into the normal alerting loop.
|
// If heartbeatJSON is not null, we go into the normal alerting loop.
|
||||||
@@ -48,8 +55,8 @@ class Discord extends NotificationProvider {
|
|||||||
value: monitorJSON["name"],
|
value: monitorJSON["name"],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Service URL",
|
name: monitorJSON["type"] === "push" ? "Service Type" : "Service URL",
|
||||||
value: url,
|
value: monitorJSON["type"] === "push" ? "Heartbeat" : address,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Time (UTC)",
|
name: "Time (UTC)",
|
||||||
@@ -83,8 +90,8 @@ class Discord extends NotificationProvider {
|
|||||||
value: monitorJSON["name"],
|
value: monitorJSON["name"],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Service URL",
|
name: monitorJSON["type"] === "push" ? "Service Type" : "Service URL",
|
||||||
value: url.startsWith("http") ? "[Visit Service](" + url + ")" : url,
|
value: monitorJSON["type"] === "push" ? "Heartbeat" : address.startsWith("http") ? "[Visit Service](" + address + ")" : address,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Time (UTC)",
|
name: "Time (UTC)",
|
||||||
@@ -92,7 +99,7 @@ class Discord extends NotificationProvider {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Ping",
|
name: "Ping",
|
||||||
value: heartbeatJSON["ping"] + "ms",
|
value: heartbeatJSON["ping"] == null ? "N/A" : heartbeatJSON["ping"] + " ms",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
}],
|
}],
|
||||||
|
@@ -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;
|
||||||
|
@@ -7,17 +7,23 @@ class NotificationProvider {
|
|||||||
name = undefined;
|
name = undefined;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param notification : BeanModel
|
* Send a notification
|
||||||
* @param msg : string General Message
|
* @param {BeanModel} notification
|
||||||
* @param monitorJSON : object Monitor details (For Up/Down only)
|
* @param {string} msg General Message
|
||||||
* @param heartbeatJSON : object Heartbeat details (For Up/Down only)
|
* @param {?Object} monitorJSON Monitor details (For Up/Down only)
|
||||||
|
* @param {?Object} heartbeatJSON Heartbeat details (For Up/Down only)
|
||||||
* @returns {Promise<string>} Return Successful Message
|
* @returns {Promise<string>} Return Successful Message
|
||||||
* Throw Error with fail msg
|
* @throws Error with fail msg
|
||||||
*/
|
*/
|
||||||
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
|
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
|
||||||
throw new Error("Have to override Notification.send(...)");
|
throw new Error("Have to override Notification.send(...)");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Throws an error
|
||||||
|
* @param {any} error The error to throw
|
||||||
|
* @throws {any} The error specified
|
||||||
|
*/
|
||||||
throwGeneralAxiosError(error) {
|
throwGeneralAxiosError(error) {
|
||||||
let msg = "Error: " + error + " ";
|
let msg = "Error: " + error + " ";
|
||||||
|
|
||||||
|
26
server/notification-providers/ntfy.js
Normal file
26
server/notification-providers/ntfy.js
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
const NotificationProvider = require("./notification-provider");
|
||||||
|
const axios = require("axios");
|
||||||
|
|
||||||
|
class Ntfy extends NotificationProvider {
|
||||||
|
|
||||||
|
name = "ntfy";
|
||||||
|
|
||||||
|
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
|
||||||
|
let okMsg = "Sent Successfully.";
|
||||||
|
try {
|
||||||
|
await axios.post(`${notification.ntfyserverurl}`, {
|
||||||
|
"topic": notification.ntfytopic,
|
||||||
|
"message": msg,
|
||||||
|
"priority": notification.ntfyPriority || 4,
|
||||||
|
"title": "Uptime-Kuma",
|
||||||
|
});
|
||||||
|
|
||||||
|
return okMsg;
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
this.throwGeneralAxiosError(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = Ntfy;
|
113
server/notification-providers/pagerduty.js
Normal file
113
server/notification-providers/pagerduty.js
Normal file
@@ -0,0 +1,113 @@
|
|||||||
|
const NotificationProvider = require("./notification-provider");
|
||||||
|
const axios = require("axios");
|
||||||
|
const { UP, DOWN, getMonitorRelativeURL } = require("../../src/util");
|
||||||
|
const { setting } = require("../util-server");
|
||||||
|
let successMessage = "Sent Successfully.";
|
||||||
|
|
||||||
|
class PagerDuty extends NotificationProvider {
|
||||||
|
name = "PagerDuty";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @inheritdoc
|
||||||
|
*/
|
||||||
|
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
|
||||||
|
try {
|
||||||
|
if (heartbeatJSON == null) {
|
||||||
|
const title = "Uptime Kuma Alert";
|
||||||
|
const monitor = {
|
||||||
|
type: "ping",
|
||||||
|
url: "Uptime Kuma Test Button",
|
||||||
|
};
|
||||||
|
return this.postNotification(notification, title, msg, monitor);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (heartbeatJSON.status === UP) {
|
||||||
|
const title = "Uptime Kuma Monitor ✅ Up";
|
||||||
|
const eventAction = notification.pagerdutyAutoResolve || null;
|
||||||
|
|
||||||
|
return this.postNotification(notification, title, heartbeatJSON.msg, monitorJSON, eventAction);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (heartbeatJSON.status === DOWN) {
|
||||||
|
const title = "Uptime Kuma Monitor 🔴 Down";
|
||||||
|
return this.postNotification(notification, title, heartbeatJSON.msg, monitorJSON, "trigger");
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
this.throwGeneralAxiosError(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if result is successful, result code should be in range 2xx
|
||||||
|
* @param {Object} result Axios response object
|
||||||
|
* @throws {Error} The status code is not in range 2xx
|
||||||
|
*/
|
||||||
|
checkResult(result) {
|
||||||
|
if (result.status == null) {
|
||||||
|
throw new Error("PagerDuty notification failed with invalid response!");
|
||||||
|
}
|
||||||
|
if (result.status < 200 || result.status >= 300) {
|
||||||
|
throw new Error("PagerDuty notification failed with status code " + result.status);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send the message
|
||||||
|
* @param {BeanModel} notification Message title
|
||||||
|
* @param {string} title Message title
|
||||||
|
* @param {string} body Message
|
||||||
|
* @param {Object} monitorInfo Monitor details (For Up/Down only)
|
||||||
|
* @param {?string} eventAction Action event for PagerDuty (trigger, acknowledge, resolve)
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
async postNotification(notification, title, body, monitorInfo, eventAction = "trigger") {
|
||||||
|
|
||||||
|
if (eventAction == null) {
|
||||||
|
return "No action required";
|
||||||
|
}
|
||||||
|
|
||||||
|
let monitorUrl;
|
||||||
|
if (monitorInfo.type === "port") {
|
||||||
|
monitorUrl = monitorInfo.hostname;
|
||||||
|
if (monitorInfo.port) {
|
||||||
|
monitorUrl += ":" + monitorInfo.port;
|
||||||
|
}
|
||||||
|
} else if (monitorInfo.hostname != null) {
|
||||||
|
monitorUrl = monitorInfo.hostname;
|
||||||
|
} else {
|
||||||
|
monitorUrl = monitorInfo.url;
|
||||||
|
}
|
||||||
|
|
||||||
|
const options = {
|
||||||
|
method: "POST",
|
||||||
|
url: notification.pagerdutyIntegrationUrl,
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
data: {
|
||||||
|
payload: {
|
||||||
|
summary: `[${title}] [${monitorInfo.name}] ${body}`,
|
||||||
|
severity: notification.pagerdutyPriority || "warning",
|
||||||
|
source: monitorUrl,
|
||||||
|
},
|
||||||
|
routing_key: notification.pagerdutyIntegrationKey,
|
||||||
|
event_action: eventAction,
|
||||||
|
dedup_key: "Uptime Kuma/" + monitorInfo.id,
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const baseURL = await setting("primaryBaseURL");
|
||||||
|
if (baseURL && monitorInfo) {
|
||||||
|
options.client = "Uptime Kuma";
|
||||||
|
options.client_url = baseURL + getMonitorRelativeURL(monitorInfo.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
let result = await axios.request(options);
|
||||||
|
this.checkResult(result);
|
||||||
|
if (result.statusText != null) {
|
||||||
|
return "PagerDuty notification succeed: " + result.statusText;
|
||||||
|
}
|
||||||
|
|
||||||
|
return successMessage;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = PagerDuty;
|
@@ -10,6 +10,7 @@ class Slack extends NotificationProvider {
|
|||||||
/**
|
/**
|
||||||
* Deprecated property notification.slackbutton
|
* Deprecated property notification.slackbutton
|
||||||
* Set it as primary base url if this is not yet set.
|
* Set it as primary base url if this is not yet set.
|
||||||
|
* @param {string} url The primary base URL to use
|
||||||
*/
|
*/
|
||||||
static async deprecateURL(url) {
|
static async deprecateURL(url) {
|
||||||
let currentPrimaryBaseURL = await setting("primaryBaseURL");
|
let currentPrimaryBaseURL = await setting("primaryBaseURL");
|
||||||
|
@@ -5,6 +5,12 @@ const { DOWN, UP } = require("../../src/util");
|
|||||||
class Teams extends NotificationProvider {
|
class Teams extends NotificationProvider {
|
||||||
name = "teams";
|
name = "teams";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate the message to send
|
||||||
|
* @param {const} status The status constant
|
||||||
|
* @param {string} monitorName Name of monitor
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
_statusMessageFactory = (status, monitorName) => {
|
_statusMessageFactory = (status, monitorName) => {
|
||||||
if (status === DOWN) {
|
if (status === DOWN) {
|
||||||
return `🔴 Application [${monitorName}] went down`;
|
return `🔴 Application [${monitorName}] went down`;
|
||||||
@@ -14,6 +20,11 @@ class Teams extends NotificationProvider {
|
|||||||
return "Notification";
|
return "Notification";
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Select theme color to use based on status
|
||||||
|
* @param {const} status The status constant
|
||||||
|
* @returns {string} Selected color in hex RGB format
|
||||||
|
*/
|
||||||
_getThemeColor = (status) => {
|
_getThemeColor = (status) => {
|
||||||
if (status === DOWN) {
|
if (status === DOWN) {
|
||||||
return "ff0000";
|
return "ff0000";
|
||||||
@@ -24,6 +35,14 @@ class Teams extends NotificationProvider {
|
|||||||
return "008cff";
|
return "008cff";
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate payload for notification
|
||||||
|
* @param {const} status The status of the monitor
|
||||||
|
* @param {string} monitorMessage Message to send
|
||||||
|
* @param {string} monitorName Name of monitor affected
|
||||||
|
* @param {string} monitorUrl URL of monitor affected
|
||||||
|
* @returns {Object}
|
||||||
|
*/
|
||||||
_notificationPayloadFactory = ({
|
_notificationPayloadFactory = ({
|
||||||
status,
|
status,
|
||||||
monitorMessage,
|
monitorMessage,
|
||||||
@@ -74,10 +93,21 @@ class Teams extends NotificationProvider {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send the notification
|
||||||
|
* @param {string} webhookUrl URL to send the request to
|
||||||
|
* @param {Object} payload Payload generated by _notificationPayloadFactory
|
||||||
|
*/
|
||||||
_sendNotification = async (webhookUrl, payload) => {
|
_sendNotification = async (webhookUrl, payload) => {
|
||||||
await axios.post(webhookUrl, payload);
|
await axios.post(webhookUrl, payload);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send a general notification
|
||||||
|
* @param {string} webhookUrl URL to send request to
|
||||||
|
* @param {string} msg Message to send
|
||||||
|
* @returns {Promise<void>}
|
||||||
|
*/
|
||||||
_handleGeneralNotification = (webhookUrl, msg) => {
|
_handleGeneralNotification = (webhookUrl, msg) => {
|
||||||
const payload = this._notificationPayloadFactory({
|
const payload = this._notificationPayloadFactory({
|
||||||
monitorMessage: msg
|
monitorMessage: msg
|
||||||
|
@@ -24,6 +24,12 @@ class WeCom extends NotificationProvider {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate the message to send
|
||||||
|
* @param {Object} heartbeatJSON Heartbeat details (For Up/Down only)
|
||||||
|
* @param {string} msg General message
|
||||||
|
* @returns {Object}
|
||||||
|
*/
|
||||||
composeMessage(heartbeatJSON, msg) {
|
composeMessage(heartbeatJSON, msg) {
|
||||||
let title;
|
let title;
|
||||||
if (msg != null && heartbeatJSON != null && heartbeatJSON["status"] === UP) {
|
if (msg != null && heartbeatJSON != null && heartbeatJSON["status"] === UP) {
|
||||||
|
@@ -2,6 +2,7 @@ const { R } = require("redbean-node");
|
|||||||
const Apprise = require("./notification-providers/apprise");
|
const Apprise = require("./notification-providers/apprise");
|
||||||
const Discord = require("./notification-providers/discord");
|
const Discord = require("./notification-providers/discord");
|
||||||
const Gotify = require("./notification-providers/gotify");
|
const Gotify = require("./notification-providers/gotify");
|
||||||
|
const Ntfy = require("./notification-providers/ntfy");
|
||||||
const Line = require("./notification-providers/line");
|
const Line = require("./notification-providers/line");
|
||||||
const LunaSea = require("./notification-providers/lunasea");
|
const LunaSea = require("./notification-providers/lunasea");
|
||||||
const Mattermost = require("./notification-providers/mattermost");
|
const Mattermost = require("./notification-providers/mattermost");
|
||||||
@@ -29,6 +30,7 @@ const SerwerSMS = require("./notification-providers/serwersms");
|
|||||||
const Stackfield = require("./notification-providers/stackfield");
|
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 GoogleChat = require("./notification-providers/google-chat");
|
||||||
|
const PagerDuty = require("./notification-providers/pagerduty");
|
||||||
const Gorush = require("./notification-providers/gorush");
|
const Gorush = require("./notification-providers/gorush");
|
||||||
const Alerta = require("./notification-providers/alerta");
|
const Alerta = require("./notification-providers/alerta");
|
||||||
const OneBot = require("./notification-providers/onebot");
|
const OneBot = require("./notification-providers/onebot");
|
||||||
@@ -38,6 +40,7 @@ class Notification {
|
|||||||
|
|
||||||
providerList = {};
|
providerList = {};
|
||||||
|
|
||||||
|
/** Initialize the notification providers */
|
||||||
static init() {
|
static init() {
|
||||||
log.info("notification", "Prepare Notification Providers");
|
log.info("notification", "Prepare Notification Providers");
|
||||||
|
|
||||||
@@ -50,6 +53,7 @@ class Notification {
|
|||||||
new Discord(),
|
new Discord(),
|
||||||
new Teams(),
|
new Teams(),
|
||||||
new Gotify(),
|
new Gotify(),
|
||||||
|
new Ntfy(),
|
||||||
new Line(),
|
new Line(),
|
||||||
new LunaSea(),
|
new LunaSea(),
|
||||||
new Feishu(),
|
new Feishu(),
|
||||||
@@ -73,6 +77,7 @@ class Notification {
|
|||||||
new Stackfield(),
|
new Stackfield(),
|
||||||
new WeCom(),
|
new WeCom(),
|
||||||
new GoogleChat(),
|
new GoogleChat(),
|
||||||
|
new PagerDuty(),
|
||||||
new Gorush(),
|
new Gorush(),
|
||||||
new Alerta(),
|
new Alerta(),
|
||||||
new OneBot(),
|
new OneBot(),
|
||||||
@@ -92,13 +97,13 @@ class Notification {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
* Send a notification
|
||||||
* @param notification : BeanModel
|
* @param {BeanModel} notification
|
||||||
* @param msg : string General Message
|
* @param {string} msg General Message
|
||||||
* @param monitorJSON : object Monitor details (For Up/Down only)
|
* @param {Object} monitorJSON Monitor details (For Up/Down only)
|
||||||
* @param heartbeatJSON : object Heartbeat details (For Up/Down only)
|
* @param {Object} heartbeatJSON Heartbeat details (For Up/Down only)
|
||||||
* @returns {Promise<string>} Successful msg
|
* @returns {Promise<string>} Successful msg
|
||||||
* Throw Error with fail msg
|
* @throws Error with fail msg
|
||||||
*/
|
*/
|
||||||
static async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
|
static async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
|
||||||
if (this.providerList[notification.type]) {
|
if (this.providerList[notification.type]) {
|
||||||
@@ -108,6 +113,13 @@ class Notification {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Save a notification
|
||||||
|
* @param {Object} notification Notification to save
|
||||||
|
* @param {?number} notificationID ID of notification to update
|
||||||
|
* @param {number} userID ID of user who adds notification
|
||||||
|
* @returns {Promise<Bean>}
|
||||||
|
*/
|
||||||
static async save(notification, notificationID, userID) {
|
static async save(notification, notificationID, userID) {
|
||||||
let bean;
|
let bean;
|
||||||
|
|
||||||
@@ -138,6 +150,12 @@ class Notification {
|
|||||||
return bean;
|
return bean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete a notification
|
||||||
|
* @param {number} notificationID ID of notification to delete
|
||||||
|
* @param {number} userID ID of user who created notification
|
||||||
|
* @returns {Promise<void>}
|
||||||
|
*/
|
||||||
static async delete(notificationID, userID) {
|
static async delete(notificationID, userID) {
|
||||||
let bean = await R.findOne("notification", " id = ? AND user_id = ? ", [
|
let bean = await R.findOne("notification", " id = ? AND user_id = ? ", [
|
||||||
notificationID,
|
notificationID,
|
||||||
@@ -151,6 +169,10 @@ class Notification {
|
|||||||
await R.trash(bean);
|
await R.trash(bean);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if apprise exists
|
||||||
|
* @returns {boolean} Does the command apprise exist?
|
||||||
|
*/
|
||||||
static checkApprise() {
|
static checkApprise() {
|
||||||
let commandExistsSync = require("command-exists").sync;
|
let commandExistsSync = require("command-exists").sync;
|
||||||
let exists = commandExistsSync("apprise");
|
let exists = commandExistsSync("apprise");
|
||||||
@@ -160,11 +182,10 @@ class Notification {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Adds a new monitor to the database.
|
* Apply the notification to every monitor
|
||||||
* @param {number} userID The ID of the user that owns this monitor.
|
* @param {number} notificationID ID of notification to apply
|
||||||
* @param {string} name The name of this monitor.
|
* @param {number} userID ID of user who created notification
|
||||||
*
|
* @returns {Promise<void>}
|
||||||
* Generated by Trelent
|
|
||||||
*/
|
*/
|
||||||
async function applyNotificationEveryMonitor(notificationID, userID) {
|
async function applyNotificationEveryMonitor(notificationID, userID) {
|
||||||
let monitors = await R.getAll("SELECT id FROM monitor WHERE user_id = ?", [
|
let monitors = await R.getAll("SELECT id FROM monitor WHERE user_id = ?", [
|
||||||
|
@@ -2,10 +2,21 @@ const passwordHashOld = require("password-hash");
|
|||||||
const bcrypt = require("bcryptjs");
|
const bcrypt = require("bcryptjs");
|
||||||
const saltRounds = 10;
|
const saltRounds = 10;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hash a password
|
||||||
|
* @param {string} password
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
exports.generate = function (password) {
|
exports.generate = function (password) {
|
||||||
return bcrypt.hashSync(password, saltRounds);
|
return bcrypt.hashSync(password, saltRounds);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Verify a password against a hash
|
||||||
|
* @param {string} password
|
||||||
|
* @param {string} hash
|
||||||
|
* @returns {boolean} Does the password match the hash?
|
||||||
|
*/
|
||||||
exports.verify = function (password, hash) {
|
exports.verify = function (password, hash) {
|
||||||
if (isSHA1(hash)) {
|
if (isSHA1(hash)) {
|
||||||
return passwordHashOld.verify(password, hash);
|
return passwordHashOld.verify(password, hash);
|
||||||
@@ -14,10 +25,19 @@ exports.verify = function (password, hash) {
|
|||||||
return bcrypt.compareSync(password, hash);
|
return bcrypt.compareSync(password, hash);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Is the hash a SHA1 hash
|
||||||
|
* @param {string} hash
|
||||||
|
* @returns {boolean}
|
||||||
|
*/
|
||||||
function isSHA1(hash) {
|
function isSHA1(hash) {
|
||||||
return (typeof hash === "string" && hash.startsWith("sha1"));
|
return (typeof hash === "string" && hash.startsWith("sha1"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Does the hash need to be rehashed?
|
||||||
|
* @returns {boolean}
|
||||||
|
*/
|
||||||
exports.needRehash = function (hash) {
|
exports.needRehash = function (hash) {
|
||||||
return isSHA1(hash);
|
return isSHA1(hash);
|
||||||
};
|
};
|
||||||
|
@@ -9,11 +9,10 @@ const util = require("./util-server");
|
|||||||
module.exports = Ping;
|
module.exports = Ping;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {string} host - The host to ping
|
* Constructor for ping class
|
||||||
* @param {object} [options] - Options for the ping command
|
* @param {string} host Host to ping
|
||||||
|
* @param {object} [options] Options for the ping command
|
||||||
* @param {array|string} [options.args] - Arguments to pass to the ping command
|
* @param {array|string} [options.args] - Arguments to pass to the ping command
|
||||||
*
|
|
||||||
* Generated by Trelent
|
|
||||||
*/
|
*/
|
||||||
function Ping(host, options) {
|
function Ping(host, options) {
|
||||||
if (!host) {
|
if (!host) {
|
||||||
@@ -82,8 +81,17 @@ function Ping(host, options) {
|
|||||||
|
|
||||||
Ping.prototype.__proto__ = events.EventEmitter.prototype;
|
Ping.prototype.__proto__ = events.EventEmitter.prototype;
|
||||||
|
|
||||||
// SEND A PING
|
/**
|
||||||
// ===========
|
* Callback for send
|
||||||
|
* @callback pingCB
|
||||||
|
* @param {any} err Any error encountered
|
||||||
|
* @param {number} ms Ping time in ms
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send a ping
|
||||||
|
* @param {pingCB} callback Callback to call with results
|
||||||
|
*/
|
||||||
Ping.prototype.send = function (callback) {
|
Ping.prototype.send = function (callback) {
|
||||||
let self = this;
|
let self = this;
|
||||||
callback = callback || function (err, ms) {
|
callback = callback || function (err, ms) {
|
||||||
@@ -157,8 +165,10 @@ Ping.prototype.send = function (callback) {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// CALL Ping#send(callback) ON A TIMER
|
/**
|
||||||
// ===================================
|
* Ping every interval
|
||||||
|
* @param {pingCB} callback Callback to call with results
|
||||||
|
*/
|
||||||
Ping.prototype.start = function (callback) {
|
Ping.prototype.start = function (callback) {
|
||||||
let self = this;
|
let self = this;
|
||||||
this._i = setInterval(function () {
|
this._i = setInterval(function () {
|
||||||
@@ -167,8 +177,7 @@ Ping.prototype.start = function (callback) {
|
|||||||
self.send(callback);
|
self.send(callback);
|
||||||
};
|
};
|
||||||
|
|
||||||
// STOP SENDING PINGS
|
/** Stop sending pings */
|
||||||
// ==================
|
|
||||||
Ping.prototype.stop = function () {
|
Ping.prototype.stop = function () {
|
||||||
clearInterval(this._i);
|
clearInterval(this._i);
|
||||||
};
|
};
|
||||||
@@ -177,7 +186,7 @@ Ping.prototype.stop = function () {
|
|||||||
* Try to convert to UTF-8 for Windows, as the ping's output on Windows is not UTF-8 and could be in other languages
|
* Try to convert to UTF-8 for Windows, as the ping's output on Windows is not UTF-8 and could be in other languages
|
||||||
* Thank @pemassi
|
* Thank @pemassi
|
||||||
* https://github.com/louislam/uptime-kuma/issues/570#issuecomment-941984094
|
* https://github.com/louislam/uptime-kuma/issues/570#issuecomment-941984094
|
||||||
* @param data
|
* @param {any} data
|
||||||
* @returns {string}
|
* @returns {string}
|
||||||
*/
|
*/
|
||||||
function convertOutput(data) {
|
function convertOutput(data) {
|
||||||
|
@@ -33,8 +33,11 @@ const monitorStatus = new PrometheusClient.Gauge({
|
|||||||
});
|
});
|
||||||
|
|
||||||
class Prometheus {
|
class Prometheus {
|
||||||
monitorLabelValues = {}
|
monitorLabelValues = {};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {Object} monitor Monitor object to monitor
|
||||||
|
*/
|
||||||
constructor(monitor) {
|
constructor(monitor) {
|
||||||
this.monitorLabelValues = {
|
this.monitorLabelValues = {
|
||||||
monitor_name: monitor.name,
|
monitor_name: monitor.name,
|
||||||
@@ -45,6 +48,11 @@ class Prometheus {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update the metrics page
|
||||||
|
* @param {Object} heartbeat Heartbeat details
|
||||||
|
* @param {Object} tlsInfo TLS details
|
||||||
|
*/
|
||||||
update(heartbeat, tlsInfo) {
|
update(heartbeat, tlsInfo) {
|
||||||
|
|
||||||
if (typeof tlsInfo !== "undefined") {
|
if (typeof tlsInfo !== "undefined") {
|
||||||
|
@@ -7,7 +7,7 @@ const { UptimeKumaServer } = require("./uptime-kuma-server");
|
|||||||
|
|
||||||
class Proxy {
|
class Proxy {
|
||||||
|
|
||||||
static SUPPORTED_PROXY_PROTOCOLS = [ "http", "https", "socks", "socks5", "socks4" ]
|
static SUPPORTED_PROXY_PROTOCOLS = [ "http", "https", "socks", "socks5", "socks4" ];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Saves and updates given proxy entity
|
* Saves and updates given proxy entity
|
||||||
|
@@ -2,11 +2,26 @@ const { RateLimiter } = require("limiter");
|
|||||||
const { log } = require("../src/util");
|
const { log } = require("../src/util");
|
||||||
|
|
||||||
class KumaRateLimiter {
|
class KumaRateLimiter {
|
||||||
|
/**
|
||||||
|
* @param {Object} config Rate limiter configuration object
|
||||||
|
*/
|
||||||
constructor(config) {
|
constructor(config) {
|
||||||
this.errorMessage = config.errorMessage;
|
this.errorMessage = config.errorMessage;
|
||||||
this.rateLimiter = new RateLimiter(config);
|
this.rateLimiter = new RateLimiter(config);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Callback for pass
|
||||||
|
* @callback passCB
|
||||||
|
* @param {Object} err Too many requests
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Should the request be passed through
|
||||||
|
* @param {passCB} callback
|
||||||
|
* @param {number} [num=1] Number of tokens to remove
|
||||||
|
* @returns {Promise<boolean>}
|
||||||
|
*/
|
||||||
async pass(callback, num = 1) {
|
async pass(callback, num = 1) {
|
||||||
const remainingRequests = await this.removeTokens(num);
|
const remainingRequests = await this.removeTokens(num);
|
||||||
log.info("rate-limit", "remaining requests: " + remainingRequests);
|
log.info("rate-limit", "remaining requests: " + remainingRequests);
|
||||||
@@ -22,6 +37,11 @@ class KumaRateLimiter {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove a given number of tokens
|
||||||
|
* @param {number} [num=1] Number of tokens to remove
|
||||||
|
* @returns {Promise<number>}
|
||||||
|
*/
|
||||||
async removeTokens(num = 1) {
|
async removeTokens(num = 1) {
|
||||||
return await this.rateLimiter.removeTokens(num);
|
return await this.rateLimiter.removeTokens(num);
|
||||||
}
|
}
|
||||||
|
@@ -1,12 +1,15 @@
|
|||||||
let express = require("express");
|
let express = require("express");
|
||||||
const { allowDevAllOrigin } = 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");
|
||||||
const dayjs = require("dayjs");
|
const dayjs = require("dayjs");
|
||||||
const { UP, flipStatus, log } = require("../../src/util");
|
const { UP, DOWN, flipStatus, log } = require("../../src/util");
|
||||||
const StatusPage = require("../model/status_page");
|
const StatusPage = require("../model/status_page");
|
||||||
const { UptimeKumaServer } = require("../uptime-kuma-server");
|
const { UptimeKumaServer } = require("../uptime-kuma-server");
|
||||||
|
const { makeBadge } = require("badge-maker");
|
||||||
|
const { badgeConstants } = require("../config");
|
||||||
|
|
||||||
let router = express.Router();
|
let router = express.Router();
|
||||||
|
|
||||||
let cache = apicache.middleware;
|
let cache = apicache.middleware;
|
||||||
@@ -34,6 +37,8 @@ router.get("/api/push/:pushToken", async (request, response) => {
|
|||||||
let pushToken = request.params.pushToken;
|
let pushToken = request.params.pushToken;
|
||||||
let msg = request.query.msg || "OK";
|
let msg = request.query.msg || "OK";
|
||||||
let ping = request.query.ping || null;
|
let ping = request.query.ping || null;
|
||||||
|
let statusString = request.query.status || "up";
|
||||||
|
let status = (statusString === "up") ? UP : DOWN;
|
||||||
|
|
||||||
let monitor = await R.findOne("monitor", " push_token = ? AND active = 1 ", [
|
let monitor = await R.findOne("monitor", " push_token = ? AND active = 1 ", [
|
||||||
pushToken
|
pushToken
|
||||||
@@ -45,7 +50,6 @@ router.get("/api/push/:pushToken", async (request, response) => {
|
|||||||
|
|
||||||
const previousHeartbeat = await Monitor.getPreviousHeartbeat(monitor.id);
|
const previousHeartbeat = await Monitor.getPreviousHeartbeat(monitor.id);
|
||||||
|
|
||||||
let status = UP;
|
|
||||||
if (monitor.isUpsideDown()) {
|
if (monitor.isUpsideDown()) {
|
||||||
status = flipStatus(status);
|
status = flipStatus(status);
|
||||||
}
|
}
|
||||||
@@ -55,7 +59,7 @@ router.get("/api/push/:pushToken", async (request, response) => {
|
|||||||
let duration = 0;
|
let duration = 0;
|
||||||
|
|
||||||
let bean = R.dispense("heartbeat");
|
let bean = R.dispense("heartbeat");
|
||||||
bean.time = R.isoDateTime(dayjs.utc());
|
bean.time = R.isoDateTimeMillis(dayjs.utc());
|
||||||
|
|
||||||
if (previousHeartbeat) {
|
if (previousHeartbeat) {
|
||||||
isFirstBeat = false;
|
isFirstBeat = false;
|
||||||
@@ -63,6 +67,7 @@ router.get("/api/push/:pushToken", async (request, response) => {
|
|||||||
duration = dayjs(bean.time).diff(dayjs(previousHeartbeat.time), "second");
|
duration = dayjs(bean.time).diff(dayjs(previousHeartbeat.time), "second");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
log.debug("router", `/api/push/ called at ${dayjs().format("YYYY-MM-DD HH:mm:ss.SSS")}`);
|
||||||
log.debug("router", "PreviousStatus: " + previousStatus);
|
log.debug("router", "PreviousStatus: " + previousStatus);
|
||||||
log.debug("router", "Current Status: " + status);
|
log.debug("router", "Current Status: " + status);
|
||||||
|
|
||||||
@@ -87,120 +92,187 @@ 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/badge/:id/status", cache("5 minutes"), async (request, response) => {
|
||||||
router.get("/api/status-page/:slug", cache("5 minutes"), async (request, response) => {
|
allowAllOrigin(response);
|
||||||
allowDevAllOrigin(response);
|
|
||||||
let slug = request.params.slug;
|
|
||||||
|
|
||||||
// Get Status Page
|
const {
|
||||||
let statusPage = await R.findOne("status_page", " slug = ? ", [
|
label,
|
||||||
slug
|
upLabel = "Up",
|
||||||
]);
|
downLabel = "Down",
|
||||||
|
upColor = badgeConstants.defaultUpColor,
|
||||||
if (!statusPage) {
|
downColor = badgeConstants.defaultDownColor,
|
||||||
response.statusCode = 404;
|
style = badgeConstants.defaultStyle,
|
||||||
response.json({
|
value, // for demo purpose only
|
||||||
msg: "Not Found"
|
} = request.query;
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Incident
|
const requestedMonitorId = parseInt(request.params.id, 10);
|
||||||
let incident = await R.findOne("incident", " pin = 1 AND active = 1 AND status_page_id = ? ", [
|
const overrideValue = value !== undefined ? parseInt(value) : undefined;
|
||||||
statusPage.id,
|
|
||||||
]);
|
|
||||||
|
|
||||||
if (incident) {
|
let publicMonitor = await R.getRow(`
|
||||||
incident = incident.toPublicJSON();
|
SELECT monitor_group.monitor_id FROM monitor_group, \`group\`
|
||||||
|
WHERE monitor_group.group_id = \`group\`.id
|
||||||
|
AND monitor_group.monitor_id = ?
|
||||||
|
AND public = 1
|
||||||
|
`,
|
||||||
|
[ requestedMonitorId ]
|
||||||
|
);
|
||||||
|
|
||||||
|
const badgeValues = { style };
|
||||||
|
|
||||||
|
if (!publicMonitor) {
|
||||||
|
// return a "N/A" badge in naColor (grey), if monitor is not public / not available / non exsitant
|
||||||
|
|
||||||
|
badgeValues.message = "N/A";
|
||||||
|
badgeValues.color = badgeConstants.naColor;
|
||||||
|
} else {
|
||||||
|
const heartbeat = await Monitor.getPreviousHeartbeat(requestedMonitorId);
|
||||||
|
const state = overrideValue !== undefined ? overrideValue : heartbeat.status === 1;
|
||||||
|
|
||||||
|
badgeValues.color = state ? upColor : downColor;
|
||||||
|
badgeValues.message = label ?? state ? upLabel : downLabel;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Public Group List
|
// build the svg based on given values
|
||||||
const publicGroupList = [];
|
const svg = makeBadge(badgeValues);
|
||||||
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
|
|
||||||
});
|
|
||||||
|
|
||||||
|
response.type("image/svg+xml");
|
||||||
|
response.send(svg);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
send403(response, error.message);
|
send403(response, error.message);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
function send403(res, msg = "") {
|
router.get("/api/badge/:id/uptime/:duration?", cache("5 minutes"), async (request, response) => {
|
||||||
res.status(403).json({
|
allowAllOrigin(response);
|
||||||
"status": "fail",
|
|
||||||
"msg": msg,
|
const {
|
||||||
});
|
label,
|
||||||
}
|
labelPrefix,
|
||||||
|
labelSuffix = badgeConstants.defaultUptimeLabelSuffix,
|
||||||
|
prefix,
|
||||||
|
suffix = badgeConstants.defaultUptimeValueSuffix,
|
||||||
|
color,
|
||||||
|
labelColor,
|
||||||
|
style = badgeConstants.defaultStyle,
|
||||||
|
value, // for demo purpose only
|
||||||
|
} = request.query;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const requestedMonitorId = parseInt(request.params.id, 10);
|
||||||
|
// if no duration is given, set value to 24 (h)
|
||||||
|
const requestedDuration = request.params.duration !== undefined ? parseInt(request.params.duration, 10) : 24;
|
||||||
|
const overrideValue = value && parseFloat(value);
|
||||||
|
|
||||||
|
let publicMonitor = await R.getRow(`
|
||||||
|
SELECT monitor_group.monitor_id FROM monitor_group, \`group\`
|
||||||
|
WHERE monitor_group.group_id = \`group\`.id
|
||||||
|
AND monitor_group.monitor_id = ?
|
||||||
|
AND public = 1
|
||||||
|
`,
|
||||||
|
[ requestedMonitorId ]
|
||||||
|
);
|
||||||
|
|
||||||
|
const badgeValues = { style };
|
||||||
|
|
||||||
|
if (!publicMonitor) {
|
||||||
|
// return a "N/A" badge in naColor (grey), if monitor is not public / not available / non exsitant
|
||||||
|
badgeValues.message = "N/A";
|
||||||
|
badgeValues.color = badgeConstants.naColor;
|
||||||
|
} else {
|
||||||
|
const uptime = overrideValue ?? await Monitor.calcUptime(
|
||||||
|
requestedDuration,
|
||||||
|
requestedMonitorId
|
||||||
|
);
|
||||||
|
|
||||||
|
// limit the displayed uptime percentage to four (two, when displayed as percent) decimal digits
|
||||||
|
const cleanUptime = parseFloat(uptime.toPrecision(4));
|
||||||
|
|
||||||
|
// use a given, custom color or calculate one based on the uptime value
|
||||||
|
badgeValues.color = color ?? percentageToColor(uptime);
|
||||||
|
// use a given, custom labelColor or use the default badge label color (defined by badge-maker)
|
||||||
|
badgeValues.labelColor = labelColor ?? "";
|
||||||
|
// build a lable string. If a custom label is given, override the default one (requestedDuration)
|
||||||
|
badgeValues.label = filterAndJoin([ labelPrefix, label ?? requestedDuration, labelSuffix ]);
|
||||||
|
badgeValues.message = filterAndJoin([ prefix, `${cleanUptime * 100}`, suffix ]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// build the SVG based on given values
|
||||||
|
const svg = makeBadge(badgeValues);
|
||||||
|
|
||||||
|
response.type("image/svg+xml");
|
||||||
|
response.send(svg);
|
||||||
|
} catch (error) {
|
||||||
|
send403(response, error.message);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
router.get("/api/badge/:id/ping/:duration?", cache("5 minutes"), async (request, response) => {
|
||||||
|
allowAllOrigin(response);
|
||||||
|
|
||||||
|
const {
|
||||||
|
label,
|
||||||
|
labelPrefix,
|
||||||
|
labelSuffix = badgeConstants.defaultPingLabelSuffix,
|
||||||
|
prefix,
|
||||||
|
suffix = badgeConstants.defaultPingValueSuffix,
|
||||||
|
color = badgeConstants.defaultPingColor,
|
||||||
|
labelColor,
|
||||||
|
style = badgeConstants.defaultStyle,
|
||||||
|
value, // for demo purpose only
|
||||||
|
} = request.query;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const requestedMonitorId = parseInt(request.params.id, 10);
|
||||||
|
|
||||||
|
// Default duration is 24 (h) if not defined in queryParam, limited to 720h (30d)
|
||||||
|
const requestedDuration = Math.min(request.params.duration ? parseInt(request.params.duration, 10) : 24, 720);
|
||||||
|
const overrideValue = value && parseFloat(value);
|
||||||
|
|
||||||
|
const publicAvgPing = parseInt(await R.getCell(`
|
||||||
|
SELECT AVG(ping) FROM monitor_group, \`group\`, heartbeat
|
||||||
|
WHERE monitor_group.group_id = \`group\`.id
|
||||||
|
AND heartbeat.time > DATETIME('now', ? || ' hours')
|
||||||
|
AND heartbeat.ping IS NOT NULL
|
||||||
|
AND public = 1
|
||||||
|
AND heartbeat.monitor_id = ?
|
||||||
|
`,
|
||||||
|
[ -requestedDuration, requestedMonitorId ]
|
||||||
|
));
|
||||||
|
|
||||||
|
const badgeValues = { style };
|
||||||
|
|
||||||
|
if (!publicAvgPing) {
|
||||||
|
// return a "N/A" badge in naColor (grey), if monitor is not public / not available / non exsitant
|
||||||
|
|
||||||
|
badgeValues.message = "N/A";
|
||||||
|
badgeValues.color = badgeConstants.naColor;
|
||||||
|
} else {
|
||||||
|
const avgPing = parseInt(overrideValue ?? publicAvgPing);
|
||||||
|
|
||||||
|
badgeValues.color = color;
|
||||||
|
// use a given, custom labelColor or use the default badge label color (defined by badge-maker)
|
||||||
|
badgeValues.labelColor = labelColor ?? "";
|
||||||
|
// build a lable string. If a custom label is given, override the default one (requestedDuration)
|
||||||
|
badgeValues.label = filterAndJoin([ labelPrefix, label ?? requestedDuration, labelSuffix ]);
|
||||||
|
badgeValues.message = filterAndJoin([ prefix, avgPing, suffix ]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// build the SVG based on given values
|
||||||
|
const svg = makeBadge(badgeValues);
|
||||||
|
|
||||||
|
response.type("image/svg+xml");
|
||||||
|
response.send(svg);
|
||||||
|
} catch (error) {
|
||||||
|
send403(response, error.message);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
module.exports = router;
|
module.exports = router;
|
||||||
|
110
server/routers/status-page-router.js
Normal file
110
server/routers/status-page-router.js
Normal file
@@ -0,0 +1,110 @@
|
|||||||
|
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);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
module.exports = router;
|
155
server/server.js
155
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, errorLog, doubleCheckPassword } = require("./util-server");
|
const { getSettings, setSettings, setting, initJWTSecret, checkLogin, startUnitTest, FBSD, doubleCheckPassword } = require("./util-server");
|
||||||
|
|
||||||
log.debug("server", "Importing Notification");
|
log.debug("server", "Importing Notification");
|
||||||
const { Notification } = require("./notification");
|
const { Notification } = require("./notification");
|
||||||
@@ -136,13 +137,6 @@ app.use(function (req, res, next) {
|
|||||||
next();
|
next();
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
|
||||||
* Total WebSocket client connected to server currently, no actual use
|
|
||||||
*
|
|
||||||
* @type {number}
|
|
||||||
*/
|
|
||||||
let totalClient = 0;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Use for decode the auth object
|
* Use for decode the auth object
|
||||||
* @type {null}
|
* @type {null}
|
||||||
@@ -155,22 +149,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);
|
||||||
@@ -186,13 +164,17 @@ try {
|
|||||||
|
|
||||||
// Entry Page
|
// Entry Page
|
||||||
app.get("/", async (request, response) => {
|
app.get("/", async (request, response) => {
|
||||||
debug(`Request Domain: ${request.hostname}`);
|
log.debug("entry", `Request Domain: ${request.hostname}`);
|
||||||
|
|
||||||
if (request.hostname in StatusPage.domainMappingList) {
|
if (request.hostname in StatusPage.domainMappingList) {
|
||||||
debug("This is a status page domain");
|
log.debug("entry", "This is a status page domain");
|
||||||
response.send(indexHTML);
|
|
||||||
|
let slug = StatusPage.domainMappingList[request.hostname];
|
||||||
|
await StatusPage.handleStatusPageResponse(response, server.indexHTML, slug);
|
||||||
|
|
||||||
} else if (exports.entryPage && exports.entryPage.startsWith("statusPage-")) {
|
} else if (exports.entryPage && exports.entryPage.startsWith("statusPage-")) {
|
||||||
response.redirect("/status/" + exports.entryPage.replace("statusPage-", ""));
|
response.redirect("/status/" + exports.entryPage.replace("statusPage-", ""));
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
response.redirect("/dashboard");
|
response.redirect("/dashboard");
|
||||||
}
|
}
|
||||||
@@ -221,7 +203,9 @@ try {
|
|||||||
// With Basic Auth using the first user's username/password
|
// With Basic Auth using the first user's username/password
|
||||||
app.get("/metrics", basicAuth, prometheusAPIMetrics());
|
app.get("/metrics", basicAuth, prometheusAPIMetrics());
|
||||||
|
|
||||||
app.use("/", express.static("dist"));
|
app.use("/", expressStaticGzip("dist", {
|
||||||
|
enableBrotli: true,
|
||||||
|
}));
|
||||||
|
|
||||||
// ./data/upload
|
// ./data/upload
|
||||||
app.use("/upload", express.static(Database.uploadDir));
|
app.use("/upload", express.static(Database.uploadDir));
|
||||||
@@ -234,12 +218,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);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -248,17 +236,11 @@ try {
|
|||||||
|
|
||||||
sendInfo(socket);
|
sendInfo(socket);
|
||||||
|
|
||||||
totalClient++;
|
|
||||||
|
|
||||||
if (needSetup) {
|
if (needSetup) {
|
||||||
log.info("server", "Redirect to setup page");
|
log.info("server", "Redirect to setup page");
|
||||||
socket.emit("setup");
|
socket.emit("setup");
|
||||||
}
|
}
|
||||||
|
|
||||||
socket.on("disconnect", () => {
|
|
||||||
totalClient--;
|
|
||||||
});
|
|
||||||
|
|
||||||
// ***************************
|
// ***************************
|
||||||
// Public Socket API
|
// Public Socket API
|
||||||
// ***************************
|
// ***************************
|
||||||
@@ -327,7 +309,7 @@ try {
|
|||||||
let user = await login(data.username, data.password);
|
let user = await login(data.username, data.password);
|
||||||
|
|
||||||
if (user) {
|
if (user) {
|
||||||
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=${getClientIp(socket)}`);
|
||||||
@@ -340,7 +322,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=${getClientIp(socket)}`);
|
||||||
|
|
||||||
@@ -417,7 +399,7 @@ try {
|
|||||||
socket.userID,
|
socket.userID,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
if (user.twofa_status == 0) {
|
if (user.twofa_status === 0) {
|
||||||
let newSecret = genSecret();
|
let newSecret = genSecret();
|
||||||
let encodedSecret = base32.encode(newSecret);
|
let encodedSecret = base32.encode(newSecret);
|
||||||
|
|
||||||
@@ -548,7 +530,7 @@ try {
|
|||||||
socket.userID,
|
socket.userID,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
if (user.twofa_status == 1) {
|
if (user.twofa_status === 1) {
|
||||||
callback({
|
callback({
|
||||||
ok: true,
|
ok: true,
|
||||||
status: true,
|
status: true,
|
||||||
@@ -672,7 +654,7 @@ try {
|
|||||||
bean.retryInterval = monitor.retryInterval;
|
bean.retryInterval = monitor.retryInterval;
|
||||||
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;
|
||||||
@@ -687,6 +669,11 @@ try {
|
|||||||
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;
|
||||||
|
|
||||||
await R.store(bean);
|
await R.store(bean);
|
||||||
|
|
||||||
@@ -1060,7 +1047,13 @@ try {
|
|||||||
try {
|
try {
|
||||||
checkLogin(socket);
|
checkLogin(socket);
|
||||||
|
|
||||||
if (data.disableAuth) {
|
// If currently is disabled auth, don't need to check
|
||||||
|
// Disabled Auth + Want to Disable Auth => No Check
|
||||||
|
// Disabled Auth + Want to Enable Auth => No Check
|
||||||
|
// Enabled Auth + Want to Disable Auth => Check!!
|
||||||
|
// Enabled Auth + Want to Enable Auth => No Check
|
||||||
|
const currentDisabledAuth = await setting("disableAuth");
|
||||||
|
if (!currentDisabledAuth && data.disableAuth) {
|
||||||
await doubleCheckPassword(socket, currentPassword);
|
await doubleCheckPassword(socket, currentPassword);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1169,7 +1162,7 @@ try {
|
|||||||
let version17x = compareVersions.compare(backupData.version, "1.7.0", ">=");
|
let version17x = compareVersions.compare(backupData.version, "1.7.0", ">=");
|
||||||
|
|
||||||
// If the import option is "overwrite" it'll clear most of the tables, except "settings" and "user"
|
// If the import option is "overwrite" it'll clear most of the tables, except "settings" and "user"
|
||||||
if (importHandle == "overwrite") {
|
if (importHandle === "overwrite") {
|
||||||
// Stops every monitor first, so it doesn't execute any heartbeat while importing
|
// Stops every monitor first, so it doesn't execute any heartbeat while importing
|
||||||
for (let id in server.monitorList) {
|
for (let id in server.monitorList) {
|
||||||
let monitor = server.monitorList[id];
|
let monitor = server.monitorList[id];
|
||||||
@@ -1193,7 +1186,7 @@ try {
|
|||||||
|
|
||||||
for (let i = 0; i < notificationListData.length; i++) {
|
for (let i = 0; i < notificationListData.length; i++) {
|
||||||
// Only starts importing the notification if the import option is "overwrite", "keep" or "skip" but the notification doesn't exists
|
// Only starts importing the notification if the import option is "overwrite", "keep" or "skip" but the notification doesn't exists
|
||||||
if ((importHandle == "skip" && notificationNameListString.includes(notificationListData[i].name) == false) || importHandle == "keep" || importHandle == "overwrite") {
|
if ((importHandle === "skip" && notificationNameListString.includes(notificationListData[i].name) === false) || importHandle === "keep" || importHandle === "overwrite") {
|
||||||
|
|
||||||
let notification = JSON.parse(notificationListData[i].config);
|
let notification = JSON.parse(notificationListData[i].config);
|
||||||
await Notification.save(notification, null, socket.userID);
|
await Notification.save(notification, null, socket.userID);
|
||||||
@@ -1228,7 +1221,7 @@ try {
|
|||||||
|
|
||||||
for (let i = 0; i < monitorListData.length; i++) {
|
for (let i = 0; i < monitorListData.length; i++) {
|
||||||
// Only starts importing the monitor if the import option is "overwrite", "keep" or "skip" but the notification doesn't exists
|
// Only starts importing the monitor if the import option is "overwrite", "keep" or "skip" but the notification doesn't exists
|
||||||
if ((importHandle == "skip" && monitorNameListString.includes(monitorListData[i].name) == false) || importHandle == "keep" || importHandle == "overwrite") {
|
if ((importHandle === "skip" && monitorNameListString.includes(monitorListData[i].name) === false) || importHandle === "keep" || importHandle === "overwrite") {
|
||||||
|
|
||||||
// Define in here every new variable for monitors which where implemented after the first version of the Import/Export function (1.6.0)
|
// Define in here every new variable for monitors which where implemented after the first version of the Import/Export function (1.6.0)
|
||||||
// --- Start ---
|
// --- Start ---
|
||||||
@@ -1254,8 +1247,11 @@ 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,
|
||||||
hostname: monitorListData[i].hostname,
|
hostname: monitorListData[i].hostname,
|
||||||
@@ -1325,7 +1321,7 @@ try {
|
|||||||
await updateMonitorNotification(bean.id, notificationIDList);
|
await updateMonitorNotification(bean.id, notificationIDList);
|
||||||
|
|
||||||
// If monitor was active start it immediately, otherwise pause it
|
// If monitor was active start it immediately, otherwise pause it
|
||||||
if (monitorListData[i].active == 1) {
|
if (monitorListData[i].active === 1) {
|
||||||
await startMonitor(socket.userID, bean.id);
|
await startMonitor(socket.userID, bean.id);
|
||||||
} else {
|
} else {
|
||||||
await pauseMonitor(socket.userID, bean.id);
|
await pauseMonitor(socket.userID, bean.id);
|
||||||
@@ -1473,11 +1469,11 @@ try {
|
|||||||
})();
|
})();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Adds or removes notifications from a monitor.
|
* Update notifications for a given monitor
|
||||||
* @param {number} monitorID The ID of the monitor to add/remove notifications from.
|
* @param {number} monitorID ID of monitor to update
|
||||||
* @param {Array.<number>} notificationIDList An array of IDs for the notifications to add/remove.
|
* @param {number[]} notificationIDList List of new notification
|
||||||
*
|
* providers to add
|
||||||
* Generated by Trelent
|
* @returns {Promise<void>}
|
||||||
*/
|
*/
|
||||||
async function updateMonitorNotification(monitorID, notificationIDList) {
|
async function updateMonitorNotification(monitorID, notificationIDList) {
|
||||||
await R.exec("DELETE FROM monitor_notification WHERE monitor_id = ? ", [
|
await R.exec("DELETE FROM monitor_notification WHERE monitor_id = ? ", [
|
||||||
@@ -1495,11 +1491,11 @@ async function updateMonitorNotification(monitorID, notificationIDList) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This function checks if the user owns a monitor with the given ID.
|
* Check if a given user owns a specific monitor
|
||||||
* @param {number} monitorID - The ID of the monitor to check ownership for.
|
* @param {number} userID
|
||||||
* @param {number} userID - The ID of the user who is trying to access this data.
|
* @param {number} monitorID
|
||||||
*
|
* @returns {Promise<void>}
|
||||||
* Generated by Trelent
|
* @throws {Error} The specified user does not own the monitor
|
||||||
*/
|
*/
|
||||||
async function checkOwner(userID, monitorID) {
|
async function checkOwner(userID, monitorID) {
|
||||||
let row = await R.getRow("SELECT id FROM monitor WHERE id = ? AND user_id = ? ", [
|
let row = await R.getRow("SELECT id FROM monitor WHERE id = ? AND user_id = ? ", [
|
||||||
@@ -1513,8 +1509,11 @@ async function checkOwner(userID, monitorID) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
* Function called after user login
|
||||||
* This function is used to send the heartbeat list of a monitor.
|
* This function is used to send the heartbeat list of a monitor.
|
||||||
* @param {Socket} socket - The socket object that will be used to send the data.
|
* @param {Socket} socket Socket.io instance
|
||||||
|
* @param {Object} user User object
|
||||||
|
* @returns {Promise<void>}
|
||||||
*/
|
*/
|
||||||
async function afterLogin(socket, user) {
|
async function afterLogin(socket, user) {
|
||||||
socket.userID = user.id;
|
socket.userID = user.id;
|
||||||
@@ -1542,9 +1541,10 @@ async function afterLogin(socket, user) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Connect to the database and patch it if necessary.
|
* Initialize the database
|
||||||
*
|
* @param {boolean} [testMode=false] Should the connection be
|
||||||
* Generated by Trelent
|
* started in test mode?
|
||||||
|
* @returns {Promise<void>}
|
||||||
*/
|
*/
|
||||||
async function initDatabase(testMode = false) {
|
async function initDatabase(testMode = false) {
|
||||||
if (! fs.existsSync(Database.path)) {
|
if (! fs.existsSync(Database.path)) {
|
||||||
@@ -1581,11 +1581,10 @@ async function initDatabase(testMode = false) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Resume a monitor.
|
* Start the specified monitor
|
||||||
* @param {string} userID - The ID of the user who owns the monitor.
|
* @param {number} userID ID of user who owns monitor
|
||||||
* @param {string} monitorID - The ID of the monitor to resume.
|
* @param {number} monitorID ID of monitor to start
|
||||||
*
|
* @returns {Promise<void>}
|
||||||
* Generated by Trelent
|
|
||||||
*/
|
*/
|
||||||
async function startMonitor(userID, monitorID) {
|
async function startMonitor(userID, monitorID) {
|
||||||
await checkOwner(userID, monitorID);
|
await checkOwner(userID, monitorID);
|
||||||
@@ -1609,16 +1608,21 @@ async function startMonitor(userID, monitorID) {
|
|||||||
monitor.start(io);
|
monitor.start(io);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Restart a given monitor
|
||||||
|
* @param {number} userID ID of user who owns monitor
|
||||||
|
* @param {number} monitorID ID of monitor to start
|
||||||
|
* @returns {Promise<void>}
|
||||||
|
*/
|
||||||
async function restartMonitor(userID, monitorID) {
|
async function restartMonitor(userID, monitorID) {
|
||||||
return await startMonitor(userID, monitorID);
|
return await startMonitor(userID, monitorID);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Pause a monitor.
|
* Pause a given monitor
|
||||||
* @param {string} userID - The ID of the user who owns the monitor.
|
* @param {number} userID ID of user who owns monitor
|
||||||
* @param {string} monitorID - The ID of the monitor to pause.
|
* @param {number} monitorID ID of monitor to start
|
||||||
*
|
* @returns {Promise<void>}
|
||||||
* Generated by Trelent
|
|
||||||
*/
|
*/
|
||||||
async function pauseMonitor(userID, monitorID) {
|
async function pauseMonitor(userID, monitorID) {
|
||||||
await checkOwner(userID, monitorID);
|
await checkOwner(userID, monitorID);
|
||||||
@@ -1635,9 +1639,7 @@ async function pauseMonitor(userID, monitorID) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/** Resume active monitors */
|
||||||
* Resume active monitors
|
|
||||||
*/
|
|
||||||
async function startMonitors() {
|
async function startMonitors() {
|
||||||
let list = await R.find("monitor", " active = 1 ");
|
let list = await R.find("monitor", " active = 1 ");
|
||||||
|
|
||||||
@@ -1653,10 +1655,10 @@ async function startMonitors() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
* Shutdown the application
|
||||||
* Stops all monitors and closes the database connection.
|
* Stops all monitors and closes the database connection.
|
||||||
* @param {string} signal The signal that triggered this function to be called.
|
* @param {string} signal The signal that triggered this function to be called.
|
||||||
*
|
* @returns {Promise<void>}
|
||||||
* Generated by Trelent
|
|
||||||
*/
|
*/
|
||||||
async function shutdownFunction(signal) {
|
async function shutdownFunction(signal) {
|
||||||
log.info("server", "Shutdown requested");
|
log.info("server", "Shutdown requested");
|
||||||
@@ -1678,6 +1680,7 @@ function getClientIp(socket) {
|
|||||||
return socket.client.conn.remoteAddress.replace(/^.*:/, "");
|
return socket.client.conn.remoteAddress.replace(/^.*:/, "");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Final function called before application exits */
|
||||||
function finalFunction() {
|
function finalFunction() {
|
||||||
log.info("server", "Graceful shutdown successful!");
|
log.info("server", "Graceful shutdown successful!");
|
||||||
}
|
}
|
||||||
@@ -1694,6 +1697,6 @@ gracefulShutdown(server.httpServer, {
|
|||||||
// Catch unexpected errors here
|
// Catch unexpected errors here
|
||||||
process.addListener("unhandledRejection", (error, promise) => {
|
process.addListener("unhandledRejection", (error, promise) => {
|
||||||
console.trace(error);
|
console.trace(error);
|
||||||
errorLog(error, false);
|
UptimeKumaServer.errorLog(error, false);
|
||||||
console.error("If you keep encountering errors, please report to https://github.com/louislam/uptime-kuma/issues");
|
console.error("If you keep encountering errors, please report to https://github.com/louislam/uptime-kuma/issues");
|
||||||
});
|
});
|
||||||
|
@@ -6,15 +6,28 @@ const io = UptimeKumaServer.getInstance().io;
|
|||||||
const prefix = "cloudflared_";
|
const prefix = "cloudflared_";
|
||||||
const cloudflared = new CloudflaredTunnel();
|
const cloudflared = new CloudflaredTunnel();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Change running state
|
||||||
|
* @param {string} running Is it running?
|
||||||
|
* @param {string} message Message to pass
|
||||||
|
*/
|
||||||
cloudflared.change = (running, message) => {
|
cloudflared.change = (running, message) => {
|
||||||
io.to("cloudflared").emit(prefix + "running", running);
|
io.to("cloudflared").emit(prefix + "running", running);
|
||||||
io.to("cloudflared").emit(prefix + "message", message);
|
io.to("cloudflared").emit(prefix + "message", message);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Emit an error message
|
||||||
|
* @param {string} errorMessage
|
||||||
|
*/
|
||||||
cloudflared.error = (errorMessage) => {
|
cloudflared.error = (errorMessage) => {
|
||||||
io.to("cloudflared").emit(prefix + "errorMessage", errorMessage);
|
io.to("cloudflared").emit(prefix + "errorMessage", errorMessage);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handler for cloudflared
|
||||||
|
* @param {Socket} socket Socket.io instance
|
||||||
|
*/
|
||||||
module.exports.cloudflaredSocketHandler = (socket) => {
|
module.exports.cloudflaredSocketHandler = (socket) => {
|
||||||
|
|
||||||
socket.on(prefix + "join", async () => {
|
socket.on(prefix + "join", async () => {
|
||||||
@@ -50,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({
|
||||||
@@ -69,6 +85,10 @@ module.exports.cloudflaredSocketHandler = (socket) => {
|
|||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Automatically start cloudflared
|
||||||
|
* @param {string} token Cloudflared tunnel token
|
||||||
|
*/
|
||||||
module.exports.autoStart = async (token) => {
|
module.exports.autoStart = async (token) => {
|
||||||
if (!token) {
|
if (!token) {
|
||||||
token = await setting("cloudflaredTunnelToken");
|
token = await setting("cloudflaredTunnelToken");
|
||||||
@@ -85,6 +105,7 @@ module.exports.autoStart = async (token) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/** Stop cloudflared */
|
||||||
module.exports.stop = async () => {
|
module.exports.stop = async () => {
|
||||||
console.log("Stop cloudflared");
|
console.log("Stop cloudflared");
|
||||||
if (cloudflared) {
|
if (cloudflared) {
|
||||||
|
@@ -1,6 +1,10 @@
|
|||||||
const { checkLogin } = require("../util-server");
|
const { checkLogin } = require("../util-server");
|
||||||
const Database = require("../database");
|
const Database = require("../database");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handlers for database
|
||||||
|
* @param {Socket} socket Socket.io instance
|
||||||
|
*/
|
||||||
module.exports = (socket) => {
|
module.exports = (socket) => {
|
||||||
|
|
||||||
// Post or edit incident
|
// Post or edit incident
|
||||||
|
@@ -4,6 +4,10 @@ const { sendProxyList } = require("../client");
|
|||||||
const { UptimeKumaServer } = require("../uptime-kuma-server");
|
const { UptimeKumaServer } = require("../uptime-kuma-server");
|
||||||
const server = UptimeKumaServer.getInstance();
|
const server = UptimeKumaServer.getInstance();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handlers for proxy
|
||||||
|
* @param {Socket} socket Socket.io instance
|
||||||
|
*/
|
||||||
module.exports.proxySocketHandler = (socket) => {
|
module.exports.proxySocketHandler = (socket) => {
|
||||||
socket.on("addProxy", async (proxy, proxyID, callback) => {
|
socket.on("addProxy", async (proxy, proxyID, callback) => {
|
||||||
try {
|
try {
|
||||||
|
@@ -8,6 +8,10 @@ const apicache = require("../modules/apicache");
|
|||||||
const StatusPage = require("../model/status_page");
|
const StatusPage = require("../model/status_page");
|
||||||
const { UptimeKumaServer } = require("../uptime-kuma-server");
|
const { UptimeKumaServer } = require("../uptime-kuma-server");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Socket handlers for status page
|
||||||
|
* @param {Socket} socket Socket.io instance to add listeners on
|
||||||
|
*/
|
||||||
module.exports.statusPageSocketHandler = (socket) => {
|
module.exports.statusPageSocketHandler = (socket) => {
|
||||||
|
|
||||||
// Post or edit incident
|
// Post or edit incident
|
||||||
@@ -338,6 +342,7 @@ module.exports.statusPageSocketHandler = (socket) => {
|
|||||||
/**
|
/**
|
||||||
* Check slug a-z, 0-9, - only
|
* Check slug a-z, 0-9, - only
|
||||||
* Regex from: https://stackoverflow.com/questions/22454258/js-regex-string-validation-for-slug
|
* Regex from: https://stackoverflow.com/questions/22454258/js-regex-string-validation-for-slug
|
||||||
|
* @param {string} slug Slug to test
|
||||||
*/
|
*/
|
||||||
function checkSlug(slug) {
|
function checkSlug(slug) {
|
||||||
if (typeof slug !== "string") {
|
if (typeof slug !== "string") {
|
||||||
|
@@ -5,13 +5,14 @@ const http = require("http");
|
|||||||
const { Server } = require("socket.io");
|
const { Server } = require("socket.io");
|
||||||
const { R } = require("redbean-node");
|
const { R } = require("redbean-node");
|
||||||
const { log } = require("../src/util");
|
const { log } = require("../src/util");
|
||||||
|
const Database = require("./database");
|
||||||
|
const util = require("util");
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* `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.
|
||||||
* @type {UptimeKumaServer}
|
* @type {UptimeKumaServer}
|
||||||
*/
|
*/
|
||||||
class UptimeKumaServer {
|
class UptimeKumaServer {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* @type {UptimeKumaServer}
|
* @type {UptimeKumaServer}
|
||||||
@@ -28,6 +29,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);
|
||||||
@@ -54,6 +61,16 @@ 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
this.io = new Server(this.httpServer);
|
this.io = new Server(this.httpServer);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -83,6 +100,32 @@ class UptimeKumaServer {
|
|||||||
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Write error to log file
|
||||||
|
* @param {any} error The error to write
|
||||||
|
* @param {boolean} outputToConsole Should the error also be output to console?
|
||||||
|
*/
|
||||||
|
static errorLog(error, outputToConsole = true) {
|
||||||
|
const errorLogStream = fs.createWriteStream(Database.dataDir + "/error.log", {
|
||||||
|
flags: "a"
|
||||||
|
});
|
||||||
|
|
||||||
|
errorLogStream.on("error", () => {
|
||||||
|
log.info("", "Cannot write to error.log");
|
||||||
|
});
|
||||||
|
|
||||||
|
if (errorLogStream) {
|
||||||
|
const dateTime = R.isoDateTime();
|
||||||
|
errorLogStream.write(`[${dateTime}] ` + util.format(error) + "\n");
|
||||||
|
|
||||||
|
if (outputToConsole) {
|
||||||
|
console.error(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
errorLogStream.end();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
|
@@ -7,9 +7,11 @@ const { Resolver } = require("dns");
|
|||||||
const childProcess = require("child_process");
|
const childProcess = require("child_process");
|
||||||
const iconv = require("iconv-lite");
|
const iconv = require("iconv-lite");
|
||||||
const chardet = require("chardet");
|
const chardet = require("chardet");
|
||||||
const fs = require("fs");
|
|
||||||
const nodeJsUtil = require("util");
|
|
||||||
const mqtt = require("mqtt");
|
const mqtt = require("mqtt");
|
||||||
|
const chroma = require("chroma-js");
|
||||||
|
const { badgeConstants } = require("./config");
|
||||||
|
const mssql = require("mssql");
|
||||||
|
const { NtlmClient } = require("axios-ntlm");
|
||||||
|
|
||||||
// From ping-lite
|
// From ping-lite
|
||||||
exports.WIN = /^win/.test(process.platform);
|
exports.WIN = /^win/.test(process.platform);
|
||||||
@@ -37,6 +39,12 @@ exports.initJWTSecret = async () => {
|
|||||||
return jwtSecretBean;
|
return jwtSecretBean;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send TCP request to specified hostname and port
|
||||||
|
* @param {string} hostname Hostname / address of machine
|
||||||
|
* @param {number} port TCP port to test
|
||||||
|
* @returns {Promise<number>} Maximum time in ms rounded to nearest integer
|
||||||
|
*/
|
||||||
exports.tcping = function (hostname, port) {
|
exports.tcping = function (hostname, port) {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
tcpp.ping({
|
tcpp.ping({
|
||||||
@@ -58,6 +66,11 @@ exports.tcping = function (hostname, port) {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ping the specified machine
|
||||||
|
* @param {string} hostname Hostname / address of machine
|
||||||
|
* @returns {Promise<number>} Time for ping in ms rounded to nearest integer
|
||||||
|
*/
|
||||||
exports.ping = async (hostname) => {
|
exports.ping = async (hostname) => {
|
||||||
try {
|
try {
|
||||||
return await exports.pingAsync(hostname);
|
return await exports.pingAsync(hostname);
|
||||||
@@ -71,6 +84,12 @@ exports.ping = async (hostname) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ping the specified machine
|
||||||
|
* @param {string} hostname Hostname / address of machine to ping
|
||||||
|
* @param {boolean} ipv6 Should IPv6 be used?
|
||||||
|
* @returns {Promise<number>} Time for ping in ms rounded to nearest integer
|
||||||
|
*/
|
||||||
exports.pingAsync = function (hostname, ipv6 = false) {
|
exports.pingAsync = function (hostname, ipv6 = false) {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const ping = new Ping(hostname, {
|
const ping = new Ping(hostname, {
|
||||||
@@ -89,6 +108,15 @@ exports.pingAsync = function (hostname, ipv6 = false) {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* MQTT Monitor
|
||||||
|
* @param {string} hostname Hostname / address of machine to test
|
||||||
|
* @param {string} topic MQTT topic
|
||||||
|
* @param {string} okMessage Expected result
|
||||||
|
* @param {Object} [options={}] MQTT options. Contains port, username,
|
||||||
|
* password and interval (interval defaults to 20)
|
||||||
|
* @returns {Promise<string>}
|
||||||
|
*/
|
||||||
exports.mqttAsync = function (hostname, topic, okMessage, options = {}) {
|
exports.mqttAsync = function (hostname, topic, okMessage, options = {}) {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const { port, username, password, interval = 20 } = options;
|
const { port, username, password, interval = 20 } = options;
|
||||||
@@ -132,7 +160,7 @@ exports.mqttAsync = function (hostname, topic, okMessage, options = {}) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
client.on("message", (messageTopic, message) => {
|
client.on("message", (messageTopic, message) => {
|
||||||
if (messageTopic == topic) {
|
if (messageTopic === topic) {
|
||||||
client.end();
|
client.end();
|
||||||
clearTimeout(timeoutID);
|
clearTimeout(timeoutID);
|
||||||
if (okMessage != null && okMessage !== "" && message.toString() !== okMessage) {
|
if (okMessage != null && okMessage !== "" && message.toString() !== okMessage) {
|
||||||
@@ -146,9 +174,40 @@ exports.mqttAsync = function (hostname, topic, okMessage, options = {}) {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
exports.dnsResolve = function (hostname, resolverServer, rrtype) {
|
/**
|
||||||
|
* 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
|
||||||
|
* @param {string} hostname The hostname of the record to lookup
|
||||||
|
* @param {string} resolverServer The DNS server to use
|
||||||
|
* @param {string} resolverPort Port the DNS server is listening on
|
||||||
|
* @param {string} rrtype The type of record to request
|
||||||
|
* @returns {Promise<(string[]|Object[]|Object)>}
|
||||||
|
*/
|
||||||
|
exports.dnsResolve = function (hostname, resolverServer, resolverPort, rrtype) {
|
||||||
const resolver = new Resolver();
|
const resolver = new Resolver();
|
||||||
resolver.setServers([ resolverServer ]);
|
// Remove brackets from IPv6 addresses so we can re-add them to
|
||||||
|
// prevent issues with ::1:5300 (::1 port 5300)
|
||||||
|
resolverServer = resolverServer.replace("[", "").replace("]", "");
|
||||||
|
resolver.setServers([ `[${resolverServer}]:${resolverPort}` ]);
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
if (rrtype === "PTR") {
|
if (rrtype === "PTR") {
|
||||||
resolver.reverse(hostname, (err, records) => {
|
resolver.reverse(hostname, (err, records) => {
|
||||||
@@ -170,6 +229,36 @@ exports.dnsResolve = function (hostname, resolverServer, rrtype) {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Run a query on SQL Server
|
||||||
|
* @param {string} connectionString The database connection string
|
||||||
|
* @param {string} query The query to validate the database with
|
||||||
|
* @returns {Promise<(string[]|Object[]|Object)>}
|
||||||
|
*/
|
||||||
|
exports.mssqlQuery = function (connectionString, query) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
mssql.on("error", err => {
|
||||||
|
reject(err);
|
||||||
|
});
|
||||||
|
|
||||||
|
mssql.connect(connectionString).then(pool => {
|
||||||
|
return pool.request()
|
||||||
|
.query(query);
|
||||||
|
}).then(result => {
|
||||||
|
resolve(result);
|
||||||
|
}).catch(err => {
|
||||||
|
reject(err);
|
||||||
|
}).finally(() => {
|
||||||
|
mssql.close();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieve value of setting based on key
|
||||||
|
* @param {string} key Key of setting to retrieve
|
||||||
|
* @returns {Promise<any>} Value
|
||||||
|
*/
|
||||||
exports.setting = async function (key) {
|
exports.setting = async function (key) {
|
||||||
let value = await R.getCell("SELECT `value` FROM setting WHERE `key` = ? ", [
|
let value = await R.getCell("SELECT `value` FROM setting WHERE `key` = ? ", [
|
||||||
key,
|
key,
|
||||||
@@ -184,6 +273,13 @@ exports.setting = async function (key) {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the specified setting to specifed 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>}
|
||||||
|
*/
|
||||||
exports.setSetting = async function (key, value, type = null) {
|
exports.setSetting = async function (key, value, type = null) {
|
||||||
let bean = await R.findOne("setting", " `key` = ? ", [
|
let bean = await R.findOne("setting", " `key` = ? ", [
|
||||||
key,
|
key,
|
||||||
@@ -197,6 +293,11 @@ exports.setSetting = async function (key, value, type = null) {
|
|||||||
await R.store(bean);
|
await R.store(bean);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get settings based on type
|
||||||
|
* @param {?string} type The type of setting
|
||||||
|
* @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` = ? ", [
|
let list = await R.getAll("SELECT `key`, `value` FROM setting WHERE `type` = ? ", [
|
||||||
type,
|
type,
|
||||||
@@ -215,6 +316,12 @@ exports.getSettings = async function (type) {
|
|||||||
return result;
|
return result;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set settings based on type
|
||||||
|
* @param {?string} type Type of settings to set
|
||||||
|
* @param {Object} data Values of settings
|
||||||
|
* @returns {Promise<void>}
|
||||||
|
*/
|
||||||
exports.setSettings = async function (type, data) {
|
exports.setSettings = async function (type, data) {
|
||||||
let keyList = Object.keys(data);
|
let keyList = Object.keys(data);
|
||||||
|
|
||||||
@@ -241,12 +348,23 @@ exports.setSettings = async function (type, data) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// ssl-checker by @dyaa
|
// ssl-checker by @dyaa
|
||||||
// param: res - response object from axios
|
//https://github.com/dyaa/ssl-checker/blob/master/src/index.ts
|
||||||
// return an object containing the certificate information
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get number of days between two dates
|
||||||
|
* @param {Date} validFrom Start date
|
||||||
|
* @param {Date} validTo End date
|
||||||
|
* @returns {number}
|
||||||
|
*/
|
||||||
const getDaysBetween = (validFrom, validTo) =>
|
const getDaysBetween = (validFrom, validTo) =>
|
||||||
Math.round(Math.abs(+validFrom - +validTo) / 8.64e7);
|
Math.round(Math.abs(+validFrom - +validTo) / 8.64e7);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get days remaining from a time range
|
||||||
|
* @param {Date} validFrom Start date
|
||||||
|
* @param {Date} validTo End date
|
||||||
|
* @returns {number}
|
||||||
|
*/
|
||||||
const getDaysRemaining = (validFrom, validTo) => {
|
const getDaysRemaining = (validFrom, validTo) => {
|
||||||
const daysRemaining = getDaysBetween(validFrom, validTo);
|
const daysRemaining = getDaysBetween(validFrom, validTo);
|
||||||
if (new Date(validTo).getTime() < new Date().getTime()) {
|
if (new Date(validTo).getTime() < new Date().getTime()) {
|
||||||
@@ -255,8 +373,11 @@ const getDaysRemaining = (validFrom, validTo) => {
|
|||||||
return daysRemaining;
|
return daysRemaining;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Fix certificate Info for display
|
/**
|
||||||
// param: info - the chain obtained from getPeerCertificate()
|
* Fix certificate info for display
|
||||||
|
* @param {Object} info The chain obtained from getPeerCertificate()
|
||||||
|
* @returns {Object} An object representing certificate information
|
||||||
|
*/
|
||||||
const parseCertificateInfo = function (info) {
|
const parseCertificateInfo = function (info) {
|
||||||
let link = info;
|
let link = info;
|
||||||
let i = 0;
|
let i = 0;
|
||||||
@@ -296,6 +417,11 @@ const parseCertificateInfo = function (info) {
|
|||||||
return info;
|
return info;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if certificate is valid
|
||||||
|
* @param {Object} res Response object from axios
|
||||||
|
* @returns {Object} Object containing certificate information
|
||||||
|
*/
|
||||||
exports.checkCertificate = function (res) {
|
exports.checkCertificate = function (res) {
|
||||||
const info = res.request.res.socket.getPeerCertificate(true);
|
const info = res.request.res.socket.getPeerCertificate(true);
|
||||||
const valid = res.request.res.socket.authorized || false;
|
const valid = res.request.res.socket.authorized || false;
|
||||||
@@ -309,12 +435,13 @@ exports.checkCertificate = function (res) {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
// Check if the provided status code is within the accepted ranges
|
/**
|
||||||
// Param: status - the status code to check
|
* Check if the provided status code is within the accepted ranges
|
||||||
// Param: accepted_codes - an array of accepted status codes
|
* @param {string} status The status code to check
|
||||||
// Return: true if the status code is within the accepted ranges, false otherwise
|
* @param {string[]} acceptedCodes An array of accepted status codes
|
||||||
// Will throw an error if the provided status code is not a valid range string or code string
|
* @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
|
||||||
|
*/
|
||||||
exports.checkStatusCode = function (status, acceptedCodes) {
|
exports.checkStatusCode = function (status, acceptedCodes) {
|
||||||
if (acceptedCodes == null || acceptedCodes.length === 0) {
|
if (acceptedCodes == null || acceptedCodes.length === 0) {
|
||||||
return false;
|
return false;
|
||||||
@@ -338,6 +465,12 @@ exports.checkStatusCode = function (status, acceptedCodes) {
|
|||||||
return false;
|
return false;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get total number of clients in room
|
||||||
|
* @param {Server} io Socket server instance
|
||||||
|
* @param {string} roomName Name of room to check
|
||||||
|
* @returns {number}
|
||||||
|
*/
|
||||||
exports.getTotalClientInRoom = (io, roomName) => {
|
exports.getTotalClientInRoom = (io, roomName) => {
|
||||||
|
|
||||||
const sockets = io.sockets;
|
const sockets = io.sockets;
|
||||||
@@ -361,17 +494,29 @@ exports.getTotalClientInRoom = (io, roomName) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Allow CORS all origins if development
|
||||||
|
* @param {Object} res Response object from axios
|
||||||
|
*/
|
||||||
exports.allowDevAllOrigin = (res) => {
|
exports.allowDevAllOrigin = (res) => {
|
||||||
if (process.env.NODE_ENV === "development") {
|
if (process.env.NODE_ENV === "development") {
|
||||||
exports.allowAllOrigin(res);
|
exports.allowAllOrigin(res);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Allow CORS all origins
|
||||||
|
* @param {Object} res Response object from axios
|
||||||
|
*/
|
||||||
exports.allowAllOrigin = (res) => {
|
exports.allowAllOrigin = (res) => {
|
||||||
res.header("Access-Control-Allow-Origin", "*");
|
res.header("Access-Control-Allow-Origin", "*");
|
||||||
res.header("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept");
|
res.header("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept");
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if a user is logged in
|
||||||
|
* @param {Socket} socket Socket instance
|
||||||
|
*/
|
||||||
exports.checkLogin = (socket) => {
|
exports.checkLogin = (socket) => {
|
||||||
if (!socket.userID) {
|
if (!socket.userID) {
|
||||||
throw new Error("You are not logged in.");
|
throw new Error("You are not logged in.");
|
||||||
@@ -380,8 +525,8 @@ exports.checkLogin = (socket) => {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* For logged-in users, double-check the password
|
* For logged-in users, double-check the password
|
||||||
* @param socket
|
* @param {Socket} socket Socket.io instance
|
||||||
* @param currentPassword
|
* @param {string} currentPassword
|
||||||
* @returns {Promise<Bean>}
|
* @returns {Promise<Bean>}
|
||||||
*/
|
*/
|
||||||
exports.doubleCheckPassword = async (socket, currentPassword) => {
|
exports.doubleCheckPassword = async (socket, currentPassword) => {
|
||||||
@@ -400,6 +545,7 @@ exports.doubleCheckPassword = async (socket, currentPassword) => {
|
|||||||
return user;
|
return user;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/** Start Unit tests */
|
||||||
exports.startUnitTest = async () => {
|
exports.startUnitTest = async () => {
|
||||||
console.log("Starting unit test...");
|
console.log("Starting unit test...");
|
||||||
const npm = /^win/.test(process.platform) ? "npm.cmd" : "npm";
|
const npm = /^win/.test(process.platform) ? "npm.cmd" : "npm";
|
||||||
@@ -420,7 +566,8 @@ exports.startUnitTest = async () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param body : Buffer
|
* Convert unknown string to UTF8
|
||||||
|
* @param {Uint8Array} body Buffer
|
||||||
* @returns {string}
|
* @returns {string}
|
||||||
*/
|
*/
|
||||||
exports.convertToUTF8 = (body) => {
|
exports.convertToUTF8 = (body) => {
|
||||||
@@ -429,23 +576,44 @@ exports.convertToUTF8 = (body) => {
|
|||||||
return str.toString();
|
return str.toString();
|
||||||
};
|
};
|
||||||
|
|
||||||
let logFile;
|
/**
|
||||||
|
* Returns a color code in hex format based on a given percentage:
|
||||||
try {
|
* 0% => hue = 10 => red
|
||||||
logFile = fs.createWriteStream("./data/error.log", {
|
* 100% => hue = 90 => green
|
||||||
flags: "a"
|
*
|
||||||
});
|
* @param {number} percentage float, 0 to 1
|
||||||
} catch (_) { }
|
* @param {number} maxHue
|
||||||
|
* @param {number} minHue, int
|
||||||
exports.errorLog = (error, outputToConsole = true) => {
|
* @returns {string}, hex value
|
||||||
|
*/
|
||||||
|
exports.percentageToColor = (percentage, maxHue = 90, minHue = 10) => {
|
||||||
|
const hue = percentage * (maxHue - minHue) + minHue;
|
||||||
try {
|
try {
|
||||||
if (logFile) {
|
return chroma(`hsl(${hue}, 90%, 40%)`).hex();
|
||||||
const dateTime = R.isoDateTime();
|
} catch (err) {
|
||||||
logFile.write(`[${dateTime}] ` + nodeJsUtil.format(error) + "\n");
|
return badgeConstants.naColor;
|
||||||
|
}
|
||||||
if (outputToConsole) {
|
};
|
||||||
console.error(error);
|
|
||||||
}
|
/**
|
||||||
}
|
* Joins and array of string to one string after filtering out empty values
|
||||||
} catch (_) { }
|
*
|
||||||
|
* @param {string[]} parts
|
||||||
|
* @param {string} connector
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
exports.filterAndJoin = (parts, 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;
|
||||||
@@ -363,6 +382,12 @@ textarea.form-control {
|
|||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
height: calc(100% - 65px);
|
height: calc(100% - 65px);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@media (max-width: 770px) {
|
||||||
|
&.scrollbar {
|
||||||
|
height: calc(100% - 40px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.item {
|
.item {
|
||||||
display: block;
|
display: block;
|
||||||
@@ -473,6 +498,14 @@ textarea.form-control {
|
|||||||
outline: none !important;
|
outline: none !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
h5.settings-subheading::after {
|
||||||
|
content: "";
|
||||||
|
display: block;
|
||||||
|
width: 50%;
|
||||||
|
padding-top: 8px;
|
||||||
|
border-bottom: 1px solid $dark-border-color;
|
||||||
|
}
|
||||||
|
|
||||||
// Localization
|
// Localization
|
||||||
|
|
||||||
@import "localization.scss";
|
@import "localization.scss";
|
||||||
|
86
src/components/ActionInput.vue
Normal file
86
src/components/ActionInput.vue
Normal file
@@ -0,0 +1,86 @@
|
|||||||
|
<template>
|
||||||
|
<div class="input-group mb-3">
|
||||||
|
<input
|
||||||
|
ref="input"
|
||||||
|
v-model="model"
|
||||||
|
class="form-control"
|
||||||
|
:type="type"
|
||||||
|
:placeholder="placeholder"
|
||||||
|
:disabled="!enabled"
|
||||||
|
>
|
||||||
|
<a class="btn btn-outline-primary" @click="action()">
|
||||||
|
<font-awesome-icon :icon="icon" />
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
/**
|
||||||
|
* Generic input field with a customizable action on the right.
|
||||||
|
* Action is passed in as a function.
|
||||||
|
*/
|
||||||
|
export default {
|
||||||
|
props: {
|
||||||
|
/**
|
||||||
|
* The value of the input field.
|
||||||
|
*/
|
||||||
|
modelValue: {
|
||||||
|
type: String,
|
||||||
|
default: ""
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* Whether the input field is enabled / disabled.
|
||||||
|
*/
|
||||||
|
enabled: {
|
||||||
|
type: Boolean,
|
||||||
|
default: true
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* Placeholder text for the input field.
|
||||||
|
*/
|
||||||
|
placeholder: {
|
||||||
|
type: String,
|
||||||
|
default: ""
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* The icon displayed in the right button of the input field.
|
||||||
|
* Accepts a Font Awesome icon string identifier.
|
||||||
|
* @example "plus"
|
||||||
|
*/
|
||||||
|
icon: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* The input type of the input field.
|
||||||
|
* @example "email"
|
||||||
|
*/
|
||||||
|
type: {
|
||||||
|
type: String,
|
||||||
|
default: "text",
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* The action to be performed when the button is clicked.
|
||||||
|
* Action is passed in as a function.
|
||||||
|
*/
|
||||||
|
action: {
|
||||||
|
type: Function,
|
||||||
|
default: () => {},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
emits: [ "update:modelValue" ],
|
||||||
|
computed: {
|
||||||
|
/**
|
||||||
|
* Send value update to parent on change.
|
||||||
|
*/
|
||||||
|
model: {
|
||||||
|
get() {
|
||||||
|
return this.modelValue;
|
||||||
|
},
|
||||||
|
set(value) {
|
||||||
|
this.$emit("update:modelValue", value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
@@ -25,10 +25,12 @@ export default {
|
|||||||
CertificateInfoRow,
|
CertificateInfoRow,
|
||||||
},
|
},
|
||||||
props: {
|
props: {
|
||||||
|
/** Object representing certificate */
|
||||||
certInfo: {
|
certInfo: {
|
||||||
type: Object,
|
type: Object,
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
|
/** Is the TLS certificate valid? */
|
||||||
valid: {
|
valid: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
required: true,
|
required: true,
|
||||||
|
@@ -56,12 +56,19 @@ export default {
|
|||||||
Datetime,
|
Datetime,
|
||||||
},
|
},
|
||||||
props: {
|
props: {
|
||||||
|
/** Object representing certificate */
|
||||||
cert: {
|
cert: {
|
||||||
type: Object,
|
type: Object,
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
/**
|
||||||
|
* Format the subject of the certificate
|
||||||
|
* @param {Object} subject Object representing the certificates
|
||||||
|
* subject
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
formatSubject(subject) {
|
formatSubject(subject) {
|
||||||
if (subject.O && subject.CN && subject.C) {
|
if (subject.O && subject.CN && subject.C) {
|
||||||
return `${subject.CN} - ${subject.O} (${subject.C})`;
|
return `${subject.CN} - ${subject.O} (${subject.C})`;
|
||||||
|
@@ -29,14 +29,17 @@ import { Modal } from "bootstrap";
|
|||||||
|
|
||||||
export default {
|
export default {
|
||||||
props: {
|
props: {
|
||||||
|
/** Style of button */
|
||||||
btnStyle: {
|
btnStyle: {
|
||||||
type: String,
|
type: String,
|
||||||
default: "btn-primary",
|
default: "btn-primary",
|
||||||
},
|
},
|
||||||
|
/** Text to use as yes */
|
||||||
yesText: {
|
yesText: {
|
||||||
type: String,
|
type: String,
|
||||||
default: "Yes", // TODO: No idea what to translate this
|
default: "Yes", // TODO: No idea what to translate this
|
||||||
},
|
},
|
||||||
|
/** Text to use as no */
|
||||||
noText: {
|
noText: {
|
||||||
type: String,
|
type: String,
|
||||||
default: "No",
|
default: "No",
|
||||||
@@ -50,9 +53,13 @@ export default {
|
|||||||
this.modal = new Modal(this.$refs.modal);
|
this.modal = new Modal(this.$refs.modal);
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
/** Show the confirm dialog */
|
||||||
show() {
|
show() {
|
||||||
this.modal.show();
|
this.modal.show();
|
||||||
},
|
},
|
||||||
|
/**
|
||||||
|
* @emits string "yes" Notify the parent when Yes is pressed
|
||||||
|
*/
|
||||||
yes() {
|
yes() {
|
||||||
this.$emit("yes");
|
this.$emit("yes");
|
||||||
},
|
},
|
||||||
|
@@ -25,33 +25,41 @@ let timeout;
|
|||||||
|
|
||||||
export default {
|
export default {
|
||||||
props: {
|
props: {
|
||||||
|
/** ID of this input */
|
||||||
id: {
|
id: {
|
||||||
type: String,
|
type: String,
|
||||||
default: ""
|
default: ""
|
||||||
},
|
},
|
||||||
|
/** Type of input */
|
||||||
type: {
|
type: {
|
||||||
type: String,
|
type: String,
|
||||||
default: "text"
|
default: "text"
|
||||||
},
|
},
|
||||||
|
/** The value of the input */
|
||||||
modelValue: {
|
modelValue: {
|
||||||
type: String,
|
type: String,
|
||||||
default: ""
|
default: ""
|
||||||
},
|
},
|
||||||
|
/** A placeholder to use */
|
||||||
placeholder: {
|
placeholder: {
|
||||||
type: String,
|
type: String,
|
||||||
default: ""
|
default: ""
|
||||||
},
|
},
|
||||||
|
/** Should the field auto complete */
|
||||||
autocomplete: {
|
autocomplete: {
|
||||||
type: String,
|
type: String,
|
||||||
default: undefined,
|
default: undefined,
|
||||||
},
|
},
|
||||||
|
/** Is the input required? */
|
||||||
required: {
|
required: {
|
||||||
type: Boolean
|
type: Boolean
|
||||||
},
|
},
|
||||||
|
/** Should the input be read only? */
|
||||||
readonly: {
|
readonly: {
|
||||||
type: String,
|
type: String,
|
||||||
default: undefined,
|
default: undefined,
|
||||||
},
|
},
|
||||||
|
/** Is the input disabled? */
|
||||||
disabled: {
|
disabled: {
|
||||||
type: String,
|
type: String,
|
||||||
default: undefined,
|
default: undefined,
|
||||||
@@ -79,14 +87,21 @@ export default {
|
|||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
|
||||||
|
/** Show the input */
|
||||||
showInput() {
|
showInput() {
|
||||||
this.visibility = "text";
|
this.visibility = "text";
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/** Hide the input */
|
||||||
hideInput() {
|
hideInput() {
|
||||||
this.visibility = "password";
|
this.visibility = "password";
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Copy the provided text to the users clipboard
|
||||||
|
* @param {string} textToCopy
|
||||||
|
* @returns {Promise<void>}
|
||||||
|
*/
|
||||||
copyToClipboard(textToCopy) {
|
copyToClipboard(textToCopy) {
|
||||||
this.icon = "check";
|
this.icon = "check";
|
||||||
|
|
||||||
|
@@ -10,11 +10,16 @@ import { sleep } from "../util.ts";
|
|||||||
export default {
|
export default {
|
||||||
|
|
||||||
props: {
|
props: {
|
||||||
value: [ String, Number ],
|
/** Value to count */
|
||||||
|
value: {
|
||||||
|
type: [ String, Number ],
|
||||||
|
default: 0,
|
||||||
|
},
|
||||||
time: {
|
time: {
|
||||||
type: Number,
|
type: Number,
|
||||||
default: 0.3,
|
default: 0.3,
|
||||||
},
|
},
|
||||||
|
/** Unit of the value */
|
||||||
unit: {
|
unit: {
|
||||||
type: String,
|
type: String,
|
||||||
default: "ms",
|
default: "ms",
|
||||||
@@ -40,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);
|
||||||
|
@@ -5,15 +5,20 @@
|
|||||||
<script>
|
<script>
|
||||||
import dayjs from "dayjs";
|
import dayjs from "dayjs";
|
||||||
import relativeTime from "dayjs/plugin/relativeTime";
|
import relativeTime from "dayjs/plugin/relativeTime";
|
||||||
import utc from "dayjs/plugin/utc";
|
|
||||||
import timezone from "dayjs/plugin/timezone"; // dependent on utc plugin
|
import timezone from "dayjs/plugin/timezone"; // dependent on utc plugin
|
||||||
|
import utc from "dayjs/plugin/utc";
|
||||||
dayjs.extend(utc);
|
dayjs.extend(utc);
|
||||||
dayjs.extend(timezone);
|
dayjs.extend(timezone);
|
||||||
dayjs.extend(relativeTime);
|
dayjs.extend(relativeTime);
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
props: {
|
props: {
|
||||||
value: String,
|
/** Value of date time */
|
||||||
|
value: {
|
||||||
|
type: String,
|
||||||
|
default: null,
|
||||||
|
},
|
||||||
|
/** Should only the date be displayed? */
|
||||||
dateOnly: {
|
dateOnly: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: false,
|
default: false,
|
||||||
|
@@ -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,15 +163,23 @@ export default {
|
|||||||
this.resize();
|
this.resize();
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
/** Resize the heartbeat bar */
|
||||||
resize() {
|
resize() {
|
||||||
if (this.$refs.wrap) {
|
if (this.$refs.wrap) {
|
||||||
this.maxBeat = Math.floor(this.$refs.wrap.clientWidth / (this.beatWidth + this.beatMargin * 2));
|
this.maxBeat = Math.floor(this.$refs.wrap.clientWidth / (this.beatWidth + this.beatMargin * 2));
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the title of the beat.
|
||||||
|
* Used as the hover tooltip on the heartbeat bar.
|
||||||
|
* @param {Object} beat Beat to get title from
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
getBeatTitle(beat) {
|
getBeatTitle(beat) {
|
||||||
return `${this.$root.datetime(beat.time)}` + ((beat.msg) ? ` - ${beat.msg}` : "");
|
return `${this.$root.datetime(beat.time)}` + ((beat.msg) ? ` - ${beat.msg}` : "");
|
||||||
}
|
},
|
||||||
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
@@ -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";
|
||||||
},
|
},
|
||||||
|
@@ -55,6 +55,7 @@ export default {
|
|||||||
};
|
};
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
/** Submit the user details and attempt to log in */
|
||||||
submit() {
|
submit() {
|
||||||
this.processing = true;
|
this.processing = true;
|
||||||
|
|
||||||
|
@@ -47,8 +47,8 @@
|
|||||||
|
|
||||||
<script>
|
<script>
|
||||||
import HeartbeatBar from "../components/HeartbeatBar.vue";
|
import HeartbeatBar from "../components/HeartbeatBar.vue";
|
||||||
import Uptime from "../components/Uptime.vue";
|
|
||||||
import Tag from "../components/Tag.vue";
|
import Tag from "../components/Tag.vue";
|
||||||
|
import Uptime from "../components/Uptime.vue";
|
||||||
import { getMonitorRelativeURL } from "../util.ts";
|
import { getMonitorRelativeURL } from "../util.ts";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
@@ -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() {
|
||||||
@@ -105,7 +118,7 @@ export default {
|
|||||||
|
|
||||||
// Simple filter by search text
|
// Simple filter by search text
|
||||||
// finds monitor name, tag name or tag value
|
// finds monitor name, tag name or tag value
|
||||||
if (this.searchText != "") {
|
if (this.searchText !== "") {
|
||||||
const loweredSearchText = this.searchText.toLowerCase();
|
const loweredSearchText = this.searchText.toLowerCase();
|
||||||
result = result.filter(monitor => {
|
result = result.filter(monitor => {
|
||||||
return monitor.name.toLowerCase().includes(loweredSearchText)
|
return monitor.name.toLowerCase().includes(loweredSearchText)
|
||||||
@@ -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 = "";
|
||||||
}
|
}
|
||||||
@@ -170,12 +190,6 @@ export default {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.dark {
|
|
||||||
.footer {
|
|
||||||
// background-color: $dark-bg;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 770px) {
|
@media (max-width: 770px) {
|
||||||
.list-header {
|
.list-header {
|
||||||
margin: -20px;
|
margin: -20px;
|
||||||
|
@@ -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}
|
||||||
*/
|
*/
|
||||||
|
@@ -18,13 +18,13 @@
|
|||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { BarController, BarElement, Chart, Filler, LinearScale, LineController, LineElement, PointElement, TimeScale, Tooltip } from "chart.js";
|
import { BarController, BarElement, Chart, Filler, LinearScale, LineController, LineElement, PointElement, TimeScale, Tooltip } from "chart.js";
|
||||||
import dayjs from "dayjs";
|
|
||||||
import utc from "dayjs/plugin/utc";
|
|
||||||
import timezone from "dayjs/plugin/timezone";
|
|
||||||
import "chartjs-adapter-dayjs";
|
import "chartjs-adapter-dayjs";
|
||||||
|
import dayjs from "dayjs";
|
||||||
|
import timezone from "dayjs/plugin/timezone";
|
||||||
|
import utc from "dayjs/plugin/utc";
|
||||||
import { LineChart } from "vue-chart-3";
|
import { LineChart } from "vue-chart-3";
|
||||||
import { useToast } from "vue-toastification";
|
import { useToast } from "vue-toastification";
|
||||||
import { DOWN } from "../util.ts";
|
import { DOWN, log } from "../util.ts";
|
||||||
|
|
||||||
dayjs.extend(utc);
|
dayjs.extend(utc);
|
||||||
dayjs.extend(timezone);
|
dayjs.extend(timezone);
|
||||||
@@ -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,
|
||||||
@@ -217,8 +218,9 @@ export default {
|
|||||||
watch: {
|
watch: {
|
||||||
// Update chart data when the selected chart period changes
|
// Update chart data when the selected chart period changes
|
||||||
chartPeriodHrs: function (newPeriod) {
|
chartPeriodHrs: function (newPeriod) {
|
||||||
|
|
||||||
|
// eslint-disable-next-line eqeqeq
|
||||||
if (newPeriod == "0") {
|
if (newPeriod == "0") {
|
||||||
newPeriod = null;
|
|
||||||
this.heartbeatList = null;
|
this.heartbeatList = null;
|
||||||
this.$root.storage().removeItem(`chart-period-${this.monitorId}`);
|
this.$root.storage().removeItem(`chart-period-${this.monitorId}`);
|
||||||
} else {
|
} else {
|
||||||
@@ -241,7 +243,11 @@ export default {
|
|||||||
// And mirror latest change to this.heartbeatList
|
// And mirror latest change to this.heartbeatList
|
||||||
this.$watch(() => this.$root.heartbeatList[this.monitorId],
|
this.$watch(() => this.$root.heartbeatList[this.monitorId],
|
||||||
(heartbeatList) => {
|
(heartbeatList) => {
|
||||||
if (this.chartPeriodHrs != 0) {
|
|
||||||
|
log.debug("ping_chart", `this.chartPeriodHrs type ${typeof this.chartPeriodHrs}, value: ${this.chartPeriodHrs}`);
|
||||||
|
|
||||||
|
// eslint-disable-next-line eqeqeq
|
||||||
|
if (this.chartPeriodHrs != "0") {
|
||||||
const newBeat = heartbeatList.at(-1);
|
const newBeat = heartbeatList.at(-1);
|
||||||
if (newBeat && dayjs.utc(newBeat.time) > dayjs.utc(this.heartbeatList.at(-1)?.time)) {
|
if (newBeat && dayjs.utc(newBeat.time) > dayjs.utc(this.heartbeatList.at(-1)?.time)) {
|
||||||
this.heartbeatList.push(heartbeatList.at(-1));
|
this.heartbeatList.push(heartbeatList.at(-1));
|
||||||
|
@@ -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) => {
|
||||||
|
@@ -72,10 +72,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,10 +96,20 @@ 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);
|
||||||
},
|
},
|
||||||
|
@@ -5,7 +5,11 @@
|
|||||||
<script>
|
<script>
|
||||||
export default {
|
export default {
|
||||||
props: {
|
props: {
|
||||||
status: Number,
|
/** Current status of monitor */
|
||||||
|
status: {
|
||||||
|
type: Number,
|
||||||
|
default: 0,
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
computed: {
|
computed: {
|
||||||
|
@@ -1,13 +1,14 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="tag-wrapper rounded d-inline-flex"
|
<div
|
||||||
:class="{ 'px-3': size == 'normal',
|
class="tag-wrapper rounded d-inline-flex"
|
||||||
'py-1': size == 'normal',
|
:class="{ 'px-3': size == 'normal',
|
||||||
'm-2': size == 'normal',
|
'py-1': size == 'normal',
|
||||||
'px-2': size == 'sm',
|
'm-2': size == 'normal',
|
||||||
'py-0': size == 'sm',
|
'px-2': size == 'sm',
|
||||||
'm-1': size == 'sm',
|
'py-0': size == 'sm',
|
||||||
}"
|
'm-1': size == 'sm',
|
||||||
:style="{ backgroundColor: item.color, fontSize: size == 'sm' ? '0.7em' : '1em' }"
|
}"
|
||||||
|
:style="{ backgroundColor: item.color, fontSize: size == 'sm' ? '0.7em' : '1em' }"
|
||||||
>
|
>
|
||||||
<span class="tag-text">{{ displayText }}</span>
|
<span class="tag-text">{{ displayText }}</span>
|
||||||
<span v-if="remove != null" class="ps-1 btn-remove" @click="remove(item)">
|
<span v-if="remove != null" class="ps-1 btn-remove" @click="remove(item)">
|
||||||
@@ -19,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",
|
||||||
@@ -34,7 +41,7 @@ export default {
|
|||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
displayText() {
|
displayText() {
|
||||||
if (this.item.value == "") {
|
if (this.item.value === "") {
|
||||||
return this.item.name;
|
return this.item.name;
|
||||||
} else {
|
} else {
|
||||||
return `${this.item.name}: ${this.item.value}`;
|
return `${this.item.name}: ${this.item.value}`;
|
||||||
|
@@ -34,18 +34,20 @@
|
|||||||
label="name"
|
label="name"
|
||||||
>
|
>
|
||||||
<template #option="{ option }">
|
<template #option="{ option }">
|
||||||
<div class="mx-2 py-1 px-3 rounded d-inline-flex"
|
<div
|
||||||
style="margin-top: -5px; margin-bottom: -5px; height: 24px;"
|
class="mx-2 py-1 px-3 rounded d-inline-flex"
|
||||||
:style="{ color: textColor(option), backgroundColor: option.color + ' !important' }"
|
style="margin-top: -5px; margin-bottom: -5px; height: 24px;"
|
||||||
|
:style="{ color: textColor(option), backgroundColor: option.color + ' !important' }"
|
||||||
>
|
>
|
||||||
<span>
|
<span>
|
||||||
{{ option.name }}</span>
|
{{ option.name }}</span>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<template #singleLabel="{ option }">
|
<template #singleLabel="{ option }">
|
||||||
<div class="py-1 px-3 rounded d-inline-flex"
|
<div
|
||||||
style="height: 24px;"
|
class="py-1 px-3 rounded d-inline-flex"
|
||||||
:style="{ color: textColor(option), backgroundColor: option.color + ' !important' }"
|
style="height: 24px;"
|
||||||
|
:style="{ color: textColor(option), backgroundColor: option.color + ' !important' }"
|
||||||
>
|
>
|
||||||
<span>{{ option.name }}</span>
|
<span>{{ option.name }}</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -53,10 +55,11 @@
|
|||||||
</vue-multiselect>
|
</vue-multiselect>
|
||||||
<div v-if="newDraftTag.select?.name == null" class="d-flex mb-2">
|
<div v-if="newDraftTag.select?.name == null" class="d-flex mb-2">
|
||||||
<div class="w-50 pe-2">
|
<div class="w-50 pe-2">
|
||||||
<input v-model="newDraftTag.name" class="form-control"
|
<input
|
||||||
:class="{'is-invalid': validateDraftTag.nameInvalid}"
|
v-model="newDraftTag.name" class="form-control"
|
||||||
:placeholder="$t('Name')"
|
:class="{'is-invalid': validateDraftTag.nameInvalid}"
|
||||||
@keydown.enter.prevent="onEnter"
|
:placeholder="$t('Name')"
|
||||||
|
@keydown.enter.prevent="onEnter"
|
||||||
/>
|
/>
|
||||||
<div class="invalid-feedback">
|
<div class="invalid-feedback">
|
||||||
{{ $t("Tag with this name already exist.") }}
|
{{ $t("Tag with this name already exist.") }}
|
||||||
@@ -75,17 +78,19 @@
|
|||||||
deselect-label=""
|
deselect-label=""
|
||||||
>
|
>
|
||||||
<template #option="{ option }">
|
<template #option="{ option }">
|
||||||
<div class="mx-2 py-1 px-3 rounded d-inline-flex"
|
<div
|
||||||
style="height: 24px; color: white;"
|
class="mx-2 py-1 px-3 rounded d-inline-flex"
|
||||||
:style="{ backgroundColor: option.color + ' !important' }"
|
style="height: 24px; color: white;"
|
||||||
|
:style="{ backgroundColor: option.color + ' !important' }"
|
||||||
>
|
>
|
||||||
<span>{{ option.name }}</span>
|
<span>{{ option.name }}</span>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<template #singleLabel="{ option }">
|
<template #singleLabel="{ option }">
|
||||||
<div class="py-1 px-3 rounded d-inline-flex"
|
<div
|
||||||
style="height: 24px; color: white;"
|
class="py-1 px-3 rounded d-inline-flex"
|
||||||
:style="{ backgroundColor: option.color + ' !important' }"
|
style="height: 24px; color: white;"
|
||||||
|
:style="{ backgroundColor: option.color + ' !important' }"
|
||||||
>
|
>
|
||||||
<span>{{ option.name }}</span>
|
<span>{{ option.name }}</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -94,10 +99,11 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="mb-2">
|
<div class="mb-2">
|
||||||
<input v-model="newDraftTag.value" class="form-control"
|
<input
|
||||||
:class="{'is-invalid': validateDraftTag.valueInvalid}"
|
v-model="newDraftTag.value" class="form-control"
|
||||||
:placeholder="$t('value (optional)')"
|
:class="{'is-invalid': validateDraftTag.valueInvalid}"
|
||||||
@keydown.enter.prevent="onEnter"
|
:placeholder="$t('value (optional)')"
|
||||||
|
@keydown.enter.prevent="onEnter"
|
||||||
/>
|
/>
|
||||||
<div class="invalid-feedback">
|
<div class="invalid-feedback">
|
||||||
{{ $t("Tag with this value already exist.") }}
|
{{ $t("Tag with this value already exist.") }}
|
||||||
@@ -123,8 +129,8 @@
|
|||||||
<script>
|
<script>
|
||||||
import { Modal } from "bootstrap";
|
import { Modal } from "bootstrap";
|
||||||
import VueMultiselect from "vue-multiselect";
|
import VueMultiselect from "vue-multiselect";
|
||||||
import Tag from "../components/Tag.vue";
|
|
||||||
import { useToast } from "vue-toastification";
|
import { useToast } from "vue-toastification";
|
||||||
|
import Tag from "../components/Tag.vue";
|
||||||
const toast = useToast();
|
const toast = useToast();
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
@@ -133,6 +139,7 @@ export default {
|
|||||||
VueMultiselect,
|
VueMultiselect,
|
||||||
},
|
},
|
||||||
props: {
|
props: {
|
||||||
|
/** Array of tags to be pre-selected */
|
||||||
preSelectedTags: {
|
preSelectedTags: {
|
||||||
type: Array,
|
type: Array,
|
||||||
default: () => [],
|
default: () => [],
|
||||||
@@ -159,14 +166,14 @@ export default {
|
|||||||
tagOptions() {
|
tagOptions() {
|
||||||
const tagOptions = this.existingTags;
|
const tagOptions = this.existingTags;
|
||||||
for (const tag of this.newTags) {
|
for (const tag of this.newTags) {
|
||||||
if (!tagOptions.find(t => t.name == tag.name && t.color == tag.color)) {
|
if (!tagOptions.find(t => t.name === tag.name && t.color === tag.color)) {
|
||||||
tagOptions.push(tag);
|
tagOptions.push(tag);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return tagOptions;
|
return tagOptions;
|
||||||
},
|
},
|
||||||
selectedTags() {
|
selectedTags() {
|
||||||
return this.preSelectedTags.concat(this.newTags).filter(tag => !this.deleteTags.find(monitorTag => monitorTag.id == tag.id));
|
return this.preSelectedTags.concat(this.newTags).filter(tag => !this.deleteTags.find(monitorTag => monitorTag.id === tag.id));
|
||||||
},
|
},
|
||||||
colorOptions() {
|
colorOptions() {
|
||||||
return [
|
return [
|
||||||
@@ -192,7 +199,7 @@ export default {
|
|||||||
let nameInvalid = false;
|
let nameInvalid = false;
|
||||||
let valueInvalid = false;
|
let valueInvalid = false;
|
||||||
let invalid = true;
|
let invalid = true;
|
||||||
if (this.deleteTags.find(tag => tag.name == this.newDraftTag.select?.name && tag.value == this.newDraftTag.value)) {
|
if (this.deleteTags.find(tag => tag.name === this.newDraftTag.select?.name && tag.value === this.newDraftTag.value)) {
|
||||||
// Undo removing a Tag
|
// Undo removing a Tag
|
||||||
nameInvalid = false;
|
nameInvalid = false;
|
||||||
valueInvalid = false;
|
valueInvalid = false;
|
||||||
@@ -202,9 +209,9 @@ export default {
|
|||||||
nameInvalid = true;
|
nameInvalid = true;
|
||||||
invalid = true;
|
invalid = true;
|
||||||
} else if (this.newTags.concat(this.preSelectedTags).filter(tag => (
|
} else if (this.newTags.concat(this.preSelectedTags).filter(tag => (
|
||||||
tag.name == this.newDraftTag.select?.name && tag.value == this.newDraftTag.value
|
tag.name === this.newDraftTag.select?.name && tag.value === this.newDraftTag.value
|
||||||
) || (
|
) || (
|
||||||
tag.name == this.newDraftTag.name && tag.value == this.newDraftTag.value
|
tag.name === this.newDraftTag.name && tag.value === this.newDraftTag.value
|
||||||
)).length > 0) {
|
)).length > 0) {
|
||||||
// Try to add a tag with existing name and value
|
// Try to add a tag with existing name and value
|
||||||
valueInvalid = true;
|
valueInvalid = true;
|
||||||
@@ -235,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) {
|
||||||
@@ -247,15 +256,26 @@ 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
|
||||||
this.newTags = this.newTags.filter(tag => !(tag.name == item.name && tag.value == item.value));
|
this.newTags = this.newTags.filter(tag => !(tag.name === item.name && tag.value === item.value));
|
||||||
} else {
|
} else {
|
||||||
// Remove an Existing Tag
|
// Remove an Existing Tag
|
||||||
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";
|
||||||
@@ -263,12 +283,13 @@ 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) {
|
||||||
if (this.deleteTags.find(tag => tag.name == this.newDraftTag.select.name && tag.value == this.newDraftTag.value)) {
|
if (this.deleteTags.find(tag => tag.name === this.newDraftTag.select.name && tag.value === this.newDraftTag.value)) {
|
||||||
// Undo removing a tag
|
// Undo removing a tag
|
||||||
this.deleteTags = this.deleteTags.filter(tag => !(tag.name == this.newDraftTag.select.name && tag.value == this.newDraftTag.value));
|
this.deleteTags = this.deleteTags.filter(tag => !(tag.name === this.newDraftTag.select.name && tag.value === this.newDraftTag.value));
|
||||||
} else {
|
} else {
|
||||||
// Add an existing Tag
|
// Add an existing Tag
|
||||||
this.newTags.push({
|
this.newTags.push({
|
||||||
@@ -290,6 +311,7 @@ export default {
|
|||||||
}
|
}
|
||||||
this.clearDraftTag();
|
this.clearDraftTag();
|
||||||
},
|
},
|
||||||
|
/** Remove a draft tag */
|
||||||
clearDraftTag() {
|
clearDraftTag() {
|
||||||
this.newDraftTag = {
|
this.newDraftTag = {
|
||||||
name: null,
|
name: null,
|
||||||
@@ -301,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;
|
||||||
@@ -345,7 +392,7 @@ export default {
|
|||||||
tagId = newTagResult.id;
|
tagId = newTagResult.id;
|
||||||
// Assign the new ID to the tags of the same name & color
|
// Assign the new ID to the tags of the same name & color
|
||||||
this.newTags.map(tag => {
|
this.newTags.map(tag => {
|
||||||
if (tag.name == newTag.name && tag.color == newTag.color) {
|
if (tag.name === newTag.name && tag.color === newTag.color) {
|
||||||
tag.id = newTagResult.id;
|
tag.id = newTagResult.id;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@@ -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,8 +5,17 @@
|
|||||||
<script>
|
<script>
|
||||||
export default {
|
export default {
|
||||||
props: {
|
props: {
|
||||||
monitor: Object,
|
/** Monitor this represents */
|
||||||
type: String,
|
monitor: {
|
||||||
|
type: Object,
|
||||||
|
default: null,
|
||||||
|
},
|
||||||
|
/** Type of monitor */
|
||||||
|
type: {
|
||||||
|
type: String,
|
||||||
|
default: null,
|
||||||
|
},
|
||||||
|
/** Is this a pill? */
|
||||||
pill: {
|
pill: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: false,
|
default: false,
|
||||||
|
@@ -8,6 +8,9 @@
|
|||||||
<a href="https://github.com/caronc/apprise/wiki#notification-services" target="_blank">https://github.com/caronc/apprise/wiki#notification-services</a>
|
<a href="https://github.com/caronc/apprise/wiki#notification-services" target="_blank">https://github.com/caronc/apprise/wiki#notification-services</a>
|
||||||
</i18n-t>
|
</i18n-t>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<label for="title" class="form-label">{{ $t("Title") }}</label>
|
||||||
|
<input id="title" v-model="$parent.notification.title" type="text" class="form-control">
|
||||||
</div>
|
</div>
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<i18n-t tag="p" keypath="Status:">
|
<i18n-t tag="p" keypath="Status:">
|
||||||
|
@@ -1,12 +1,11 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label for="clicksendsms-login" class="form-label">API Username</label>
|
<label for="clicksendsms-login" class="form-label">{{ $t("API Username") }}</label>
|
||||||
<div class="form-text">
|
<i18n-t tag="div" class="form-text" keypath="wayToGetClickSendSMSToken">
|
||||||
{{ $t("apiCredentials") }}
|
|
||||||
<a href="http://dashboard.clicksend.com/account/subaccounts" target="_blank">{{ $t("here") }}</a>
|
<a href="http://dashboard.clicksend.com/account/subaccounts" target="_blank">{{ $t("here") }}</a>
|
||||||
</div>
|
</i18n-t>
|
||||||
<input id="clicksendsms-login" v-model="$parent.notification.clicksendsmsLogin" type="text" class="form-control" required>
|
<input id="clicksendsms-login" v-model="$parent.notification.clicksendsmsLogin" type="text" class="form-control" required>
|
||||||
<label for="clicksendsms-key" class="form-label">API Key</label>
|
<label for="clicksendsms-key" class="form-label">{{ $t("API Key") }}</label>
|
||||||
<HiddenInput id="clicksendsms-key" v-model="$parent.notification.clicksendsmsPassword" :required="true" autocomplete="one-time-code"></HiddenInput>
|
<HiddenInput id="clicksendsms-key" v-model="$parent.notification.clicksendsmsPassword" :required="true" autocomplete="one-time-code"></HiddenInput>
|
||||||
</div>
|
</div>
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
@@ -16,15 +15,15 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label for="clicksendsms-to-number" class="form-label">Recipient Number</label>
|
<label for="clicksendsms-to-number" class="form-label">{{ $t("Recipient Number") }}</label>
|
||||||
<input id="clicksendsms-to-number" v-model="$parent.notification.clicksendsmsToNumber" type="text" minlength="8" maxlength="14" class="form-control" required>
|
<input id="clicksendsms-to-number" v-model="$parent.notification.clicksendsmsToNumber" type="text" minlength="8" maxlength="14" class="form-control" required>
|
||||||
</div>
|
</div>
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label for="clicksendsms-sender-name" class="form-label">From Name/Number -
|
<label for="clicksendsms-sender-name" class="form-label">{{ $t("From Name/Number") }} -
|
||||||
<a href="https://help.clicksend.com/article/4kgj7krx00-what-is-a-sender-id-or-sender-number" target="_blank">More Info</a>
|
<a href="https://help.clicksend.com/article/4kgj7krx00-what-is-a-sender-id-or-sender-number" target="_blank">{{ $t("Read more") }}</a>
|
||||||
</label>
|
</label>
|
||||||
<input id="clicksendsms-sender-name" v-model="$parent.notification.clicksendsmsSenderName" type="text" minlength="3" maxlength="11" class="form-control">
|
<input id="clicksendsms-sender-name" v-model="$parent.notification.clicksendsmsSenderName" type="text" minlength="3" maxlength="11" class="form-control">
|
||||||
<div class="form-text">Leave blank to use a shared sender number.</div>
|
<div class="form-text">{{ $t("Leave blank to use a shared sender number.") }}</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<script>
|
<script>
|
||||||
|
@@ -7,7 +7,7 @@
|
|||||||
<b>{{ $t("Basic Settings") }}</b>
|
<b>{{ $t("Basic Settings") }}</b>
|
||||||
</i18n-t>
|
</i18n-t>
|
||||||
<div class="mb-3" style="margin-top: 12px;">
|
<div class="mb-3" style="margin-top: 12px;">
|
||||||
<label for="line-user-id" class="form-label">User ID</label>
|
<label for="line-user-id" class="form-label">{{ $t("User ID") }}</label>
|
||||||
<input id="line-user-id" v-model="$parent.notification.lineUserID" type="text" class="form-control" required>
|
<input id="line-user-id" v-model="$parent.notification.lineUserID" type="text" class="form-control" required>
|
||||||
</div>
|
</div>
|
||||||
<i18n-t tag="div" keypath="lineDevConsoleTo" class="form-text">
|
<i18n-t tag="div" keypath="lineDevConsoleTo" class="form-text">
|
||||||
|
30
src/components/notifications/Ntfy.vue
Normal file
30
src/components/notifications/Ntfy.vue
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
<template>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="ntfy-ntfytopic" class="form-label">{{ $t("ntfy Topic") }}</label>
|
||||||
|
<div class="input-group mb-3">
|
||||||
|
<input id="ntfy-ntfytopic" v-model="$parent.notification.ntfytopic" type="text" class="form-control" required>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="ntfy-server-url" class="form-label">{{ $t("Server URL") }}</label>
|
||||||
|
<div class="input-group mb-3">
|
||||||
|
<input id="ntfy-server-url" v-model="$parent.notification.ntfyserverurl" type="text" class="form-control" required>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="ntfy-priority" class="form-label">{{ $t("Priority") }}</label>
|
||||||
|
<input id="ntfy-priority" v-model="$parent.notification.ntfyPriority" type="number" class="form-control" required min="1" max="5" step="1">
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
mounted() {
|
||||||
|
if (typeof this.$parent.notification.ntfyPriority === "undefined") {
|
||||||
|
this.$parent.notification.ntfyserverurl = "https://ntfy.sh";
|
||||||
|
this.$parent.notification.ntfyPriority = 5;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
@@ -1,18 +1,18 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label for="octopush-version" class="form-label">Octopush API Version</label>
|
<label for="octopush-version" class="form-label">{{ $t("Octopush API Version") }}</label>
|
||||||
<select id="octopush-version" v-model="$parent.notification.octopushVersion" class="form-select">
|
<select id="octopush-version" v-model="$parent.notification.octopushVersion" class="form-select">
|
||||||
<option value="2">Octopush (endpoint: api.octopush.com)</option>
|
<option value="2">{{ $t("octopush") }} ({{ $t("endpoint") }}: api.octopush.com)</option>
|
||||||
<option value="1">Legacy Octopush-DM (endpoint: www.octopush-dm.com)</option>
|
<option value="1">{{ $t("Legacy Octopush-DM") }} ({{ $t("endpoint") }}: www.octopush-dm.com)</option>
|
||||||
</select>
|
</select>
|
||||||
<div class="form-text">
|
<div class="form-text">
|
||||||
{{ $t("octopushLegacyHint") }}
|
{{ $t("octopushLegacyHint") }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label for="octopush-key" class="form-label">API KEY</label>
|
<label for="octopush-key" class="form-label">{{ $t("octopushAPIKey") }}</label>
|
||||||
<HiddenInput id="octopush-key" v-model="$parent.notification.octopushAPIKey" :required="true" autocomplete="one-time-code"></HiddenInput>
|
<HiddenInput id="octopush-key" v-model="$parent.notification.octopushAPIKey" :required="true" autocomplete="one-time-code"></HiddenInput>
|
||||||
<label for="octopush-login" class="form-label">API LOGIN</label>
|
<label for="octopush-login" class="form-label">{{ $t("octopushLogin") }}</label>
|
||||||
<input id="octopush-login" v-model="$parent.notification.octopushLogin" type="text" class="form-control" required>
|
<input id="octopush-login" v-model="$parent.notification.octopushLogin" type="text" class="form-control" required>
|
||||||
</div>
|
</div>
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
|
45
src/components/notifications/PagerDuty.vue
Normal file
45
src/components/notifications/PagerDuty.vue
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
<template>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="pagerduty-integration-key" class="form-label">{{ $t("Integration Key") }}</label>
|
||||||
|
<HiddenInput id="pagerduty-integration-key" v-model="$parent.notification.pagerdutyIntegrationKey" :required="true" autocomplete="false"></HiddenInput>
|
||||||
|
<i18n-t tag="div" keypath="wayToGetPagerDutyKey" class="form-text">
|
||||||
|
<a href="https://support.pagerduty.com/docs/services-and-integrations" target="_blank">{{ $t("here") }}</a>
|
||||||
|
</i18n-t>
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="pagerduty-integration-url" class="form-label">{{ $t("Integration URL") }}</label>
|
||||||
|
<input id="pagerduty-integration-url" v-model="$parent.notification.pagerdutyIntegrationUrl" type="text" class="form-control" autocomplete="false">
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="pagerduty-priority" class="form-label">{{ $t("Priority") }}</label>
|
||||||
|
<select id="pagerduty-priority" v-model="$parent.notification.pagerdutyPriority" class="form-select">
|
||||||
|
<option value="info">{{ $t("info") }}</option>
|
||||||
|
<option value="warning" selected="selected">{{ $t("warning") }}</option>
|
||||||
|
<option value="error">{{ $t("error") }}</option>
|
||||||
|
<option value="critical">{{ $t("critical") }}</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="pagerduty-resolve" class="form-label">{{ $t("Auto resolve or acknowledged") }}</label>
|
||||||
|
<select id="pagerduty-resolve" v-model="$parent.notification.pagerdutyAutoResolve" class="form-select">
|
||||||
|
<option value="0" selected="selected">{{ $t("do nothing") }}</option>
|
||||||
|
<option value="acknowledge">{{ $t("auto acknowledged") }}</option>
|
||||||
|
<option value="resolve">{{ $t("auto resolve") }}</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import HiddenInput from "../HiddenInput.vue";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
components: {
|
||||||
|
HiddenInput,
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
if (typeof this.$parent.notification.pagerdutyIntegrationUrl === "undefined") {
|
||||||
|
this.$parent.notification.pagerdutyIntegrationUrl = "https://events.pagerduty.com/v2/enqueue";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
@@ -1,8 +1,8 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label for="promosms-login" class="form-label">API LOGIN</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">API PASSWORD</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">
|
||||||
|
@@ -18,28 +18,29 @@
|
|||||||
</select>
|
</select>
|
||||||
<label for="pushover-sound" class="form-label">{{ $t("Notification Sound") }}</label>
|
<label for="pushover-sound" class="form-label">{{ $t("Notification Sound") }}</label>
|
||||||
<select id="pushover-sound" v-model="$parent.notification.pushoversounds" class="form-select">
|
<select id="pushover-sound" v-model="$parent.notification.pushoversounds" class="form-select">
|
||||||
<option>pushover</option>
|
<option value="pushover">{{ $t("pushoversounds pushover") }}</option>
|
||||||
<option>bike</option>
|
<option value="bike">{{ $t("pushoversounds bike") }}</option>
|
||||||
<option>bugle</option>
|
<option value="bugle">{{ $t("pushoversounds bugle") }}</option>
|
||||||
<option>cashregister</option>
|
<option value="cashregister">{{ $t("pushoversounds cashregister") }}</option>
|
||||||
<option>classical</option>
|
<option value="classical">{{ $t("pushoversounds classical") }}</option>
|
||||||
<option>cosmic</option>
|
<option value="cosmic">{{ $t("pushoversounds cosmic") }}</option>
|
||||||
<option>falling</option>
|
<option value="falling">{{ $t("pushoversounds falling") }}</option>
|
||||||
<option>gamelan</option>
|
<option value="gamelan">{{ $t("pushoversounds gamelan") }}</option>
|
||||||
<option>incoming</option>
|
<option value="incoming">{{ $t("pushoversounds incoming") }}</option>
|
||||||
<option>intermission</option>
|
<option value="intermission">{{ $t("pushoversounds intermission") }}</option>
|
||||||
<option>mechanical</option>
|
<option value="magic">{{ $t("pushoversounds magic") }}</option>
|
||||||
<option>pianobar</option>
|
<option value="mechanical">{{ $t("pushoversounds mechanical") }}</option>
|
||||||
<option>siren</option>
|
<option value="pianobar">{{ $t("pushoversounds pianobar") }}</option>
|
||||||
<option>spacealarm</option>
|
<option value="siren">{{ $t("pushoversounds siren") }}</option>
|
||||||
<option>tugboat</option>
|
<option value="spacealarm">{{ $t("pushoversounds spacealarm") }}</option>
|
||||||
<option>alien</option>
|
<option value="tugboat">{{ $t("pushoversounds tugboat") }}</option>
|
||||||
<option>climb</option>
|
<option value="alien">{{ $t("pushoversounds alien") }}</option>
|
||||||
<option>persistent</option>
|
<option value="climb">{{ $t("pushoversounds climb") }}</option>
|
||||||
<option>echo</option>
|
<option value="persistent">{{ $t("pushoversounds persistent") }}</option>
|
||||||
<option>updown</option>
|
<option value="echo">{{ $t("pushoversounds echo") }}</option>
|
||||||
<option>vibrate</option>
|
<option value="updown">{{ $t("pushoversounds updown") }}</option>
|
||||||
<option>none</option>
|
<option value="vibrate">{{ $t("pushoversounds vibrate") }}</option>
|
||||||
|
<option value="none">{{ $t("pushoversounds none") }}</option>
|
||||||
</select>
|
</select>
|
||||||
<div class="form-text">
|
<div class="form-text">
|
||||||
<span style="color: red;"><sup>*</sup></span>{{ $t("Required") }}
|
<span style="color: red;"><sup>*</sup></span>{{ $t("Required") }}
|
||||||
|
@@ -1,11 +1,11 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label for="pushy-app-token" class="form-label">API_KEY</label>
|
<label for="pushy-app-token" class="form-label">{{ $t("pushyAPIKey") }}</label>
|
||||||
<HiddenInput id="pushy-app-token" v-model="$parent.notification.pushyAPIKey" :required="true" autocomplete="one-time-code"></HiddenInput>
|
<HiddenInput id="pushy-app-token" v-model="$parent.notification.pushyAPIKey" :required="true" autocomplete="one-time-code"></HiddenInput>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label for="pushy-user-key" class="form-label">USER_TOKEN</label>
|
<label for="pushy-user-key" class="form-label">{{ $t("pushyToken") }}</label>
|
||||||
<div class="input-group mb-3">
|
<div class="input-group mb-3">
|
||||||
<HiddenInput id="pushy-user-key" v-model="$parent.notification.pushyToken" :required="true" autocomplete="one-time-code"></HiddenInput>
|
<HiddenInput id="pushy-user-key" v-model="$parent.notification.pushyToken" :required="true" autocomplete="one-time-code"></HiddenInput>
|
||||||
</div>
|
</div>
|
||||||
|
@@ -1,6 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label for="push-api-key" class="form-label">API_KEY</label>
|
<label for="push-api-key" class="form-label">{{ $t("API Key") }}</label>
|
||||||
<HiddenInput id="push-api-key" v-model="$parent.notification.pushAPIKey" :required="true" autocomplete="one-time-code"></HiddenInput>
|
<HiddenInput id="push-api-key" v-model="$parent.notification.pushAPIKey" :required="true" autocomplete="one-time-code"></HiddenInput>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@@ -4,6 +4,7 @@ import Discord from "./Discord.vue";
|
|||||||
import Webhook from "./Webhook.vue";
|
import Webhook from "./Webhook.vue";
|
||||||
import Signal from "./Signal.vue";
|
import Signal from "./Signal.vue";
|
||||||
import Gotify from "./Gotify.vue";
|
import Gotify from "./Gotify.vue";
|
||||||
|
import Ntfy from "./Ntfy.vue";
|
||||||
import Slack from "./Slack.vue";
|
import Slack from "./Slack.vue";
|
||||||
import RocketChat from "./RocketChat.vue";
|
import RocketChat from "./RocketChat.vue";
|
||||||
import Teams from "./Teams.vue";
|
import Teams from "./Teams.vue";
|
||||||
@@ -27,6 +28,7 @@ import SerwerSMS from "./SerwerSMS.vue";
|
|||||||
import Stackfield from "./Stackfield.vue";
|
import Stackfield from "./Stackfield.vue";
|
||||||
import WeCom from "./WeCom.vue";
|
import WeCom from "./WeCom.vue";
|
||||||
import GoogleChat from "./GoogleChat.vue";
|
import GoogleChat from "./GoogleChat.vue";
|
||||||
|
import PagerDuty from "./PagerDuty.vue";
|
||||||
import Gorush from "./Gorush.vue";
|
import Gorush from "./Gorush.vue";
|
||||||
import Alerta from "./Alerta.vue";
|
import Alerta from "./Alerta.vue";
|
||||||
import OneBot from "./OneBot.vue";
|
import OneBot from "./OneBot.vue";
|
||||||
@@ -45,6 +47,7 @@ const NotificationFormList = {
|
|||||||
"teams": Teams,
|
"teams": Teams,
|
||||||
"signal": Signal,
|
"signal": Signal,
|
||||||
"gotify": Gotify,
|
"gotify": Gotify,
|
||||||
|
"ntfy": Ntfy,
|
||||||
"slack": Slack,
|
"slack": Slack,
|
||||||
"rocket.chat": RocketChat,
|
"rocket.chat": RocketChat,
|
||||||
"pushover": Pushover,
|
"pushover": Pushover,
|
||||||
@@ -67,6 +70,7 @@ const NotificationFormList = {
|
|||||||
"stackfield": Stackfield,
|
"stackfield": Stackfield,
|
||||||
"WeCom": WeCom,
|
"WeCom": WeCom,
|
||||||
"GoogleChat": GoogleChat,
|
"GoogleChat": GoogleChat,
|
||||||
|
"PagerDuty": PagerDuty,
|
||||||
"gorush": Gorush,
|
"gorush": Gorush,
|
||||||
"alerta": Alerta,
|
"alerta": Alerta,
|
||||||
"OneBot": OneBot,
|
"OneBot": OneBot,
|
||||||
|
@@ -9,11 +9,11 @@
|
|||||||
|
|
||||||
<div class="mt-1">
|
<div class="mt-1">
|
||||||
<div class="form-check">
|
<div class="form-check">
|
||||||
<label><input v-model="settings.checkUpdate" type="checkbox" @change="saveSettings()" /> Show update if available</label>
|
<label><input v-model="settings.checkUpdate" type="checkbox" @change="saveSettings()" /> {{ $t("Show update if available") }}</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-check">
|
<div class="form-check">
|
||||||
<label><input v-model="settings.checkBeta" type="checkbox" :disabled="!settings.checkUpdate" @change="saveSettings()" /> Also check beta release</label>
|
<label><input v-model="settings.checkBeta" type="checkbox" :disabled="!settings.checkUpdate" @change="saveSettings()" /> {{ $t("Also check beta release") }}</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@@ -133,10 +133,15 @@ export default {
|
|||||||
},
|
},
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
|
/**
|
||||||
|
* Show the confimation dialog confirming the configuration
|
||||||
|
* be imported
|
||||||
|
*/
|
||||||
confirmImport() {
|
confirmImport() {
|
||||||
this.$refs.confirmImport.show();
|
this.$refs.confirmImport.show();
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/** Download a backup of the configuration */
|
||||||
downloadBackup() {
|
downloadBackup() {
|
||||||
let time = dayjs().format("YYYY_MM_DD-hh_mm_ss");
|
let time = dayjs().format("YYYY_MM_DD-hh_mm_ss");
|
||||||
let fileName = `Uptime_Kuma_Backup_${time}.json`;
|
let fileName = `Uptime_Kuma_Backup_${time}.json`;
|
||||||
@@ -157,6 +162,10 @@ export default {
|
|||||||
downloadItem.click();
|
downloadItem.click();
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Import the specified backup file
|
||||||
|
* @returns {?string}
|
||||||
|
*/
|
||||||
importBackup() {
|
importBackup() {
|
||||||
this.processing = true;
|
this.processing = true;
|
||||||
let uploadItem = document.getElementById("import-backend").files;
|
let uploadItem = document.getElementById("import-backend").files;
|
||||||
|
@@ -178,10 +178,12 @@ export default {
|
|||||||
},
|
},
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
|
/** Save the settings */
|
||||||
saveGeneral() {
|
saveGeneral() {
|
||||||
localStorage.timezone = this.$root.userTimezone;
|
localStorage.timezone = this.$root.userTimezone;
|
||||||
this.saveSettings();
|
this.saveSettings();
|
||||||
},
|
},
|
||||||
|
/** Get the base URL of the application */
|
||||||
autoGetPrimaryBaseURL() {
|
autoGetPrimaryBaseURL() {
|
||||||
this.settings.primaryBaseURL = location.protocol + "//" + location.host;
|
this.settings.primaryBaseURL = location.protocol + "//" + location.host;
|
||||||
},
|
},
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user