mirror of
https://github.com/louislam/uptime-kuma.git
synced 2025-09-14 23:47:00 +08:00
Compare commits
567 Commits
1.10.0
...
1.14.0-bet
Author | SHA1 | Date | |
---|---|---|---|
|
316e65d35a | ||
|
df5ba02f3f | ||
|
c9fa183712 | ||
|
0b9b5102ec | ||
|
c399984b7f | ||
|
0afa0be5c2 | ||
|
6a30dbd71a | ||
|
a2d9474e85 | ||
|
8479e772cd | ||
|
2e50ef0e8f | ||
|
4fb2c69dd1 | ||
|
c08910a65c | ||
|
943c904256 | ||
|
25b5edea7f | ||
|
7bbaeffd3e | ||
|
008dc27f52 | ||
|
5027fcd320 | ||
|
d5e68f8453 | ||
|
fcb577097b | ||
|
082c2dd32d | ||
|
e89356b283 | ||
|
6014b9534f | ||
|
8b45a95cc3 | ||
|
02becfd113 | ||
|
8ad992eac8 | ||
|
c4e74c9943 | ||
|
fee88b32e3 | ||
|
ffc5bca51d | ||
|
511b9dd425 | ||
|
e9dd64b6f0 | ||
|
355aec46dc | ||
|
c9deea9fdf | ||
|
70311f7a5a | ||
|
4b99160b1f | ||
|
48d679234a | ||
|
d8b32d652f | ||
|
d3d1656625 | ||
|
8e78e62eee | ||
|
706d6cee07 | ||
|
43eed45bae | ||
|
19b7e2ba5e | ||
|
99042e6991 | ||
|
f54084c888 | ||
|
87d3853b8e | ||
|
4738581c66 | ||
|
3218a0eee8 | ||
|
87ee3c20bd | ||
|
38e6e846bf | ||
|
92ab2b12d0 | ||
|
04e3394d02 | ||
|
f6cd2f60ca | ||
|
53cea7f8d3 | ||
|
aef7719426 | ||
|
514b9fb68a | ||
|
da32a1aa19 | ||
|
7a69f9f56f | ||
|
c50c20faa4 | ||
|
cb6eeaef34 | ||
|
6674005e8b | ||
|
ee3d7d8b42 | ||
|
a277cfe9e8 | ||
|
95b0df0270 | ||
|
f02e9c44ec | ||
|
bb2b5cd6ac | ||
|
b72a2d350f | ||
|
71be030733 | ||
|
73b338bba6 | ||
|
82ea896bbc | ||
|
f1f4b3b377 | ||
|
a6b52b7ba6 | ||
|
b8dea3a823 | ||
|
0da6e6b1fb | ||
|
44fb2a88f2 | ||
|
623b06e33c | ||
|
7d3cbff794 | ||
|
61d0a0abce | ||
|
7fd5b61bab | ||
|
96289fe014 | ||
|
381605aca1 | ||
|
742c6bcaa3 | ||
|
be88351eb3 | ||
|
34a0b54b93 | ||
|
e11ea7b061 | ||
|
12237dec6e | ||
|
f6272155af | ||
|
630b441a2d | ||
|
1ecd2e45d0 | ||
|
5922771909 | ||
|
623d03dc6f | ||
|
f52e527850 | ||
|
28d72fcd08 | ||
|
6c7a0ff7d3 | ||
|
2abdf2efad | ||
|
71af08189e | ||
|
d32ba7cadd | ||
|
775d1696fa | ||
|
7fb16d2f9a | ||
|
40991fbc28 | ||
|
bf20f9d290 | ||
|
5fa14161c4 | ||
|
5a2a59250d | ||
|
fcee93cbea | ||
|
668dffc2c5 | ||
|
210eebe144 | ||
|
4b04a9c214 | ||
|
909618a29a | ||
|
4a4ffc96dd | ||
|
3713692bdd | ||
|
76f991ecd8 | ||
|
84dcd81f21 | ||
|
f65d0654a6 | ||
|
b0bda9f9d2 | ||
|
ad2130b7b5 | ||
|
4545eec3fe | ||
|
3adda48f3a | ||
|
cafa61e3af | ||
|
58ee071fae | ||
|
9173838e1b | ||
|
833d9381ff | ||
|
73d904952d | ||
|
4e95e9ea51 | ||
|
c22cc4d794 | ||
|
8cbdefdc0d | ||
|
2f5beefa37 | ||
|
dae5ff690a | ||
|
fb9a206542 | ||
|
dc3da45dd6 | ||
|
82049a2387 | ||
|
d7a839aa52 | ||
|
aef0a66205 | ||
|
37be7df9b0 | ||
|
243fab5f26 | ||
|
8d981c8f0b | ||
|
220e46bc83 | ||
|
59cdacc052 | ||
|
00738edbe7 | ||
|
27bfae67af | ||
|
719a136d1e | ||
|
502c7f87e7 | ||
|
78a732409b | ||
|
c0c6419980 | ||
|
5474368263 | ||
|
e87cdf4d09 | ||
|
bb1c951a96 | ||
|
1033ca5cf4 | ||
|
18ec42b060 | ||
|
7c7dbf68c1 | ||
|
3e96504813 | ||
|
d765b1c57a | ||
|
5f778b9763 | ||
|
c68f7944e3 | ||
|
a9efdabcec | ||
|
b9dfcd1291 | ||
|
04d93c2747 | ||
|
c65d771fad | ||
|
3f8a396090 | ||
|
9681957adf | ||
|
95a2c967c6 | ||
|
50d6e888c2 | ||
|
ae14ad5a84 | ||
|
edd9202de9 | ||
|
a97d2a5498 | ||
|
72ce28a541 | ||
|
1e2a8453c6 | ||
|
1fa4a16663 | ||
|
6a57c443fd | ||
|
8078d0618d | ||
|
9e27acb511 | ||
|
78d76512ba | ||
|
2cc7a990ff | ||
|
157f0de61a | ||
|
88c3d952d3 | ||
|
e3a0eaf6af | ||
|
8bbf55777e | ||
|
c0e0698c21 | ||
|
14d8095f12 | ||
|
fa490d0bf1 | ||
|
c52c8a4206 | ||
|
9789d8cde8 | ||
|
ccb3d85a48 | ||
|
333505b039 | ||
|
602da565eb | ||
|
b62d94184a | ||
|
e0175d0010 | ||
|
3246055696 | ||
|
b3a690f3b1 | ||
|
7bc8c447cd | ||
|
69ff6831ab | ||
|
88a798704b | ||
|
783173fd1f | ||
|
0dba06e48b | ||
|
281fe365c0 | ||
|
8e7c0a6163 | ||
|
0671e4ea2b | ||
|
cd8eaef903 | ||
|
51f5c009e3 | ||
|
3bf62c9ceb | ||
|
7b11539cff | ||
|
b4a3d68356 | ||
|
b31af8a15c | ||
|
60f67ccb35 | ||
|
81a9807a0a | ||
|
3681934d05 | ||
|
d5d63474d8 | ||
|
a6fd626fb8 | ||
|
3a5b413af4 | ||
|
595cd93220 | ||
|
e12c1511db | ||
|
f3112c0b85 | ||
|
af07850ddf | ||
|
211b44269c | ||
|
7638b73645 | ||
|
a997f8e4f9 | ||
|
09dbb143ea | ||
|
f19e983818 | ||
|
d0ed99a310 | ||
|
258d93be72 | ||
|
986ddd92ff | ||
|
c75c6c5640 | ||
|
5aed36b470 | ||
|
76b9fb967f | ||
|
b58120d258 | ||
|
79f99ce215 | ||
|
e7e30bf497 | ||
|
efaa55ad1f | ||
|
32a898bee5 | ||
|
561a0a3c9a | ||
|
daac9ddffc | ||
|
6bd2ee8c69 | ||
|
45dca072b2 | ||
|
7d8b72c6c0 | ||
|
40cc885eb8 | ||
|
742ad083e5 | ||
|
27f4f5ee0b | ||
|
41f1686147 | ||
|
faab1ead92 | ||
|
f1007ad42f | ||
|
dd28ecaa2d | ||
|
ffa585376d | ||
|
c1c1e2ba5b | ||
|
2f7e24191a | ||
|
0fce1b4b9b | ||
|
11c2e86bfe | ||
|
1bbf17f3da | ||
|
39f8b30b36 | ||
|
ffb2c2996b | ||
|
65896ed035 | ||
|
b13b20bd95 | ||
|
8febff9282 | ||
|
90f2497548 | ||
|
a9df7b4a14 | ||
|
cefe43800f | ||
|
eaf370637e | ||
|
23796723dd | ||
|
51b7a2badb | ||
|
74c584f544 | ||
|
c3c4db52ec | ||
|
aba6cb2c52 | ||
|
ff0e85737f | ||
|
4713820da7 | ||
|
a99e87c02c | ||
|
3f8ca82434 | ||
|
60f1eb7b45 | ||
|
55a593f75d | ||
|
a0d51a15cf | ||
|
5a08b42e4f | ||
|
6961af005e | ||
|
847a19afc1 | ||
|
7532e7fd3e | ||
|
63a3704836 | ||
|
3e87eb596f | ||
|
c679613f7e | ||
|
bd8fa17887 | ||
|
d1a99b0a22 | ||
|
3b9fac2942 | ||
|
812e80030b | ||
|
b89efa49aa | ||
|
6490ef3787 | ||
|
329c8cbc2d | ||
|
2bf9764cec | ||
|
c116754360 | ||
|
2c7a701c84 | ||
|
fd1fce0143 | ||
|
52e0d74a1e | ||
|
23532aaafe | ||
|
ab61acab63 | ||
|
06aab3dee8 | ||
|
13acdd4c65 | ||
|
fe0bce268d | ||
|
ed64853125 | ||
|
0f822d3b2a | ||
|
6bda5c6329 | ||
|
44bc98a453 | ||
|
f9751d0c01 | ||
|
53df9a36e3 | ||
|
ccfd04a431 | ||
|
9324137123 | ||
|
9f063cf477 | ||
|
b83c896d0c | ||
|
aa37383065 | ||
|
5e2c39eb4b | ||
|
2a1f011f05 | ||
|
ea43422ccf | ||
|
8063449f49 | ||
|
b6ad4c845a | ||
|
cdcdf377ec | ||
|
30a345d8b6 | ||
|
83d60fea29 | ||
|
2304c53c8d | ||
|
1bbd744d02 | ||
|
2e0e35a1ee | ||
|
1e92487f30 | ||
|
edd2534a1b | ||
|
f6ef390c76 | ||
|
d4b86dc472 | ||
|
46fa6a56fa | ||
|
ec5037f30d | ||
|
81a194d826 | ||
|
64b3e04d3f | ||
|
4ee829ab25 | ||
|
bcc3cec7d6 | ||
|
f8c5015e3f | ||
|
8f3ec33591 | ||
|
c5fe3a64c2 | ||
|
2a1456cfd0 | ||
|
69dfc0c0d2 | ||
|
6d11289257 | ||
|
590859a95b | ||
|
f9c0ff1841 | ||
|
a8566acbaa | ||
|
4b07ec23fe | ||
|
0e50b71290 | ||
|
390b50353f | ||
|
d7cb4fa331 | ||
|
e18d4b6ad0 | ||
|
f6fc3737fc | ||
|
4005856ba6 | ||
|
72a59ce7a4 | ||
|
40b70277c7 | ||
|
a2bc74c4fd | ||
|
a48176bd48 | ||
|
7cfc5c64b7 | ||
|
624cd862a5 | ||
|
0ca68f791f | ||
|
6127eab517 | ||
|
0de7fb69f6 | ||
|
a42932a43e | ||
|
a6072a0e30 | ||
|
475a466c7e | ||
|
5bc68d7f3b | ||
|
000703837b | ||
|
b10cecb362 | ||
|
6d6cb2ad49 | ||
|
cb76801b85 | ||
|
aa92727a61 | ||
|
56dfa05642 | ||
|
8ad6bd31d4 | ||
|
a71569379e | ||
|
8398466860 | ||
|
8050cb8e99 | ||
|
71492aeb3a | ||
|
5ee5ea909d | ||
|
a09b97f778 | ||
|
e0a08e6b5d | ||
|
6f5cbbdf69 | ||
|
34ee342d3e | ||
|
f793aa5264 | ||
|
728485d686 | ||
|
cb3429d3c7 | ||
|
807519d07d | ||
|
0d69b4426e | ||
|
8bb8b0a53c | ||
|
a4841eb8aa | ||
|
2ef2a42e87 | ||
|
9473cd6919 | ||
|
74f18a2b3f | ||
|
f9cd0eb084 | ||
|
6a845bd937 | ||
|
c91f517121 | ||
|
7899707582 | ||
|
12215af2f4 | ||
|
d4bfe57b79 | ||
|
dcc91d6c72 | ||
|
a041a7964a | ||
|
76611ecaca | ||
|
f802154456 | ||
|
9fb461976d | ||
|
c8e364911f | ||
|
88bc08e7b7 | ||
|
03aeab0421 | ||
|
f331f1a63e | ||
|
d645e29455 | ||
|
b4507f9706 | ||
|
fc6d0d1fca | ||
|
b62f1475ee | ||
|
d47d8517a8 | ||
|
19d2db6c8c | ||
|
5a8162747c | ||
|
220108ebc6 | ||
|
984a3704e0 | ||
|
909412c87e | ||
|
481fd3a05f | ||
|
5434e2da4f | ||
|
b3d348dcea | ||
|
0aca0455ab | ||
|
8f3ef734bc | ||
|
120eb0d85f | ||
|
4aaed0837e | ||
|
60657132c0 | ||
|
76cbef85d5 | ||
|
e17ef02008 | ||
|
f33d55c92d | ||
|
67849a9e84 | ||
|
ee79a34148 | ||
|
d2f0480889 | ||
|
c36190bba6 | ||
|
4b3fae53d4 | ||
|
4dd60cba3d | ||
|
4bc84d2122 | ||
|
a796f80018 | ||
|
40cb22e671 | ||
|
d95258e7db | ||
|
baae4b5a5e | ||
|
c1b118a0f6 | ||
|
9c5466890e | ||
|
bf8dbd78b3 | ||
|
6cd130de38 | ||
|
a864b72e03 | ||
|
5070927478 | ||
|
bedc1f8617 | ||
|
077f3837d9 | ||
|
aea128a85b | ||
|
c50b2b636a | ||
|
a284703d9e | ||
|
64ec766423 | ||
|
186c11540f | ||
|
4d947d9374 | ||
|
4888c97d86 | ||
|
50593f3edf | ||
|
c1267e9b3b | ||
|
2ca7a5b962 | ||
|
9f0c66d775 | ||
|
a1f9a82537 | ||
|
37e6ca8d77 | ||
|
0b0fd6609d | ||
|
3a32fd6f42 | ||
|
97cb060cf5 | ||
|
5afb29f8f9 | ||
|
f9b8dbf4db | ||
|
92a5f18bf5 | ||
|
dce908a07b | ||
|
4155f84eec | ||
|
94ffeeeab6 | ||
|
3d222ac5f5 | ||
|
c811c1ccde | ||
|
bd3d34400d | ||
|
5d3bf68123 | ||
|
1f77526210 | ||
|
88ed965d69 | ||
|
7f4d5a0f76 | ||
|
df813fbdee | ||
|
07742799ed | ||
|
f65cc655c0 | ||
|
1a218aaa17 | ||
|
369cad90c1 | ||
|
f9bb48de13 | ||
|
74d2b38cb6 | ||
|
7bba4fe2d0 | ||
|
be3a791e6e | ||
|
9747048890 | ||
|
d5d957b748 | ||
|
5cdb5edeb3 | ||
|
73c18b6ff0 | ||
|
567ea346fe | ||
|
453f6fbadf | ||
|
dd79042128 | ||
|
583e6bf978 | ||
|
b1fca7c1a7 | ||
|
19dd11d624 | ||
|
42ce34b6c7 | ||
|
b7a9d1474f | ||
|
31fa67452e | ||
|
9ef3727c91 | ||
|
ed39485af9 | ||
|
daef238a70 | ||
|
4cc433166e | ||
|
28f530394e | ||
|
b0615d347b | ||
|
be19336149 | ||
|
94508cae2f | ||
|
265cca9ed1 | ||
|
267654c987 | ||
|
2c85491ee0 | ||
|
5d836cf05d | ||
|
ba46fb6b1c | ||
|
5df34cd137 | ||
|
bf64095cea | ||
|
2333d1c7a7 | ||
|
95bae8289d | ||
|
45f7c647a6 | ||
|
dff1056bb1 | ||
|
62222c0336 | ||
|
733d0af75f | ||
|
b88e74fad8 | ||
|
734762b773 | ||
|
0275d7a42b | ||
|
41a6d1b701 | ||
|
34d8984e3a | ||
|
c92153c97e | ||
|
ad82ab0305 | ||
|
f952d283c6 | ||
|
e164fabf81 | ||
|
bc69a331ee | ||
|
e4506963d9 | ||
|
222540898b | ||
|
baf3612ece | ||
|
8f44b9f618 | ||
|
210566c7af | ||
|
0481a241f3 | ||
|
179ca232bc | ||
|
0dcb7aed21 | ||
|
23736549f9 | ||
|
665c263c03 | ||
|
c5e6628803 | ||
|
3a1d8ddc11 | ||
|
bc5f61b3ec | ||
|
314fa18bdc | ||
|
57389fab2c | ||
|
ee2c54cfd1 | ||
|
82cde7c847 | ||
|
1ba2034701 | ||
|
dee131c25d | ||
|
e5d6410caf | ||
|
e496c3b3be | ||
|
69f5112b38 | ||
|
c094dc0c5b | ||
|
1fb9b25d13 | ||
|
9a135deac2 | ||
|
8e6173c05e | ||
|
dec84282ed | ||
|
df80f413b5 | ||
|
17e59f1d8d | ||
|
973c2bb429 | ||
|
da0eaddeb8 | ||
|
b2bc8d9db9 | ||
|
541068ff3b | ||
|
83ee46454a | ||
|
6d1baa329a | ||
|
75b21c905f | ||
|
60e12f4bfa | ||
|
191b81ee07 | ||
|
f3651a1219 | ||
|
12ef9f39c5 | ||
|
4004926e64 | ||
|
4d3d6d6e25 | ||
|
d06e5ef6fa | ||
|
b12b848d97 | ||
|
bb96a577ca | ||
|
8840ca618b | ||
|
8ec858fd14 | ||
|
74688e69aa | ||
|
b32bfb3ff1 | ||
|
24664cde2c | ||
|
348c5ec995 | ||
|
9143b73f84 | ||
|
5e6d945095 | ||
|
036218f711 |
@@ -28,6 +28,8 @@ SECURITY.md
|
|||||||
tsconfig.json
|
tsconfig.json
|
||||||
.env
|
.env
|
||||||
/tmp
|
/tmp
|
||||||
|
/babel.config.js
|
||||||
|
/ecosystem.config.js
|
||||||
|
|
||||||
### .gitignore content (commented rules are duplicated)
|
### .gitignore content (commented rules are duplicated)
|
||||||
|
|
||||||
@@ -42,4 +44,6 @@ dist-ssr
|
|||||||
#!/data/.gitkeep
|
#!/data/.gitkeep
|
||||||
#.vscode
|
#.vscode
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
### End of .gitignore content
|
### End of .gitignore content
|
||||||
|
58
.github/ISSUE_TEMPLATE/ask-for-help.yaml
vendored
58
.github/ISSUE_TEMPLATE/ask-for-help.yaml
vendored
@@ -1,8 +1,23 @@
|
|||||||
name: "❓ Ask for help"
|
name: "❓ Ask for help"
|
||||||
description: "Submit any question related to Uptime Kuma"
|
description: "Submit any question related to Uptime Kuma"
|
||||||
title: "[Help]: <title>"
|
#title: "[Help] "
|
||||||
labels: [help]
|
labels: [help]
|
||||||
body:
|
body:
|
||||||
|
- type: checkboxes
|
||||||
|
id: no-duplicate-issues
|
||||||
|
attributes:
|
||||||
|
label: "⚠️ Please verify that this bug has NOT been raised before."
|
||||||
|
description: "Search in the issues sections by clicking [HERE](https://github.com/louislam/uptime-kuma/issues?q=)"
|
||||||
|
options:
|
||||||
|
- label: "I checked and didn't find similar issue"
|
||||||
|
required: true
|
||||||
|
- type: checkboxes
|
||||||
|
attributes:
|
||||||
|
label: "🛡️ Security Policy"
|
||||||
|
description: Please review the security policy before reporting security related issues/bugs.
|
||||||
|
options:
|
||||||
|
- label: I agree to have read this project [Security Policy](https://github.com/louislam/uptime-kuma/security/policy)
|
||||||
|
required: true
|
||||||
- type: textarea
|
- type: textarea
|
||||||
id: steps-to-reproduce
|
id: steps-to-reproduce
|
||||||
validations:
|
validations:
|
||||||
@@ -14,17 +29,17 @@ body:
|
|||||||
- type: input
|
- type: input
|
||||||
id: uptime-kuma-version
|
id: uptime-kuma-version
|
||||||
attributes:
|
attributes:
|
||||||
label: "🐻 Uptime-Kuma version"
|
label: "🐻 Uptime-Kuma Version"
|
||||||
description: "Which version of Uptime-Kuma are you running?"
|
description: "Which version of Uptime-Kuma are you running? Please do NOT provide the docker tag such as latest or 1"
|
||||||
placeholder: "Ex. 1.9.x"
|
placeholder: "Ex. 1.10.0"
|
||||||
validations:
|
validations:
|
||||||
required: true
|
required: true
|
||||||
- type: input
|
- type: input
|
||||||
id: operating-system
|
id: operating-system
|
||||||
attributes:
|
attributes:
|
||||||
label: "💻 Operating System"
|
label: "💻 Operating System and Arch"
|
||||||
description: "Which OS is your server/device running on?"
|
description: "Which OS is your server/device running on?"
|
||||||
placeholder: "Ex. Ubuntu 20.04"
|
placeholder: "Ex. Ubuntu 20.04 x86"
|
||||||
validations:
|
validations:
|
||||||
required: true
|
required: true
|
||||||
- type: input
|
- type: input
|
||||||
@@ -32,23 +47,15 @@ body:
|
|||||||
attributes:
|
attributes:
|
||||||
label: "🌐 Browser"
|
label: "🌐 Browser"
|
||||||
description: "Which browser are you running on?"
|
description: "Which browser are you running on?"
|
||||||
placeholder: "Ex. Firefox"
|
placeholder: "Ex. Google Chrome 95.0.4638.69"
|
||||||
validations:
|
validations:
|
||||||
required: true
|
required: true
|
||||||
- type: input
|
- type: input
|
||||||
id: docker-version
|
id: docker-version
|
||||||
attributes:
|
attributes:
|
||||||
label: "🐋 Docker"
|
label: "🐋 Docker Version"
|
||||||
description: "If running with Docker, which version are you running?"
|
description: "If running with Docker, which version are you running?"
|
||||||
placeholder: "Ex. 20.10.9"
|
placeholder: "Ex. Docker 20.10.9 / K8S / Podman"
|
||||||
validations:
|
|
||||||
required: false
|
|
||||||
- type: input
|
|
||||||
id: docker-image-tag
|
|
||||||
attributes:
|
|
||||||
label: "🏷️ Docker Image Tag"
|
|
||||||
description: "Which Docker image tag are you using? If running '1' or 'latest', please specify image hash."
|
|
||||||
placeholder: "Ex. 1.9.1"
|
|
||||||
validations:
|
validations:
|
||||||
required: false
|
required: false
|
||||||
- type: input
|
- type: input
|
||||||
@@ -56,21 +63,6 @@ body:
|
|||||||
attributes:
|
attributes:
|
||||||
label: "🟩 NodeJS Version"
|
label: "🟩 NodeJS Version"
|
||||||
description: "If running with Node.js? which version are you running?"
|
description: "If running with Node.js? which version are you running?"
|
||||||
placeholder: "14.x"
|
placeholder: "Ex. 14.18.0"
|
||||||
validations:
|
validations:
|
||||||
required: false
|
required: false
|
||||||
- type: checkboxes
|
|
||||||
id: no-duplicate-issues
|
|
||||||
attributes:
|
|
||||||
label: "⚠️ Please verify that this question has NOT been raised before."
|
|
||||||
description: "Search in the issues sections by clicking [HERE](https://github.com/louislam/uptime-kuma/issues?q=)"
|
|
||||||
options:
|
|
||||||
- label: "I checked and didn't find similar question"
|
|
||||||
required: true
|
|
||||||
- type: checkboxes
|
|
||||||
attributes:
|
|
||||||
label: "🛡️ Security Policy"
|
|
||||||
description: Please review the security policy before reporting security related issues/bugs.
|
|
||||||
options:
|
|
||||||
- label: I agree to have read this project [Security Policy](https://github.com/louislam/uptime-kuma/security/policy)
|
|
||||||
required: true
|
|
||||||
|
161
.github/ISSUE_TEMPLATE/bug_report.yaml
vendored
161
.github/ISSUE_TEMPLATE/bug_report.yaml
vendored
@@ -1,88 +1,8 @@
|
|||||||
name: "🐛 Bug Report"
|
name: "🐛 Bug Report"
|
||||||
description: "Submit a bug report to help us improve"
|
description: "Submit a bug report to help us improve"
|
||||||
title: "[Bug]: <title>"
|
#title: "[Bug] "
|
||||||
labels: [bug]
|
labels: [bug]
|
||||||
body:
|
body:
|
||||||
- type: textarea
|
|
||||||
id: steps-to-reproduce
|
|
||||||
validations:
|
|
||||||
required: true
|
|
||||||
attributes:
|
|
||||||
label: "👟 Reproduction steps"
|
|
||||||
description: "How do you trigger this bug? Please walk us through it step by step."
|
|
||||||
placeholder: "..."
|
|
||||||
- type: textarea
|
|
||||||
id: expected-behavior
|
|
||||||
validations:
|
|
||||||
required: true
|
|
||||||
attributes:
|
|
||||||
label: "👍 Expected behavior"
|
|
||||||
description: "What did you think would happen?"
|
|
||||||
placeholder: "..."
|
|
||||||
- type: textarea
|
|
||||||
id: actual-behavior
|
|
||||||
validations:
|
|
||||||
required: true
|
|
||||||
attributes:
|
|
||||||
label: "👎 Actual Behavior"
|
|
||||||
description: "What actually happen?"
|
|
||||||
placeholder: "..."
|
|
||||||
- type: input
|
|
||||||
id: uptime-kuma-version
|
|
||||||
attributes:
|
|
||||||
label: "🐻 Uptime-Kuma version"
|
|
||||||
description: "Which version of Uptime-Kuma are you running?"
|
|
||||||
placeholder: "Ex. 1.9.x"
|
|
||||||
validations:
|
|
||||||
required: true
|
|
||||||
- type: input
|
|
||||||
id: operating-system
|
|
||||||
attributes:
|
|
||||||
label: "💻 Operating System"
|
|
||||||
description: "Which OS is your server/device running on?"
|
|
||||||
placeholder: "Ex. Ubuntu 20.04"
|
|
||||||
validations:
|
|
||||||
required: true
|
|
||||||
- type: input
|
|
||||||
id: browser-vendor
|
|
||||||
attributes:
|
|
||||||
label: "🌐 Browser"
|
|
||||||
description: "Which browser are you running on?"
|
|
||||||
placeholder: "Ex. Firefox"
|
|
||||||
validations:
|
|
||||||
required: true
|
|
||||||
- type: input
|
|
||||||
id: docker-version
|
|
||||||
attributes:
|
|
||||||
label: "🐋 Docker"
|
|
||||||
description: "If running with Docker, which version are you running?"
|
|
||||||
placeholder: "Ex. 20.10.9"
|
|
||||||
validations:
|
|
||||||
required: false
|
|
||||||
- type: input
|
|
||||||
id: docker-image-tag
|
|
||||||
attributes:
|
|
||||||
label: "🏷️ Docker Image Tag"
|
|
||||||
description: "Which Docker image tag are you using? If running '1' or 'latest', please specify image hash."
|
|
||||||
placeholder: "Ex. 1.9.1"
|
|
||||||
validations:
|
|
||||||
required: false
|
|
||||||
- type: input
|
|
||||||
id: nodejs-version
|
|
||||||
attributes:
|
|
||||||
label: "🟩 NodeJS Version"
|
|
||||||
description: "If running with Node.js? which version are you running?"
|
|
||||||
placeholder: "14.x"
|
|
||||||
validations:
|
|
||||||
required: false
|
|
||||||
- type: textarea
|
|
||||||
id: logs
|
|
||||||
attributes:
|
|
||||||
label: "📝 Relevant log output"
|
|
||||||
description: Please copy and paste any relevant log output. This will be automatically formatted into code, so no need for backticks.
|
|
||||||
render: shell
|
|
||||||
validations:
|
|
||||||
required: false
|
|
||||||
- type: checkboxes
|
- type: checkboxes
|
||||||
id: no-duplicate-issues
|
id: no-duplicate-issues
|
||||||
attributes:
|
attributes:
|
||||||
@@ -98,3 +18,82 @@ body:
|
|||||||
options:
|
options:
|
||||||
- label: I agree to have read this project [Security Policy](https://github.com/louislam/uptime-kuma/security/policy)
|
- label: I agree to have read this project [Security Policy](https://github.com/louislam/uptime-kuma/security/policy)
|
||||||
required: true
|
required: true
|
||||||
|
- type: textarea
|
||||||
|
id: description
|
||||||
|
validations:
|
||||||
|
required: false
|
||||||
|
attributes:
|
||||||
|
label: "Description"
|
||||||
|
description: "You could also upload screenshots"
|
||||||
|
- type: textarea
|
||||||
|
id: steps-to-reproduce
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
attributes:
|
||||||
|
label: "👟 Reproduction steps"
|
||||||
|
description: "How do you trigger this bug? Please walk us through it step by step."
|
||||||
|
placeholder: "..."
|
||||||
|
- type: textarea
|
||||||
|
id: expected-behavior
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
attributes:
|
||||||
|
label: "👀 Expected behavior"
|
||||||
|
description: "What did you think would happen?"
|
||||||
|
placeholder: "..."
|
||||||
|
- type: textarea
|
||||||
|
id: actual-behavior
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
attributes:
|
||||||
|
label: "😓 Actual Behavior"
|
||||||
|
description: "What actually happen?"
|
||||||
|
placeholder: "..."
|
||||||
|
- type: input
|
||||||
|
id: uptime-kuma-version
|
||||||
|
attributes:
|
||||||
|
label: "🐻 Uptime-Kuma Version"
|
||||||
|
description: "Which version of Uptime-Kuma are you running? Please do NOT provide the docker tag such as latest or 1"
|
||||||
|
placeholder: "Ex. 1.10.0"
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
- type: input
|
||||||
|
id: operating-system
|
||||||
|
attributes:
|
||||||
|
label: "💻 Operating System and Arch"
|
||||||
|
description: "Which OS is your server/device running on?"
|
||||||
|
placeholder: "Ex. Ubuntu 20.04 x86"
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
- type: input
|
||||||
|
id: browser-vendor
|
||||||
|
attributes:
|
||||||
|
label: "🌐 Browser"
|
||||||
|
description: "Which browser are you running on?"
|
||||||
|
placeholder: "Ex. Google Chrome 95.0.4638.69"
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
- type: input
|
||||||
|
id: docker-version
|
||||||
|
attributes:
|
||||||
|
label: "🐋 Docker Version"
|
||||||
|
description: "If running with Docker, which version are you running?"
|
||||||
|
placeholder: "Ex. Docker 20.10.9 / K8S / Podman"
|
||||||
|
validations:
|
||||||
|
required: false
|
||||||
|
- type: input
|
||||||
|
id: nodejs-version
|
||||||
|
attributes:
|
||||||
|
label: "🟩 NodeJS Version"
|
||||||
|
description: "If running with Node.js? which version are you running?"
|
||||||
|
placeholder: "Ex. 14.18.0"
|
||||||
|
validations:
|
||||||
|
required: false
|
||||||
|
- type: textarea
|
||||||
|
id: logs
|
||||||
|
attributes:
|
||||||
|
label: "📝 Relevant log output"
|
||||||
|
description: Please copy and paste any relevant log output. This will be automatically formatted into code, so no need for backticks.
|
||||||
|
render: shell
|
||||||
|
validations:
|
||||||
|
required: false
|
||||||
|
20
.github/ISSUE_TEMPLATE/feature_request.yaml
vendored
20
.github/ISSUE_TEMPLATE/feature_request.yaml
vendored
@@ -1,8 +1,16 @@
|
|||||||
name: 🚀 Feature Request
|
name: 🚀 Feature Request
|
||||||
description: "Submit a proposal for a new feature"
|
description: "Submit a proposal for a new feature"
|
||||||
title: "[Feature]: <title>"
|
#title: "[Feature] "
|
||||||
labels: [enhancement]
|
labels: [feature-request]
|
||||||
body:
|
body:
|
||||||
|
- type: checkboxes
|
||||||
|
id: no-duplicate-issues
|
||||||
|
attributes:
|
||||||
|
label: "⚠️ Please verify that this feature request has NOT been suggested before."
|
||||||
|
description: "Search in the issues sections by clicking [HERE](https://github.com/louislam/uptime-kuma/issues?q=)"
|
||||||
|
options:
|
||||||
|
- label: "I checked and didn't find similar feature request"
|
||||||
|
required: true
|
||||||
- type: dropdown
|
- type: dropdown
|
||||||
id: feature-area
|
id: feature-area
|
||||||
attributes:
|
attributes:
|
||||||
@@ -49,11 +57,3 @@ body:
|
|||||||
label: "📝 Additional Context"
|
label: "📝 Additional Context"
|
||||||
description: "Add any other context or screenshots about the feature request here."
|
description: "Add any other context or screenshots about the feature request here."
|
||||||
placeholder: "..."
|
placeholder: "..."
|
||||||
- type: checkboxes
|
|
||||||
id: no-duplicate-issues
|
|
||||||
attributes:
|
|
||||||
label: "⚠️ Please verify that this feature request has NOT been suggested before."
|
|
||||||
description: "Search in the issues sections by clicking [HERE](https://github.com/louislam/uptime-kuma/issues?q=)"
|
|
||||||
options:
|
|
||||||
- label: "I checked and didn't find similar feature request"
|
|
||||||
required: true
|
|
||||||
|
8
.github/PULL_REQUEST_TEMPLATE.md
vendored
8
.github/PULL_REQUEST_TEMPLATE.md
vendored
@@ -4,10 +4,10 @@ Fixes #(issue)
|
|||||||
|
|
||||||
## Type of change
|
## Type of change
|
||||||
|
|
||||||
Please delete options that are not relevant.
|
Please delete any options that are not relevant.
|
||||||
|
|
||||||
- Bug fix (non-breaking change which fixes an issue)
|
- Bug fix (non-breaking change which fixes an issue)
|
||||||
- User Interface
|
- User interface (UI)
|
||||||
- New feature (non-breaking change which adds functionality)
|
- New feature (non-breaking change which adds functionality)
|
||||||
- Breaking change (fix or feature that would cause existing functionality to not work as expected)
|
- Breaking change (fix or feature that would cause existing functionality to not work as expected)
|
||||||
- Translation update
|
- Translation update
|
||||||
@@ -18,9 +18,11 @@ Please delete options that are not relevant.
|
|||||||
|
|
||||||
- [ ] My code follows the style guidelines of this project
|
- [ ] My code follows the style guidelines of this project
|
||||||
- [ ] 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 test 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
|
||||||
- [ ] 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)
|
||||||
|
|
||||||
## Screenshots (if any)
|
## Screenshots (if any)
|
||||||
|
|
||||||
|
Please do not use any external image service. Instead, just paste in or drag and drop the image here, and it will be uploaded automatically.
|
||||||
|
26
.github/workflows/close-incorrect-issue.yml
vendored
Normal file
26
.github/workflows/close-incorrect-issue.yml
vendored
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
|
||||||
|
name: Close Incorrect Issue
|
||||||
|
|
||||||
|
on:
|
||||||
|
issues:
|
||||||
|
types: [opened]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
close-incorrect-issue:
|
||||||
|
runs-on: ${{ matrix.os }}
|
||||||
|
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
os: [ubuntu-latest]
|
||||||
|
node-version: [16.x]
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v2
|
||||||
|
|
||||||
|
- name: Use Node.js ${{ matrix.node-version }}
|
||||||
|
uses: actions/setup-node@v2
|
||||||
|
with:
|
||||||
|
node-version: ${{ matrix.node-version }}
|
||||||
|
cache: 'npm'
|
||||||
|
- run: npm ci
|
||||||
|
- run: node extra/close-incorrect-issue.js ${{ secrets.GITHUB_TOKEN }} ${{ github.event.issue.number }} ${{ github.event.issue.user.login }}
|
22
.github/workflows/stale-bot.yml
vendored
22
.github/workflows/stale-bot.yml
vendored
@@ -1,22 +0,0 @@
|
|||||||
name: 'Automatically close stale issues and PRs'
|
|
||||||
on:
|
|
||||||
schedule:
|
|
||||||
- cron: '0 0 * * *'
|
|
||||||
#Run once a day at midnight
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
stale:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- uses: actions/stale@v4
|
|
||||||
with:
|
|
||||||
stale-issue-message: 'We are clearing up our old issues and your ticket has been open for 6 months with no activity. Remove stale label or comment or this will be closed in 7 days.'
|
|
||||||
stale-pr-message: 'We are clearing up our old Pull Requests and yours has been open for 6 months with no activity. Remove stale label or comment or this will be closed in 7 days.'
|
|
||||||
close-issue-message: 'This issue was closed because it has been stalled for 7 days with no activity.'
|
|
||||||
close-pr-message: 'This PR was closed because it has been stalled for 7 days with no activity.'
|
|
||||||
days-before-stale: 180
|
|
||||||
days-before-close: 7
|
|
||||||
exempt-issue-labels: 'News,Medium,High,discussion,bug,doc,'
|
|
||||||
exempt-pr-labels: 'awaiting-approval,work-in-progress,enhancement,'
|
|
||||||
exempt-issue-assignees: 'louislam'
|
|
||||||
exempt-pr-assignees: 'louislam'
|
|
106
CONTRIBUTING.md
106
CONTRIBUTING.md
@@ -1,8 +1,8 @@
|
|||||||
# Project Info
|
# Project Info
|
||||||
|
|
||||||
First of all, thank you everyone who made pull requests for Uptime Kuma, I never thought GitHub Community can be that nice! And also because of this, I also never thought other people actually read my code and edit my code. It is not structed and commented so well, lol. Sorry about that.
|
First of all, thank you everyone who made pull requests for Uptime Kuma, I never thought GitHub Community can be that nice! And also because of this, I also never thought other people actually read my code and edit my code. It is not structured and commented so well, lol. Sorry about that.
|
||||||
|
|
||||||
The project was created with vite.js (vue3). Then I created a sub-directory called "server" for server part. Both frontend and backend share the same package.json.
|
The project was created with vite.js (vue3). Then I created a subdirectory called "server" for server part. Both frontend and backend share the same package.json.
|
||||||
|
|
||||||
The frontend code build into "dist" directory. The server (express.js) exposes the "dist" directory as root of the endpoint. This is how production is working.
|
The frontend code build into "dist" directory. The server (express.js) exposes the "dist" directory as root of the endpoint. This is how production is working.
|
||||||
|
|
||||||
@@ -27,10 +27,20 @@ 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?
|
||||||
|
|
||||||
Generally, if the pull request is working fine and it do not affect any existing logic, workflow and perfomance, I will merge into the master branch once it is tested.
|
⚠️ 2022-03-02 Update:
|
||||||
|
|
||||||
If you are not sure whether I will accept your pull request, feel free to create an empty pull request draft first.
|
Since I found that merging pull requests is a pretty heavy task for me, I try to rearrange it.
|
||||||
|
|
||||||
|
✅ Accept:
|
||||||
|
- Bug/Security fix
|
||||||
|
- Translations
|
||||||
|
- Adding notification providers
|
||||||
|
|
||||||
|
❌ Avoid:
|
||||||
|
- Large pull requests
|
||||||
|
- New big features
|
||||||
|
|
||||||
|
My long story here: https://www.reddit.com/r/UptimeKuma/comments/t1t6or/comment/hynyijx/
|
||||||
|
|
||||||
### Recommended Pull Request Guideline
|
### Recommended Pull Request Guideline
|
||||||
|
|
||||||
@@ -41,46 +51,9 @@ If you are not sure whether I will accept your pull request, feel free to create
|
|||||||
`git commit -m "[empty commit] pull request for <YOUR TASK NAME>" --allow-empty`
|
`git commit -m "[empty commit] pull request for <YOUR TASK NAME>" --allow-empty`
|
||||||
1. Push to your fork repo
|
1. Push to your fork repo
|
||||||
1. Create a pull request: https://github.com/louislam/uptime-kuma/compare
|
1. Create a pull request: https://github.com/louislam/uptime-kuma/compare
|
||||||
1. Write a proper description
|
1. Write a proper description
|
||||||
1. Click "Change to draft"
|
1. Click "Change to draft"
|
||||||
|
|
||||||
### Pull Request Examples
|
|
||||||
|
|
||||||
Here are some example situations in the past.
|
|
||||||
|
|
||||||
#### ✅ High - Medium Priority
|
|
||||||
|
|
||||||
Easy to review, no breaking change and not touching the existing code
|
|
||||||
|
|
||||||
- Add a new notification
|
|
||||||
- Add a chart
|
|
||||||
- Fix a bug
|
|
||||||
- Translations
|
|
||||||
- Add a independent new feature
|
|
||||||
|
|
||||||
#### *️⃣ Requires one more reviewer
|
|
||||||
|
|
||||||
I do not have such knowledge to test it.
|
|
||||||
|
|
||||||
- Add k8s supports
|
|
||||||
|
|
||||||
#### ⚠ Low Priority - Harsh Mode
|
|
||||||
|
|
||||||
Some pull requests are required to modifiy the core. To be honest, I do not want anyone to try to do that, because it would spend a lot of your time. I will review your pull request harshly. Also you may need to write a lot of unit tests to ensure that there is no breaking change.
|
|
||||||
|
|
||||||
- Touch large parts of code of any very important features
|
|
||||||
- Touch monitoring logic
|
|
||||||
- Drop a table or drop a column for any reason
|
|
||||||
- Touch the entry point of Docker or Node.js
|
|
||||||
- Modifiy auth
|
|
||||||
|
|
||||||
|
|
||||||
#### *️⃣ Low Priority
|
|
||||||
|
|
||||||
It changed my current workflow and require further studies.
|
|
||||||
|
|
||||||
- Change my release approach
|
|
||||||
|
|
||||||
#### ❌ Won't Merge
|
#### ❌ Won't Merge
|
||||||
|
|
||||||
- Any breaking changes
|
- Any breaking changes
|
||||||
@@ -114,7 +87,7 @@ I personally do not like something need to learn so much and need to config so m
|
|||||||
|
|
||||||
- Node.js >= 14
|
- Node.js >= 14
|
||||||
- Git
|
- Git
|
||||||
- IDE that supports ESLint and EditorConfig (I am using Intellji Idea)
|
- IDE that supports ESLint and EditorConfig (I am using IntelliJ IDEA)
|
||||||
- A SQLite tool (SQLite Expert Personal is suggested)
|
- A SQLite tool (SQLite Expert Personal is suggested)
|
||||||
|
|
||||||
## Install dependencies
|
## Install dependencies
|
||||||
@@ -141,9 +114,9 @@ express.js is just used for serving the frontend built files (index.html, .js an
|
|||||||
|
|
||||||
- model/ (Object model, auto mapping to the database table name)
|
- model/ (Object model, auto mapping to the database table name)
|
||||||
- modules/ (Modified 3rd-party modules)
|
- modules/ (Modified 3rd-party modules)
|
||||||
- notification-providers/ (indivdual notification logic)
|
- notification-providers/ (individual notification logic)
|
||||||
- routers/ (Express Routers)
|
- routers/ (Express Routers)
|
||||||
- scoket-handler (Socket.io Handlers)
|
- socket-handler (Socket.io Handlers)
|
||||||
- server.js (Server main logic)
|
- server.js (Server main logic)
|
||||||
|
|
||||||
## How to start the Frontend Dev Server
|
## How to start the Frontend Dev Server
|
||||||
@@ -201,7 +174,7 @@ ncu -u -t patch
|
|||||||
npm install
|
npm install
|
||||||
```
|
```
|
||||||
|
|
||||||
Since previously updating vite 2.5.10 to 2.6.0 broke the application completely, from now on, it should update patch release version only.
|
Since previously updating Vite 2.5.10 to 2.6.0 broke the application completely, from now on, it should update patch release version only.
|
||||||
|
|
||||||
Patch release = the third digit ([Semantic Versioning](https://semver.org/))
|
Patch release = the third digit ([Semantic Versioning](https://semver.org/))
|
||||||
|
|
||||||
@@ -209,47 +182,56 @@ Patch release = the third digit ([Semantic Versioning](https://semver.org/))
|
|||||||
|
|
||||||
Please read: https://github.com/louislam/uptime-kuma/tree/master/src/languages
|
Please read: https://github.com/louislam/uptime-kuma/tree/master/src/languages
|
||||||
|
|
||||||
|
|
||||||
## Wiki
|
## Wiki
|
||||||
|
|
||||||
Since there is no way to make a pull request to wiki's repo, I have setup another repo to do that.
|
Since there is no way to make a pull request to wiki's repo, I have set up another repo to do that.
|
||||||
|
|
||||||
https://github.com/louislam/uptime-kuma-wiki
|
https://github.com/louislam/uptime-kuma-wiki
|
||||||
|
|
||||||
|
## Maintainer
|
||||||
## Maintainer
|
|
||||||
|
|
||||||
Check the latest issues and pull requests:
|
Check the latest issues and pull requests:
|
||||||
https://github.com/louislam/uptime-kuma/issues?q=sort%3Aupdated-desc
|
https://github.com/louislam/uptime-kuma/issues?q=sort%3Aupdated-desc
|
||||||
|
|
||||||
### Release Procedures
|
### Release Procedures
|
||||||
|
|
||||||
1. Draft a release note
|
1. Draft a release note
|
||||||
1. Make sure the repo is cleared
|
2. Make sure the repo is cleared
|
||||||
1. `npm run update-version 1.X.X`
|
3. `npm run release-final with env vars: `VERSION` and `GITHUB_TOKEN`
|
||||||
1. `npm run build`
|
4. Wait until the `Press any key to continue`
|
||||||
1. `npm run build-docker`
|
5. `git push`
|
||||||
1. `git push`
|
6. Publish the release note as 1.X.X
|
||||||
1. Publish the release note as 1.X.X
|
7. Press any key to continue
|
||||||
1. `npm run upload-artifacts`
|
8. SSH to demo site server and update to 1.X.X
|
||||||
1. SSH to demo site server and update to 1.X.X
|
|
||||||
|
|
||||||
Checking:
|
Checking:
|
||||||
|
|
||||||
- Check all tags is fine on https://hub.docker.com/r/louislam/uptime-kuma/tags
|
- Check all tags is fine on https://hub.docker.com/r/louislam/uptime-kuma/tags
|
||||||
- Try the Docker image with tag 1.X.X (Clean install / amd64 / arm64 / armv7)
|
- Try the Docker image with tag 1.X.X (Clean install / amd64 / arm64 / armv7)
|
||||||
- Try clean install with Node.js
|
- Try clean installation with Node.js
|
||||||
|
|
||||||
|
### Release Beta Procedures
|
||||||
|
|
||||||
|
1. Draft a release note, check "This is a pre-release"
|
||||||
|
2. Make sure the repo is cleared
|
||||||
|
3. `npm run release-beta` with env vars: `VERSION` and `GITHUB_TOKEN`
|
||||||
|
4. Wait until the `Press any key to continue`
|
||||||
|
5. Publish the release note as 1.X.X-beta.X
|
||||||
|
6. Press any key to continue
|
||||||
|
|
||||||
### Release Wiki
|
### Release Wiki
|
||||||
|
|
||||||
#### Setup Repo
|
#### Setup Repo
|
||||||
```
|
|
||||||
|
```bash
|
||||||
git clone https://github.com/louislam/uptime-kuma-wiki.git
|
git clone https://github.com/louislam/uptime-kuma-wiki.git
|
||||||
cd uptime-kuma-wiki
|
cd uptime-kuma-wiki
|
||||||
git remote add production https://github.com/louislam/uptime-kuma.wiki.git
|
git remote add production https://github.com/louislam/uptime-kuma.wiki.git
|
||||||
```
|
```
|
||||||
|
|
||||||
#### Push to Production Wiki
|
#### Push to Production Wiki
|
||||||
```
|
|
||||||
|
```bash
|
||||||
git pull
|
git pull
|
||||||
git push production master
|
git push production master
|
||||||
```
|
```
|
||||||
|
|
||||||
|
48
README.md
48
README.md
@@ -17,13 +17,13 @@ Try it!
|
|||||||
|
|
||||||
https://demo.uptime.kuma.pet
|
https://demo.uptime.kuma.pet
|
||||||
|
|
||||||
It is a temporary live demo, all data will be deleted after 10 minutes. The server is located at Tokyo, so if you live far from there it may affect your experience. I suggest that you should install and try it out for the best demo experience.
|
It is a temporary live demo, all data will be deleted after 10 minutes. The server is located in Tokyo, so if you live far from there, it may affect your experience. I suggest that you should install and try it out for the best demo experience.
|
||||||
|
|
||||||
VPS is sponsored by Uptime Kuma sponsors on [Open Collective](https://opencollective.com/uptime-kuma)! Thank you so much!
|
VPS is sponsored by Uptime Kuma sponsors on [Open Collective](https://opencollective.com/uptime-kuma)! Thank you so much!
|
||||||
|
|
||||||
## ⭐ Features
|
## ⭐ Features
|
||||||
|
|
||||||
* Monitoring uptime for HTTP(s) / TCP / Ping / DNS Record / Push.
|
* Monitoring uptime for HTTP(s) / TCP / HTTP(s) Keyword / Ping / DNS Record / Push / Steam Game Server.
|
||||||
* Fancy, Reactive, Fast UI/UX.
|
* Fancy, Reactive, Fast UI/UX.
|
||||||
* Notifications via Telegram, Discord, Gotify, Slack, Pushover, Email (SMTP), and [70+ notification services, click here for the full list](https://github.com/louislam/uptime-kuma/tree/master/src/components/notifications).
|
* Notifications via Telegram, Discord, Gotify, Slack, Pushover, Email (SMTP), and [70+ notification services, click here for the full list](https://github.com/louislam/uptime-kuma/tree/master/src/components/notifications).
|
||||||
* 20 second intervals.
|
* 20 second intervals.
|
||||||
@@ -37,15 +37,19 @@ VPS is sponsored by Uptime Kuma sponsors on [Open Collective](https://opencollec
|
|||||||
### 🐳 Docker
|
### 🐳 Docker
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
docker volume create uptime-kuma
|
|
||||||
docker run -d --restart=always -p 3001:3001 -v uptime-kuma:/app/data --name uptime-kuma louislam/uptime-kuma:1
|
docker run -d --restart=always -p 3001:3001 -v uptime-kuma:/app/data --name uptime-kuma louislam/uptime-kuma:1
|
||||||
```
|
```
|
||||||
|
|
||||||
|
⚠️ Please use a **local volume** only. Other types such as NFS are not supported.
|
||||||
|
|
||||||
Browse to http://localhost:3001 after starting.
|
Browse to http://localhost:3001 after starting.
|
||||||
|
|
||||||
### 💪🏻 Non-Docker
|
### 💪🏻 Non-Docker
|
||||||
|
|
||||||
Required Tools: Node.js >= 14, git and pm2.
|
Required Tools:
|
||||||
|
- [Node.js](https://nodejs.org/en/download/) >= 14
|
||||||
|
- [Git](https://git-scm.com/downloads)
|
||||||
|
- [pm2](https://pm2.keymetrics.io/) - For run in background
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Update your npm to the latest version
|
# Update your npm to the latest version
|
||||||
@@ -59,15 +63,29 @@ npm run setup
|
|||||||
node server/server.js
|
node server/server.js
|
||||||
|
|
||||||
# (Recommended) Option 2. Run in background using PM2
|
# (Recommended) Option 2. Run in background using PM2
|
||||||
# Install PM2 if you don't have it: npm install pm2 -g
|
# Install PM2 if you don't have it:
|
||||||
pm2 start server/server.js --name uptime-kuma
|
npm install pm2 -g && pm2 install pm2-logrotate
|
||||||
```
|
|
||||||
|
|
||||||
|
# Start Server
|
||||||
|
pm2 start server/server.js --name uptime-kuma
|
||||||
|
|
||||||
|
|
||||||
|
```
|
||||||
Browse to http://localhost:3001 after starting.
|
Browse to http://localhost:3001 after starting.
|
||||||
|
|
||||||
|
More useful PM2 Commands
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# If you want to see the current console output
|
||||||
|
pm2 monit
|
||||||
|
|
||||||
|
# If you want to add it to startup
|
||||||
|
pm2 save && pm2 startup
|
||||||
|
```
|
||||||
|
|
||||||
### Advanced Installation
|
### Advanced Installation
|
||||||
|
|
||||||
If you need more options or need to browse via a reserve proxy, please read:
|
If you need more options or need to browse via a reverse proxy, please read:
|
||||||
|
|
||||||
https://github.com/louislam/uptime-kuma/wiki/%F0%9F%94%A7-How-to-Install
|
https://github.com/louislam/uptime-kuma/wiki/%F0%9F%94%A7-How-to-Install
|
||||||
|
|
||||||
@@ -87,6 +105,12 @@ Project Plan:
|
|||||||
|
|
||||||
https://github.com/louislam/uptime-kuma/projects/1
|
https://github.com/louislam/uptime-kuma/projects/1
|
||||||
|
|
||||||
|
## ❤️ Sponsors
|
||||||
|
|
||||||
|
Thank you so much! (GitHub Sponsors will be updated manually. OpenCollective sponsors will be updated automatically, the list will be cached by GitHub though. It may need some time to be updated)
|
||||||
|
|
||||||
|
<img src="https://uptime.kuma.pet/sponsors?v=6" alt />
|
||||||
|
|
||||||
## 🖼 More Screenshots
|
## 🖼 More Screenshots
|
||||||
|
|
||||||
Light Mode:
|
Light Mode:
|
||||||
@@ -107,7 +131,7 @@ Telegram Notification Sample:
|
|||||||
|
|
||||||
## Motivation
|
## Motivation
|
||||||
|
|
||||||
* I was looking for a self-hosted monitoring tool like "Uptime Robot", but it is hard to find a suitable one. One of the close ones is statping. Unfortunately, it is not stable and unmaintained.
|
* I was looking for a self-hosted monitoring tool like "Uptime Robot", but it is hard to find a suitable one. One of the close ones is statping. Unfortunately, it is not stable and no longer maintained.
|
||||||
* Want to build a fancy UI.
|
* Want to build a fancy UI.
|
||||||
* Learn Vue 3 and vite.js.
|
* Learn Vue 3 and vite.js.
|
||||||
* Show the power of Bootstrap 5.
|
* Show the power of Bootstrap 5.
|
||||||
@@ -120,7 +144,7 @@ If you love this project, please consider giving me a ⭐.
|
|||||||
|
|
||||||
### Issues Page
|
### Issues Page
|
||||||
|
|
||||||
You can discuss or ask for help in [Issues](https://github.com/louislam/uptime-kuma/issues).
|
You can discuss or ask for help in [issues](https://github.com/louislam/uptime-kuma/issues).
|
||||||
|
|
||||||
### Subreddit
|
### Subreddit
|
||||||
|
|
||||||
@@ -132,8 +156,8 @@ https://www.reddit.com/r/UptimeKuma/
|
|||||||
|
|
||||||
If you want to report a bug or request a new feature. Free feel to open a [new issue](https://github.com/louislam/uptime-kuma/issues).
|
If you want to report a bug or request a new feature. Free feel to open a [new issue](https://github.com/louislam/uptime-kuma/issues).
|
||||||
|
|
||||||
If you want to translate Uptime Kuma into your langauge, please read: https://github.com/louislam/uptime-kuma/tree/master/src/languages
|
If you want to translate Uptime Kuma into your language, please read: https://github.com/louislam/uptime-kuma/tree/master/src/languages
|
||||||
|
|
||||||
If you want to modify Uptime Kuma, this guideline may be useful for you: https://github.com/louislam/uptime-kuma/blob/master/CONTRIBUTING.md
|
If you want to modify Uptime Kuma, this guideline may be useful for you: https://github.com/louislam/uptime-kuma/blob/master/CONTRIBUTING.md
|
||||||
|
|
||||||
English proofreading is needed too because my grammar is not that great sadly. Feel free to correct my grammar in this readme, source code, or wiki.
|
Unfortunately, English proofreading is needed too because my grammar is not that great. Feel free to correct my grammar in this README, source code, or wiki.
|
||||||
|
12
SECURITY.md
12
SECURITY.md
@@ -1,5 +1,11 @@
|
|||||||
# Security Policy
|
# Security Policy
|
||||||
|
|
||||||
|
## Reporting a Vulnerability
|
||||||
|
|
||||||
|
Please report security issues to uptime@kuma.pet.
|
||||||
|
|
||||||
|
Do not use the issue tracker or discuss it in the public as it will cause more damage.
|
||||||
|
|
||||||
## Supported Versions
|
## Supported Versions
|
||||||
|
|
||||||
Use this section to tell people about which versions of your project are
|
Use this section to tell people about which versions of your project are
|
||||||
@@ -23,9 +29,3 @@ currently being supported with security updates.
|
|||||||
| debian | :white_check_mark: |
|
| debian | :white_check_mark: |
|
||||||
| alpine | :white_check_mark: |
|
| alpine | :white_check_mark: |
|
||||||
| All other tags | ❌ |
|
| All other tags | ❌ |
|
||||||
|
|
||||||
## Reporting a Vulnerability
|
|
||||||
|
|
||||||
Please report security issues to uptime@kuma.pet.
|
|
||||||
|
|
||||||
Do not use the issue tracker or discuss it in the public as it will cause more damage.
|
|
||||||
|
33
config/jest-debug-env.js
Normal file
33
config/jest-debug-env.js
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
const PuppeteerEnvironment = require("jest-environment-puppeteer");
|
||||||
|
const util = require("util");
|
||||||
|
|
||||||
|
class DebugEnv extends PuppeteerEnvironment {
|
||||||
|
async handleTestEvent(event, state) {
|
||||||
|
const ignoredEvents = [
|
||||||
|
"setup",
|
||||||
|
"add_hook",
|
||||||
|
"start_describe_definition",
|
||||||
|
"add_test",
|
||||||
|
"finish_describe_definition",
|
||||||
|
"run_start",
|
||||||
|
"run_describe_start",
|
||||||
|
"test_start",
|
||||||
|
"hook_start",
|
||||||
|
"hook_success",
|
||||||
|
"test_fn_start",
|
||||||
|
"test_fn_success",
|
||||||
|
"test_done",
|
||||||
|
"run_describe_finish",
|
||||||
|
"run_finish",
|
||||||
|
"teardown",
|
||||||
|
"test_fn_failure",
|
||||||
|
];
|
||||||
|
if (!ignoredEvents.includes(event.name)) {
|
||||||
|
console.log(
|
||||||
|
new Date().toString() + ` Unhandled event [${event.name}] ` + util.inspect(event)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = DebugEnv;
|
@@ -1,6 +1,20 @@
|
|||||||
module.exports = {
|
module.exports = {
|
||||||
"launch": {
|
"launch": {
|
||||||
|
"dumpio": true,
|
||||||
|
"slowMo": 500,
|
||||||
"headless": process.env.HEADLESS_TEST || false,
|
"headless": process.env.HEADLESS_TEST || false,
|
||||||
"userDataDir": "./data/test-chrome-profile",
|
"userDataDir": "./data/test-chrome-profile",
|
||||||
|
args: [
|
||||||
|
"--disable-setuid-sandbox",
|
||||||
|
"--disable-gpu",
|
||||||
|
"--disable-dev-shm-usage",
|
||||||
|
"--no-default-browser-check",
|
||||||
|
"--no-experiments",
|
||||||
|
"--no-first-run",
|
||||||
|
"--no-pings",
|
||||||
|
"--no-sandbox",
|
||||||
|
"--no-zygote",
|
||||||
|
"--single-process",
|
||||||
|
],
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@@ -5,6 +5,7 @@ module.exports = {
|
|||||||
"__DEV__": true
|
"__DEV__": true
|
||||||
},
|
},
|
||||||
"testRegex": "./test/e2e.spec.js",
|
"testRegex": "./test/e2e.spec.js",
|
||||||
|
"testEnvironment": "./config/jest-debug-env.js",
|
||||||
"rootDir": "..",
|
"rootDir": "..",
|
||||||
"testTimeout": 30000,
|
"testTimeout": 30000,
|
||||||
};
|
};
|
||||||
|
10
db/patch-monitor-basic-auth.sql
Normal file
10
db/patch-monitor-basic-auth.sql
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
-- You should not modify if this have pushed to Github, unless it does serious wrong with the db.
|
||||||
|
BEGIN TRANSACTION;
|
||||||
|
|
||||||
|
ALTER TABLE monitor
|
||||||
|
ADD basic_auth_user TEXT default null;
|
||||||
|
|
||||||
|
ALTER TABLE monitor
|
||||||
|
ADD basic_auth_pass TEXT default null;
|
||||||
|
|
||||||
|
COMMIT;
|
7
db/patch-monitor-expiry-notification.sql
Normal file
7
db/patch-monitor-expiry-notification.sql
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
-- You should not modify if this have pushed to Github, unless it does serious wrong with the db.
|
||||||
|
BEGIN TRANSACTION;
|
||||||
|
|
||||||
|
ALTER TABLE monitor
|
||||||
|
ADD expiry_notification BOOLEAN default 1;
|
||||||
|
|
||||||
|
COMMIT;
|
23
db/patch-proxy.sql
Normal file
23
db/patch-proxy.sql
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
-- You should not modify if this have pushed to Github, unless it does serious wrong with the db.
|
||||||
|
BEGIN TRANSACTION;
|
||||||
|
|
||||||
|
CREATE TABLE proxy (
|
||||||
|
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
|
||||||
|
user_id INT NOT NULL,
|
||||||
|
protocol VARCHAR(10) NOT NULL,
|
||||||
|
host VARCHAR(255) NOT NULL,
|
||||||
|
port SMALLINT NOT NULL,
|
||||||
|
auth BOOLEAN NOT NULL,
|
||||||
|
username VARCHAR(255) NULL,
|
||||||
|
password VARCHAR(255) NULL,
|
||||||
|
active BOOLEAN NOT NULL DEFAULT 1,
|
||||||
|
'default' BOOLEAN NOT NULL DEFAULT 0,
|
||||||
|
created_date DATETIME DEFAULT (DATETIME('now')) NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
ALTER TABLE monitor ADD COLUMN proxy_id INTEGER REFERENCES proxy(id);
|
||||||
|
|
||||||
|
CREATE INDEX proxy_id ON monitor (proxy_id);
|
||||||
|
CREATE INDEX proxy_user_id ON proxy (user_id);
|
||||||
|
|
||||||
|
COMMIT;
|
31
db/patch-status-page.sql
Normal file
31
db/patch-status-page.sql
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
-- You should not modify if this have pushed to Github, unless it does serious wrong with the db.
|
||||||
|
BEGIN TRANSACTION;
|
||||||
|
|
||||||
|
CREATE TABLE [status_page](
|
||||||
|
[id] INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
|
||||||
|
[slug] VARCHAR(255) NOT NULL UNIQUE,
|
||||||
|
[title] VARCHAR(255) NOT NULL,
|
||||||
|
[description] TEXT,
|
||||||
|
[icon] VARCHAR(255) NOT NULL,
|
||||||
|
[theme] VARCHAR(30) NOT NULL,
|
||||||
|
[published] BOOLEAN NOT NULL DEFAULT 1,
|
||||||
|
[search_engine_index] BOOLEAN NOT NULL DEFAULT 1,
|
||||||
|
[show_tags] BOOLEAN NOT NULL DEFAULT 0,
|
||||||
|
[password] VARCHAR,
|
||||||
|
[created_date] DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
[modified_date] DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE UNIQUE INDEX [slug] ON [status_page]([slug]);
|
||||||
|
|
||||||
|
|
||||||
|
CREATE TABLE [status_page_cname](
|
||||||
|
[id] INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
|
||||||
|
[status_page_id] INTEGER NOT NULL REFERENCES [status_page]([id]) ON DELETE CASCADE ON UPDATE CASCADE,
|
||||||
|
[domain] VARCHAR NOT NULL UNIQUE
|
||||||
|
);
|
||||||
|
|
||||||
|
ALTER TABLE incident ADD status_page_id INTEGER;
|
||||||
|
ALTER TABLE [group] ADD status_page_id INTEGER;
|
||||||
|
|
||||||
|
COMMIT;
|
@@ -1,8 +1,8 @@
|
|||||||
# DON'T UPDATE TO alpine3.13, 1.14, see #41.
|
# DON'T UPDATE TO alpine3.13, 1.14, see #41.
|
||||||
FROM node:14-alpine3.12
|
FROM node:16-alpine3.12
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
# Install apprise, iputils for non-root ping, setpriv
|
# Install apprise, iputils for non-root ping, setpriv
|
||||||
RUN apk add --no-cache iputils setpriv dumb-init python3 py3-cryptography py3-pip py3-six py3-yaml py3-click py3-markdown py3-requests py3-requests-oauthlib && \
|
RUN apk add --no-cache iputils setpriv dumb-init python3 py3-cryptography py3-pip py3-six py3-yaml py3-click py3-markdown py3-requests py3-requests-oauthlib && \
|
||||||
pip3 --no-cache-dir install apprise && \
|
pip3 --no-cache-dir install apprise==0.9.7 && \
|
||||||
rm -rf /root/.cache
|
rm -rf /root/.cache
|
||||||
|
@@ -1,12 +1,26 @@
|
|||||||
# DON'T UPDATE TO node:14-bullseye-slim, see #372.
|
# DON'T UPDATE TO node:14-bullseye-slim, see #372.
|
||||||
# If the image changed, the second stage image should be changed too
|
# If the image changed, the second stage image should be changed too
|
||||||
FROM node:14-buster-slim
|
FROM node:16-buster-slim
|
||||||
|
ARG TARGETPLATFORM
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Install Curl
|
||||||
# Install Apprise, add sqlite3 cli for debugging in the future, iputils-ping for ping, util-linux for setpriv
|
# Install Apprise, add sqlite3 cli for debugging in the future, iputils-ping for ping, util-linux for setpriv
|
||||||
# Stupid python3 and python3-pip actually install a lot of useless things into Debian, specific --no-install-recommends to skip them, make the base even smaller than alpine!
|
# Stupid python3 and python3-pip actually install a lot of useless things into Debian, specify --no-install-recommends to skip them, make the base even smaller than alpine!
|
||||||
RUN apt update && \
|
RUN apt update && \
|
||||||
apt --yes --no-install-recommends install python3 python3-pip python3-cryptography python3-six python3-yaml python3-click python3-markdown python3-requests python3-requests-oauthlib \
|
apt --yes --no-install-recommends install python3 python3-pip python3-cryptography python3-six python3-yaml python3-click python3-markdown python3-requests python3-requests-oauthlib \
|
||||||
sqlite3 iputils-ping util-linux dumb-init && \
|
sqlite3 iputils-ping util-linux dumb-init && \
|
||||||
pip3 --no-cache-dir install apprise && \
|
pip3 --no-cache-dir install apprise==0.9.7 && \
|
||||||
rm -rf /var/lib/apt/lists/*
|
rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
# Install cloudflared
|
||||||
|
# dpkg --add-architecture arm: cloudflared do not provide armhf, this is workaround. Read more: https://github.com/cloudflare/cloudflared/issues/583
|
||||||
|
COPY extra/download-cloudflared.js ./extra/download-cloudflared.js
|
||||||
|
RUN node ./extra/download-cloudflared.js $TARGETPLATFORM && \
|
||||||
|
dpkg --add-architecture arm && \
|
||||||
|
apt update && \
|
||||||
|
apt --yes --no-install-recommends install ./cloudflared.deb && \
|
||||||
|
rm -rf /var/lib/apt/lists/* && \
|
||||||
|
rm -f cloudflared.deb
|
||||||
|
|
||||||
|
@@ -5,7 +5,7 @@ version: '3.3'
|
|||||||
|
|
||||||
services:
|
services:
|
||||||
uptime-kuma:
|
uptime-kuma:
|
||||||
image: louislam/uptime-kuma
|
image: louislam/uptime-kuma:1
|
||||||
container_name: uptime-kuma
|
container_name: uptime-kuma
|
||||||
volumes:
|
volumes:
|
||||||
- ./uptime-kuma:/app/data
|
- ./uptime-kuma:/app/data
|
||||||
|
@@ -33,7 +33,7 @@ RUN apt update && \
|
|||||||
|
|
||||||
COPY --from=build /app /app
|
COPY --from=build /app /app
|
||||||
|
|
||||||
ARG VERSION=1.9.1
|
ARG VERSION
|
||||||
ARG GITHUB_TOKEN
|
ARG GITHUB_TOKEN
|
||||||
ARG TARGETARCH
|
ARG TARGETARCH
|
||||||
ARG PLATFORM=debian
|
ARG PLATFORM=debian
|
||||||
|
71
extra/beta/update-version.js
Normal file
71
extra/beta/update-version.js
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
const pkg = require("../../package.json");
|
||||||
|
const fs = require("fs");
|
||||||
|
const child_process = require("child_process");
|
||||||
|
const util = require("../../src/util");
|
||||||
|
|
||||||
|
util.polyfill();
|
||||||
|
|
||||||
|
const oldVersion = pkg.version;
|
||||||
|
const version = process.env.VERSION;
|
||||||
|
|
||||||
|
console.log("Beta Version: " + version);
|
||||||
|
|
||||||
|
if (!version || !version.includes("-beta.")) {
|
||||||
|
console.error("invalid version, beta version only");
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
const exists = tagExists(version);
|
||||||
|
|
||||||
|
if (! exists) {
|
||||||
|
// Process package.json
|
||||||
|
pkg.version = version;
|
||||||
|
fs.writeFileSync("package.json", JSON.stringify(pkg, null, 4) + "\n");
|
||||||
|
commit(version);
|
||||||
|
tag(version);
|
||||||
|
|
||||||
|
} else {
|
||||||
|
console.log("version tag exists, please delete the tag or use another tag");
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
function commit(version) {
|
||||||
|
let msg = "Update to " + version;
|
||||||
|
|
||||||
|
let res = child_process.spawnSync("git", ["commit", "-m", msg, "-a"]);
|
||||||
|
let stdout = res.stdout.toString().trim();
|
||||||
|
console.log(stdout);
|
||||||
|
|
||||||
|
if (stdout.includes("no changes added to commit")) {
|
||||||
|
throw new Error("commit error");
|
||||||
|
}
|
||||||
|
|
||||||
|
res = child_process.spawnSync("git", ["push", "origin", "master"]);
|
||||||
|
console.log(res.stdout.toString().trim());
|
||||||
|
}
|
||||||
|
|
||||||
|
function tag(version) {
|
||||||
|
let res = child_process.spawnSync("git", ["tag", version]);
|
||||||
|
console.log(res.stdout.toString().trim());
|
||||||
|
|
||||||
|
res = child_process.spawnSync("git", ["push", "origin", version]);
|
||||||
|
console.log(res.stdout.toString().trim());
|
||||||
|
}
|
||||||
|
|
||||||
|
function tagExists(version) {
|
||||||
|
if (! version) {
|
||||||
|
throw new Error("invalid version");
|
||||||
|
}
|
||||||
|
|
||||||
|
let res = child_process.spawnSync("git", ["tag", "-l", version]);
|
||||||
|
|
||||||
|
return res.stdout.toString().trim() === version;
|
||||||
|
}
|
||||||
|
|
||||||
|
function safeDelete(dir) {
|
||||||
|
if (fs.existsSync(dir)) {
|
||||||
|
fs.rmdirSync(dir, {
|
||||||
|
recursive: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
57
extra/close-incorrect-issue.js
Normal file
57
extra/close-incorrect-issue.js
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
const github = require("@actions/github");
|
||||||
|
|
||||||
|
(async () => {
|
||||||
|
try {
|
||||||
|
const token = process.argv[2];
|
||||||
|
const issueNumber = process.argv[3];
|
||||||
|
const username = process.argv[4];
|
||||||
|
|
||||||
|
const client = github.getOctokit(token).rest;
|
||||||
|
|
||||||
|
const issue = {
|
||||||
|
owner: "louislam",
|
||||||
|
repo: "uptime-kuma",
|
||||||
|
number: issueNumber,
|
||||||
|
};
|
||||||
|
|
||||||
|
const labels = (
|
||||||
|
await client.issues.listLabelsOnIssue({
|
||||||
|
owner: issue.owner,
|
||||||
|
repo: issue.repo,
|
||||||
|
issue_number: issue.number
|
||||||
|
})
|
||||||
|
).data.map(({ name }) => name);
|
||||||
|
|
||||||
|
if (labels.length === 0) {
|
||||||
|
console.log("Bad format here");
|
||||||
|
|
||||||
|
await client.issues.addLabels({
|
||||||
|
owner: issue.owner,
|
||||||
|
repo: issue.repo,
|
||||||
|
issue_number: issue.number,
|
||||||
|
labels: ["invalid-format"]
|
||||||
|
});
|
||||||
|
|
||||||
|
// Add the issue closing comment
|
||||||
|
await client.issues.createComment({
|
||||||
|
owner: issue.owner,
|
||||||
|
repo: issue.repo,
|
||||||
|
issue_number: issue.number,
|
||||||
|
body: `@${username}: Hello! :wave:\n\nThis issue is being automatically closed because it does not follow the issue template. Please DO NOT open a blank issue.`
|
||||||
|
});
|
||||||
|
|
||||||
|
// Close the issue
|
||||||
|
await client.issues.update({
|
||||||
|
owner: issue.owner,
|
||||||
|
repo: issue.repo,
|
||||||
|
issue_number: issue.number,
|
||||||
|
state: "closed"
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
console.log("Pass!");
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.log(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
})();
|
44
extra/download-cloudflared.js
Normal file
44
extra/download-cloudflared.js
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
//
|
||||||
|
|
||||||
|
const http = require("https"); // or 'https' for https:// URLs
|
||||||
|
const fs = require("fs");
|
||||||
|
|
||||||
|
const platform = process.argv[2];
|
||||||
|
|
||||||
|
if (!platform) {
|
||||||
|
console.error("No platform??");
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
let arch = null;
|
||||||
|
|
||||||
|
if (platform === "linux/amd64") {
|
||||||
|
arch = "amd64";
|
||||||
|
} else if (platform === "linux/arm64") {
|
||||||
|
arch = "arm64";
|
||||||
|
} else if (platform === "linux/arm/v7") {
|
||||||
|
arch = "arm";
|
||||||
|
} else {
|
||||||
|
console.error("Invalid platform?? " + platform);
|
||||||
|
}
|
||||||
|
|
||||||
|
const file = fs.createWriteStream("cloudflared.deb");
|
||||||
|
get("https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-" + arch + ".deb");
|
||||||
|
|
||||||
|
function get(url) {
|
||||||
|
http.get(url, function (res) {
|
||||||
|
if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
|
||||||
|
console.log("Redirect to " + res.headers.location);
|
||||||
|
get(res.headers.location);
|
||||||
|
} else if (res.statusCode >= 200 && res.statusCode < 300) {
|
||||||
|
res.pipe(file);
|
||||||
|
|
||||||
|
res.on("end", function () {
|
||||||
|
console.log("Downloaded");
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
console.error(res.statusCode);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
@@ -4,6 +4,7 @@ const tar = require("tar");
|
|||||||
|
|
||||||
const packageJSON = require("../package.json");
|
const packageJSON = require("../package.json");
|
||||||
const fs = require("fs");
|
const fs = require("fs");
|
||||||
|
const rmSync = require("./fs-rmSync.js");
|
||||||
const version = packageJSON.version;
|
const version = packageJSON.version;
|
||||||
|
|
||||||
const filename = "dist.tar.gz";
|
const filename = "dist.tar.gz";
|
||||||
@@ -21,7 +22,7 @@ function download(url) {
|
|||||||
if (fs.existsSync("./dist")) {
|
if (fs.existsSync("./dist")) {
|
||||||
|
|
||||||
if (fs.existsSync("./dist-backup")) {
|
if (fs.existsSync("./dist-backup")) {
|
||||||
fs.rmdirSync("./dist-backup", {
|
rmSync("./dist-backup", {
|
||||||
recursive: true
|
recursive: true
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -35,7 +36,7 @@ function download(url) {
|
|||||||
|
|
||||||
tarStream.on("close", () => {
|
tarStream.on("close", () => {
|
||||||
if (fs.existsSync("./dist-backup")) {
|
if (fs.existsSync("./dist-backup")) {
|
||||||
fs.rmdirSync("./dist-backup", {
|
rmSync("./dist-backup", {
|
||||||
recursive: true
|
recursive: true
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
19
extra/env2arg.js
Normal file
19
extra/env2arg.js
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
|
||||||
|
const childProcess = require("child_process");
|
||||||
|
let env = process.env;
|
||||||
|
|
||||||
|
let cmd = process.argv[2];
|
||||||
|
let args = process.argv.slice(3);
|
||||||
|
let replacedArgs = [];
|
||||||
|
|
||||||
|
for (let arg of args) {
|
||||||
|
for (let key in env) {
|
||||||
|
arg = arg.replaceAll(`$${key}`, env[key]);
|
||||||
|
}
|
||||||
|
replacedArgs.push(arg);
|
||||||
|
}
|
||||||
|
|
||||||
|
let child = childProcess.spawn(cmd, replacedArgs);
|
||||||
|
child.stdout.pipe(process.stdout);
|
||||||
|
child.stderr.pipe(process.stderr);
|
23
extra/fs-rmSync.js
Normal file
23
extra/fs-rmSync.js
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
const fs = require("fs");
|
||||||
|
/**
|
||||||
|
* Detect if `fs.rmSync` is available
|
||||||
|
* to avoid the runtime deprecation warning triggered for using `fs.rmdirSync` with `{ recursive: true }` in Node.js v16,
|
||||||
|
* or the `recursive` property removing completely in the future Node.js version.
|
||||||
|
* See the link below.
|
||||||
|
*
|
||||||
|
* @todo Once we drop the support for Node.js v14 (or at least versions before v14.14.0), we can safely replace this function with `fs.rmSync`, since `fs.rmSync` was add in Node.js v14.14.0 and currently we supports all the Node.js v14 versions that include the versions before the v14.14.0, and this function have almost the same signature with `fs.rmSync`.
|
||||||
|
* @link https://nodejs.org/docs/latest-v16.x/api/deprecations.html#dep0147-fsrmdirpath--recursive-true- the deprecation infomation of `fs.rmdirSync`
|
||||||
|
* @link https://nodejs.org/docs/latest-v16.x/api/fs.html#fsrmsyncpath-options the document of `fs.rmSync`
|
||||||
|
* @param {fs.PathLike} path Valid types for path values in "fs".
|
||||||
|
* @param {fs.RmDirOptions} [options] options for `fs.rmdirSync`, if `fs.rmSync` is available and property `recursive` is true, it will automatically have property `force` with value `true`.
|
||||||
|
*/
|
||||||
|
const rmSync = (path, options) => {
|
||||||
|
if (typeof fs.rmSync === "function") {
|
||||||
|
if (options.recursive) {
|
||||||
|
options.force = true;
|
||||||
|
}
|
||||||
|
return fs.rmSync(path, options);
|
||||||
|
}
|
||||||
|
return fs.rmdirSync(path, options);
|
||||||
|
};
|
||||||
|
module.exports = rmSync;
|
@@ -1,25 +1,41 @@
|
|||||||
/*
|
/*
|
||||||
* This script should be run after a period of time (180s), because the server may need some time to prepare.
|
* This script should be run after a period of time (180s), because the server may need some time to prepare.
|
||||||
*/
|
*/
|
||||||
|
const { FBSD } = require("../server/util-server");
|
||||||
|
|
||||||
process.env.NODE_TLS_REJECT_UNAUTHORIZED = "0";
|
process.env.NODE_TLS_REJECT_UNAUTHORIZED = "0";
|
||||||
|
|
||||||
let client;
|
let client;
|
||||||
|
|
||||||
if (process.env.SSL_KEY && process.env.SSL_CERT) {
|
const sslKey = process.env.UPTIME_KUMA_SSL_KEY || process.env.SSL_KEY || undefined;
|
||||||
|
const sslCert = process.env.UPTIME_KUMA_SSL_CERT || process.env.SSL_CERT || undefined;
|
||||||
|
|
||||||
|
if (sslKey && sslCert) {
|
||||||
client = require("https");
|
client = require("https");
|
||||||
} else {
|
} else {
|
||||||
client = require("http");
|
client = require("http");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If host is omitted, the server will accept connections on the unspecified IPv6 address (::) when IPv6 is available and the unspecified IPv4 address (0.0.0.0) otherwise.
|
||||||
|
// Dual-stack support for (::)
|
||||||
|
let hostname = process.env.UPTIME_KUMA_HOST;
|
||||||
|
|
||||||
|
// Also read HOST if not *BSD, as HOST is a system environment variable in FreeBSD
|
||||||
|
if (!hostname && !FBSD) {
|
||||||
|
hostname = process.env.HOST;
|
||||||
|
}
|
||||||
|
|
||||||
|
const port = parseInt(process.env.UPTIME_KUMA_PORT || process.env.PORT || 3001);
|
||||||
|
|
||||||
let options = {
|
let options = {
|
||||||
host: process.env.HOST || "127.0.0.1",
|
host: hostname || "127.0.0.1",
|
||||||
port: parseInt(process.env.PORT) || 3001,
|
port: port,
|
||||||
timeout: 28 * 1000,
|
timeout: 28 * 1000,
|
||||||
};
|
};
|
||||||
|
|
||||||
let request = client.request(options, (res) => {
|
let request = client.request(options, (res) => {
|
||||||
console.log(`Health Check OK [Res Code: ${res.statusCode}]`);
|
console.log(`Health Check OK [Res Code: ${res.statusCode}]`);
|
||||||
if (res.statusCode === 200) {
|
if (res.statusCode === 302) {
|
||||||
process.exit(0);
|
process.exit(0);
|
||||||
} else {
|
} else {
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
|
@@ -189,7 +189,7 @@ if (type == "local") {
|
|||||||
bash("check=$(pm2 --version)");
|
bash("check=$(pm2 --version)");
|
||||||
if (check == "") {
|
if (check == "") {
|
||||||
println("Installing PM2");
|
println("Installing PM2");
|
||||||
bash("npm install pm2 -g");
|
bash("npm install pm2 -g && pm2 install pm2-logrotate");
|
||||||
bash("pm2 startup");
|
bash("pm2 startup");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
6
extra/press-any-key.js
Normal file
6
extra/press-any-key.js
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
console.log("Git Push and Publish the release note on github, then press any key to continue");
|
||||||
|
|
||||||
|
process.stdin.setRawMode(true);
|
||||||
|
process.stdin.resume();
|
||||||
|
process.stdin.on("data", process.exit.bind(process, 0));
|
||||||
|
|
60
extra/remove-2fa.js
Normal file
60
extra/remove-2fa.js
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
console.log("== Uptime Kuma Remove 2FA Tool ==");
|
||||||
|
console.log("Loading the database");
|
||||||
|
|
||||||
|
const Database = require("../server/database");
|
||||||
|
const { R } = require("redbean-node");
|
||||||
|
const readline = require("readline");
|
||||||
|
const TwoFA = require("../server/2fa");
|
||||||
|
const args = require("args-parser")(process.argv);
|
||||||
|
const rl = readline.createInterface({
|
||||||
|
input: process.stdin,
|
||||||
|
output: process.stdout
|
||||||
|
});
|
||||||
|
|
||||||
|
const main = async () => {
|
||||||
|
Database.init(args);
|
||||||
|
await Database.connect();
|
||||||
|
|
||||||
|
try {
|
||||||
|
// No need to actually reset the password for testing, just make sure no connection problem. It is ok for now.
|
||||||
|
if (!process.env.TEST_BACKEND) {
|
||||||
|
const user = await R.findOne("user");
|
||||||
|
if (! user) {
|
||||||
|
throw new Error("user not found, have you installed?");
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("Found user: " + user.username);
|
||||||
|
|
||||||
|
let ans = await question("Are you sure want to remove 2FA? [y/N]");
|
||||||
|
|
||||||
|
if (ans.toLowerCase() === "y") {
|
||||||
|
await TwoFA.disable2FA(user.id);
|
||||||
|
console.log("2FA has been removed successfully.");
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error("Error: " + e.message);
|
||||||
|
}
|
||||||
|
|
||||||
|
await Database.close();
|
||||||
|
rl.close();
|
||||||
|
|
||||||
|
console.log("Finished.");
|
||||||
|
};
|
||||||
|
|
||||||
|
function question(question) {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
rl.question(question, (answer) => {
|
||||||
|
resolve(answer);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!process.env.TEST_BACKEND) {
|
||||||
|
main();
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
main,
|
||||||
|
};
|
@@ -1,7 +1,5 @@
|
|||||||
console.log("== Uptime Kuma Reset Password Tool ==");
|
console.log("== Uptime Kuma Reset Password Tool ==");
|
||||||
|
|
||||||
console.log("Loading the database");
|
|
||||||
|
|
||||||
const Database = require("../server/database");
|
const Database = require("../server/database");
|
||||||
const { R } = require("redbean-node");
|
const { R } = require("redbean-node");
|
||||||
const readline = require("readline");
|
const readline = require("readline");
|
||||||
@@ -13,8 +11,9 @@ const rl = readline.createInterface({
|
|||||||
});
|
});
|
||||||
|
|
||||||
const main = async () => {
|
const main = async () => {
|
||||||
|
console.log("Connecting the database");
|
||||||
Database.init(args);
|
Database.init(args);
|
||||||
await Database.connect();
|
await Database.connect(false, false, true);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// No need to actually reset the password for testing, just make sure no connection problem. It is ok for now.
|
// No need to actually reset the password for testing, just make sure no connection problem. It is ok for now.
|
||||||
|
@@ -3,6 +3,7 @@
|
|||||||
import fs from "fs";
|
import fs from "fs";
|
||||||
import path from "path";
|
import path from "path";
|
||||||
import util from "util";
|
import util from "util";
|
||||||
|
import rmSync from "../fs-rmSync.js";
|
||||||
|
|
||||||
// https://stackoverflow.com/questions/13786160/copy-folder-recursively-in-node-js
|
// https://stackoverflow.com/questions/13786160/copy-folder-recursively-in-node-js
|
||||||
/**
|
/**
|
||||||
@@ -30,7 +31,7 @@ console.log("Arguments:", process.argv);
|
|||||||
const baseLangCode = process.argv[2] || "en";
|
const baseLangCode = process.argv[2] || "en";
|
||||||
console.log("Base Lang: " + baseLangCode);
|
console.log("Base Lang: " + baseLangCode);
|
||||||
if (fs.existsSync("./languages")) {
|
if (fs.existsSync("./languages")) {
|
||||||
fs.rmdirSync("./languages", { recursive: true });
|
rmSync("./languages", { recursive: true });
|
||||||
}
|
}
|
||||||
copyRecursiveSync("../../src/languages", "./languages");
|
copyRecursiveSync("../../src/languages", "./languages");
|
||||||
|
|
||||||
@@ -40,7 +41,7 @@ const files = fs.readdirSync("./languages");
|
|||||||
console.log("Files:", files);
|
console.log("Files:", files);
|
||||||
|
|
||||||
for (const file of files) {
|
for (const file of files) {
|
||||||
if (!file.endsWith(".js")) {
|
if (! file.endsWith(".js")) {
|
||||||
console.log("Skipping " + file);
|
console.log("Skipping " + file);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -82,5 +83,5 @@ for (const file of files) {
|
|||||||
fs.writeFileSync(`../../src/languages/${file}`, code);
|
fs.writeFileSync(`../../src/languages/${file}`, code);
|
||||||
}
|
}
|
||||||
|
|
||||||
fs.rmdirSync("./languages", { recursive: true });
|
rmSync("./languages", { recursive: true });
|
||||||
console.log("Done. Fixing formatting by ESLint...");
|
console.log("Done. Fixing formatting by ESLint...");
|
||||||
|
@@ -1,14 +1,13 @@
|
|||||||
const pkg = require("../package.json");
|
const pkg = require("../package.json");
|
||||||
const fs = require("fs");
|
const fs = require("fs");
|
||||||
|
const rmSync = require("./fs-rmSync.js");
|
||||||
const child_process = require("child_process");
|
const child_process = require("child_process");
|
||||||
const util = require("../src/util");
|
const util = require("../src/util");
|
||||||
|
|
||||||
util.polyfill();
|
util.polyfill();
|
||||||
|
|
||||||
const oldVersion = pkg.version;
|
const newVersion = process.env.VERSION;
|
||||||
const newVersion = process.argv[2];
|
|
||||||
|
|
||||||
console.log("Old Version: " + oldVersion);
|
|
||||||
console.log("New Version: " + newVersion);
|
console.log("New Version: " + newVersion);
|
||||||
|
|
||||||
if (! newVersion) {
|
if (! newVersion) {
|
||||||
@@ -22,23 +21,20 @@ if (! exists) {
|
|||||||
|
|
||||||
// Process package.json
|
// Process package.json
|
||||||
pkg.version = newVersion;
|
pkg.version = newVersion;
|
||||||
pkg.scripts.setup = pkg.scripts.setup.replaceAll(oldVersion, newVersion);
|
|
||||||
pkg.scripts["build-docker"] = pkg.scripts["build-docker"].replaceAll(oldVersion, newVersion);
|
// Replace the version: https://regex101.com/r/hmj2Bc/1
|
||||||
pkg.scripts["build-docker-alpine"] = pkg.scripts["build-docker-alpine"].replaceAll(oldVersion, newVersion);
|
pkg.scripts.setup = pkg.scripts.setup.replace(/(git checkout )([^\s]+)/, `$1${newVersion}`);
|
||||||
pkg.scripts["build-docker-debian"] = pkg.scripts["build-docker-debian"].replaceAll(oldVersion, newVersion);
|
|
||||||
fs.writeFileSync("package.json", JSON.stringify(pkg, null, 4) + "\n");
|
fs.writeFileSync("package.json", JSON.stringify(pkg, null, 4) + "\n");
|
||||||
|
|
||||||
commit(newVersion);
|
commit(newVersion);
|
||||||
tag(newVersion);
|
tag(newVersion);
|
||||||
|
|
||||||
updateWiki(oldVersion, newVersion);
|
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
console.log("version exists");
|
console.log("version exists");
|
||||||
}
|
}
|
||||||
|
|
||||||
function commit(version) {
|
function commit(version) {
|
||||||
let msg = "update to " + version;
|
let msg = "Update to " + version;
|
||||||
|
|
||||||
let res = child_process.spawnSync("git", ["commit", "-m", msg, "-a"]);
|
let res = child_process.spawnSync("git", ["commit", "-m", msg, "-a"]);
|
||||||
let stdout = res.stdout.toString().trim();
|
let stdout = res.stdout.toString().trim();
|
||||||
@@ -63,38 +59,3 @@ function tagExists(version) {
|
|||||||
|
|
||||||
return res.stdout.toString().trim() === version;
|
return res.stdout.toString().trim() === version;
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateWiki(oldVersion, newVersion) {
|
|
||||||
const wikiDir = "./tmp/wiki";
|
|
||||||
const howToUpdateFilename = "./tmp/wiki/🆙-How-to-Update.md";
|
|
||||||
|
|
||||||
safeDelete(wikiDir);
|
|
||||||
|
|
||||||
child_process.spawnSync("git", ["clone", "https://github.com/louislam/uptime-kuma.wiki.git", wikiDir]);
|
|
||||||
let content = fs.readFileSync(howToUpdateFilename).toString();
|
|
||||||
content = content.replaceAll(`git checkout ${oldVersion}`, `git checkout ${newVersion}`);
|
|
||||||
fs.writeFileSync(howToUpdateFilename, content);
|
|
||||||
|
|
||||||
child_process.spawnSync("git", ["add", "-A"], {
|
|
||||||
cwd: wikiDir,
|
|
||||||
});
|
|
||||||
|
|
||||||
child_process.spawnSync("git", ["commit", "-m", `Update to ${newVersion} from ${oldVersion}`], {
|
|
||||||
cwd: wikiDir,
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log("Pushing to Github");
|
|
||||||
child_process.spawnSync("git", ["push"], {
|
|
||||||
cwd: wikiDir,
|
|
||||||
});
|
|
||||||
|
|
||||||
safeDelete(wikiDir);
|
|
||||||
}
|
|
||||||
|
|
||||||
function safeDelete(dir) {
|
|
||||||
if (fs.existsSync(dir)) {
|
|
||||||
fs.rmdirSync(dir, {
|
|
||||||
recursive: true,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
48
extra/update-wiki-version.js
Normal file
48
extra/update-wiki-version.js
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
const child_process = require("child_process");
|
||||||
|
const fs = require("fs");
|
||||||
|
|
||||||
|
const newVersion = process.env.VERSION;
|
||||||
|
|
||||||
|
if (!newVersion) {
|
||||||
|
console.log("Missing version");
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
updateWiki(newVersion);
|
||||||
|
|
||||||
|
function updateWiki(newVersion) {
|
||||||
|
const wikiDir = "./tmp/wiki";
|
||||||
|
const howToUpdateFilename = "./tmp/wiki/🆙-How-to-Update.md";
|
||||||
|
|
||||||
|
safeDelete(wikiDir);
|
||||||
|
|
||||||
|
child_process.spawnSync("git", ["clone", "https://github.com/louislam/uptime-kuma.wiki.git", wikiDir]);
|
||||||
|
let content = fs.readFileSync(howToUpdateFilename).toString();
|
||||||
|
|
||||||
|
// Replace the version: https://regex101.com/r/hmj2Bc/1
|
||||||
|
content = content.replace(/(git checkout )([^\s]+)/, `$1${newVersion}`);
|
||||||
|
fs.writeFileSync(howToUpdateFilename, content);
|
||||||
|
|
||||||
|
child_process.spawnSync("git", ["add", "-A"], {
|
||||||
|
cwd: wikiDir,
|
||||||
|
});
|
||||||
|
|
||||||
|
child_process.spawnSync("git", ["commit", "-m", `Update to ${newVersion}`], {
|
||||||
|
cwd: wikiDir,
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log("Pushing to Github");
|
||||||
|
child_process.spawnSync("git", ["push"], {
|
||||||
|
cwd: wikiDir,
|
||||||
|
});
|
||||||
|
|
||||||
|
safeDelete(wikiDir);
|
||||||
|
}
|
||||||
|
|
||||||
|
function safeDelete(dir) {
|
||||||
|
if (fs.existsSync(dir)) {
|
||||||
|
fs.rmdirSync(dir, {
|
||||||
|
recursive: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
@@ -159,7 +159,7 @@ fi
|
|||||||
check=$(pm2 --version)
|
check=$(pm2 --version)
|
||||||
if [ "$check" == "" ]; then
|
if [ "$check" == "" ]; then
|
||||||
"echo" "-e" "Installing PM2"
|
"echo" "-e" "Installing PM2"
|
||||||
npm install pm2 -g
|
npm install pm2 -g && pm2 install pm2-logrotate
|
||||||
pm2 startup
|
pm2 startup
|
||||||
fi
|
fi
|
||||||
mkdir -p $installPath
|
mkdir -p $installPath
|
||||||
|
14985
package-lock.json
generated
14985
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
95
package.json
95
package.json
@@ -1,13 +1,13 @@
|
|||||||
{
|
{
|
||||||
"name": "uptime-kuma",
|
"name": "uptime-kuma",
|
||||||
"version": "1.10.0",
|
"version": "1.14.0-beta.2",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://github.com/louislam/uptime-kuma.git"
|
"url": "https://github.com/louislam/uptime-kuma.git"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": "14.*"
|
"node": "14.* || >=16.*"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"install-legacy": "npm install --legacy-peer-deps",
|
"install-legacy": "npm install --legacy-peer-deps",
|
||||||
@@ -22,25 +22,25 @@
|
|||||||
"build": "vite build --config ./config/vite.config.js",
|
"build": "vite build --config ./config/vite.config.js",
|
||||||
"test": "node test/prepare-test-server.js && node server/server.js --port=3002 --data-dir=./data/test/ --test",
|
"test": "node test/prepare-test-server.js && node server/server.js --port=3002 --data-dir=./data/test/ --test",
|
||||||
"test-with-build": "npm run build && npm test",
|
"test-with-build": "npm run build && npm test",
|
||||||
"jest": "node test/prepare-jest.js && npm run jest-frontend && npm run jest-backend && jest --config=./config/jest.config.js",
|
"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",
|
||||||
"jest-backend": "cross-env TEST_BACKEND=1 jest --config=./config/jest-backend.config.js",
|
"jest-backend": "cross-env TEST_BACKEND=1 jest --config=./config/jest-backend.config.js",
|
||||||
"tsc": "tsc",
|
"tsc": "tsc",
|
||||||
"vite-preview-dist": "vite preview --host --config ./config/vite.config.js",
|
"vite-preview-dist": "vite preview --host --config ./config/vite.config.js",
|
||||||
"build-docker": "npm run build-docker-debian && npm run build-docker-alpine",
|
"build-docker": "npm run build && npm run build-docker-debian && npm run build-docker-alpine",
|
||||||
"build-docker-alpine-base": "docker buildx build -f docker/alpine-base.dockerfile --platform linux/amd64,linux/arm64,linux/arm/v7 -t louislam/uptime-kuma:base-alpine . --push",
|
"build-docker-alpine-base": "docker buildx build -f docker/alpine-base.dockerfile --platform linux/amd64,linux/arm64,linux/arm/v7 -t louislam/uptime-kuma:base-alpine . --push",
|
||||||
"build-docker-debian-base": "docker buildx build -f docker/debian-base.dockerfile --platform linux/amd64,linux/arm64,linux/arm/v7 -t louislam/uptime-kuma:base-debian . --push",
|
"build-docker-debian-base": "docker buildx build -f docker/debian-base.dockerfile --platform linux/amd64,linux/arm64,linux/arm/v7 -t louislam/uptime-kuma:base-debian . --push",
|
||||||
"build-docker-alpine": "docker buildx build -f docker/dockerfile-alpine --platform linux/amd64,linux/arm64,linux/arm/v7 -t louislam/uptime-kuma:alpine -t louislam/uptime-kuma:1-alpine -t louislam/uptime-kuma:1.10.0-alpine --target release . --push",
|
"build-docker-alpine": "node ./extra/env2arg.js docker buildx build -f docker/dockerfile-alpine --platform linux/amd64,linux/arm64,linux/arm/v7 -t louislam/uptime-kuma:alpine -t louislam/uptime-kuma:1-alpine -t louislam/uptime-kuma:$VERSION-alpine --target release . --push",
|
||||||
"build-docker-debian": "docker buildx build -f docker/dockerfile --platform linux/amd64,linux/arm64,linux/arm/v7 -t louislam/uptime-kuma -t louislam/uptime-kuma:1 -t louislam/uptime-kuma:1.10.0 -t louislam/uptime-kuma:debian -t louislam/uptime-kuma:1-debian -t louislam/uptime-kuma:1.10.0-debian --target release . --push",
|
"build-docker-debian": "node ./extra/env2arg.js docker buildx build -f docker/dockerfile --platform linux/amd64,linux/arm64,linux/arm/v7 -t louislam/uptime-kuma -t louislam/uptime-kuma:1 -t louislam/uptime-kuma:$VERSION -t louislam/uptime-kuma:debian -t louislam/uptime-kuma:1-debian -t louislam/uptime-kuma:$VERSION-debian --target release . --push",
|
||||||
"build-docker-nightly": "docker buildx build -f docker/dockerfile --platform linux/amd64,linux/arm64,linux/arm/v7 -t louislam/uptime-kuma:nightly --target nightly . --push",
|
"build-docker-nightly": "npm run build && docker buildx build -f docker/dockerfile --platform linux/amd64,linux/arm64,linux/arm/v7 -t louislam/uptime-kuma:nightly --target nightly . --push",
|
||||||
"build-docker-nightly-alpine": "docker buildx build -f docker/dockerfile-alpine --platform linux/amd64,linux/arm64,linux/arm/v7 -t louislam/uptime-kuma:nightly-alpine --target nightly . --push",
|
"build-docker-nightly-alpine": "docker buildx build -f docker/dockerfile-alpine --platform linux/amd64,linux/arm64,linux/arm/v7 -t louislam/uptime-kuma:nightly-alpine --target nightly . --push",
|
||||||
"build-docker-nightly-amd64": "docker buildx build -f docker/dockerfile --platform linux/amd64 -t louislam/uptime-kuma:nightly-amd64 --target nightly . --push --progress plain",
|
"build-docker-nightly-amd64": "docker buildx build -f docker/dockerfile --platform linux/amd64 -t louislam/uptime-kuma:nightly-amd64 --target nightly . --push --progress plain",
|
||||||
"upload-artifacts": "docker buildx build -f docker/dockerfile --platform linux/amd64 -t louislam/uptime-kuma:upload-artifact --build-arg GITHUB_TOKEN --target upload-artifact . --progress plain",
|
"upload-artifacts": "docker buildx build -f docker/dockerfile --platform linux/amd64 -t louislam/uptime-kuma:upload-artifact --build-arg VERSION --build-arg GITHUB_TOKEN --target upload-artifact . --progress plain",
|
||||||
"setup": "git checkout 1.10.0 && npm ci --production && npm run download-dist",
|
"setup": "git checkout 1.13.2 && npm ci --production && npm run download-dist",
|
||||||
"download-dist": "node extra/download-dist.js",
|
"download-dist": "node extra/download-dist.js",
|
||||||
"update-version": "node extra/update-version.js",
|
|
||||||
"mark-as-nightly": "node extra/mark-as-nightly.js",
|
"mark-as-nightly": "node extra/mark-as-nightly.js",
|
||||||
"reset-password": "node extra/reset-password.js",
|
"reset-password": "node extra/reset-password.js",
|
||||||
|
"remove-2fa": "node extra/remove-2fa.js",
|
||||||
"compile-install-script": "@powershell -NoProfile -ExecutionPolicy Unrestricted -Command ./extra/compile-install-script.ps1",
|
"compile-install-script": "@powershell -NoProfile -ExecutionPolicy Unrestricted -Command ./extra/compile-install-script.ps1",
|
||||||
"test-install-script-centos7": "npm run compile-install-script && docker build --progress plain -f test/test_install_script/centos7.dockerfile .",
|
"test-install-script-centos7": "npm run compile-install-script && docker build --progress plain -f test/test_install_script/centos7.dockerfile .",
|
||||||
"test-install-script-alpine3": "npm run compile-install-script && docker build --progress plain -f test/test_install_script/alpine3.dockerfile .",
|
"test-install-script-alpine3": "npm run compile-install-script && docker build --progress plain -f test/test_install_script/alpine3.dockerfile .",
|
||||||
@@ -49,83 +49,94 @@
|
|||||||
"test-nodejs16": "docker build --progress plain -f test/ubuntu-nodejs16.dockerfile .",
|
"test-nodejs16": "docker build --progress plain -f test/ubuntu-nodejs16.dockerfile .",
|
||||||
"simple-dns-server": "node extra/simple-dns-server.js",
|
"simple-dns-server": "node extra/simple-dns-server.js",
|
||||||
"update-language-files-with-base-lang": "cd extra/update-language-files && node index.js %npm_config_base_lang% && eslint ../../src/languages/**.js --fix",
|
"update-language-files-with-base-lang": "cd extra/update-language-files && node index.js %npm_config_base_lang% && eslint ../../src/languages/**.js --fix",
|
||||||
"update-language-files": "cd extra/update-language-files && node index.js && eslint ../../src/languages/**.js --fix"
|
"update-language-files": "cd extra/update-language-files && node index.js && eslint ../../src/languages/**.js --fix",
|
||||||
|
"ncu-patch": "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-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"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@fortawesome/fontawesome-svg-core": "~1.2.36",
|
"@fortawesome/fontawesome-svg-core": "~1.2.36",
|
||||||
"@fortawesome/free-regular-svg-icons": "~5.15.4",
|
"@fortawesome/free-regular-svg-icons": "~5.15.4",
|
||||||
"@fortawesome/free-solid-svg-icons": "~5.15.4",
|
"@fortawesome/free-solid-svg-icons": "~5.15.4",
|
||||||
"@fortawesome/vue-fontawesome": "~3.0.0-4",
|
"@fortawesome/vue-fontawesome": "~3.0.0-5",
|
||||||
"@louislam/sqlite3": "~6.0.0",
|
"@louislam/sqlite3": "~6.0.1",
|
||||||
"@popperjs/core": "~2.10.2",
|
"@popperjs/core": "~2.10.2",
|
||||||
"args-parser": "~1.3.0",
|
"args-parser": "~1.3.0",
|
||||||
"axios": "~0.21.4",
|
"axios": "~0.26.1",
|
||||||
"bcryptjs": "~2.4.3",
|
"bcryptjs": "~2.4.3",
|
||||||
"bootstrap": "~5.1.3",
|
"bootstrap": "5.1.3",
|
||||||
"bree": "~6.3.1",
|
"bree": "~7.1.5",
|
||||||
"chardet": "^1.3.0",
|
"chardet": "^1.3.0",
|
||||||
"chart.js": "~3.6.0",
|
"chart.js": "~3.6.2",
|
||||||
"chartjs-adapter-dayjs": "~1.0.0",
|
"chartjs-adapter-dayjs": "~1.0.0",
|
||||||
"check-password-strength": "^2.0.3",
|
"check-password-strength": "^2.0.5",
|
||||||
"command-exists": "~1.2.9",
|
"command-exists": "~1.2.9",
|
||||||
"compare-versions": "~3.6.0",
|
"compare-versions": "~3.6.0",
|
||||||
"dayjs": "~1.10.7",
|
"dayjs": "~1.10.8",
|
||||||
"express": "~4.17.1",
|
"express": "~4.17.3",
|
||||||
"express-basic-auth": "~1.2.0",
|
"express-basic-auth": "~1.2.1",
|
||||||
|
"favico.js": "^0.3.10",
|
||||||
"form-data": "~4.0.0",
|
"form-data": "~4.0.0",
|
||||||
"http-graceful-shutdown": "~3.1.4",
|
"http-graceful-shutdown": "~3.1.7",
|
||||||
|
"http-proxy-agent": "^5.0.0",
|
||||||
|
"https-proxy-agent": "^5.0.0",
|
||||||
"iconv-lite": "^0.6.3",
|
"iconv-lite": "^0.6.3",
|
||||||
"jsonwebtoken": "~8.5.1",
|
"jsonwebtoken": "~8.5.1",
|
||||||
"jwt-decode": "^3.1.2",
|
"jwt-decode": "^3.1.2",
|
||||||
"limiter": "^2.1.0",
|
"limiter": "^2.1.0",
|
||||||
|
"node-cloudflared-tunnel": "~1.0.9",
|
||||||
"nodemailer": "~6.6.5",
|
"nodemailer": "~6.6.5",
|
||||||
"notp": "~2.0.3",
|
"notp": "~2.0.3",
|
||||||
"password-hash": "~1.2.2",
|
"password-hash": "~1.2.2",
|
||||||
"postcss-rtlcss": "~3.4.1",
|
"postcss-rtlcss": "~3.4.1",
|
||||||
"postcss-scss": "~4.0.1",
|
"postcss-scss": "~4.0.3",
|
||||||
"prom-client": "~13.2.0",
|
"prom-client": "~13.2.0",
|
||||||
"prometheus-api-metrics": "~3.2.0",
|
"prometheus-api-metrics": "~3.2.1",
|
||||||
"qrcode": "~1.4.4",
|
"qrcode": "~1.5.0",
|
||||||
"redbean-node": "0.1.3",
|
"redbean-node": "0.1.3",
|
||||||
"socket.io": "~4.2.0",
|
"socket.io": "~4.4.1",
|
||||||
"socket.io-client": "~4.2.0",
|
"socket.io-client": "~4.4.1",
|
||||||
|
"socks-proxy-agent": "^6.1.1",
|
||||||
"tar": "^6.1.11",
|
"tar": "^6.1.11",
|
||||||
"tcp-ping": "~0.1.1",
|
"tcp-ping": "~0.1.1",
|
||||||
"thirty-two": "~1.0.2",
|
"thirty-two": "~1.0.2",
|
||||||
"timezones-list": "~3.0.1",
|
"timezones-list": "~3.0.1",
|
||||||
"v-pagination-3": "~0.1.7",
|
"v-pagination-3": "~0.1.7",
|
||||||
"vue": "next",
|
"vue": "next",
|
||||||
"vue-chart-3": "~0.5.11",
|
"vue-chart-3": "3.0.9",
|
||||||
"vue-confirm-dialog": "~1.0.2",
|
"vue-confirm-dialog": "~1.0.2",
|
||||||
"vue-contenteditable": "~3.0.4",
|
"vue-contenteditable": "~3.0.4",
|
||||||
"vue-i18n": "~9.1.9",
|
"vue-i18n": "~9.1.9",
|
||||||
"vue-image-crop-upload": "~3.0.3",
|
"vue-image-crop-upload": "~3.0.3",
|
||||||
"vue-multiselect": "~3.0.0-alpha.2",
|
"vue-multiselect": "~3.0.0-alpha.2",
|
||||||
"vue-qrcode": "~1.0.0",
|
"vue-qrcode": "~1.0.0",
|
||||||
"vue-router": "~4.0.11",
|
"vue-router": "~4.0.14",
|
||||||
"vue-toastification": "~2.0.0-rc.1",
|
"vue-toastification": "~2.0.0-rc.5",
|
||||||
"vuedraggable": "~4.1.0"
|
"vuedraggable": "~4.1.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/eslint-parser": "~7.15.7",
|
"@actions/github": "~5.0.1",
|
||||||
|
"@babel/eslint-parser": "~7.15.8",
|
||||||
"@babel/preset-env": "^7.15.8",
|
"@babel/preset-env": "^7.15.8",
|
||||||
"@types/bootstrap": "~5.1.6",
|
"@types/bootstrap": "~5.1.9",
|
||||||
"@vitejs/plugin-legacy": "~1.6.2",
|
"@vitejs/plugin-legacy": "~1.6.4",
|
||||||
"@vitejs/plugin-vue": "~1.9.4",
|
"@vitejs/plugin-vue": "~1.9.4",
|
||||||
"@vue/compiler-sfc": "~3.2.20",
|
"@vue/compiler-sfc": "~3.2.31",
|
||||||
"babel-plugin-rewire": "~1.2.0",
|
"babel-plugin-rewire": "~1.2.0",
|
||||||
"core-js": "~3.18.1",
|
"core-js": "~3.18.3",
|
||||||
"cross-env": "~7.0.3",
|
"cross-env": "~7.0.3",
|
||||||
"dns2": "~2.0.1",
|
"dns2": "~2.0.1",
|
||||||
"eslint": "~7.32.0",
|
"eslint": "~7.32.0",
|
||||||
"eslint-plugin-vue": "~7.18.0",
|
"eslint-plugin-vue": "~7.18.0",
|
||||||
"jest": "~27.2.4",
|
"jest": "~27.2.5",
|
||||||
"jest-puppeteer": "~6.0.0",
|
"jest-puppeteer": "~6.0.3",
|
||||||
"puppeteer": "~10.4.0",
|
"npm-check-updates": "^12.5.5",
|
||||||
|
"puppeteer": "~13.1.3",
|
||||||
"sass": "~1.42.1",
|
"sass": "~1.42.1",
|
||||||
"stylelint": "~13.13.1",
|
"stylelint": "~14.2.0",
|
||||||
"stylelint-config-standard": "~22.0.0",
|
"stylelint-config-standard": "~24.0.0",
|
||||||
"typescript": "~4.4.3",
|
"typescript": "~4.4.4",
|
||||||
"vite": "~2.6.13"
|
"vite": "~2.6.14"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
14
server/2fa.js
Normal file
14
server/2fa.js
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
const { checkLogin } = require("./util-server");
|
||||||
|
const { R } = require("redbean-node");
|
||||||
|
|
||||||
|
class TwoFA {
|
||||||
|
|
||||||
|
static async disable2FA(userID) {
|
||||||
|
return await R.exec("UPDATE `user` SET twofa_status = 0 WHERE id = ? ", [
|
||||||
|
userID,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = TwoFA;
|
@@ -12,6 +12,10 @@ const { loginRateLimiter } = require("./rate-limiter");
|
|||||||
* @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") {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
let user = await R.findOne("user", " username = ? AND active = 1 ", [
|
let user = await R.findOne("user", " username = ? AND active = 1 ", [
|
||||||
username,
|
username,
|
||||||
]);
|
]);
|
||||||
@@ -31,31 +35,34 @@ exports.login = async function (username, password) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
function myAuthorizer(username, password, callback) {
|
function myAuthorizer(username, password, callback) {
|
||||||
setting("disableAuth").then((result) => {
|
// Login Rate Limit
|
||||||
if (result) {
|
loginRateLimiter.pass(null, 0).then((pass) => {
|
||||||
callback(null, true);
|
if (pass) {
|
||||||
} else {
|
exports.login(username, password).then((user) => {
|
||||||
// Login Rate Limit
|
callback(null, user != null);
|
||||||
loginRateLimiter.pass(null, 0).then((pass) => {
|
|
||||||
if (pass) {
|
|
||||||
exports.login(username, password).then((user) => {
|
|
||||||
callback(null, user != null);
|
|
||||||
|
|
||||||
if (user == null) {
|
if (user == null) {
|
||||||
loginRateLimiter.removeTokens(1);
|
loginRateLimiter.removeTokens(1);
|
||||||
}
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
callback(null, false);
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
} else {
|
||||||
|
callback(null, false);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
exports.basicAuth = basicAuth({
|
exports.basicAuth = async function (req, res, next) {
|
||||||
authorizer: myAuthorizer,
|
const middleware = basicAuth({
|
||||||
authorizeAsync: true,
|
authorizer: myAuthorizer,
|
||||||
challenge: true,
|
authorizeAsync: true,
|
||||||
});
|
challenge: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
const disabledAuth = await setting("disableAuth");
|
||||||
|
|
||||||
|
if (!disabledAuth) {
|
||||||
|
middleware(req, res, next);
|
||||||
|
} else {
|
||||||
|
next();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
@@ -1,5 +1,6 @@
|
|||||||
const { setSetting } = require("./util-server");
|
const { setSetting, setting } = require("./util-server");
|
||||||
const axios = require("axios");
|
const axios = require("axios");
|
||||||
|
const compareVersions = require("compare-versions");
|
||||||
|
|
||||||
exports.version = require("../package.json").version;
|
exports.version = require("../package.json").version;
|
||||||
exports.latestVersion = null;
|
exports.latestVersion = null;
|
||||||
@@ -16,6 +17,19 @@ exports.startInterval = () => {
|
|||||||
res.data.slow = "1000.0.0";
|
res.data.slow = "1000.0.0";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (await setting("checkUpdate") === false) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let checkBeta = await setting("checkBeta");
|
||||||
|
|
||||||
|
if (checkBeta && res.data.beta) {
|
||||||
|
if (compareVersions.compare(res.data.beta, res.data.beta, ">")) {
|
||||||
|
exports.latestVersion = res.data.beta;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (res.data.slow) {
|
if (res.data.slow) {
|
||||||
exports.latestVersion = res.data.slow;
|
exports.latestVersion = res.data.slow;
|
||||||
}
|
}
|
||||||
|
@@ -83,6 +83,23 @@ async function sendImportantHeartbeatList(socket, monitorID, toUser = false, ove
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delivers proxy list
|
||||||
|
*
|
||||||
|
* @param socket
|
||||||
|
* @return {Promise<Bean[]>}
|
||||||
|
*/
|
||||||
|
async function sendProxyList(socket) {
|
||||||
|
const timeLogger = new TimeLogger();
|
||||||
|
|
||||||
|
const list = await R.find("proxy", " user_id = ? ", [socket.userID]);
|
||||||
|
io.to(socket.userID).emit("proxyList", list.map(bean => bean.export()));
|
||||||
|
|
||||||
|
timeLogger.print("Send Proxy List");
|
||||||
|
|
||||||
|
return list;
|
||||||
|
}
|
||||||
|
|
||||||
async function sendInfo(socket) {
|
async function sendInfo(socket) {
|
||||||
socket.emit("info", {
|
socket.emit("info", {
|
||||||
version: checkVersion.version,
|
version: checkVersion.version,
|
||||||
@@ -95,6 +112,6 @@ module.exports = {
|
|||||||
sendNotificationList,
|
sendNotificationList,
|
||||||
sendImportantHeartbeatList,
|
sendImportantHeartbeatList,
|
||||||
sendHeartbeatList,
|
sendHeartbeatList,
|
||||||
sendInfo
|
sendProxyList,
|
||||||
|
sendInfo,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@@ -52,6 +52,10 @@ class Database {
|
|||||||
"patch-http-monitor-method-body-and-headers.sql": true,
|
"patch-http-monitor-method-body-and-headers.sql": true,
|
||||||
"patch-2fa-invalidate-used-token.sql": true,
|
"patch-2fa-invalidate-used-token.sql": true,
|
||||||
"patch-notification_sent_history.sql": true,
|
"patch-notification_sent_history.sql": true,
|
||||||
|
"patch-monitor-basic-auth.sql": true,
|
||||||
|
"patch-status-page.sql": true,
|
||||||
|
"patch-proxy.sql": true,
|
||||||
|
"patch-monitor-expiry-notification.sql": true,
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -79,7 +83,7 @@ class Database {
|
|||||||
console.log(`Data Dir: ${Database.dataDir}`);
|
console.log(`Data Dir: ${Database.dataDir}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
static async connect() {
|
static async connect(testMode = false, autoloadModels = true, noLog = false) {
|
||||||
const acquireConnectionTimeout = 120 * 1000;
|
const acquireConnectionTimeout = 120 * 1000;
|
||||||
|
|
||||||
const Dialect = require("knex/lib/dialects/sqlite3/index.js");
|
const Dialect = require("knex/lib/dialects/sqlite3/index.js");
|
||||||
@@ -109,18 +113,33 @@ class Database {
|
|||||||
|
|
||||||
// Auto map the model to a bean object
|
// Auto map the model to a bean object
|
||||||
R.freeze(true);
|
R.freeze(true);
|
||||||
await R.autoloadModels("./server/model");
|
|
||||||
|
if (autoloadModels) {
|
||||||
|
await R.autoloadModels("./server/model");
|
||||||
|
}
|
||||||
|
|
||||||
await R.exec("PRAGMA foreign_keys = ON");
|
await R.exec("PRAGMA foreign_keys = ON");
|
||||||
// Change to WAL
|
if (testMode) {
|
||||||
await R.exec("PRAGMA journal_mode = WAL");
|
// Change to MEMORY
|
||||||
|
await R.exec("PRAGMA journal_mode = MEMORY");
|
||||||
|
} else {
|
||||||
|
// Change to WAL
|
||||||
|
await R.exec("PRAGMA journal_mode = WAL");
|
||||||
|
}
|
||||||
await R.exec("PRAGMA cache_size = -12000");
|
await R.exec("PRAGMA cache_size = -12000");
|
||||||
await R.exec("PRAGMA auto_vacuum = FULL");
|
await R.exec("PRAGMA auto_vacuum = FULL");
|
||||||
|
|
||||||
console.log("SQLite config:");
|
// This ensures that an operating system crash or power failure will not corrupt the database.
|
||||||
console.log(await R.getAll("PRAGMA journal_mode"));
|
// FULL synchronous is very safe, but it is also slower.
|
||||||
console.log(await R.getAll("PRAGMA cache_size"));
|
// Read more: https://sqlite.org/pragma.html#pragma_synchronous
|
||||||
console.log("SQLite Version: " + await R.getCell("SELECT sqlite_version()"));
|
await R.exec("PRAGMA synchronous = FULL");
|
||||||
|
|
||||||
|
if (!noLog) {
|
||||||
|
console.log("SQLite config:");
|
||||||
|
console.log(await R.getAll("PRAGMA journal_mode"));
|
||||||
|
console.log(await R.getAll("PRAGMA cache_size"));
|
||||||
|
console.log("SQLite Version: " + await R.getCell("SELECT sqlite_version()"));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static async patch() {
|
static async patch() {
|
||||||
@@ -164,6 +183,7 @@ class Database {
|
|||||||
}
|
}
|
||||||
|
|
||||||
await this.patch2();
|
await this.patch2();
|
||||||
|
await this.migrateNewStatusPage();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -205,6 +225,74 @@ class Database {
|
|||||||
await setSetting("databasePatchedFiles", databasePatchedFiles);
|
await setSetting("databasePatchedFiles", databasePatchedFiles);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Migrate status page value in setting to "status_page" table
|
||||||
|
* @returns {Promise<void>}
|
||||||
|
*/
|
||||||
|
static async migrateNewStatusPage() {
|
||||||
|
|
||||||
|
// Fix 1.13.0 empty slug bug
|
||||||
|
await R.exec("UPDATE status_page SET slug = 'empty-slug-recover' WHERE TRIM(slug) = ''");
|
||||||
|
|
||||||
|
let title = await setting("title");
|
||||||
|
|
||||||
|
if (title) {
|
||||||
|
console.log("Migrating Status Page");
|
||||||
|
|
||||||
|
let statusPageCheck = await R.findOne("status_page", " slug = 'default' ");
|
||||||
|
|
||||||
|
if (statusPageCheck !== null) {
|
||||||
|
console.log("Migrating Status Page - Skip, default slug record is already existing");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let statusPage = R.dispense("status_page");
|
||||||
|
statusPage.slug = "default";
|
||||||
|
statusPage.title = title;
|
||||||
|
statusPage.description = await setting("description");
|
||||||
|
statusPage.icon = await setting("icon");
|
||||||
|
statusPage.theme = await setting("statusPageTheme");
|
||||||
|
statusPage.published = !!await setting("statusPagePublished");
|
||||||
|
statusPage.search_engine_index = !!await setting("searchEngineIndex");
|
||||||
|
statusPage.show_tags = !!await setting("statusPageTags");
|
||||||
|
statusPage.password = null;
|
||||||
|
|
||||||
|
if (!statusPage.title) {
|
||||||
|
statusPage.title = "My Status Page";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!statusPage.icon) {
|
||||||
|
statusPage.icon = "";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!statusPage.theme) {
|
||||||
|
statusPage.theme = "light";
|
||||||
|
}
|
||||||
|
|
||||||
|
let id = await R.store(statusPage);
|
||||||
|
|
||||||
|
await R.exec("UPDATE incident SET status_page_id = ? WHERE status_page_id IS NULL", [
|
||||||
|
id
|
||||||
|
]);
|
||||||
|
|
||||||
|
await R.exec("UPDATE [group] SET status_page_id = ? WHERE status_page_id IS NULL", [
|
||||||
|
id
|
||||||
|
]);
|
||||||
|
|
||||||
|
await R.exec("DELETE FROM setting WHERE type = 'statusPage'");
|
||||||
|
|
||||||
|
// Migrate Entry Page if it is status page
|
||||||
|
let entryPage = await setting("entryPage");
|
||||||
|
|
||||||
|
if (entryPage === "statusPage") {
|
||||||
|
await setSetting("entryPage", "statusPage-default", "general");
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("Migrating Status Page - Done");
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Used it patch2() only
|
* Used it patch2() only
|
||||||
* @param sqlFilename
|
* @param sqlFilename
|
||||||
|
@@ -1,7 +1,7 @@
|
|||||||
const path = require("path");
|
const path = require("path");
|
||||||
const Bree = require("bree");
|
const Bree = require("bree");
|
||||||
const { SHARE_ENV } = require("worker_threads");
|
const { SHARE_ENV } = require("worker_threads");
|
||||||
|
let bree;
|
||||||
const jobs = [
|
const jobs = [
|
||||||
{
|
{
|
||||||
name: "clear-old-data",
|
name: "clear-old-data",
|
||||||
@@ -10,7 +10,7 @@ const jobs = [
|
|||||||
];
|
];
|
||||||
|
|
||||||
const initBackgroundJobs = function (args) {
|
const initBackgroundJobs = function (args) {
|
||||||
const bree = new Bree({
|
bree = new Bree({
|
||||||
root: path.resolve("server", "jobs"),
|
root: path.resolve("server", "jobs"),
|
||||||
jobs,
|
jobs,
|
||||||
worker: {
|
worker: {
|
||||||
@@ -26,6 +26,13 @@ const initBackgroundJobs = function (args) {
|
|||||||
return bree;
|
return bree;
|
||||||
};
|
};
|
||||||
|
|
||||||
module.exports = {
|
const stopBackgroundJobs = function () {
|
||||||
initBackgroundJobs
|
if (bree) {
|
||||||
|
bree.stop();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
initBackgroundJobs,
|
||||||
|
stopBackgroundJobs
|
||||||
};
|
};
|
||||||
|
@@ -3,12 +3,12 @@ const { R } = require("redbean-node");
|
|||||||
|
|
||||||
class Group extends BeanModel {
|
class Group extends BeanModel {
|
||||||
|
|
||||||
async toPublicJSON() {
|
async toPublicJSON(showTags = false) {
|
||||||
let monitorBeanList = await this.getMonitorList();
|
let monitorBeanList = await this.getMonitorList();
|
||||||
let monitorList = [];
|
let monitorList = [];
|
||||||
|
|
||||||
for (let bean of monitorBeanList) {
|
for (let bean of monitorBeanList) {
|
||||||
monitorList.push(await bean.toPublicJSON());
|
monitorList.push(await bean.toPublicJSON(showTags));
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
@@ -11,6 +11,7 @@ const { tcping, ping, dnsResolve, checkCertificate, checkStatusCode, getTotalCli
|
|||||||
const { R } = require("redbean-node");
|
const { R } = require("redbean-node");
|
||||||
const { BeanModel } = require("redbean-node/dist/bean-model");
|
const { BeanModel } = require("redbean-node/dist/bean-model");
|
||||||
const { Notification } = require("../notification");
|
const { Notification } = require("../notification");
|
||||||
|
const { 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");
|
||||||
@@ -24,18 +25,22 @@ const apicache = require("../modules/apicache");
|
|||||||
class Monitor extends BeanModel {
|
class Monitor extends BeanModel {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Return a 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
|
||||||
*/
|
*/
|
||||||
async toPublicJSON() {
|
async toPublicJSON(showTags = false) {
|
||||||
return {
|
let obj = {
|
||||||
id: this.id,
|
id: this.id,
|
||||||
name: this.name,
|
name: this.name,
|
||||||
};
|
};
|
||||||
|
if (showTags) {
|
||||||
|
obj.tags = await this.getTags();
|
||||||
|
}
|
||||||
|
return obj;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Return a object that ready to parse to JSON
|
* Return an object that ready to parse to JSON
|
||||||
*/
|
*/
|
||||||
async toJSON() {
|
async toJSON() {
|
||||||
|
|
||||||
@@ -49,7 +54,7 @@ class Monitor extends BeanModel {
|
|||||||
notificationIDList[bean.notification_id] = true;
|
notificationIDList[bean.notification_id] = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
const tags = 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]);
|
const tags = await this.getTags();
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: this.id,
|
id: this.id,
|
||||||
@@ -58,6 +63,8 @@ class Monitor extends BeanModel {
|
|||||||
method: this.method,
|
method: this.method,
|
||||||
body: this.body,
|
body: this.body,
|
||||||
headers: this.headers,
|
headers: this.headers,
|
||||||
|
basic_auth_user: this.basic_auth_user,
|
||||||
|
basic_auth_pass: this.basic_auth_pass,
|
||||||
hostname: this.hostname,
|
hostname: this.hostname,
|
||||||
port: this.port,
|
port: this.port,
|
||||||
maxretries: this.maxretries,
|
maxretries: this.maxretries,
|
||||||
@@ -67,6 +74,7 @@ class Monitor extends BeanModel {
|
|||||||
interval: this.interval,
|
interval: this.interval,
|
||||||
retryInterval: this.retryInterval,
|
retryInterval: this.retryInterval,
|
||||||
keyword: this.keyword,
|
keyword: this.keyword,
|
||||||
|
expiryNotification: this.isEnabledExpiryNotification(),
|
||||||
ignoreTls: this.getIgnoreTls(),
|
ignoreTls: this.getIgnoreTls(),
|
||||||
upsideDown: this.isUpsideDown(),
|
upsideDown: this.isUpsideDown(),
|
||||||
maxredirects: this.maxredirects,
|
maxredirects: this.maxredirects,
|
||||||
@@ -75,11 +83,29 @@ class Monitor extends BeanModel {
|
|||||||
dns_resolve_server: this.dns_resolve_server,
|
dns_resolve_server: this.dns_resolve_server,
|
||||||
dns_last_result: this.dns_last_result,
|
dns_last_result: this.dns_last_result,
|
||||||
pushToken: this.pushToken,
|
pushToken: this.pushToken,
|
||||||
|
proxyId: this.proxy_id,
|
||||||
notificationIDList,
|
notificationIDList,
|
||||||
tags: tags,
|
tags: tags,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Encode user and password to Base64 encoding
|
||||||
|
* for HTTP "basic" auth, as per RFC-7617
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
encodeBase64(user, pass) {
|
||||||
|
return Buffer.from(user + ":" + pass).toString("base64");
|
||||||
|
}
|
||||||
|
|
||||||
|
isEnabledExpiryNotification() {
|
||||||
|
return Boolean(this.expiryNotification);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Parse to boolean
|
* Parse to boolean
|
||||||
* @returns {boolean}
|
* @returns {boolean}
|
||||||
@@ -108,6 +134,19 @@ class Monitor extends BeanModel {
|
|||||||
|
|
||||||
const beat = async () => {
|
const beat = async () => {
|
||||||
|
|
||||||
|
let beatInterval = this.interval;
|
||||||
|
|
||||||
|
if (! beatInterval) {
|
||||||
|
beatInterval = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (demoMode) {
|
||||||
|
if (beatInterval < 20) {
|
||||||
|
console.log("beat interval too low, reset to 20s");
|
||||||
|
beatInterval = 20;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Expose here for prometheus update
|
// Expose here for prometheus update
|
||||||
// undefined if not https
|
// undefined if not https
|
||||||
let tlsInfo = undefined;
|
let tlsInfo = undefined;
|
||||||
@@ -141,25 +180,59 @@ class Monitor extends BeanModel {
|
|||||||
// Do not do any queries/high loading things before the "bean.ping"
|
// Do not do any queries/high loading things before the "bean.ping"
|
||||||
let startTime = dayjs().valueOf();
|
let startTime = dayjs().valueOf();
|
||||||
|
|
||||||
|
// HTTP basic auth
|
||||||
|
let basicAuthHeader = {};
|
||||||
|
if (this.basic_auth_user) {
|
||||||
|
basicAuthHeader = {
|
||||||
|
"Authorization": "Basic " + this.encodeBase64(this.basic_auth_user, this.basic_auth_pass),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const httpsAgentOptions = {
|
||||||
|
maxCachedSessions: 0, // Use Custom agent to disable session reuse (https://github.com/nodejs/node/issues/3940)
|
||||||
|
rejectUnauthorized: !this.getIgnoreTls(),
|
||||||
|
};
|
||||||
|
|
||||||
|
debug(`[${this.name}] Prepare Options for axios`);
|
||||||
|
|
||||||
const options = {
|
const options = {
|
||||||
url: this.url,
|
url: this.url,
|
||||||
method: (this.method || "get").toLowerCase(),
|
method: (this.method || "get").toLowerCase(),
|
||||||
...(this.body ? { data: JSON.parse(this.body) } : {}),
|
...(this.body ? { data: JSON.parse(this.body) } : {}),
|
||||||
timeout: this.interval * 1000 * 0.8,
|
timeout: this.interval * 1000 * 0.8,
|
||||||
headers: {
|
headers: {
|
||||||
"Accept": "*/*",
|
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9",
|
||||||
"User-Agent": "Uptime-Kuma/" + version,
|
"User-Agent": "Uptime-Kuma/" + version,
|
||||||
...(this.headers ? JSON.parse(this.headers) : {}),
|
...(this.headers ? JSON.parse(this.headers) : {}),
|
||||||
|
...(basicAuthHeader),
|
||||||
},
|
},
|
||||||
httpsAgent: new https.Agent({
|
|
||||||
maxCachedSessions: 0, // Use Custom agent to disable session reuse (https://github.com/nodejs/node/issues/3940)
|
|
||||||
rejectUnauthorized: ! this.getIgnoreTls(),
|
|
||||||
}),
|
|
||||||
maxRedirects: this.maxredirects,
|
maxRedirects: this.maxredirects,
|
||||||
validateStatus: (status) => {
|
validateStatus: (status) => {
|
||||||
return checkStatusCode(status, this.getAcceptedStatuscodes());
|
return checkStatusCode(status, this.getAcceptedStatuscodes());
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (this.proxy_id) {
|
||||||
|
const proxy = await R.load("proxy", this.proxy_id);
|
||||||
|
|
||||||
|
if (proxy && proxy.active) {
|
||||||
|
const { httpAgent, httpsAgent } = Proxy.createAgents(proxy, {
|
||||||
|
httpsAgentOptions: httpsAgentOptions,
|
||||||
|
});
|
||||||
|
|
||||||
|
options.proxy = false;
|
||||||
|
options.httpAgent = httpAgent;
|
||||||
|
options.httpsAgent = httpsAgent;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!options.httpsAgent) {
|
||||||
|
options.httpsAgent = new https.Agent(httpsAgentOptions);
|
||||||
|
}
|
||||||
|
|
||||||
|
debug(`[${this.name}] Axios Options: ${JSON.stringify(options)}`);
|
||||||
|
debug(`[${this.name}] Axios Request`);
|
||||||
|
|
||||||
let res = await axios.request(options);
|
let res = await axios.request(options);
|
||||||
bean.msg = `${res.status} - ${res.statusText}`;
|
bean.msg = `${res.status} - ${res.statusText}`;
|
||||||
bean.ping = dayjs().valueOf() - startTime;
|
bean.ping = dayjs().valueOf() - startTime;
|
||||||
@@ -167,12 +240,13 @@ class Monitor extends BeanModel {
|
|||||||
// Check certificate if https is used
|
// Check certificate if https is used
|
||||||
let certInfoStartTime = dayjs().valueOf();
|
let certInfoStartTime = dayjs().valueOf();
|
||||||
if (this.getUrl()?.protocol === "https:") {
|
if (this.getUrl()?.protocol === "https:") {
|
||||||
|
debug(`[${this.name}] Check cert`);
|
||||||
try {
|
try {
|
||||||
let tlsInfoObject = checkCertificate(res);
|
let tlsInfoObject = checkCertificate(res);
|
||||||
tlsInfo = await this.updateTlsInfo(tlsInfoObject);
|
tlsInfo = await this.updateTlsInfo(tlsInfoObject);
|
||||||
|
|
||||||
if (!this.getIgnoreTls()) {
|
if (!this.getIgnoreTls() && this.isEnabledExpiryNotification()) {
|
||||||
debug("call sendCertNotification");
|
debug(`[${this.name}] call sendCertNotification`);
|
||||||
await this.sendCertNotification(tlsInfoObject);
|
await this.sendCertNotification(tlsInfoObject);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -271,11 +345,14 @@ class Monitor extends BeanModel {
|
|||||||
debug("heartbeatCount" + heartbeatCount + " " + time);
|
debug("heartbeatCount" + heartbeatCount + " " + time);
|
||||||
|
|
||||||
if (heartbeatCount <= 0) {
|
if (heartbeatCount <= 0) {
|
||||||
|
// Fix #922, since previous heartbeat could be inserted by api, it should get from database
|
||||||
|
previousBeat = await Monitor.getPreviousHeartbeat(this.id);
|
||||||
|
|
||||||
throw new Error("No heartbeat in the time window");
|
throw new Error("No heartbeat in the time window");
|
||||||
} else {
|
} else {
|
||||||
// No need to insert successful heartbeat for push type, so end here
|
// No need to insert successful heartbeat for push type, so end here
|
||||||
retries = 0;
|
retries = 0;
|
||||||
this.heartbeatInterval = setTimeout(beat, this.interval * 1000);
|
this.heartbeatInterval = setTimeout(beat, beatInterval * 1000);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -349,17 +426,19 @@ class Monitor extends BeanModel {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let beatInterval = this.interval;
|
debug(`[${this.name}] Check isImportant`);
|
||||||
|
|
||||||
let isImportant = Monitor.isImportantBeat(isFirstBeat, previousBeat?.status, bean.status);
|
let isImportant = Monitor.isImportantBeat(isFirstBeat, previousBeat?.status, bean.status);
|
||||||
|
|
||||||
// Mark as important if status changed, ignore pending pings,
|
// Mark as important if status changed, ignore pending pings,
|
||||||
// Don't notify if disrupted changes to up
|
// Don't notify if disrupted changes to up
|
||||||
if (isImportant) {
|
if (isImportant) {
|
||||||
bean.important = true;
|
bean.important = true;
|
||||||
|
|
||||||
|
debug(`[${this.name}] sendNotification`);
|
||||||
await Monitor.sendNotification(isFirstBeat, this, bean);
|
await Monitor.sendNotification(isFirstBeat, this, bean);
|
||||||
|
|
||||||
// Clear Status Page Cache
|
// Clear Status Page Cache
|
||||||
|
debug(`[${this.name}] apicache clear`);
|
||||||
apicache.clear();
|
apicache.clear();
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
@@ -377,24 +456,23 @@ class Monitor extends BeanModel {
|
|||||||
console.warn(`Monitor #${this.id} '${this.name}': Failing: ${bean.msg} | Interval: ${beatInterval} seconds | Type: ${this.type}`);
|
console.warn(`Monitor #${this.id} '${this.name}': Failing: ${bean.msg} | Interval: ${beatInterval} seconds | Type: ${this.type}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
debug(`[${this.name}] Send to socket`);
|
||||||
io.to(this.user_id).emit("heartbeat", bean.toJSON());
|
io.to(this.user_id).emit("heartbeat", bean.toJSON());
|
||||||
Monitor.sendStats(io, this.id, this.user_id);
|
Monitor.sendStats(io, this.id, this.user_id);
|
||||||
|
|
||||||
|
debug(`[${this.name}] Store`);
|
||||||
await R.store(bean);
|
await R.store(bean);
|
||||||
|
|
||||||
|
debug(`[${this.name}] prometheus.update`);
|
||||||
prometheus.update(bean, tlsInfo);
|
prometheus.update(bean, tlsInfo);
|
||||||
|
|
||||||
previousBeat = bean;
|
previousBeat = bean;
|
||||||
|
|
||||||
if (! this.isStop) {
|
if (! this.isStop) {
|
||||||
|
debug(`[${this.name}] SetTimeout for next check.`);
|
||||||
if (demoMode) {
|
|
||||||
if (beatInterval < 20) {
|
|
||||||
console.log("beat interval too low, reset to 20s");
|
|
||||||
beatInterval = 20;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
this.heartbeatInterval = setTimeout(safeBeat, beatInterval * 1000);
|
this.heartbeatInterval = setTimeout(safeBeat, beatInterval * 1000);
|
||||||
|
} else {
|
||||||
|
console.log(`[${this.name}] isStop = true, no next check.`);
|
||||||
}
|
}
|
||||||
|
|
||||||
};
|
};
|
||||||
@@ -427,6 +505,12 @@ class Monitor extends BeanModel {
|
|||||||
stop() {
|
stop() {
|
||||||
clearTimeout(this.heartbeatInterval);
|
clearTimeout(this.heartbeatInterval);
|
||||||
this.isStop = true;
|
this.isStop = true;
|
||||||
|
|
||||||
|
this.prometheus().remove();
|
||||||
|
}
|
||||||
|
|
||||||
|
prometheus() {
|
||||||
|
return new Prometheus(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -715,6 +799,15 @@ class Monitor extends BeanModel {
|
|||||||
debug("No notification, no need to send cert notification");
|
debug("No notification, no need to send cert notification");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static async getPreviousHeartbeat(monitorID) {
|
||||||
|
return await R.getRow(`
|
||||||
|
SELECT status, time FROM heartbeat
|
||||||
|
WHERE id = (select MAX(id) from heartbeat where monitor_id = ?)
|
||||||
|
`, [
|
||||||
|
monitorID
|
||||||
|
]);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = Monitor;
|
module.exports = Monitor;
|
||||||
|
21
server/model/proxy.js
Normal file
21
server/model/proxy.js
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
const { BeanModel } = require("redbean-node/dist/bean-model");
|
||||||
|
|
||||||
|
class Proxy extends BeanModel {
|
||||||
|
toJSON() {
|
||||||
|
return {
|
||||||
|
id: this._id,
|
||||||
|
userId: this._user_id,
|
||||||
|
protocol: this._protocol,
|
||||||
|
host: this._host,
|
||||||
|
port: this._port,
|
||||||
|
auth: !!this._auth,
|
||||||
|
username: this._username,
|
||||||
|
password: this._password,
|
||||||
|
active: !!this._active,
|
||||||
|
default: !!this._default,
|
||||||
|
createdDate: this._created_date,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = Proxy;
|
126
server/model/status_page.js
Normal file
126
server/model/status_page.js
Normal file
@@ -0,0 +1,126 @@
|
|||||||
|
const { BeanModel } = require("redbean-node/dist/bean-model");
|
||||||
|
const { R } = require("redbean-node");
|
||||||
|
|
||||||
|
class StatusPage extends BeanModel {
|
||||||
|
|
||||||
|
static domainMappingList = { };
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return object like this: { "test-uptime.kuma.pet": "default" }
|
||||||
|
* @returns {Promise<void>}
|
||||||
|
*/
|
||||||
|
static async loadDomainMappingList() {
|
||||||
|
StatusPage.domainMappingList = await R.getAssoc(`
|
||||||
|
SELECT domain, slug
|
||||||
|
FROM status_page, status_page_cname
|
||||||
|
WHERE status_page.id = status_page_cname.status_page_id
|
||||||
|
`);
|
||||||
|
}
|
||||||
|
|
||||||
|
static async sendStatusPageList(io, socket) {
|
||||||
|
let result = {};
|
||||||
|
|
||||||
|
let list = await R.findAll("status_page", " ORDER BY title ");
|
||||||
|
|
||||||
|
for (let item of list) {
|
||||||
|
result[item.id] = await item.toJSON();
|
||||||
|
}
|
||||||
|
|
||||||
|
io.to(socket.userID).emit("statusPageList", result);
|
||||||
|
return list;
|
||||||
|
}
|
||||||
|
|
||||||
|
async updateDomainNameList(domainNameList) {
|
||||||
|
|
||||||
|
if (!Array.isArray(domainNameList)) {
|
||||||
|
throw new Error("Invalid array");
|
||||||
|
}
|
||||||
|
|
||||||
|
let trx = await R.begin();
|
||||||
|
|
||||||
|
await trx.exec("DELETE FROM status_page_cname WHERE status_page_id = ?", [
|
||||||
|
this.id,
|
||||||
|
]);
|
||||||
|
|
||||||
|
try {
|
||||||
|
for (let domain of domainNameList) {
|
||||||
|
if (typeof domain !== "string") {
|
||||||
|
throw new Error("Invalid domain");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (domain.trim() === "") {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the domain name is used in another status page, delete it
|
||||||
|
await trx.exec("DELETE FROM status_page_cname WHERE domain = ?", [
|
||||||
|
domain,
|
||||||
|
]);
|
||||||
|
|
||||||
|
let mapping = trx.dispense("status_page_cname");
|
||||||
|
mapping.status_page_id = this.id;
|
||||||
|
mapping.domain = domain;
|
||||||
|
await trx.store(mapping);
|
||||||
|
}
|
||||||
|
await trx.commit();
|
||||||
|
} catch (error) {
|
||||||
|
await trx.rollback();
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getDomainNameList() {
|
||||||
|
let domainList = [];
|
||||||
|
for (let domain in StatusPage.domainMappingList) {
|
||||||
|
let s = StatusPage.domainMappingList[domain];
|
||||||
|
|
||||||
|
if (this.slug === s) {
|
||||||
|
domainList.push(domain);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return domainList;
|
||||||
|
}
|
||||||
|
|
||||||
|
async toJSON() {
|
||||||
|
return {
|
||||||
|
id: this.id,
|
||||||
|
slug: this.slug,
|
||||||
|
title: this.title,
|
||||||
|
description: this.description,
|
||||||
|
icon: this.getIcon(),
|
||||||
|
theme: this.theme,
|
||||||
|
published: !!this.published,
|
||||||
|
showTags: !!this.show_tags,
|
||||||
|
domainNameList: this.getDomainNameList(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async toPublicJSON() {
|
||||||
|
return {
|
||||||
|
slug: this.slug,
|
||||||
|
title: this.title,
|
||||||
|
description: this.description,
|
||||||
|
icon: this.getIcon(),
|
||||||
|
theme: this.theme,
|
||||||
|
published: !!this.published,
|
||||||
|
showTags: !!this.show_tags,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
static async slugToID(slug) {
|
||||||
|
return await R.getCell("SELECT id FROM status_page WHERE slug = ? ", [
|
||||||
|
slug
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
getIcon() {
|
||||||
|
if (!this.icon) {
|
||||||
|
return "/icon.svg";
|
||||||
|
} else {
|
||||||
|
return this.icon;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = StatusPage;
|
67
server/notification-providers/alerta.js
Normal file
67
server/notification-providers/alerta.js
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
const NotificationProvider = require("./notification-provider");
|
||||||
|
const { DOWN, UP } = require("../../src/util");
|
||||||
|
const axios = require("axios");
|
||||||
|
|
||||||
|
class Alerta extends NotificationProvider {
|
||||||
|
|
||||||
|
name = "alerta";
|
||||||
|
|
||||||
|
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
|
||||||
|
let okMsg = "Sent Successfully.";
|
||||||
|
|
||||||
|
try {
|
||||||
|
let alertaUrl = `${notification.alertaApiEndpoint}`;
|
||||||
|
let config = {
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json;charset=UTF-8",
|
||||||
|
"Authorization": "Key " + notification.alertaApiKey,
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let data = {
|
||||||
|
environment: notification.alertaEnvironment,
|
||||||
|
severity: "critical",
|
||||||
|
correlate: [],
|
||||||
|
service: [ "UptimeKuma" ],
|
||||||
|
value: "Timeout",
|
||||||
|
tags: [ "uptimekuma" ],
|
||||||
|
attributes: {},
|
||||||
|
origin: "uptimekuma",
|
||||||
|
type: "exceptionAlert",
|
||||||
|
};
|
||||||
|
|
||||||
|
if (heartbeatJSON == null) {
|
||||||
|
let postData = Object.assign({
|
||||||
|
event: "msg",
|
||||||
|
text: msg,
|
||||||
|
group: "uptimekuma-msg",
|
||||||
|
resource: "Message",
|
||||||
|
}, data);
|
||||||
|
|
||||||
|
await axios.post(alertaUrl, postData, config);
|
||||||
|
} else {
|
||||||
|
let datadup = Object.assign( {
|
||||||
|
correlate: ["service_up", "service_down"],
|
||||||
|
event: monitorJSON["type"],
|
||||||
|
group: "uptimekuma-" + monitorJSON["type"],
|
||||||
|
resource: monitorJSON["name"],
|
||||||
|
}, data );
|
||||||
|
|
||||||
|
if (heartbeatJSON["status"] == DOWN) {
|
||||||
|
datadup.severity = notification.alertaAlertState; // critical
|
||||||
|
datadup.text = "Service " + monitorJSON["type"] + " is down.";
|
||||||
|
await axios.post(alertaUrl, datadup, config);
|
||||||
|
} else if (heartbeatJSON["status"] == UP) {
|
||||||
|
datadup.severity = notification.alertaRecoverState; // cleaned
|
||||||
|
datadup.text = "Service " + monitorJSON["type"] + " is up.";
|
||||||
|
await axios.post(alertaUrl, datadup, config);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return okMsg;
|
||||||
|
} catch (error) {
|
||||||
|
this.throwGeneralAxiosError(error);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = Alerta;
|
@@ -14,8 +14,8 @@ class DingDing extends NotificationProvider {
|
|||||||
let params = {
|
let params = {
|
||||||
msgtype: "markdown",
|
msgtype: "markdown",
|
||||||
markdown: {
|
markdown: {
|
||||||
title: monitorJSON["name"],
|
title: `[${this.statusToString(heartbeatJSON["status"])}] ${monitorJSON["name"]}`,
|
||||||
text: `## [${this.statusToString(heartbeatJSON["status"])}] \n > ${heartbeatJSON["msg"]} \n > Time(UTC):${heartbeatJSON["time"]}`,
|
text: `## [${this.statusToString(heartbeatJSON["status"])}] ${monitorJSON["name"]} \n > ${heartbeatJSON["msg"]} \n > Time(UTC):${heartbeatJSON["time"]}`,
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
if (this.sendToDingDing(notification, params)) {
|
if (this.sendToDingDing(notification, params)) {
|
||||||
|
@@ -27,7 +27,7 @@ class Feishu extends NotificationProvider {
|
|||||||
content: {
|
content: {
|
||||||
post: {
|
post: {
|
||||||
zh_cn: {
|
zh_cn: {
|
||||||
title: "UptimeKuma Alert: " + monitorJSON["name"],
|
title: "UptimeKuma Alert: [Down] " + monitorJSON["name"],
|
||||||
content: [
|
content: [
|
||||||
[
|
[
|
||||||
{
|
{
|
||||||
@@ -54,7 +54,7 @@ class Feishu extends NotificationProvider {
|
|||||||
content: {
|
content: {
|
||||||
post: {
|
post: {
|
||||||
zh_cn: {
|
zh_cn: {
|
||||||
title: "UptimeKuma Alert: " + monitorJSON["name"],
|
title: "UptimeKuma Alert: [Up] " + monitorJSON["name"],
|
||||||
content: [
|
content: [
|
||||||
[
|
[
|
||||||
{
|
{
|
||||||
|
47
server/notification-providers/google-chat.js
Normal file
47
server/notification-providers/google-chat.js
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
const NotificationProvider = require("./notification-provider");
|
||||||
|
const axios = require("axios");
|
||||||
|
const { setting } = require("../util-server");
|
||||||
|
const { getMonitorRelativeURL } = require("../../src/util");
|
||||||
|
const { DOWN, UP } = require("../../src/util");
|
||||||
|
|
||||||
|
class GoogleChat extends NotificationProvider {
|
||||||
|
|
||||||
|
name = "GoogleChat";
|
||||||
|
|
||||||
|
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
|
||||||
|
let okMsg = "Sent Successfully.";
|
||||||
|
try {
|
||||||
|
// Google Chat message formatting: https://developers.google.com/chat/api/guides/message-formats/basic
|
||||||
|
|
||||||
|
let textMsg = ''
|
||||||
|
if (heartbeatJSON && heartbeatJSON.status === UP) {
|
||||||
|
textMsg = `✅ Application is back online\n`;
|
||||||
|
} else if (heartbeatJSON && heartbeatJSON.status === DOWN) {
|
||||||
|
textMsg = `🔴 Application went down\n`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (monitorJSON && monitorJSON.name) {
|
||||||
|
textMsg += `*${monitorJSON.name}*\n`;
|
||||||
|
}
|
||||||
|
|
||||||
|
textMsg += `${msg}`;
|
||||||
|
|
||||||
|
const baseURL = await setting("primaryBaseURL");
|
||||||
|
if (baseURL && monitorJSON) {
|
||||||
|
textMsg += `\n${baseURL + getMonitorRelativeURL(monitorJSON.id)}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = {
|
||||||
|
"text": textMsg,
|
||||||
|
};
|
||||||
|
|
||||||
|
await axios.post(notification.googleChatWebhookURL, data);
|
||||||
|
return okMsg;
|
||||||
|
} catch (error) {
|
||||||
|
this.throwGeneralAxiosError(error);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = GoogleChat;
|
42
server/notification-providers/gorush.js
Normal file
42
server/notification-providers/gorush.js
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
const NotificationProvider = require("./notification-provider");
|
||||||
|
const axios = require("axios");
|
||||||
|
|
||||||
|
class Gorush extends NotificationProvider {
|
||||||
|
|
||||||
|
name = "gorush";
|
||||||
|
|
||||||
|
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
|
||||||
|
let okMsg = "Sent Successfully.";
|
||||||
|
|
||||||
|
let platformMapping = {
|
||||||
|
"ios": 1,
|
||||||
|
"android": 2,
|
||||||
|
"huawei": 3,
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
let data = {
|
||||||
|
"notifications": [
|
||||||
|
{
|
||||||
|
"tokens": [notification.gorushDeviceToken],
|
||||||
|
"platform": platformMapping[notification.gorushPlatform],
|
||||||
|
"message": msg,
|
||||||
|
// Optional
|
||||||
|
"title": notification.gorushTitle,
|
||||||
|
"priority": notification.gorushPriority,
|
||||||
|
"retry": parseInt(notification.gorushRetry) || 0,
|
||||||
|
"topic": notification.gorushTopic,
|
||||||
|
}
|
||||||
|
]
|
||||||
|
};
|
||||||
|
let config = {};
|
||||||
|
|
||||||
|
await axios.post(`${notification.gorushServerURL}/api/push`, data, config);
|
||||||
|
return okMsg;
|
||||||
|
} catch (error) {
|
||||||
|
this.throwGeneralAxiosError(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = Gorush;
|
@@ -15,12 +15,17 @@ class Mattermost extends NotificationProvider {
|
|||||||
let mattermostTestData = {
|
let mattermostTestData = {
|
||||||
username: mattermostUserName,
|
username: mattermostUserName,
|
||||||
text: msg,
|
text: msg,
|
||||||
}
|
};
|
||||||
await axios.post(notification.mattermostWebhookUrl, mattermostTestData)
|
await axios.post(notification.mattermostWebhookUrl, mattermostTestData);
|
||||||
return okMsg;
|
return okMsg;
|
||||||
}
|
}
|
||||||
|
|
||||||
const mattermostChannel = notification.mattermostchannel;
|
let mattermostChannel;
|
||||||
|
|
||||||
|
if (typeof notification.mattermostchannel === "string") {
|
||||||
|
mattermostChannel = notification.mattermostchannel.toLowerCase();
|
||||||
|
}
|
||||||
|
|
||||||
const mattermostIconEmoji = notification.mattermosticonemo;
|
const mattermostIconEmoji = notification.mattermosticonemo;
|
||||||
const mattermostIconUrl = notification.mattermosticonurl;
|
const mattermostIconUrl = notification.mattermosticonurl;
|
||||||
|
|
||||||
|
@@ -7,40 +7,35 @@ class Pushover extends NotificationProvider {
|
|||||||
|
|
||||||
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
|
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
|
||||||
let okMsg = "Sent Successfully.";
|
let okMsg = "Sent Successfully.";
|
||||||
let pushoverlink = "https://api.pushover.net/1/messages.json"
|
let pushoverlink = "https://api.pushover.net/1/messages.json";
|
||||||
|
|
||||||
|
let data = {
|
||||||
|
"message": "<b>Uptime Kuma Alert</b>\n\n<b>Message</b>:" + msg,
|
||||||
|
"user": notification.pushoveruserkey,
|
||||||
|
"token": notification.pushoverapptoken,
|
||||||
|
"sound": notification.pushoversounds,
|
||||||
|
"priority": notification.pushoverpriority,
|
||||||
|
"title": notification.pushovertitle,
|
||||||
|
"retry": "30",
|
||||||
|
"expire": "3600",
|
||||||
|
"html": 1,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (notification.pushoverdevice) {
|
||||||
|
data.device = notification.pushoverdevice;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (heartbeatJSON == null) {
|
if (heartbeatJSON == null) {
|
||||||
let data = {
|
await axios.post(pushoverlink, data);
|
||||||
"message": "<b>Uptime Kuma Pushover testing successful.</b>",
|
return okMsg;
|
||||||
"user": notification.pushoveruserkey,
|
} else {
|
||||||
"token": notification.pushoverapptoken,
|
data.message += "\n<b>Time (UTC)</b>:" + heartbeatJSON["time"];
|
||||||
"sound": notification.pushoversounds,
|
await axios.post(pushoverlink, data);
|
||||||
"priority": notification.pushoverpriority,
|
|
||||||
"title": notification.pushovertitle,
|
|
||||||
"retry": "30",
|
|
||||||
"expire": "3600",
|
|
||||||
"html": 1,
|
|
||||||
}
|
|
||||||
await axios.post(pushoverlink, data)
|
|
||||||
return okMsg;
|
return okMsg;
|
||||||
}
|
}
|
||||||
|
|
||||||
let data = {
|
|
||||||
"message": "<b>Uptime Kuma Alert</b>\n\n<b>Message</b>:" + msg + "\n<b>Time (UTC)</b>:" + heartbeatJSON["time"],
|
|
||||||
"user": notification.pushoveruserkey,
|
|
||||||
"token": notification.pushoverapptoken,
|
|
||||||
"sound": notification.pushoversounds,
|
|
||||||
"priority": notification.pushoverpriority,
|
|
||||||
"title": notification.pushovertitle,
|
|
||||||
"retry": "30",
|
|
||||||
"expire": "3600",
|
|
||||||
"html": 1,
|
|
||||||
}
|
|
||||||
await axios.post(pushoverlink, data)
|
|
||||||
return okMsg;
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.throwGeneralAxiosError(error)
|
this.throwGeneralAxiosError(error);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
44
server/notification-providers/serwersms.js
Normal file
44
server/notification-providers/serwersms.js
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
const NotificationProvider = require("./notification-provider");
|
||||||
|
const axios = require("axios");
|
||||||
|
|
||||||
|
class SerwerSMS extends NotificationProvider {
|
||||||
|
|
||||||
|
name = "serwersms";
|
||||||
|
|
||||||
|
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
|
||||||
|
let okMsg = "Sent Successfully.";
|
||||||
|
|
||||||
|
try {
|
||||||
|
let config = {
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let data = {
|
||||||
|
"username": notification.serwersmsUsername,
|
||||||
|
"password": notification.serwersmsPassword,
|
||||||
|
"phone": notification.serwersmsPhoneNumber,
|
||||||
|
"text": msg.replace(/[^\x00-\x7F]/g, ""),
|
||||||
|
"sender": notification.serwersmsSenderName,
|
||||||
|
};
|
||||||
|
|
||||||
|
let resp = await axios.post("https://api2.serwersms.pl/messages/send_sms", data, config);
|
||||||
|
|
||||||
|
if (!resp.data.success) {
|
||||||
|
if (resp.data.error) {
|
||||||
|
let error = `SerwerSMS.pl API returned error code ${resp.data.error.code} (${resp.data.error.type}) with error message: ${resp.data.error.message}`;
|
||||||
|
this.throwGeneralAxiosError(error);
|
||||||
|
} else {
|
||||||
|
let error = "SerwerSMS.pl API returned an unexpected response";
|
||||||
|
this.throwGeneralAxiosError(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return okMsg;
|
||||||
|
} catch (error) {
|
||||||
|
this.throwGeneralAxiosError(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = SerwerSMS;
|
@@ -14,9 +14,21 @@ class SMTP extends NotificationProvider {
|
|||||||
secure: notification.smtpSecure,
|
secure: notification.smtpSecure,
|
||||||
tls: {
|
tls: {
|
||||||
rejectUnauthorized: notification.smtpIgnoreTLSError || false,
|
rejectUnauthorized: notification.smtpIgnoreTLSError || false,
|
||||||
},
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Fix #1129
|
||||||
|
if (notification.smtpDkimDomain) {
|
||||||
|
config.dkim = {
|
||||||
|
domainName: notification.smtpDkimDomain,
|
||||||
|
keySelector: notification.smtpDkimKeySelector,
|
||||||
|
privateKey: notification.smtpDkimPrivateKey,
|
||||||
|
hashAlgo: notification.smtpDkimHashAlgo,
|
||||||
|
headerFieldNames: notification.smtpDkimheaderFieldNames,
|
||||||
|
skipFields: notification.smtpDkimskipFields,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
// Should fix the issue in https://github.com/louislam/uptime-kuma/issues/26#issuecomment-896373904
|
// Should fix the issue in https://github.com/louislam/uptime-kuma/issues/26#issuecomment-896373904
|
||||||
if (notification.smtpUsername || notification.smtpPassword) {
|
if (notification.smtpUsername || notification.smtpPassword) {
|
||||||
config.auth = {
|
config.auth = {
|
||||||
|
41
server/notification-providers/stackfield.js
Normal file
41
server/notification-providers/stackfield.js
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
const NotificationProvider = require("./notification-provider");
|
||||||
|
const axios = require("axios");
|
||||||
|
const { setting } = require("../util-server");
|
||||||
|
const { getMonitorRelativeURL } = require("../../src/util");
|
||||||
|
|
||||||
|
class Stackfield extends NotificationProvider {
|
||||||
|
|
||||||
|
name = "stackfield";
|
||||||
|
|
||||||
|
async send(notification, msg, monitorJSON = null) {
|
||||||
|
let okMsg = "Sent Successfully.";
|
||||||
|
try {
|
||||||
|
// Stackfield message formatting: https://www.stackfield.com/help/formatting-messages-2001
|
||||||
|
|
||||||
|
let textMsg = "+Uptime Kuma Alert+";
|
||||||
|
|
||||||
|
if (monitorJSON && monitorJSON.name) {
|
||||||
|
textMsg += `\n*${monitorJSON.name}*`;
|
||||||
|
}
|
||||||
|
|
||||||
|
textMsg += `\n${msg}`;
|
||||||
|
|
||||||
|
const baseURL = await setting("primaryBaseURL");
|
||||||
|
if (baseURL) {
|
||||||
|
textMsg += `\n${baseURL + getMonitorRelativeURL(monitorJSON.id)}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = {
|
||||||
|
"Title": textMsg,
|
||||||
|
};
|
||||||
|
|
||||||
|
await axios.post(notification.stackfieldwebhookURL, data);
|
||||||
|
return okMsg;
|
||||||
|
} catch (error) {
|
||||||
|
this.throwGeneralAxiosError(error);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = Stackfield;
|
23
server/notification-providers/techulus-push.js
Normal file
23
server/notification-providers/techulus-push.js
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
const NotificationProvider = require("./notification-provider");
|
||||||
|
const axios = require("axios");
|
||||||
|
|
||||||
|
class TechulusPush extends NotificationProvider {
|
||||||
|
|
||||||
|
name = "PushByTechulus";
|
||||||
|
|
||||||
|
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
|
||||||
|
let okMsg = "Sent Successfully.";
|
||||||
|
|
||||||
|
try {
|
||||||
|
await axios.post(`https://push.techulus.com/api/v1/notify/${notification.pushAPIKey}`, {
|
||||||
|
"title": "Uptime-Kuma",
|
||||||
|
"body": msg,
|
||||||
|
})
|
||||||
|
return okMsg;
|
||||||
|
} catch (error) {
|
||||||
|
this.throwGeneralAxiosError(error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = TechulusPush;
|
47
server/notification-providers/wecom.js
Normal file
47
server/notification-providers/wecom.js
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
const NotificationProvider = require("./notification-provider");
|
||||||
|
const axios = require("axios");
|
||||||
|
const { DOWN, UP } = require("../../src/util");
|
||||||
|
|
||||||
|
class WeCom extends NotificationProvider {
|
||||||
|
|
||||||
|
name = "WeCom";
|
||||||
|
|
||||||
|
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
|
||||||
|
let okMsg = "Sent Successfully.";
|
||||||
|
|
||||||
|
try {
|
||||||
|
let WeComUrl = "https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=" + notification.weComBotKey;
|
||||||
|
let config = {
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json"
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let body = this.composeMessage(heartbeatJSON, msg);
|
||||||
|
await axios.post(WeComUrl, body, config);
|
||||||
|
return okMsg;
|
||||||
|
} catch (error) {
|
||||||
|
this.throwGeneralAxiosError(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
composeMessage(heartbeatJSON, msg) {
|
||||||
|
let title;
|
||||||
|
if (msg != null && heartbeatJSON != null && heartbeatJSON['status'] == UP) {
|
||||||
|
title = "UptimeKuma Monitor Up";
|
||||||
|
}
|
||||||
|
if (msg != null && heartbeatJSON != null && heartbeatJSON["status"] == DOWN) {
|
||||||
|
title = "UptimeKuma Monitor Down";
|
||||||
|
}
|
||||||
|
if (msg != null) {
|
||||||
|
title = "UptimeKuma Message";
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
msgtype: "text",
|
||||||
|
text: {
|
||||||
|
content: title + msg
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = WeCom;
|
@@ -12,6 +12,7 @@ const ClickSendSMS = require("./notification-providers/clicksendsms");
|
|||||||
const Pushbullet = require("./notification-providers/pushbullet");
|
const Pushbullet = require("./notification-providers/pushbullet");
|
||||||
const Pushover = require("./notification-providers/pushover");
|
const Pushover = require("./notification-providers/pushover");
|
||||||
const Pushy = require("./notification-providers/pushy");
|
const Pushy = require("./notification-providers/pushy");
|
||||||
|
const TechulusPush = require("./notification-providers/techulus-push");
|
||||||
const RocketChat = require("./notification-providers/rocket-chat");
|
const RocketChat = require("./notification-providers/rocket-chat");
|
||||||
const Signal = require("./notification-providers/signal");
|
const Signal = require("./notification-providers/signal");
|
||||||
const Slack = require("./notification-providers/slack");
|
const Slack = require("./notification-providers/slack");
|
||||||
@@ -23,6 +24,12 @@ const Feishu = require("./notification-providers/feishu");
|
|||||||
const AliyunSms = require("./notification-providers/aliyun-sms");
|
const AliyunSms = require("./notification-providers/aliyun-sms");
|
||||||
const DingDing = require("./notification-providers/dingding");
|
const DingDing = require("./notification-providers/dingding");
|
||||||
const Bark = require("./notification-providers/bark");
|
const Bark = require("./notification-providers/bark");
|
||||||
|
const SerwerSMS = require("./notification-providers/serwersms");
|
||||||
|
const Stackfield = require("./notification-providers/stackfield");
|
||||||
|
const WeCom = require("./notification-providers/wecom");
|
||||||
|
const GoogleChat = require("./notification-providers/google-chat");
|
||||||
|
const Gorush = require("./notification-providers/gorush");
|
||||||
|
const Alerta = require("./notification-providers/alerta");
|
||||||
|
|
||||||
class Notification {
|
class Notification {
|
||||||
|
|
||||||
@@ -51,6 +58,7 @@ class Notification {
|
|||||||
new Pushbullet(),
|
new Pushbullet(),
|
||||||
new Pushover(),
|
new Pushover(),
|
||||||
new Pushy(),
|
new Pushy(),
|
||||||
|
new TechulusPush(),
|
||||||
new RocketChat(),
|
new RocketChat(),
|
||||||
new Signal(),
|
new Signal(),
|
||||||
new Slack(),
|
new Slack(),
|
||||||
@@ -58,6 +66,12 @@ class Notification {
|
|||||||
new Telegram(),
|
new Telegram(),
|
||||||
new Webhook(),
|
new Webhook(),
|
||||||
new Bark(),
|
new Bark(),
|
||||||
|
new SerwerSMS(),
|
||||||
|
new Stackfield(),
|
||||||
|
new WeCom(),
|
||||||
|
new GoogleChat(),
|
||||||
|
new Gorush(),
|
||||||
|
new Alerta(),
|
||||||
];
|
];
|
||||||
|
|
||||||
for (let item of list) {
|
for (let item of list) {
|
||||||
|
@@ -48,7 +48,7 @@ function Ping(host, options) {
|
|||||||
this._args = (options.args) ? options.args : [ "-n", "-t", timeout, "-c", "1", host ];
|
this._args = (options.args) ? options.args : [ "-n", "-t", timeout, "-c", "1", host ];
|
||||||
this._regmatch = /=([0-9.]+?) ms/;
|
this._regmatch = /=([0-9.]+?) ms/;
|
||||||
|
|
||||||
} else if (util.FBSD) {
|
} else if (util.BSD) {
|
||||||
this._bin = "/sbin/ping";
|
this._bin = "/sbin/ping";
|
||||||
|
|
||||||
const defaultArgs = [ "-n", "-t", timeout, "-c", "1", host ];
|
const defaultArgs = [ "-n", "-t", timeout, "-c", "1", host ];
|
||||||
|
@@ -60,7 +60,9 @@ class Prometheus {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
monitor_cert_days_remaining.set(this.monitorLabelValues, tlsInfo.certInfo.daysRemaining);
|
if (tlsInfo.certInfo != null) {
|
||||||
|
monitor_cert_days_remaining.set(this.monitorLabelValues, tlsInfo.certInfo.daysRemaining);
|
||||||
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
}
|
}
|
||||||
@@ -84,6 +86,16 @@ class Prometheus {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
remove() {
|
||||||
|
try {
|
||||||
|
monitor_cert_days_remaining.remove(this.monitorLabelValues);
|
||||||
|
monitor_cert_is_valid.remove(this.monitorLabelValues);
|
||||||
|
monitor_response_time.remove(this.monitorLabelValues);
|
||||||
|
monitor_status.remove(this.monitorLabelValues);
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
|
187
server/proxy.js
Normal file
187
server/proxy.js
Normal file
@@ -0,0 +1,187 @@
|
|||||||
|
const { R } = require("redbean-node");
|
||||||
|
const HttpProxyAgent = require("http-proxy-agent");
|
||||||
|
const HttpsProxyAgent = require("https-proxy-agent");
|
||||||
|
const SocksProxyAgent = require("socks-proxy-agent");
|
||||||
|
const { debug } = require("../src/util");
|
||||||
|
const server = require("./server");
|
||||||
|
|
||||||
|
class Proxy {
|
||||||
|
|
||||||
|
static SUPPORTED_PROXY_PROTOCOLS = ["http", "https", "socks", "socks5", "socks4"]
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Saves and updates given proxy entity
|
||||||
|
*
|
||||||
|
* @param proxy
|
||||||
|
* @param proxyID
|
||||||
|
* @param userID
|
||||||
|
* @return {Promise<Bean>}
|
||||||
|
*/
|
||||||
|
static async save(proxy, proxyID, userID) {
|
||||||
|
let bean;
|
||||||
|
|
||||||
|
if (proxyID) {
|
||||||
|
bean = await R.findOne("proxy", " id = ? AND user_id = ? ", [proxyID, userID]);
|
||||||
|
|
||||||
|
if (!bean) {
|
||||||
|
throw new Error("proxy not found");
|
||||||
|
}
|
||||||
|
|
||||||
|
} else {
|
||||||
|
bean = R.dispense("proxy");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Make sure given proxy protocol is supported
|
||||||
|
if (!this.SUPPORTED_PROXY_PROTOCOLS.includes(proxy.protocol)) {
|
||||||
|
throw new Error(`
|
||||||
|
Unsupported proxy protocol "${proxy.protocol}.
|
||||||
|
Supported protocols are ${this.SUPPORTED_PROXY_PROTOCOLS.join(", ")}."`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// When proxy is default update deactivate old default proxy
|
||||||
|
if (proxy.default) {
|
||||||
|
await R.exec("UPDATE proxy SET `default` = 0 WHERE `default` = 1");
|
||||||
|
}
|
||||||
|
|
||||||
|
bean.user_id = userID;
|
||||||
|
bean.protocol = proxy.protocol;
|
||||||
|
bean.host = proxy.host;
|
||||||
|
bean.port = proxy.port;
|
||||||
|
bean.auth = proxy.auth;
|
||||||
|
bean.username = proxy.username;
|
||||||
|
bean.password = proxy.password;
|
||||||
|
bean.active = proxy.active || true;
|
||||||
|
bean.default = proxy.default || false;
|
||||||
|
|
||||||
|
await R.store(bean);
|
||||||
|
|
||||||
|
if (proxy.applyExisting) {
|
||||||
|
await applyProxyEveryMonitor(bean.id, userID);
|
||||||
|
}
|
||||||
|
|
||||||
|
return bean;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Deletes proxy with given id and removes it from monitors
|
||||||
|
*
|
||||||
|
* @param proxyID
|
||||||
|
* @param userID
|
||||||
|
* @return {Promise<void>}
|
||||||
|
*/
|
||||||
|
static async delete(proxyID, userID) {
|
||||||
|
const bean = await R.findOne("proxy", " id = ? AND user_id = ? ", [proxyID, userID]);
|
||||||
|
|
||||||
|
if (!bean) {
|
||||||
|
throw new Error("proxy not found");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete removed proxy from monitors if exists
|
||||||
|
await R.exec("UPDATE monitor SET proxy_id = null WHERE proxy_id = ?", [proxyID]);
|
||||||
|
|
||||||
|
// Delete proxy from list
|
||||||
|
await R.trash(bean);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create HTTP and HTTPS agents related with given proxy bean object
|
||||||
|
*
|
||||||
|
* @param proxy proxy bean object
|
||||||
|
* @param options http and https agent options
|
||||||
|
* @return {{httpAgent: Agent, httpsAgent: Agent}}
|
||||||
|
*/
|
||||||
|
static createAgents(proxy, options) {
|
||||||
|
const { httpAgentOptions, httpsAgentOptions } = options || {};
|
||||||
|
let agent;
|
||||||
|
let httpAgent;
|
||||||
|
let httpsAgent;
|
||||||
|
|
||||||
|
const proxyOptions = {
|
||||||
|
protocol: proxy.protocol,
|
||||||
|
host: proxy.host,
|
||||||
|
port: proxy.port,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (proxy.auth) {
|
||||||
|
proxyOptions.auth = `${proxy.username}:${proxy.password}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
debug(`Proxy Options: ${JSON.stringify(proxyOptions)}`);
|
||||||
|
debug(`HTTP Agent Options: ${JSON.stringify(httpAgentOptions)}`);
|
||||||
|
debug(`HTTPS Agent Options: ${JSON.stringify(httpsAgentOptions)}`);
|
||||||
|
|
||||||
|
switch (proxy.protocol) {
|
||||||
|
case "http":
|
||||||
|
case "https":
|
||||||
|
httpAgent = new HttpProxyAgent({
|
||||||
|
...httpAgentOptions || {},
|
||||||
|
...proxyOptions
|
||||||
|
});
|
||||||
|
|
||||||
|
httpsAgent = new HttpsProxyAgent({
|
||||||
|
...httpsAgentOptions || {},
|
||||||
|
...proxyOptions,
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
case "socks":
|
||||||
|
case "socks5":
|
||||||
|
case "socks4":
|
||||||
|
agent = new SocksProxyAgent({
|
||||||
|
...httpAgentOptions,
|
||||||
|
...httpsAgentOptions,
|
||||||
|
...proxyOptions,
|
||||||
|
});
|
||||||
|
|
||||||
|
httpAgent = agent;
|
||||||
|
httpsAgent = agent;
|
||||||
|
break;
|
||||||
|
|
||||||
|
default: throw new Error(`Unsupported proxy protocol provided. ${proxy.protocol}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
httpAgent,
|
||||||
|
httpsAgent
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reload proxy settings for current monitors
|
||||||
|
* @returns {Promise<void>}
|
||||||
|
*/
|
||||||
|
static async reloadProxy() {
|
||||||
|
let updatedList = await R.getAssoc("SELECT id, proxy_id FROM monitor");
|
||||||
|
|
||||||
|
for (let monitorID in server.monitorList) {
|
||||||
|
let monitor = server.monitorList[monitorID];
|
||||||
|
|
||||||
|
if (updatedList[monitorID]) {
|
||||||
|
monitor.proxy_id = updatedList[monitorID].proxy_id;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Applies given proxy id to monitors
|
||||||
|
*
|
||||||
|
* @param proxyID
|
||||||
|
* @param userID
|
||||||
|
* @return {Promise<void>}
|
||||||
|
*/
|
||||||
|
async function applyProxyEveryMonitor(proxyID, userID) {
|
||||||
|
// Find all monitors with id and proxy id
|
||||||
|
const monitors = await R.getAll("SELECT id, proxy_id FROM monitor WHERE user_id = ?", [userID]);
|
||||||
|
|
||||||
|
// Update proxy id not match with given proxy id
|
||||||
|
for (const monitor of monitors) {
|
||||||
|
if (monitor.proxy_id !== proxyID) {
|
||||||
|
await R.exec("UPDATE monitor SET proxy_id = ? WHERE id = ?", [proxyID, monitor.id]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
Proxy,
|
||||||
|
};
|
@@ -34,6 +34,14 @@ const loginRateLimiter = new KumaRateLimiter({
|
|||||||
errorMessage: "Too frequently, try again later."
|
errorMessage: "Too frequently, try again later."
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const twoFaRateLimiter = new KumaRateLimiter({
|
||||||
|
tokensPerInterval: 30,
|
||||||
|
interval: "minute",
|
||||||
|
fireImmediately: true,
|
||||||
|
errorMessage: "Too frequently, try again later."
|
||||||
|
});
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
loginRateLimiter
|
loginRateLimiter,
|
||||||
|
twoFaRateLimiter,
|
||||||
};
|
};
|
||||||
|
@@ -6,14 +6,25 @@ 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, debug } = require("../../src/util");
|
const { UP, flipStatus, debug } = require("../../src/util");
|
||||||
|
const StatusPage = require("../model/status_page");
|
||||||
let router = express.Router();
|
let router = express.Router();
|
||||||
|
|
||||||
let cache = apicache.middleware;
|
let cache = apicache.middleware;
|
||||||
let io = server.io;
|
let io = server.io;
|
||||||
|
|
||||||
router.get("/api/entry-page", async (_, response) => {
|
router.get("/api/entry-page", async (request, response) => {
|
||||||
allowDevAllOrigin(response);
|
allowDevAllOrigin(response);
|
||||||
response.json(server.entryPage);
|
|
||||||
|
let result = { };
|
||||||
|
|
||||||
|
if (request.hostname in StatusPage.domainMappingList) {
|
||||||
|
result.type = "statusPageMatchedDomain";
|
||||||
|
result.statusPageSlug = StatusPage.domainMappingList[request.hostname];
|
||||||
|
} else {
|
||||||
|
result.type = "entryPage";
|
||||||
|
result.entryPage = server.entryPage;
|
||||||
|
}
|
||||||
|
response.json(result);
|
||||||
});
|
});
|
||||||
|
|
||||||
router.get("/api/push/:pushToken", async (request, response) => {
|
router.get("/api/push/:pushToken", async (request, response) => {
|
||||||
@@ -31,12 +42,7 @@ router.get("/api/push/:pushToken", async (request, response) => {
|
|||||||
throw new Error("Monitor not found or not active.");
|
throw new Error("Monitor not found or not active.");
|
||||||
}
|
}
|
||||||
|
|
||||||
const previousHeartbeat = await R.getRow(`
|
const previousHeartbeat = await Monitor.getPreviousHeartbeat(monitor.id);
|
||||||
SELECT status, time FROM heartbeat
|
|
||||||
WHERE id = (select MAX(id) from heartbeat where monitor_id = ?)
|
|
||||||
`, [
|
|
||||||
monitor.id
|
|
||||||
]);
|
|
||||||
|
|
||||||
let status = UP;
|
let status = UP;
|
||||||
if (monitor.isUpsideDown()) {
|
if (monitor.isUpsideDown()) {
|
||||||
@@ -87,88 +93,80 @@ router.get("/api/push/:pushToken", async (request, response) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Status Page Config
|
// Status page config, incident, monitor list
|
||||||
router.get("/api/status-page/config", async (_request, response) => {
|
router.get("/api/status-page/:slug", cache("5 minutes"), async (request, response) => {
|
||||||
allowDevAllOrigin(response);
|
allowDevAllOrigin(response);
|
||||||
|
let slug = request.params.slug;
|
||||||
|
|
||||||
let config = await getSettings("statusPage");
|
// Get Status Page
|
||||||
|
let statusPage = await R.findOne("status_page", " slug = ? ", [
|
||||||
|
slug
|
||||||
|
]);
|
||||||
|
|
||||||
if (! config.statusPageTheme) {
|
if (!statusPage) {
|
||||||
config.statusPageTheme = "light";
|
response.statusCode = 404;
|
||||||
|
response.json({
|
||||||
|
msg: "Not Found"
|
||||||
|
});
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (! config.statusPagePublished) {
|
|
||||||
config.statusPagePublished = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (! config.title) {
|
|
||||||
config.title = "Uptime Kuma";
|
|
||||||
}
|
|
||||||
|
|
||||||
response.json(config);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Status Page - Get the current Incident
|
|
||||||
// Can fetch only if published
|
|
||||||
router.get("/api/status-page/incident", async (_, response) => {
|
|
||||||
allowDevAllOrigin(response);
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await checkPublished();
|
// Incident
|
||||||
|
let incident = await R.findOne("incident", " pin = 1 AND active = 1 AND status_page_id = ? ", [
|
||||||
let incident = await R.findOne("incident", " pin = 1 AND active = 1");
|
statusPage.id,
|
||||||
|
]);
|
||||||
|
|
||||||
if (incident) {
|
if (incident) {
|
||||||
incident = incident.toPublicJSON();
|
incident = incident.toPublicJSON();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Public Group List
|
||||||
|
const publicGroupList = [];
|
||||||
|
const showTags = !!statusPage.show_tags;
|
||||||
|
debug("Show Tags???" + showTags);
|
||||||
|
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({
|
response.json({
|
||||||
ok: true,
|
config: await statusPage.toPublicJSON(),
|
||||||
incident,
|
incident,
|
||||||
|
publicGroupList
|
||||||
});
|
});
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
send403(response, error.message);
|
send403(response, error.message);
|
||||||
}
|
}
|
||||||
});
|
|
||||||
|
|
||||||
// Status Page - Monitor List
|
|
||||||
// Can fetch only if published
|
|
||||||
router.get("/api/status-page/monitor-list", cache("5 minutes"), async (_request, response) => {
|
|
||||||
allowDevAllOrigin(response);
|
|
||||||
|
|
||||||
try {
|
|
||||||
await checkPublished();
|
|
||||||
const publicGroupList = [];
|
|
||||||
let list = await R.find("group", " public = 1 ORDER BY weight ");
|
|
||||||
|
|
||||||
for (let groupBean of list) {
|
|
||||||
publicGroupList.push(await groupBean.toPublicJSON());
|
|
||||||
}
|
|
||||||
|
|
||||||
response.json(publicGroupList);
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
send403(response, error.message);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// Status Page Polling Data
|
// Status Page Polling Data
|
||||||
// Can fetch only if published
|
// Can fetch only if published
|
||||||
router.get("/api/status-page/heartbeat", cache("5 minutes"), async (_request, response) => {
|
router.get("/api/status-page/heartbeat/:slug", cache("1 minutes"), async (request, response) => {
|
||||||
allowDevAllOrigin(response);
|
allowDevAllOrigin(response);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await checkPublished();
|
|
||||||
|
|
||||||
let heartbeatList = {};
|
let heartbeatList = {};
|
||||||
let uptimeList = {};
|
let uptimeList = {};
|
||||||
|
|
||||||
|
let slug = request.params.slug;
|
||||||
|
let statusPageID = await StatusPage.slugToID(slug);
|
||||||
|
|
||||||
let monitorIDList = await R.getCol(`
|
let monitorIDList = await R.getCol(`
|
||||||
SELECT monitor_group.monitor_id FROM monitor_group, \`group\`
|
SELECT monitor_group.monitor_id FROM monitor_group, \`group\`
|
||||||
WHERE monitor_group.group_id = \`group\`.id
|
WHERE monitor_group.group_id = \`group\`.id
|
||||||
AND public = 1
|
AND public = 1
|
||||||
`);
|
AND \`group\`.status_page_id = ?
|
||||||
|
`, [
|
||||||
|
statusPageID
|
||||||
|
]);
|
||||||
|
|
||||||
for (let monitorID of monitorIDList) {
|
for (let monitorID of monitorIDList) {
|
||||||
let list = await R.getAll(`
|
let list = await R.getAll(`
|
||||||
@@ -197,22 +195,12 @@ router.get("/api/status-page/heartbeat", cache("5 minutes"), async (_request, re
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
async function checkPublished() {
|
|
||||||
if (! await isPublished()) {
|
|
||||||
throw new Error("The status page is not published");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Default is published
|
* Default is published
|
||||||
* @returns {Promise<boolean>}
|
* @returns {Promise<boolean>}
|
||||||
*/
|
*/
|
||||||
async function isPublished() {
|
async function isPublished() {
|
||||||
const value = await setting("statusPagePublished");
|
return true;
|
||||||
if (value === null) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return value;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function send403(res, msg = "") {
|
function send403(res, msg = "") {
|
||||||
|
303
server/server.js
303
server/server.js
@@ -1,4 +1,15 @@
|
|||||||
console.log("Welcome to Uptime Kuma");
|
console.log("Welcome to Uptime Kuma");
|
||||||
|
|
||||||
|
// Check Node.js Version
|
||||||
|
const nodeVersion = parseInt(process.versions.node.split(".")[0]);
|
||||||
|
const requiredVersion = 14;
|
||||||
|
console.log(`Your Node.js version: ${nodeVersion}`);
|
||||||
|
|
||||||
|
if (nodeVersion < requiredVersion) {
|
||||||
|
console.error(`Error: Your Node.js version is not supported, please upgrade to Node.js >= ${requiredVersion}.`);
|
||||||
|
process.exit(-1);
|
||||||
|
}
|
||||||
|
|
||||||
const args = require("args-parser")(process.argv);
|
const args = require("args-parser")(process.argv);
|
||||||
const { sleep, debug, getRandomInt, genSecret } = require("../src/util");
|
const { sleep, debug, getRandomInt, genSecret } = require("../src/util");
|
||||||
const config = require("./config");
|
const config = require("./config");
|
||||||
@@ -37,22 +48,46 @@ debug("Importing 2FA Modules");
|
|||||||
const notp = require("notp");
|
const notp = require("notp");
|
||||||
const base32 = require("thirty-two");
|
const base32 = require("thirty-two");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* `module.exports` (alias: `server`) should be inside this class, in order to avoid circular dependency issue.
|
||||||
|
* @type {UptimeKumaServer}
|
||||||
|
*/
|
||||||
|
class UptimeKumaServer {
|
||||||
|
/**
|
||||||
|
* Main monitor list
|
||||||
|
* @type {{}}
|
||||||
|
*/
|
||||||
|
monitorList = {};
|
||||||
|
entryPage = "dashboard";
|
||||||
|
|
||||||
|
async sendMonitorList(socket) {
|
||||||
|
let list = await getMonitorJSONList(socket.userID);
|
||||||
|
io.to(socket.userID).emit("monitorList", list);
|
||||||
|
return list;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const server = module.exports = new UptimeKumaServer();
|
||||||
|
|
||||||
console.log("Importing this project modules");
|
console.log("Importing this project modules");
|
||||||
debug("Importing Monitor");
|
debug("Importing Monitor");
|
||||||
const Monitor = require("./model/monitor");
|
const Monitor = require("./model/monitor");
|
||||||
debug("Importing Settings");
|
debug("Importing Settings");
|
||||||
const { getSettings, setSettings, setting, initJWTSecret, checkLogin, startUnitTest, FBSD, errorLog } = require("./util-server");
|
const { getSettings, setSettings, setting, initJWTSecret, checkLogin, startUnitTest, FBSD, errorLog, doubleCheckPassword } = require("./util-server");
|
||||||
|
|
||||||
debug("Importing Notification");
|
debug("Importing Notification");
|
||||||
const { Notification } = require("./notification");
|
const { Notification } = require("./notification");
|
||||||
Notification.init();
|
Notification.init();
|
||||||
|
|
||||||
|
debug("Importing Proxy");
|
||||||
|
const { Proxy } = require("./proxy");
|
||||||
|
|
||||||
debug("Importing Database");
|
debug("Importing Database");
|
||||||
const Database = require("./database");
|
const Database = require("./database");
|
||||||
|
|
||||||
debug("Importing Background Jobs");
|
debug("Importing Background Jobs");
|
||||||
const { initBackgroundJobs } = require("./jobs");
|
const { initBackgroundJobs, stopBackgroundJobs } = require("./jobs");
|
||||||
const { loginRateLimiter } = require("./rate-limiter");
|
const { loginRateLimiter, twoFaRateLimiter } = require("./rate-limiter");
|
||||||
|
|
||||||
const { basicAuth } = require("./auth");
|
const { basicAuth } = require("./auth");
|
||||||
const { login } = require("./auth");
|
const { login } = require("./auth");
|
||||||
@@ -80,6 +115,7 @@ const port = parseInt(process.env.UPTIME_KUMA_PORT || process.env.PORT || args.p
|
|||||||
const sslKey = process.env.UPTIME_KUMA_SSL_KEY || process.env.SSL_KEY || args["ssl-key"] || undefined;
|
const sslKey = process.env.UPTIME_KUMA_SSL_KEY || process.env.SSL_KEY || args["ssl-key"] || undefined;
|
||||||
const sslCert = process.env.UPTIME_KUMA_SSL_CERT || process.env.SSL_CERT || args["ssl-cert"] || undefined;
|
const sslCert = process.env.UPTIME_KUMA_SSL_CERT || process.env.SSL_CERT || args["ssl-cert"] || undefined;
|
||||||
const disableFrameSameOrigin = !!process.env.UPTIME_KUMA_DISABLE_FRAME_SAMEORIGIN || args["disable-frame-sameorigin"] || false;
|
const disableFrameSameOrigin = !!process.env.UPTIME_KUMA_DISABLE_FRAME_SAMEORIGIN || args["disable-frame-sameorigin"] || false;
|
||||||
|
const cloudflaredToken = args["cloudflared-token"] || process.env.UPTIME_KUMA_CLOUDFLARED_TOKEN || undefined;
|
||||||
|
|
||||||
// 2FA / notp verification defaults
|
// 2FA / notp verification defaults
|
||||||
const twofa_verification_opts = {
|
const twofa_verification_opts = {
|
||||||
@@ -100,26 +136,30 @@ if (config.demoMode) {
|
|||||||
console.log("Creating express and socket.io instance");
|
console.log("Creating express and socket.io instance");
|
||||||
const app = express();
|
const app = express();
|
||||||
|
|
||||||
let server;
|
let httpServer;
|
||||||
|
|
||||||
if (sslKey && sslCert) {
|
if (sslKey && sslCert) {
|
||||||
console.log("Server Type: HTTPS");
|
console.log("Server Type: HTTPS");
|
||||||
server = https.createServer({
|
httpServer = https.createServer({
|
||||||
key: fs.readFileSync(sslKey),
|
key: fs.readFileSync(sslKey),
|
||||||
cert: fs.readFileSync(sslCert)
|
cert: fs.readFileSync(sslCert)
|
||||||
}, app);
|
}, app);
|
||||||
} else {
|
} else {
|
||||||
console.log("Server Type: HTTP");
|
console.log("Server Type: HTTP");
|
||||||
server = http.createServer(app);
|
httpServer = http.createServer(app);
|
||||||
}
|
}
|
||||||
|
|
||||||
const io = new Server(server);
|
const io = new Server(httpServer);
|
||||||
module.exports.io = io;
|
module.exports.io = io;
|
||||||
|
|
||||||
// Must be after io instantiation
|
// Must be after io instantiation
|
||||||
const { sendNotificationList, sendHeartbeatList, sendImportantHeartbeatList, sendInfo } = require("./client");
|
const { sendNotificationList, sendHeartbeatList, sendImportantHeartbeatList, sendInfo, sendProxyList } = require("./client");
|
||||||
const { statusPageSocketHandler } = require("./socket-handlers/status-page-socket-handler");
|
const { statusPageSocketHandler } = require("./socket-handlers/status-page-socket-handler");
|
||||||
const databaseSocketHandler = require("./socket-handlers/database-socket-handler");
|
const databaseSocketHandler = require("./socket-handlers/database-socket-handler");
|
||||||
|
const TwoFA = require("./2fa");
|
||||||
|
const StatusPage = require("./model/status_page");
|
||||||
|
const { cloudflaredSocketHandler, autoStart: cloudflaredAutoStart, stop: cloudflaredStop } = require("./socket-handlers/cloudflared-socket-handler");
|
||||||
|
const { proxySocketHandler } = require("./socket-handlers/proxy-socket-handler");
|
||||||
|
|
||||||
app.use(express.json());
|
app.use(express.json());
|
||||||
|
|
||||||
@@ -144,12 +184,6 @@ let totalClient = 0;
|
|||||||
*/
|
*/
|
||||||
let jwtSecret = null;
|
let jwtSecret = null;
|
||||||
|
|
||||||
/**
|
|
||||||
* Main monitor list
|
|
||||||
* @type {{}}
|
|
||||||
*/
|
|
||||||
let monitorList = {};
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Show Setup Page
|
* Show Setup Page
|
||||||
* @type {boolean}
|
* @type {boolean}
|
||||||
@@ -172,13 +206,12 @@ try {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
exports.entryPage = "dashboard";
|
|
||||||
|
|
||||||
(async () => {
|
(async () => {
|
||||||
Database.init(args);
|
Database.init(args);
|
||||||
await initDatabase();
|
await initDatabase(testMode);
|
||||||
|
|
||||||
exports.entryPage = await setting("entryPage");
|
exports.entryPage = await setting("entryPage");
|
||||||
|
await StatusPage.loadDomainMappingList();
|
||||||
|
|
||||||
console.log("Adding route");
|
console.log("Adding route");
|
||||||
|
|
||||||
@@ -186,6 +219,20 @@ exports.entryPage = "dashboard";
|
|||||||
// Normal Router here
|
// Normal Router here
|
||||||
// ***************************
|
// ***************************
|
||||||
|
|
||||||
|
// Entry Page
|
||||||
|
app.get("/", async (request, response) => {
|
||||||
|
debug(`Request Domain: ${request.hostname}`);
|
||||||
|
|
||||||
|
if (request.hostname in StatusPage.domainMappingList) {
|
||||||
|
debug("This is a status page domain");
|
||||||
|
response.send(indexHTML);
|
||||||
|
} else if (exports.entryPage && exports.entryPage.startsWith("statusPage-")) {
|
||||||
|
response.redirect("/status/" + exports.entryPage.replace("statusPage-", ""));
|
||||||
|
} else {
|
||||||
|
response.redirect("/dashboard");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// Robots.txt
|
// Robots.txt
|
||||||
app.get("/robots.txt", async (_request, response) => {
|
app.get("/robots.txt", async (_request, response) => {
|
||||||
let txt = "User-agent: *\nDisallow:";
|
let txt = "User-agent: *\nDisallow:";
|
||||||
@@ -283,6 +330,15 @@ exports.entryPage = "dashboard";
|
|||||||
socket.on("login", async (data, callback) => {
|
socket.on("login", async (data, callback) => {
|
||||||
console.log("Login");
|
console.log("Login");
|
||||||
|
|
||||||
|
// Checking
|
||||||
|
if (typeof callback !== "function") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!data) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Login Rate Limit
|
// Login Rate Limit
|
||||||
if (! await loginRateLimiter.pass(callback)) {
|
if (! await loginRateLimiter.pass(callback)) {
|
||||||
return;
|
return;
|
||||||
@@ -341,14 +397,27 @@ exports.entryPage = "dashboard";
|
|||||||
});
|
});
|
||||||
|
|
||||||
socket.on("logout", async (callback) => {
|
socket.on("logout", async (callback) => {
|
||||||
|
// Rate Limit
|
||||||
|
if (! await loginRateLimiter.pass(callback)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
socket.leave(socket.userID);
|
socket.leave(socket.userID);
|
||||||
socket.userID = null;
|
socket.userID = null;
|
||||||
callback();
|
|
||||||
|
if (typeof callback === "function") {
|
||||||
|
callback();
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
socket.on("prepare2FA", async (callback) => {
|
socket.on("prepare2FA", async (currentPassword, callback) => {
|
||||||
try {
|
try {
|
||||||
|
if (! await twoFaRateLimiter.pass(callback)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
checkLogin(socket);
|
checkLogin(socket);
|
||||||
|
await doubleCheckPassword(socket, currentPassword);
|
||||||
|
|
||||||
let user = await R.findOne("user", " id = ? AND active = 1 ", [
|
let user = await R.findOne("user", " id = ? AND active = 1 ", [
|
||||||
socket.userID,
|
socket.userID,
|
||||||
@@ -383,14 +452,19 @@ exports.entryPage = "dashboard";
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
callback({
|
callback({
|
||||||
ok: false,
|
ok: false,
|
||||||
msg: "Error while trying to prepare 2FA.",
|
msg: error.message,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
socket.on("save2FA", async (callback) => {
|
socket.on("save2FA", async (currentPassword, callback) => {
|
||||||
try {
|
try {
|
||||||
|
if (! await twoFaRateLimiter.pass(callback)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
checkLogin(socket);
|
checkLogin(socket);
|
||||||
|
await doubleCheckPassword(socket, currentPassword);
|
||||||
|
|
||||||
await R.exec("UPDATE `user` SET twofa_status = 1 WHERE id = ? ", [
|
await R.exec("UPDATE `user` SET twofa_status = 1 WHERE id = ? ", [
|
||||||
socket.userID,
|
socket.userID,
|
||||||
@@ -403,18 +477,20 @@ exports.entryPage = "dashboard";
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
callback({
|
callback({
|
||||||
ok: false,
|
ok: false,
|
||||||
msg: "Error while trying to change 2FA.",
|
msg: error.message,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
socket.on("disable2FA", async (callback) => {
|
socket.on("disable2FA", async (currentPassword, callback) => {
|
||||||
try {
|
try {
|
||||||
checkLogin(socket);
|
if (! await twoFaRateLimiter.pass(callback)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
await R.exec("UPDATE `user` SET twofa_status = 0 WHERE id = ? ", [
|
checkLogin(socket);
|
||||||
socket.userID,
|
await doubleCheckPassword(socket, currentPassword);
|
||||||
]);
|
await TwoFA.disable2FA(socket.userID);
|
||||||
|
|
||||||
callback({
|
callback({
|
||||||
ok: true,
|
ok: true,
|
||||||
@@ -423,36 +499,47 @@ exports.entryPage = "dashboard";
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
callback({
|
callback({
|
||||||
ok: false,
|
ok: false,
|
||||||
msg: "Error while trying to change 2FA.",
|
msg: error.message,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
socket.on("verifyToken", async (token, callback) => {
|
socket.on("verifyToken", async (token, currentPassword, callback) => {
|
||||||
let user = await R.findOne("user", " id = ? AND active = 1 ", [
|
try {
|
||||||
socket.userID,
|
checkLogin(socket);
|
||||||
]);
|
await doubleCheckPassword(socket, currentPassword);
|
||||||
|
|
||||||
let verify = notp.totp.verify(token, user.twofa_secret, twofa_verification_opts);
|
let user = await R.findOne("user", " id = ? AND active = 1 ", [
|
||||||
|
socket.userID,
|
||||||
|
]);
|
||||||
|
|
||||||
if (user.twofa_last_token !== token && verify) {
|
let verify = notp.totp.verify(token, user.twofa_secret, twofa_verification_opts);
|
||||||
callback({
|
|
||||||
ok: true,
|
if (user.twofa_last_token !== token && verify) {
|
||||||
valid: true,
|
callback({
|
||||||
});
|
ok: true,
|
||||||
} else {
|
valid: true,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
callback({
|
||||||
|
ok: false,
|
||||||
|
msg: "Invalid Token.",
|
||||||
|
valid: false,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
callback({
|
callback({
|
||||||
ok: false,
|
ok: false,
|
||||||
msg: "Invalid Token.",
|
msg: error.message,
|
||||||
valid: false,
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
socket.on("twoFAStatus", async (callback) => {
|
socket.on("twoFAStatus", async (callback) => {
|
||||||
checkLogin(socket);
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
checkLogin(socket);
|
||||||
|
|
||||||
let user = await R.findOne("user", " id = ? AND active = 1 ", [
|
let user = await R.findOne("user", " id = ? AND active = 1 ", [
|
||||||
socket.userID,
|
socket.userID,
|
||||||
]);
|
]);
|
||||||
@@ -469,9 +556,10 @@ exports.entryPage = "dashboard";
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
console.log(error);
|
||||||
callback({
|
callback({
|
||||||
ok: false,
|
ok: false,
|
||||||
msg: "Error while trying to get 2FA status.",
|
msg: error.message,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -532,8 +620,8 @@ exports.entryPage = "dashboard";
|
|||||||
|
|
||||||
await updateMonitorNotification(bean.id, notificationIDList);
|
await updateMonitorNotification(bean.id, notificationIDList);
|
||||||
|
|
||||||
|
await server.sendMonitorList(socket);
|
||||||
await startMonitor(socket.userID, bean.id);
|
await startMonitor(socket.userID, bean.id);
|
||||||
await sendMonitorList(socket);
|
|
||||||
|
|
||||||
callback({
|
callback({
|
||||||
ok: true,
|
ok: true,
|
||||||
@@ -560,12 +648,17 @@ exports.entryPage = "dashboard";
|
|||||||
throw new Error("Permission denied.");
|
throw new Error("Permission denied.");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Reset Prometheus labels
|
||||||
|
server.monitorList[monitor.id]?.prometheus()?.remove();
|
||||||
|
|
||||||
bean.name = monitor.name;
|
bean.name = monitor.name;
|
||||||
bean.type = monitor.type;
|
bean.type = monitor.type;
|
||||||
bean.url = monitor.url;
|
bean.url = monitor.url;
|
||||||
bean.method = monitor.method;
|
bean.method = monitor.method;
|
||||||
bean.body = monitor.body;
|
bean.body = monitor.body;
|
||||||
bean.headers = monitor.headers;
|
bean.headers = monitor.headers;
|
||||||
|
bean.basic_auth_user = monitor.basic_auth_user;
|
||||||
|
bean.basic_auth_pass = monitor.basic_auth_pass;
|
||||||
bean.interval = monitor.interval;
|
bean.interval = monitor.interval;
|
||||||
bean.retryInterval = monitor.retryInterval;
|
bean.retryInterval = monitor.retryInterval;
|
||||||
bean.hostname = monitor.hostname;
|
bean.hostname = monitor.hostname;
|
||||||
@@ -573,12 +666,14 @@ exports.entryPage = "dashboard";
|
|||||||
bean.port = monitor.port;
|
bean.port = monitor.port;
|
||||||
bean.keyword = monitor.keyword;
|
bean.keyword = monitor.keyword;
|
||||||
bean.ignoreTls = monitor.ignoreTls;
|
bean.ignoreTls = monitor.ignoreTls;
|
||||||
|
bean.expiryNotification = monitor.expiryNotification;
|
||||||
bean.upsideDown = monitor.upsideDown;
|
bean.upsideDown = monitor.upsideDown;
|
||||||
bean.maxredirects = monitor.maxredirects;
|
bean.maxredirects = monitor.maxredirects;
|
||||||
bean.accepted_statuscodes_json = JSON.stringify(monitor.accepted_statuscodes);
|
bean.accepted_statuscodes_json = JSON.stringify(monitor.accepted_statuscodes);
|
||||||
bean.dns_resolve_type = monitor.dns_resolve_type;
|
bean.dns_resolve_type = monitor.dns_resolve_type;
|
||||||
bean.dns_resolve_server = monitor.dns_resolve_server;
|
bean.dns_resolve_server = monitor.dns_resolve_server;
|
||||||
bean.pushToken = monitor.pushToken;
|
bean.pushToken = monitor.pushToken;
|
||||||
|
bean.proxyId = Number.isInteger(monitor.proxyId) ? monitor.proxyId : null;
|
||||||
|
|
||||||
await R.store(bean);
|
await R.store(bean);
|
||||||
|
|
||||||
@@ -588,7 +683,7 @@ exports.entryPage = "dashboard";
|
|||||||
await restartMonitor(socket.userID, bean.id);
|
await restartMonitor(socket.userID, bean.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
await sendMonitorList(socket);
|
await server.sendMonitorList(socket);
|
||||||
|
|
||||||
callback({
|
callback({
|
||||||
ok: true,
|
ok: true,
|
||||||
@@ -608,7 +703,7 @@ exports.entryPage = "dashboard";
|
|||||||
socket.on("getMonitorList", async (callback) => {
|
socket.on("getMonitorList", async (callback) => {
|
||||||
try {
|
try {
|
||||||
checkLogin(socket);
|
checkLogin(socket);
|
||||||
await sendMonitorList(socket);
|
await server.sendMonitorList(socket);
|
||||||
callback({
|
callback({
|
||||||
ok: true,
|
ok: true,
|
||||||
});
|
});
|
||||||
@@ -682,7 +777,7 @@ exports.entryPage = "dashboard";
|
|||||||
try {
|
try {
|
||||||
checkLogin(socket);
|
checkLogin(socket);
|
||||||
await startMonitor(socket.userID, monitorID);
|
await startMonitor(socket.userID, monitorID);
|
||||||
await sendMonitorList(socket);
|
await server.sendMonitorList(socket);
|
||||||
|
|
||||||
callback({
|
callback({
|
||||||
ok: true,
|
ok: true,
|
||||||
@@ -701,7 +796,7 @@ exports.entryPage = "dashboard";
|
|||||||
try {
|
try {
|
||||||
checkLogin(socket);
|
checkLogin(socket);
|
||||||
await pauseMonitor(socket.userID, monitorID);
|
await pauseMonitor(socket.userID, monitorID);
|
||||||
await sendMonitorList(socket);
|
await server.sendMonitorList(socket);
|
||||||
|
|
||||||
callback({
|
callback({
|
||||||
ok: true,
|
ok: true,
|
||||||
@@ -722,9 +817,9 @@ exports.entryPage = "dashboard";
|
|||||||
|
|
||||||
console.log(`Delete Monitor: ${monitorID} User ID: ${socket.userID}`);
|
console.log(`Delete Monitor: ${monitorID} User ID: ${socket.userID}`);
|
||||||
|
|
||||||
if (monitorID in monitorList) {
|
if (monitorID in server.monitorList) {
|
||||||
monitorList[monitorID].stop();
|
server.monitorList[monitorID].stop();
|
||||||
delete monitorList[monitorID];
|
delete server.monitorList[monitorID];
|
||||||
}
|
}
|
||||||
|
|
||||||
await R.exec("DELETE FROM monitor WHERE id = ? AND user_id = ? ", [
|
await R.exec("DELETE FROM monitor WHERE id = ? AND user_id = ? ", [
|
||||||
@@ -737,7 +832,7 @@ exports.entryPage = "dashboard";
|
|||||||
msg: "Deleted Successfully.",
|
msg: "Deleted Successfully.",
|
||||||
});
|
});
|
||||||
|
|
||||||
await sendMonitorList(socket);
|
await server.sendMonitorList(socket);
|
||||||
// Clear heartbeat list on client
|
// Clear heartbeat list on client
|
||||||
await sendImportantHeartbeatList(socket, monitorID, true, true);
|
await sendImportantHeartbeatList(socket, monitorID, true, true);
|
||||||
|
|
||||||
@@ -915,21 +1010,13 @@ exports.entryPage = "dashboard";
|
|||||||
throw new Error("Password is too weak. It should contain alphabetic and numeric characters. It must be at least 6 characters in length.");
|
throw new Error("Password is too weak. It should contain alphabetic and numeric characters. It must be at least 6 characters in length.");
|
||||||
}
|
}
|
||||||
|
|
||||||
let user = await R.findOne("user", " id = ? AND active = 1 ", [
|
let user = await doubleCheckPassword(socket, password.currentPassword);
|
||||||
socket.userID,
|
await user.resetPassword(password.newPassword);
|
||||||
]);
|
|
||||||
|
|
||||||
if (user && passwordHash.verify(password.currentPassword, user.password)) {
|
callback({
|
||||||
|
ok: true,
|
||||||
user.resetPassword(password.newPassword);
|
msg: "Password has been updated successfully.",
|
||||||
|
});
|
||||||
callback({
|
|
||||||
ok: true,
|
|
||||||
msg: "Password has been updated successfully.",
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
throw new Error("Incorrect current password");
|
|
||||||
}
|
|
||||||
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
callback({
|
callback({
|
||||||
@@ -956,10 +1043,14 @@ exports.entryPage = "dashboard";
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
socket.on("setSettings", async (data, callback) => {
|
socket.on("setSettings", async (data, currentPassword, callback) => {
|
||||||
try {
|
try {
|
||||||
checkLogin(socket);
|
checkLogin(socket);
|
||||||
|
|
||||||
|
if (data.disableAuth) {
|
||||||
|
await doubleCheckPassword(socket, currentPassword);
|
||||||
|
}
|
||||||
|
|
||||||
await setSettings("general", data);
|
await setSettings("general", data);
|
||||||
exports.entryPage = data.entryPage;
|
exports.entryPage = data.entryPage;
|
||||||
|
|
||||||
@@ -1059,6 +1150,7 @@ exports.entryPage = "dashboard";
|
|||||||
console.log(`Importing Backup, User ID: ${socket.userID}, Version: ${backupData.version}`);
|
console.log(`Importing Backup, User ID: ${socket.userID}, Version: ${backupData.version}`);
|
||||||
|
|
||||||
let notificationListData = backupData.notificationList;
|
let notificationListData = backupData.notificationList;
|
||||||
|
let proxyListData = backupData.proxyList;
|
||||||
let monitorListData = backupData.monitorList;
|
let monitorListData = backupData.monitorList;
|
||||||
|
|
||||||
let version17x = compareVersions.compare(backupData.version, "1.7.0", ">=");
|
let version17x = compareVersions.compare(backupData.version, "1.7.0", ">=");
|
||||||
@@ -1066,8 +1158,8 @@ exports.entryPage = "dashboard";
|
|||||||
// 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 monitorList) {
|
for (let id in server.monitorList) {
|
||||||
let monitor = monitorList[id];
|
let monitor = server.monitorList[id];
|
||||||
await monitor.stop();
|
await monitor.stop();
|
||||||
}
|
}
|
||||||
await R.exec("DELETE FROM heartbeat");
|
await R.exec("DELETE FROM heartbeat");
|
||||||
@@ -1077,6 +1169,7 @@ exports.entryPage = "dashboard";
|
|||||||
await R.exec("DELETE FROM monitor_tag");
|
await R.exec("DELETE FROM monitor_tag");
|
||||||
await R.exec("DELETE FROM tag");
|
await R.exec("DELETE FROM tag");
|
||||||
await R.exec("DELETE FROM monitor");
|
await R.exec("DELETE FROM monitor");
|
||||||
|
await R.exec("DELETE FROM proxy");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Only starts importing if the backup file contains at least one notification
|
// Only starts importing if the backup file contains at least one notification
|
||||||
@@ -1096,6 +1189,24 @@ exports.entryPage = "dashboard";
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Only starts importing if the backup file contains at least one proxy
|
||||||
|
if (proxyListData.length >= 1) {
|
||||||
|
const proxies = await R.findAll("proxy");
|
||||||
|
|
||||||
|
// Loop over proxy list and save proxies
|
||||||
|
for (const proxy of proxyListData) {
|
||||||
|
const exists = proxies.find(item => item.id === proxy.id);
|
||||||
|
|
||||||
|
// Do not process when proxy already exists in import handle is skip and keep
|
||||||
|
if (["skip", "keep"].includes(importHandle) && !exists) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save proxy as new entry if exists update exists one
|
||||||
|
await Proxy.save(proxy, exists ? proxy.id : undefined, proxy.userId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Only starts importing if the backup file contains at least one monitor
|
// Only starts importing if the backup file contains at least one monitor
|
||||||
if (monitorListData.length >= 1) {
|
if (monitorListData.length >= 1) {
|
||||||
// Get every existing monitor name and puts them in one simple string
|
// Get every existing monitor name and puts them in one simple string
|
||||||
@@ -1130,6 +1241,8 @@ exports.entryPage = "dashboard";
|
|||||||
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,
|
||||||
|
basic_auth_user: monitorListData[i].basic_auth_user,
|
||||||
|
basic_auth_pass: monitorListData[i].basic_auth_pass,
|
||||||
interval: monitorListData[i].interval,
|
interval: monitorListData[i].interval,
|
||||||
retryInterval: retryInterval,
|
retryInterval: retryInterval,
|
||||||
hostname: monitorListData[i].hostname,
|
hostname: monitorListData[i].hostname,
|
||||||
@@ -1143,6 +1256,7 @@ exports.entryPage = "dashboard";
|
|||||||
dns_resolve_type: monitorListData[i].dns_resolve_type,
|
dns_resolve_type: monitorListData[i].dns_resolve_type,
|
||||||
dns_resolve_server: monitorListData[i].dns_resolve_server,
|
dns_resolve_server: monitorListData[i].dns_resolve_server,
|
||||||
notificationIDList: {},
|
notificationIDList: {},
|
||||||
|
proxy_id: monitorListData[i].proxy_id || null,
|
||||||
};
|
};
|
||||||
|
|
||||||
if (monitorListData[i].pushToken) {
|
if (monitorListData[i].pushToken) {
|
||||||
@@ -1208,7 +1322,7 @@ exports.entryPage = "dashboard";
|
|||||||
}
|
}
|
||||||
|
|
||||||
await sendNotificationList(socket);
|
await sendNotificationList(socket);
|
||||||
await sendMonitorList(socket);
|
await server.sendMonitorList(socket);
|
||||||
}
|
}
|
||||||
|
|
||||||
callback({
|
callback({
|
||||||
@@ -1296,7 +1410,9 @@ exports.entryPage = "dashboard";
|
|||||||
|
|
||||||
// Status Page Socket Handler for admin only
|
// Status Page Socket Handler for admin only
|
||||||
statusPageSocketHandler(socket);
|
statusPageSocketHandler(socket);
|
||||||
|
cloudflaredSocketHandler(socket);
|
||||||
databaseSocketHandler(socket);
|
databaseSocketHandler(socket);
|
||||||
|
proxySocketHandler(socket);
|
||||||
|
|
||||||
debug("added all socket handlers");
|
debug("added all socket handlers");
|
||||||
|
|
||||||
@@ -1317,12 +1433,12 @@ exports.entryPage = "dashboard";
|
|||||||
|
|
||||||
console.log("Init the server");
|
console.log("Init the server");
|
||||||
|
|
||||||
server.once("error", async (err) => {
|
httpServer.once("error", async (err) => {
|
||||||
console.error("Cannot listen: " + err.message);
|
console.error("Cannot listen: " + err.message);
|
||||||
await Database.close();
|
await shutdownFunction();
|
||||||
});
|
});
|
||||||
|
|
||||||
server.listen(port, hostname, () => {
|
httpServer.listen(port, hostname, () => {
|
||||||
if (hostname) {
|
if (hostname) {
|
||||||
console.log(`Listening on ${hostname}:${port}`);
|
console.log(`Listening on ${hostname}:${port}`);
|
||||||
} else {
|
} else {
|
||||||
@@ -1338,6 +1454,9 @@ exports.entryPage = "dashboard";
|
|||||||
|
|
||||||
initBackgroundJobs(args);
|
initBackgroundJobs(args);
|
||||||
|
|
||||||
|
// Start cloudflared at the end if configured
|
||||||
|
await cloudflaredAutoStart(cloudflaredToken);
|
||||||
|
|
||||||
})();
|
})();
|
||||||
|
|
||||||
async function updateMonitorNotification(monitorID, notificationIDList) {
|
async function updateMonitorNotification(monitorID, notificationIDList) {
|
||||||
@@ -1366,21 +1485,18 @@ async function checkOwner(userID, monitorID) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function sendMonitorList(socket) {
|
|
||||||
let list = await getMonitorJSONList(socket.userID);
|
|
||||||
io.to(socket.userID).emit("monitorList", list);
|
|
||||||
return list;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function afterLogin(socket, user) {
|
async function afterLogin(socket, user) {
|
||||||
socket.userID = user.id;
|
socket.userID = user.id;
|
||||||
socket.join(user.id);
|
socket.join(user.id);
|
||||||
|
|
||||||
let monitorList = await sendMonitorList(socket);
|
let monitorList = await server.sendMonitorList(socket);
|
||||||
sendNotificationList(socket);
|
sendNotificationList(socket);
|
||||||
|
sendProxyList(socket);
|
||||||
|
|
||||||
await sleep(500);
|
await sleep(500);
|
||||||
|
|
||||||
|
await StatusPage.sendStatusPageList(io, socket);
|
||||||
|
|
||||||
for (let monitorID in monitorList) {
|
for (let monitorID in monitorList) {
|
||||||
await sendHeartbeatList(socket, monitorID);
|
await sendHeartbeatList(socket, monitorID);
|
||||||
}
|
}
|
||||||
@@ -1408,14 +1524,14 @@ async function getMonitorJSONList(userID) {
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function initDatabase() {
|
async function initDatabase(testMode = false) {
|
||||||
if (! fs.existsSync(Database.path)) {
|
if (! fs.existsSync(Database.path)) {
|
||||||
console.log("Copying Database");
|
console.log("Copying Database");
|
||||||
fs.copyFileSync(Database.templatePath, Database.path);
|
fs.copyFileSync(Database.templatePath, Database.path);
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log("Connecting to the Database");
|
console.log("Connecting to the Database");
|
||||||
await Database.connect();
|
await Database.connect(testMode);
|
||||||
console.log("Connected");
|
console.log("Connected");
|
||||||
|
|
||||||
// Patch the database
|
// Patch the database
|
||||||
@@ -1456,11 +1572,11 @@ async function startMonitor(userID, monitorID) {
|
|||||||
monitorID,
|
monitorID,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
if (monitor.id in monitorList) {
|
if (monitor.id in server.monitorList) {
|
||||||
monitorList[monitor.id].stop();
|
server.monitorList[monitor.id].stop();
|
||||||
}
|
}
|
||||||
|
|
||||||
monitorList[monitor.id] = monitor;
|
server.monitorList[monitor.id] = monitor;
|
||||||
monitor.start(io);
|
monitor.start(io);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1478,8 +1594,8 @@ async function pauseMonitor(userID, monitorID) {
|
|||||||
userID,
|
userID,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
if (monitorID in monitorList) {
|
if (monitorID in server.monitorList) {
|
||||||
monitorList[monitorID].stop();
|
server.monitorList[monitorID].stop();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1490,7 +1606,7 @@ async function startMonitors() {
|
|||||||
let list = await R.find("monitor", " active = 1 ");
|
let list = await R.find("monitor", " active = 1 ");
|
||||||
|
|
||||||
for (let monitor of list) {
|
for (let monitor of list) {
|
||||||
monitorList[monitor.id] = monitor;
|
server.monitorList[monitor.id] = monitor;
|
||||||
}
|
}
|
||||||
|
|
||||||
for (let monitor of list) {
|
for (let monitor of list) {
|
||||||
@@ -1505,19 +1621,22 @@ async function shutdownFunction(signal) {
|
|||||||
console.log("Called signal: " + signal);
|
console.log("Called signal: " + signal);
|
||||||
|
|
||||||
console.log("Stopping all monitors");
|
console.log("Stopping all monitors");
|
||||||
for (let id in monitorList) {
|
for (let id in server.monitorList) {
|
||||||
let monitor = monitorList[id];
|
let monitor = server.monitorList[id];
|
||||||
monitor.stop();
|
monitor.stop();
|
||||||
}
|
}
|
||||||
await sleep(2000);
|
await sleep(2000);
|
||||||
await Database.close();
|
await Database.close();
|
||||||
|
|
||||||
|
stopBackgroundJobs();
|
||||||
|
await cloudflaredStop();
|
||||||
}
|
}
|
||||||
|
|
||||||
function finalFunction() {
|
function finalFunction() {
|
||||||
console.log("Graceful shutdown successful!");
|
console.log("Graceful shutdown successful!");
|
||||||
}
|
}
|
||||||
|
|
||||||
gracefulShutdown(server, {
|
gracefulShutdown(httpServer, {
|
||||||
signals: "SIGINT SIGTERM",
|
signals: "SIGINT SIGTERM",
|
||||||
timeout: 30000, // timeout: 30 secs
|
timeout: 30000, // timeout: 30 secs
|
||||||
development: false, // not in dev mode
|
development: false, // not in dev mode
|
||||||
|
90
server/socket-handlers/cloudflared-socket-handler.js
Normal file
90
server/socket-handlers/cloudflared-socket-handler.js
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
const { checkLogin, setSetting, setting, doubleCheckPassword } = require("../util-server");
|
||||||
|
const { CloudflaredTunnel } = require("node-cloudflared-tunnel");
|
||||||
|
const { io } = require("../server");
|
||||||
|
|
||||||
|
const prefix = "cloudflared_";
|
||||||
|
const cloudflared = new CloudflaredTunnel();
|
||||||
|
|
||||||
|
cloudflared.change = (running, message) => {
|
||||||
|
io.to("cloudflared").emit(prefix + "running", running);
|
||||||
|
io.to("cloudflared").emit(prefix + "message", message);
|
||||||
|
};
|
||||||
|
|
||||||
|
cloudflared.error = (errorMessage) => {
|
||||||
|
io.to("cloudflared").emit(prefix + "errorMessage", errorMessage);
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports.cloudflaredSocketHandler = (socket) => {
|
||||||
|
|
||||||
|
socket.on(prefix + "join", async () => {
|
||||||
|
try {
|
||||||
|
checkLogin(socket);
|
||||||
|
socket.join("cloudflared");
|
||||||
|
io.to(socket.userID).emit(prefix + "installed", cloudflared.checkInstalled());
|
||||||
|
io.to(socket.userID).emit(prefix + "running", cloudflared.running);
|
||||||
|
io.to(socket.userID).emit(prefix + "token", await setting("cloudflaredTunnelToken"));
|
||||||
|
} catch (error) { }
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.on(prefix + "leave", async () => {
|
||||||
|
try {
|
||||||
|
checkLogin(socket);
|
||||||
|
socket.leave("cloudflared");
|
||||||
|
} catch (error) { }
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.on(prefix + "start", async (token) => {
|
||||||
|
try {
|
||||||
|
checkLogin(socket);
|
||||||
|
if (token && typeof token === "string") {
|
||||||
|
await setSetting("cloudflaredTunnelToken", token);
|
||||||
|
cloudflared.token = token;
|
||||||
|
} else {
|
||||||
|
cloudflared.token = null;
|
||||||
|
}
|
||||||
|
cloudflared.start();
|
||||||
|
} catch (error) { }
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.on(prefix + "stop", async (currentPassword, callback) => {
|
||||||
|
try {
|
||||||
|
checkLogin(socket);
|
||||||
|
await doubleCheckPassword(socket, currentPassword);
|
||||||
|
cloudflared.stop();
|
||||||
|
} catch (error) {
|
||||||
|
callback({
|
||||||
|
ok: false,
|
||||||
|
msg: error.message,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.on(prefix + "removeToken", async () => {
|
||||||
|
try {
|
||||||
|
checkLogin(socket);
|
||||||
|
await setSetting("cloudflaredTunnelToken", "");
|
||||||
|
} catch (error) { }
|
||||||
|
});
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports.autoStart = async (token) => {
|
||||||
|
if (!token) {
|
||||||
|
token = await setting("cloudflaredTunnelToken");
|
||||||
|
} else {
|
||||||
|
// Override the current token via args or env var
|
||||||
|
await setSetting("cloudflaredTunnelToken", token);
|
||||||
|
console.log("Use cloudflared token from args or env var");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (token) {
|
||||||
|
console.log("Start cloudflared");
|
||||||
|
cloudflared.token = token;
|
||||||
|
cloudflared.start();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports.stop = async () => {
|
||||||
|
console.log("Stop cloudflared");
|
||||||
|
cloudflared.stop();
|
||||||
|
};
|
53
server/socket-handlers/proxy-socket-handler.js
Normal file
53
server/socket-handlers/proxy-socket-handler.js
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
const { checkLogin } = require("../util-server");
|
||||||
|
const { Proxy } = require("../proxy");
|
||||||
|
const { sendProxyList } = require("../client");
|
||||||
|
const server = require("../server");
|
||||||
|
|
||||||
|
module.exports.proxySocketHandler = (socket) => {
|
||||||
|
socket.on("addProxy", async (proxy, proxyID, callback) => {
|
||||||
|
try {
|
||||||
|
checkLogin(socket);
|
||||||
|
|
||||||
|
const proxyBean = await Proxy.save(proxy, proxyID, socket.userID);
|
||||||
|
await sendProxyList(socket);
|
||||||
|
|
||||||
|
if (proxy.applyExisting) {
|
||||||
|
await Proxy.reloadProxy();
|
||||||
|
await server.sendMonitorList(socket);
|
||||||
|
}
|
||||||
|
|
||||||
|
callback({
|
||||||
|
ok: true,
|
||||||
|
msg: "Saved",
|
||||||
|
id: proxyBean.id,
|
||||||
|
});
|
||||||
|
|
||||||
|
} catch (e) {
|
||||||
|
callback({
|
||||||
|
ok: false,
|
||||||
|
msg: e.message,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.on("deleteProxy", async (proxyID, callback) => {
|
||||||
|
try {
|
||||||
|
checkLogin(socket);
|
||||||
|
|
||||||
|
await Proxy.delete(proxyID, socket.userID);
|
||||||
|
await sendProxyList(socket);
|
||||||
|
await Proxy.reloadProxy();
|
||||||
|
|
||||||
|
callback({
|
||||||
|
ok: true,
|
||||||
|
msg: "Deleted",
|
||||||
|
});
|
||||||
|
|
||||||
|
} catch (e) {
|
||||||
|
callback({
|
||||||
|
ok: false,
|
||||||
|
msg: e.message,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
@@ -1,25 +1,36 @@
|
|||||||
const { R } = require("redbean-node");
|
const { R } = require("redbean-node");
|
||||||
const { checkLogin, setSettings } = require("../util-server");
|
const { checkLogin, setSettings, setSetting } = require("../util-server");
|
||||||
const dayjs = require("dayjs");
|
const dayjs = require("dayjs");
|
||||||
const { debug } = require("../../src/util");
|
const { debug } = require("../../src/util");
|
||||||
const ImageDataURI = require("../image-data-uri");
|
const ImageDataURI = require("../image-data-uri");
|
||||||
const Database = require("../database");
|
const Database = require("../database");
|
||||||
const apicache = require("../modules/apicache");
|
const apicache = require("../modules/apicache");
|
||||||
|
const StatusPage = require("../model/status_page");
|
||||||
|
const server = require("../server");
|
||||||
|
|
||||||
module.exports.statusPageSocketHandler = (socket) => {
|
module.exports.statusPageSocketHandler = (socket) => {
|
||||||
|
|
||||||
// Post or edit incident
|
// Post or edit incident
|
||||||
socket.on("postIncident", async (incident, callback) => {
|
socket.on("postIncident", async (slug, incident, callback) => {
|
||||||
try {
|
try {
|
||||||
checkLogin(socket);
|
checkLogin(socket);
|
||||||
|
|
||||||
await R.exec("UPDATE incident SET pin = 0 ");
|
let statusPageID = await StatusPage.slugToID(slug);
|
||||||
|
|
||||||
|
if (!statusPageID) {
|
||||||
|
throw new Error("slug is not found");
|
||||||
|
}
|
||||||
|
|
||||||
|
await R.exec("UPDATE incident SET pin = 0 WHERE status_page_id = ? ", [
|
||||||
|
statusPageID
|
||||||
|
]);
|
||||||
|
|
||||||
let incidentBean;
|
let incidentBean;
|
||||||
|
|
||||||
if (incident.id) {
|
if (incident.id) {
|
||||||
incidentBean = await R.findOne("incident", " id = ?", [
|
incidentBean = await R.findOne("incident", " id = ? AND status_page_id = ? ", [
|
||||||
incident.id
|
incident.id,
|
||||||
|
statusPageID
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -31,6 +42,7 @@ module.exports.statusPageSocketHandler = (socket) => {
|
|||||||
incidentBean.content = incident.content;
|
incidentBean.content = incident.content;
|
||||||
incidentBean.style = incident.style;
|
incidentBean.style = incident.style;
|
||||||
incidentBean.pin = true;
|
incidentBean.pin = true;
|
||||||
|
incidentBean.status_page_id = statusPageID;
|
||||||
|
|
||||||
if (incident.id) {
|
if (incident.id) {
|
||||||
incidentBean.lastUpdatedDate = R.isoDateTime(dayjs.utc());
|
incidentBean.lastUpdatedDate = R.isoDateTime(dayjs.utc());
|
||||||
@@ -52,11 +64,15 @@ module.exports.statusPageSocketHandler = (socket) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
socket.on("unpinIncident", async (callback) => {
|
socket.on("unpinIncident", async (slug, callback) => {
|
||||||
try {
|
try {
|
||||||
checkLogin(socket);
|
checkLogin(socket);
|
||||||
|
|
||||||
await R.exec("UPDATE incident SET pin = 0 WHERE pin = 1");
|
let statusPageID = await StatusPage.slugToID(slug);
|
||||||
|
|
||||||
|
await R.exec("UPDATE incident SET pin = 0 WHERE pin = 1 AND status_page_id = ? ", [
|
||||||
|
statusPageID
|
||||||
|
]);
|
||||||
|
|
||||||
callback({
|
callback({
|
||||||
ok: true,
|
ok: true,
|
||||||
@@ -69,14 +85,46 @@ module.exports.statusPageSocketHandler = (socket) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Save Status Page
|
socket.on("getStatusPage", async (slug, callback) => {
|
||||||
// imgDataUrl Only Accept PNG!
|
|
||||||
socket.on("saveStatusPage", async (config, imgDataUrl, publicGroupList, callback) => {
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
checkLogin(socket);
|
checkLogin(socket);
|
||||||
|
|
||||||
apicache.clear();
|
let statusPage = await R.findOne("status_page", " slug = ? ", [
|
||||||
|
slug
|
||||||
|
]);
|
||||||
|
|
||||||
|
if (!statusPage) {
|
||||||
|
throw new Error("No slug?");
|
||||||
|
}
|
||||||
|
|
||||||
|
callback({
|
||||||
|
ok: true,
|
||||||
|
config: await statusPage.toJSON(),
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
callback({
|
||||||
|
ok: false,
|
||||||
|
msg: error.message,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Save Status Page
|
||||||
|
// imgDataUrl Only Accept PNG!
|
||||||
|
socket.on("saveStatusPage", async (slug, config, imgDataUrl, publicGroupList, callback) => {
|
||||||
|
try {
|
||||||
|
checkLogin(socket);
|
||||||
|
|
||||||
|
// Save Config
|
||||||
|
let statusPage = await R.findOne("status_page", " slug = ? ", [
|
||||||
|
slug
|
||||||
|
]);
|
||||||
|
|
||||||
|
if (!statusPage) {
|
||||||
|
throw new Error("No slug?");
|
||||||
|
}
|
||||||
|
|
||||||
|
checkSlug(config.slug);
|
||||||
|
|
||||||
const header = "data:image/png;base64,";
|
const header = "data:image/png;base64,";
|
||||||
|
|
||||||
@@ -88,16 +136,31 @@ module.exports.statusPageSocketHandler = (socket) => {
|
|||||||
throw new Error("Only allowed PNG logo.");
|
throw new Error("Only allowed PNG logo.");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const filename = `logo${statusPage.id}.png`;
|
||||||
|
|
||||||
// Convert to file
|
// Convert to file
|
||||||
await ImageDataURI.outputFile(imgDataUrl, Database.uploadDir + "logo.png");
|
await ImageDataURI.outputFile(imgDataUrl, Database.uploadDir + filename);
|
||||||
config.logo = "/upload/logo.png?t=" + Date.now();
|
config.logo = `/upload/${filename}?t=` + Date.now();
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
config.icon = imgDataUrl;
|
config.icon = imgDataUrl;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Save Config
|
statusPage.slug = config.slug;
|
||||||
await setSettings("statusPage", config);
|
statusPage.title = config.title;
|
||||||
|
statusPage.description = config.description;
|
||||||
|
statusPage.icon = config.logo;
|
||||||
|
statusPage.theme = config.theme;
|
||||||
|
//statusPage.published = ;
|
||||||
|
//statusPage.search_engine_index = ;
|
||||||
|
statusPage.show_tags = config.showTags;
|
||||||
|
//statusPage.password = null;
|
||||||
|
statusPage.modified_date = R.isoDateTime();
|
||||||
|
|
||||||
|
await R.store(statusPage);
|
||||||
|
|
||||||
|
await statusPage.updateDomainNameList(config.domainNameList);
|
||||||
|
await StatusPage.loadDomainMappingList();
|
||||||
|
|
||||||
// Save Public Group List
|
// Save Public Group List
|
||||||
const groupIDList = [];
|
const groupIDList = [];
|
||||||
@@ -106,13 +169,15 @@ module.exports.statusPageSocketHandler = (socket) => {
|
|||||||
for (let group of publicGroupList) {
|
for (let group of publicGroupList) {
|
||||||
let groupBean;
|
let groupBean;
|
||||||
if (group.id) {
|
if (group.id) {
|
||||||
groupBean = await R.findOne("group", " id = ? AND public = 1 ", [
|
groupBean = await R.findOne("group", " id = ? AND public = 1 AND status_page_id = ? ", [
|
||||||
group.id
|
group.id,
|
||||||
|
statusPage.id
|
||||||
]);
|
]);
|
||||||
} else {
|
} else {
|
||||||
groupBean = R.dispense("group");
|
groupBean = R.dispense("group");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
groupBean.status_page_id = statusPage.id;
|
||||||
groupBean.name = group.name;
|
groupBean.name = group.name;
|
||||||
groupBean.public = true;
|
groupBean.public = true;
|
||||||
groupBean.weight = groupOrder++;
|
groupBean.weight = groupOrder++;
|
||||||
@@ -124,7 +189,6 @@ module.exports.statusPageSocketHandler = (socket) => {
|
|||||||
]);
|
]);
|
||||||
|
|
||||||
let monitorOrder = 1;
|
let monitorOrder = 1;
|
||||||
console.log(group.monitorList);
|
|
||||||
|
|
||||||
for (let monitor of group.monitorList) {
|
for (let monitor of group.monitorList) {
|
||||||
let relationBean = R.dispense("monitor_group");
|
let relationBean = R.dispense("monitor_group");
|
||||||
@@ -141,7 +205,20 @@ module.exports.statusPageSocketHandler = (socket) => {
|
|||||||
// Delete groups that not in the list
|
// Delete groups that not in the list
|
||||||
debug("Delete groups that not in the list");
|
debug("Delete groups that not in the list");
|
||||||
const slots = groupIDList.map(() => "?").join(",");
|
const slots = groupIDList.map(() => "?").join(",");
|
||||||
await R.exec(`DELETE FROM \`group\` WHERE id NOT IN (${slots})`, groupIDList);
|
|
||||||
|
const data = [
|
||||||
|
...groupIDList,
|
||||||
|
statusPage.id
|
||||||
|
];
|
||||||
|
await R.exec(`DELETE FROM \`group\` WHERE id NOT IN (${slots}) AND status_page_id = ?`, data);
|
||||||
|
|
||||||
|
// Also change entry page to new slug if it is the default one, and slug is changed.
|
||||||
|
if (server.entryPage === "statusPage-" + slug && statusPage.slug !== slug) {
|
||||||
|
server.entryPage = "statusPage-" + statusPage.slug;
|
||||||
|
await setSetting("entryPage", server.entryPage, "general");
|
||||||
|
}
|
||||||
|
|
||||||
|
apicache.clear();
|
||||||
|
|
||||||
callback({
|
callback({
|
||||||
ok: true,
|
ok: true,
|
||||||
@@ -149,7 +226,7 @@ module.exports.statusPageSocketHandler = (socket) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log(error);
|
console.error(error);
|
||||||
|
|
||||||
callback({
|
callback({
|
||||||
ok: false,
|
ok: false,
|
||||||
@@ -158,4 +235,115 @@ module.exports.statusPageSocketHandler = (socket) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Add a new status page
|
||||||
|
socket.on("addStatusPage", async (title, slug, callback) => {
|
||||||
|
try {
|
||||||
|
checkLogin(socket);
|
||||||
|
|
||||||
|
title = title?.trim();
|
||||||
|
slug = slug?.trim();
|
||||||
|
|
||||||
|
// Check empty
|
||||||
|
if (!title || !slug) {
|
||||||
|
throw new Error("Please input all fields");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Make sure slug is string
|
||||||
|
if (typeof slug !== "string") {
|
||||||
|
throw new Error("Slug -Accept string only");
|
||||||
|
}
|
||||||
|
|
||||||
|
// lower case only
|
||||||
|
slug = slug.toLowerCase();
|
||||||
|
|
||||||
|
checkSlug(slug);
|
||||||
|
|
||||||
|
let statusPage = R.dispense("status_page");
|
||||||
|
statusPage.slug = slug;
|
||||||
|
statusPage.title = title;
|
||||||
|
statusPage.theme = "light";
|
||||||
|
statusPage.icon = "";
|
||||||
|
await R.store(statusPage);
|
||||||
|
|
||||||
|
callback({
|
||||||
|
ok: true,
|
||||||
|
msg: "OK!"
|
||||||
|
});
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
callback({
|
||||||
|
ok: false,
|
||||||
|
msg: error.message,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Delete a status page
|
||||||
|
socket.on("deleteStatusPage", async (slug, callback) => {
|
||||||
|
try {
|
||||||
|
checkLogin(socket);
|
||||||
|
|
||||||
|
let statusPageID = await StatusPage.slugToID(slug);
|
||||||
|
|
||||||
|
if (statusPageID) {
|
||||||
|
|
||||||
|
// Reset entry page if it is the default one.
|
||||||
|
if (server.entryPage === "statusPage-" + slug) {
|
||||||
|
server.entryPage = "dashboard";
|
||||||
|
await setSetting("entryPage", server.entryPage, "general");
|
||||||
|
}
|
||||||
|
|
||||||
|
// No need to delete records from `status_page_cname`, because it has cascade foreign key.
|
||||||
|
// But for incident & group, it is hard to add cascade foreign key during migration, so they have to be deleted manually.
|
||||||
|
|
||||||
|
// Delete incident
|
||||||
|
await R.exec("DELETE FROM incident WHERE status_page_id = ? ", [
|
||||||
|
statusPageID
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Delete group
|
||||||
|
await R.exec("DELETE FROM `group` WHERE status_page_id = ? ", [
|
||||||
|
statusPageID
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Delete status_page
|
||||||
|
await R.exec("DELETE FROM status_page WHERE id = ? ", [
|
||||||
|
statusPageID
|
||||||
|
]);
|
||||||
|
|
||||||
|
} else {
|
||||||
|
throw new Error("Status Page is not found");
|
||||||
|
}
|
||||||
|
|
||||||
|
callback({
|
||||||
|
ok: true,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
callback({
|
||||||
|
ok: false,
|
||||||
|
msg: error.message,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check slug a-z, 0-9, - only
|
||||||
|
* Regex from: https://stackoverflow.com/questions/22454258/js-regex-string-validation-for-slug
|
||||||
|
*/
|
||||||
|
function checkSlug(slug) {
|
||||||
|
if (typeof slug !== "string") {
|
||||||
|
throw new Error("Slug must be string");
|
||||||
|
}
|
||||||
|
|
||||||
|
slug = slug.trim();
|
||||||
|
|
||||||
|
if (!slug) {
|
||||||
|
throw new Error("Slug cannot be empty");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!slug.match(/^[A-Za-z0-9]+(?:-[A-Za-z0-9]+)*$/)) {
|
||||||
|
throw new Error("Invalid Slug");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@@ -1,9 +1,8 @@
|
|||||||
const tcpp = require("tcp-ping");
|
const tcpp = require("tcp-ping");
|
||||||
const Ping = require("./ping-lite");
|
const Ping = require("./ping-lite");
|
||||||
const { R } = require("redbean-node");
|
const { R } = require("redbean-node");
|
||||||
const { debug } = require("../src/util");
|
const { debug, genSecret } = require("../src/util");
|
||||||
const passwordHash = require("./password-hash");
|
const passwordHash = require("./password-hash");
|
||||||
const dayjs = require("dayjs");
|
|
||||||
const { Resolver } = require("dns");
|
const { Resolver } = require("dns");
|
||||||
const child_process = require("child_process");
|
const child_process = require("child_process");
|
||||||
const iconv = require("iconv-lite");
|
const iconv = require("iconv-lite");
|
||||||
@@ -16,6 +15,7 @@ exports.WIN = /^win/.test(process.platform);
|
|||||||
exports.LIN = /^linux/.test(process.platform);
|
exports.LIN = /^linux/.test(process.platform);
|
||||||
exports.MAC = /^darwin/.test(process.platform);
|
exports.MAC = /^darwin/.test(process.platform);
|
||||||
exports.FBSD = /^freebsd/.test(process.platform);
|
exports.FBSD = /^freebsd/.test(process.platform);
|
||||||
|
exports.BSD = /bsd$/.test(process.platform);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Init or reset JWT secret
|
* Init or reset JWT secret
|
||||||
@@ -31,7 +31,7 @@ exports.initJWTSecret = async () => {
|
|||||||
jwtSecretBean.key = "jwtSecret";
|
jwtSecretBean.key = "jwtSecret";
|
||||||
}
|
}
|
||||||
|
|
||||||
jwtSecretBean.value = passwordHash.generate(dayjs() + "");
|
jwtSecretBean.value = passwordHash.generate(genSecret());
|
||||||
await R.store(jwtSecretBean);
|
await R.store(jwtSecretBean);
|
||||||
return jwtSecretBean;
|
return jwtSecretBean;
|
||||||
};
|
};
|
||||||
@@ -201,8 +201,13 @@ const getDaysRemaining = (validFrom, validTo) => {
|
|||||||
// param: info - the chain obtained from getPeerCertificate()
|
// param: info - the chain obtained from getPeerCertificate()
|
||||||
const parseCertificateInfo = function (info) {
|
const parseCertificateInfo = function (info) {
|
||||||
let link = info;
|
let link = info;
|
||||||
|
let i = 0;
|
||||||
|
|
||||||
|
const existingList = {};
|
||||||
|
|
||||||
while (link) {
|
while (link) {
|
||||||
|
debug(`[${i}] ${link.fingerprint}`);
|
||||||
|
|
||||||
if (!link.valid_from || !link.valid_to) {
|
if (!link.valid_from || !link.valid_to) {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -210,15 +215,24 @@ const parseCertificateInfo = function (info) {
|
|||||||
link.validFor = link.subjectaltname?.replace(/DNS:|IP Address:/g, "").split(", ");
|
link.validFor = link.subjectaltname?.replace(/DNS:|IP Address:/g, "").split(", ");
|
||||||
link.daysRemaining = getDaysRemaining(new Date(), link.validTo);
|
link.daysRemaining = getDaysRemaining(new Date(), link.validTo);
|
||||||
|
|
||||||
|
existingList[link.fingerprint] = true;
|
||||||
|
|
||||||
// Move up the chain until loop is encountered
|
// Move up the chain until loop is encountered
|
||||||
if (link.issuerCertificate == null) {
|
if (link.issuerCertificate == null) {
|
||||||
break;
|
break;
|
||||||
} else if (link.fingerprint == link.issuerCertificate.fingerprint) {
|
} else if (link.issuerCertificate.fingerprint in existingList) {
|
||||||
|
debug(`[Last] ${link.issuerCertificate.fingerprint}`);
|
||||||
link.issuerCertificate = null;
|
link.issuerCertificate = null;
|
||||||
break;
|
break;
|
||||||
} else {
|
} else {
|
||||||
link = link.issuerCertificate;
|
link = link.issuerCertificate;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Should be no use, but just in case.
|
||||||
|
if (i > 500) {
|
||||||
|
throw new Error("Dead loop occurred in parseCertificateInfo");
|
||||||
|
}
|
||||||
|
i++;
|
||||||
}
|
}
|
||||||
|
|
||||||
return info;
|
return info;
|
||||||
@@ -228,6 +242,7 @@ exports.checkCertificate = function (res) {
|
|||||||
const info = res.request.res.socket.getPeerCertificate(true);
|
const info = res.request.res.socket.getPeerCertificate(true);
|
||||||
const valid = res.request.res.socket.authorized || false;
|
const valid = res.request.res.socket.authorized || false;
|
||||||
|
|
||||||
|
debug("Parsing Certificate Info");
|
||||||
const parsedInfo = parseCertificateInfo(info);
|
const parsedInfo = parseCertificateInfo(info);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -305,6 +320,28 @@ exports.checkLogin = (socket) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* For logged-in users, double-check the password
|
||||||
|
* @param socket
|
||||||
|
* @param currentPassword
|
||||||
|
* @returns {Promise<Bean>}
|
||||||
|
*/
|
||||||
|
exports.doubleCheckPassword = async (socket, currentPassword) => {
|
||||||
|
if (typeof currentPassword !== "string") {
|
||||||
|
throw new Error("Wrong data type?");
|
||||||
|
}
|
||||||
|
|
||||||
|
let user = await R.findOne("user", " id = ? AND active = 1 ", [
|
||||||
|
socket.userID,
|
||||||
|
]);
|
||||||
|
|
||||||
|
if (!user || !passwordHash.verify(currentPassword, user.password)) {
|
||||||
|
throw new Error("Incorrect current password");
|
||||||
|
}
|
||||||
|
|
||||||
|
return user;
|
||||||
|
};
|
||||||
|
|
||||||
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";
|
||||||
|
@@ -22,6 +22,18 @@ textarea.form-control {
|
|||||||
width: 10px;
|
width: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.list-group {
|
||||||
|
border-radius: 0.75rem;
|
||||||
|
|
||||||
|
.dark & {
|
||||||
|
.list-group-item {
|
||||||
|
background-color: $dark-bg;
|
||||||
|
color: $dark-font-color;
|
||||||
|
border-color: $dark-border-color;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
::-webkit-scrollbar-thumb {
|
::-webkit-scrollbar-thumb {
|
||||||
background: #ccc;
|
background: #ccc;
|
||||||
border-radius: 20px;
|
border-radius: 20px;
|
||||||
@@ -92,6 +104,10 @@ textarea.form-control {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.btn-dark {
|
||||||
|
background-color: #161B22;
|
||||||
|
}
|
||||||
|
|
||||||
@media (max-width: 550px) {
|
@media (max-width: 550px) {
|
||||||
.table-shadow-box {
|
.table-shadow-box {
|
||||||
padding: 10px !important;
|
padding: 10px !important;
|
||||||
@@ -144,6 +160,10 @@ textarea.form-control {
|
|||||||
background-color: #090c10;
|
background-color: #090c10;
|
||||||
color: $dark-font-color;
|
color: $dark-font-color;
|
||||||
|
|
||||||
|
mark, .mark {
|
||||||
|
background-color: #b6ad86;
|
||||||
|
}
|
||||||
|
|
||||||
&::-webkit-scrollbar-thumb, ::-webkit-scrollbar-thumb {
|
&::-webkit-scrollbar-thumb, ::-webkit-scrollbar-thumb {
|
||||||
background: $dark-border-color;
|
background: $dark-border-color;
|
||||||
}
|
}
|
||||||
@@ -156,13 +176,24 @@ textarea.form-control {
|
|||||||
|
|
||||||
.form-check-input {
|
.form-check-input {
|
||||||
background-color: $dark-bg2;
|
background-color: $dark-bg2;
|
||||||
|
border-color: $dark-border-color;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-group-text {
|
||||||
|
background-color: #282f39;
|
||||||
|
border-color: $dark-border-color;
|
||||||
|
color: $dark-font-color;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-check-input:checked {
|
||||||
|
border-color: $primary; // Re-apply bootstrap border
|
||||||
}
|
}
|
||||||
|
|
||||||
.form-switch .form-check-input {
|
.form-switch .form-check-input {
|
||||||
background-color: #232f3b;
|
background-color: #232f3b;
|
||||||
}
|
}
|
||||||
|
|
||||||
a,
|
a:not(.btn),
|
||||||
.table,
|
.table,
|
||||||
.nav-link {
|
.nav-link {
|
||||||
color: $dark-font-color;
|
color: $dark-font-color;
|
||||||
@@ -189,7 +220,7 @@ textarea.form-control {
|
|||||||
opacity: 1;
|
opacity: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.table-hover > tbody > tr:hover {
|
.table-hover > tbody > tr:hover > * {
|
||||||
--bs-table-accent-bg: #070a10;
|
--bs-table-accent-bg: #070a10;
|
||||||
color: $dark-font-color;
|
color: $dark-font-color;
|
||||||
}
|
}
|
||||||
@@ -313,13 +344,24 @@ textarea.form-control {
|
|||||||
opacity: 0;
|
opacity: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.slide-fade-up-enter-active {
|
||||||
|
transition: all 0.2s $easing-in;
|
||||||
|
}
|
||||||
|
|
||||||
|
.slide-fade-up-leave-active {
|
||||||
|
transition: all 0.2s $easing-in;
|
||||||
|
}
|
||||||
|
|
||||||
|
.slide-fade-up-enter-from,
|
||||||
|
.slide-fade-up-leave-to {
|
||||||
|
transform: translateY(-50px);
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
.monitor-list {
|
.monitor-list {
|
||||||
&.scrollbar {
|
&.scrollbar {
|
||||||
min-height: calc(100vh - 240px);
|
|
||||||
max-height: calc(100vh - 30px);
|
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
position: sticky;
|
height: calc(100% - 65px);
|
||||||
top: 10px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.item {
|
.item {
|
||||||
@@ -346,6 +388,10 @@ textarea.form-control {
|
|||||||
&.active {
|
&.active {
|
||||||
background-color: #cdf8f4;
|
background-color: #cdf8f4;
|
||||||
}
|
}
|
||||||
|
.tags {
|
||||||
|
// Removes margin to line up tags list with uptime percentage
|
||||||
|
margin-left: -0.25rem;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -378,6 +424,10 @@ textarea.form-control {
|
|||||||
background-color: rgba(239, 239, 239, 0.7);
|
background-color: rgba(239, 239, 239, 0.7);
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
|
|
||||||
|
&.no-bg {
|
||||||
|
background-color: transparent !important;
|
||||||
|
}
|
||||||
|
|
||||||
&:focus {
|
&:focus {
|
||||||
outline: 0 solid #eee;
|
outline: 0 solid #eee;
|
||||||
background-color: rgba(245, 245, 245, 0.9);
|
background-color: rgba(245, 245, 245, 0.9);
|
||||||
@@ -415,6 +465,10 @@ textarea.form-control {
|
|||||||
border-radius: 10px !important;
|
border-radius: 10px !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.spinner {
|
||||||
|
color: $primary;
|
||||||
|
}
|
||||||
|
|
||||||
// Localization
|
// Localization
|
||||||
|
|
||||||
@import "localization.scss";
|
@import "localization.scss";
|
||||||
|
@@ -12,6 +12,7 @@ $dark-font-color2: #020b05;
|
|||||||
$dark-bg: #0d1117;
|
$dark-bg: #0d1117;
|
||||||
$dark-bg2: #070a10;
|
$dark-bg2: #070a10;
|
||||||
$dark-border-color: #1d2634;
|
$dark-border-color: #1d2634;
|
||||||
|
$dark-header-bg: #161b22;
|
||||||
|
|
||||||
$easing-in: cubic-bezier(0.54, 0.78, 0.55, 0.97);
|
$easing-in: cubic-bezier(0.54, 0.78, 0.55, 0.97);
|
||||||
$easing-out: cubic-bezier(0.25, 0.46, 0.45, 0.94);
|
$easing-out: cubic-bezier(0.25, 0.46, 0.45, 0.94);
|
||||||
|
@@ -11,23 +11,23 @@
|
|||||||
<table class="text-start">
|
<table class="text-start">
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr class="my-3">
|
<tr class="my-3">
|
||||||
<td class="px-3">Subject:</td>
|
<td class="px-3">{{ $t("Subject:") }}</td>
|
||||||
<td>{{ formatSubject(cert.subject) }}</td>
|
<td>{{ formatSubject(cert.subject) }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr class="my-3">
|
<tr class="my-3">
|
||||||
<td class="px-3">Valid To:</td>
|
<td class="px-3">{{ $t("Valid To:") }}</td>
|
||||||
<td><Datetime :value="cert.validTo" /></td>
|
<td><Datetime :value="cert.validTo" /></td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr class="my-3">
|
<tr class="my-3">
|
||||||
<td class="px-3">Days Remaining:</td>
|
<td class="px-3">{{ $t("Days Remaining:") }}</td>
|
||||||
<td>{{ cert.daysRemaining }}</td>
|
<td>{{ cert.daysRemaining }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr class="my-3">
|
<tr class="my-3">
|
||||||
<td class="px-3">Issuer:</td>
|
<td class="px-3">{{ $t("Issuer:") }}</td>
|
||||||
<td>{{ formatSubject(cert.issuer) }}</td>
|
<td>{{ formatSubject(cert.issuer) }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr class="my-3">
|
<tr class="my-3">
|
||||||
<td class="px-3">Fingerprint:</td>
|
<td class="px-3">{{ $t("Fingerprint:") }}</td>
|
||||||
<td>{{ cert.fingerprint }}</td>
|
<td>{{ cert.fingerprint }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
|
@@ -16,8 +16,8 @@
|
|||||||
|
|
||||||
<div v-if="tokenRequired">
|
<div v-if="tokenRequired">
|
||||||
<div class="form-floating mt-3">
|
<div class="form-floating mt-3">
|
||||||
<input id="floatingToken" v-model="token" type="text" maxlength="6" class="form-control" placeholder="123456">
|
<input id="otp" v-model="token" type="text" maxlength="6" class="form-control" placeholder="123456">
|
||||||
<label for="floatingToken">{{ $t("Token") }}</label>
|
<label for="otp">{{ $t("Token") }}</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="shadow-box mb-3">
|
<div class="shadow-box mb-3" :style="boxStyle">
|
||||||
<div class="list-header">
|
<div class="list-header">
|
||||||
<div class="placeholder"></div>
|
<div class="placeholder"></div>
|
||||||
<div class="search-wrapper">
|
<div class="search-wrapper">
|
||||||
@@ -9,7 +9,9 @@
|
|||||||
<a v-if="searchText != ''" class="search-icon" @click="clearSearchText">
|
<a v-if="searchText != ''" class="search-icon" @click="clearSearchText">
|
||||||
<font-awesome-icon icon="times" />
|
<font-awesome-icon icon="times" />
|
||||||
</a>
|
</a>
|
||||||
<input v-model="searchText" class="form-control search-input" :placeholder="$t('Search...')" />
|
<form>
|
||||||
|
<input v-model="searchText" class="form-control search-input" :placeholder="$t('Search...')" autocomplete="off" />
|
||||||
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="monitor-list" :class="{ scrollbar: scrollbar }">
|
<div class="monitor-list" :class="{ scrollbar: scrollbar }">
|
||||||
@@ -63,9 +65,16 @@ export default {
|
|||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
searchText: "",
|
searchText: "",
|
||||||
|
windowTop: 0,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
|
boxStyle() {
|
||||||
|
return {
|
||||||
|
height: `calc(100vh - 160px + ${this.windowTop}px)`,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
sortedMonitorList() {
|
sortedMonitorList() {
|
||||||
let result = Object.values(this.$root.monitorList);
|
let result = Object.values(this.$root.monitorList);
|
||||||
|
|
||||||
@@ -108,7 +117,20 @@ export default {
|
|||||||
return result;
|
return result;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
mounted() {
|
||||||
|
window.addEventListener("scroll", this.onScroll);
|
||||||
|
},
|
||||||
|
beforeUnmount() {
|
||||||
|
window.removeEventListener("scroll", this.onScroll);
|
||||||
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
onScroll() {
|
||||||
|
if (window.top.scrollY <= 133) {
|
||||||
|
this.windowTop = window.top.scrollY;
|
||||||
|
} else {
|
||||||
|
this.windowTop = 133;
|
||||||
|
}
|
||||||
|
},
|
||||||
monitorURL(id) {
|
monitorURL(id) {
|
||||||
return getMonitorRelativeURL(id);
|
return getMonitorRelativeURL(id);
|
||||||
},
|
},
|
||||||
@@ -122,6 +144,12 @@ export default {
|
|||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
@import "../assets/vars.scss";
|
@import "../assets/vars.scss";
|
||||||
|
|
||||||
|
.shadow-box {
|
||||||
|
height: calc(100vh - 150px);
|
||||||
|
position: sticky;
|
||||||
|
top: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
.small-padding {
|
.small-padding {
|
||||||
padding-left: 5px !important;
|
padding-left: 5px !important;
|
||||||
padding-right: 5px !important;
|
padding-right: 5px !important;
|
||||||
@@ -137,11 +165,17 @@ export default {
|
|||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
|
|
||||||
.dark & {
|
.dark & {
|
||||||
background-color: #161b22;
|
background-color: $dark-header-bg;
|
||||||
border-bottom: 0;
|
border-bottom: 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.dark {
|
||||||
|
.footer {
|
||||||
|
// background-color: $dark-bg;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@media (max-width: 770px) {
|
@media (max-width: 770px) {
|
||||||
.list-header {
|
.list-header {
|
||||||
margin: -20px;
|
margin: -20px;
|
||||||
|
@@ -85,7 +85,9 @@ export default {
|
|||||||
model: null,
|
model: null,
|
||||||
processing: false,
|
processing: false,
|
||||||
id: null,
|
id: null,
|
||||||
notificationTypes: Object.keys(NotificationFormList),
|
notificationTypes: Object.keys(NotificationFormList).sort((a, b) => {
|
||||||
|
return a.toLowerCase().localeCompare(b.toLowerCase());
|
||||||
|
}),
|
||||||
notification: {
|
notification: {
|
||||||
name: "",
|
name: "",
|
||||||
/** @type { null | keyof NotificationFormList } */
|
/** @type { null | keyof NotificationFormList } */
|
||||||
@@ -143,12 +145,9 @@ export default {
|
|||||||
this.id = null;
|
this.id = null;
|
||||||
this.notification = {
|
this.notification = {
|
||||||
name: "",
|
name: "",
|
||||||
type: null,
|
type: "telegram",
|
||||||
isDefault: false,
|
isDefault: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Set Default value here
|
|
||||||
this.notification.type = this.notificationTypes[0];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
this.modal.show();
|
this.modal.show();
|
||||||
|
206
src/components/ProxyDialog.vue
Normal file
206
src/components/ProxyDialog.vue
Normal file
@@ -0,0 +1,206 @@
|
|||||||
|
<template>
|
||||||
|
<form @submit.prevent="submit">
|
||||||
|
<div ref="modal" class="modal fade" tabindex="-1" data-bs-backdrop="static">
|
||||||
|
<div class="modal-dialog">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h5 id="exampleModalLabel" class="modal-title">
|
||||||
|
{{ $t("Setup Proxy") }}
|
||||||
|
</h5>
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close" />
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="proxy-protocol" class="form-label">{{ $t("Proxy Protocol") }}</label>
|
||||||
|
<select id="proxy-protocol" v-model="proxy.protocol" class="form-select">
|
||||||
|
<option value="https">HTTPS</option>
|
||||||
|
<option value="http">HTTP</option>
|
||||||
|
<option value="socks">SOCKS</option>
|
||||||
|
<option value="socks5">SOCKS v5</option>
|
||||||
|
<option value="socks4">SOCKS v4</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="proxy-host" class="form-label">{{ $t("Proxy Server") }}</label>
|
||||||
|
<div class="d-flex">
|
||||||
|
<input id="proxy-host" v-model="proxy.host" type="text" class="form-control" required :placeholder="$t('Server Address')">
|
||||||
|
<input v-model="proxy.port" type="number" class="form-control ms-2" style="width: 100px" required min="1" max="65535" :placeholder="$t('Port')">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<div class="form-check form-switch">
|
||||||
|
<input id="mark-auth" v-model="proxy.auth" class="form-check-input" type="checkbox">
|
||||||
|
<label for="mark-auth" class="form-check-label">{{ $t("Proxy server has authentication") }}</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="proxy.auth" class="mb-3">
|
||||||
|
<label for="proxy-username" class="form-label">{{ $t("User") }}</label>
|
||||||
|
<input id="proxy-username" v-model="proxy.username" type="text" class="form-control" required>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="proxy.auth" class="mb-3">
|
||||||
|
<label for="proxy-password" class="form-label">{{ $t("Password") }}</label>
|
||||||
|
<input id="proxy-password" v-model="proxy.password" type="password" class="form-control" required>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3 mt-4">
|
||||||
|
<hr class="dropdown-divider mb-4">
|
||||||
|
|
||||||
|
<div class="form-check form-switch">
|
||||||
|
<input id="mark-active" v-model="proxy.active" class="form-check-input" type="checkbox">
|
||||||
|
<label for="mark-active" class="form-check-label">{{ $t("enabled") }}</label>
|
||||||
|
</div>
|
||||||
|
<div class="form-text">
|
||||||
|
{{ $t("enableProxyDescription") }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<br />
|
||||||
|
|
||||||
|
<div class="form-check form-switch">
|
||||||
|
<input id="mark-default" v-model="proxy.default" class="form-check-input" type="checkbox">
|
||||||
|
<label for="mark-default" class="form-check-label">{{ $t("setAsDefault") }}</label>
|
||||||
|
</div>
|
||||||
|
<div class="form-text">
|
||||||
|
{{ $t("setAsDefaultProxyDescription") }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<br />
|
||||||
|
|
||||||
|
<div class="form-check form-switch">
|
||||||
|
<input id="apply-existing" v-model="proxy.applyExisting" class="form-check-input" type="checkbox">
|
||||||
|
<label class="form-check-label" for="apply-existing">{{ $t("Apply on all existing monitors") }}</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button v-if="id" type="button" class="btn btn-danger" :disabled="processing" @click="deleteConfirm">
|
||||||
|
{{ $t("Delete") }}
|
||||||
|
</button>
|
||||||
|
<button type="submit" class="btn btn-primary" :disabled="processing">
|
||||||
|
<div v-if="processing" class="spinner-border spinner-border-sm me-1"></div>
|
||||||
|
{{ $t("Save") }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<Confirm ref="confirmDelete" btn-style="btn-danger" :yes-text="$t('Yes')" :no-text="$t('No')" @yes="deleteProxy">
|
||||||
|
{{ $t("deleteProxyMsg") }}
|
||||||
|
</Confirm>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import { Modal } from "bootstrap";
|
||||||
|
|
||||||
|
import Confirm from "./Confirm.vue";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
components: {
|
||||||
|
Confirm,
|
||||||
|
},
|
||||||
|
props: {},
|
||||||
|
emits: ["added"],
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
model: null,
|
||||||
|
processing: false,
|
||||||
|
id: null,
|
||||||
|
proxy: {
|
||||||
|
protocol: null,
|
||||||
|
host: null,
|
||||||
|
port: null,
|
||||||
|
auth: false,
|
||||||
|
username: null,
|
||||||
|
password: null,
|
||||||
|
active: false,
|
||||||
|
default: false,
|
||||||
|
applyExisting: false,
|
||||||
|
}
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
mounted() {
|
||||||
|
this.modal = new Modal(this.$refs.modal);
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
deleteConfirm() {
|
||||||
|
this.modal.hide();
|
||||||
|
this.$refs.confirmDelete.show();
|
||||||
|
},
|
||||||
|
|
||||||
|
show(proxyID) {
|
||||||
|
if (proxyID) {
|
||||||
|
this.id = proxyID;
|
||||||
|
|
||||||
|
for (let proxy of this.$root.proxyList) {
|
||||||
|
if (proxy.id === proxyID) {
|
||||||
|
this.proxy = proxy;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this.id = null;
|
||||||
|
this.proxy = {
|
||||||
|
protocol: "https",
|
||||||
|
host: null,
|
||||||
|
port: null,
|
||||||
|
auth: false,
|
||||||
|
username: null,
|
||||||
|
password: null,
|
||||||
|
active: true,
|
||||||
|
default: false,
|
||||||
|
applyExisting: false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
this.modal.show();
|
||||||
|
},
|
||||||
|
|
||||||
|
submit() {
|
||||||
|
this.processing = true;
|
||||||
|
this.$root.getSocket().emit("addProxy", this.proxy, this.id, (res) => {
|
||||||
|
this.$root.toastRes(res);
|
||||||
|
this.processing = false;
|
||||||
|
|
||||||
|
if (res.ok) {
|
||||||
|
this.modal.hide();
|
||||||
|
|
||||||
|
// Emit added event, doesn't emit edit.
|
||||||
|
if (! this.id) {
|
||||||
|
this.$emit("added", res.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
deleteProxy() {
|
||||||
|
this.processing = true;
|
||||||
|
this.$root.getSocket().emit("deleteProxy", this.id, (res) => {
|
||||||
|
this.$root.toastRes(res);
|
||||||
|
this.processing = false;
|
||||||
|
|
||||||
|
if (res.ok) {
|
||||||
|
this.modal.hide();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
@import "../assets/vars.scss";
|
||||||
|
|
||||||
|
.dark {
|
||||||
|
.modal-dialog .form-text, .modal-dialog p {
|
||||||
|
color: $dark-font-color;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
@@ -41,6 +41,9 @@
|
|||||||
<Uptime :monitor="monitor.element" type="24" :pill="true" />
|
<Uptime :monitor="monitor.element" type="24" :pill="true" />
|
||||||
{{ monitor.element.name }}
|
{{ monitor.element.name }}
|
||||||
</div>
|
</div>
|
||||||
|
<div v-if="showTags" class="tags">
|
||||||
|
<Tag v-for="tag in monitor.element.tags" :key="tag" :item="tag" :size="'sm'" />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div :key="$root.userHeartbeatBar" class="col-3 col-md-4">
|
<div :key="$root.userHeartbeatBar" class="col-3 col-md-4">
|
||||||
<HeartbeatBar size="small" :monitor-id="monitor.element.id" />
|
<HeartbeatBar size="small" :monitor-id="monitor.element.id" />
|
||||||
@@ -59,18 +62,23 @@
|
|||||||
import Draggable from "vuedraggable";
|
import Draggable from "vuedraggable";
|
||||||
import HeartbeatBar from "./HeartbeatBar.vue";
|
import HeartbeatBar from "./HeartbeatBar.vue";
|
||||||
import Uptime from "./Uptime.vue";
|
import Uptime from "./Uptime.vue";
|
||||||
|
import Tag from "./Tag.vue";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
Draggable,
|
Draggable,
|
||||||
HeartbeatBar,
|
HeartbeatBar,
|
||||||
Uptime,
|
Uptime,
|
||||||
|
Tag,
|
||||||
},
|
},
|
||||||
props: {
|
props: {
|
||||||
editMode: {
|
editMode: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
|
showTags: {
|
||||||
|
type: Boolean,
|
||||||
|
}
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
|
67
src/components/ToggleSection.vue
Normal file
67
src/components/ToggleSection.vue
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
<template>
|
||||||
|
<div class="my-3 py-3">
|
||||||
|
<h5 @click="isOpen = !isOpen">
|
||||||
|
<div
|
||||||
|
class="
|
||||||
|
w-50
|
||||||
|
d-flex
|
||||||
|
justify-content-between
|
||||||
|
align-items-center
|
||||||
|
pe-2
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<span class="pb-2">{{ heading }}</span>
|
||||||
|
<font-awesome-icon
|
||||||
|
icon="chevron-down"
|
||||||
|
class="animated"
|
||||||
|
:class="{ open: isOpen }"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</h5>
|
||||||
|
<transition name="slide-fade-up">
|
||||||
|
<div v-if="isOpen" class="mt-3">
|
||||||
|
<slot></slot>
|
||||||
|
</div>
|
||||||
|
</transition>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
props: {
|
||||||
|
heading: {
|
||||||
|
type: String,
|
||||||
|
default: "",
|
||||||
|
},
|
||||||
|
defaultOpen: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
isOpen: this.defaultOpen,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
@import "../assets/vars.scss";
|
||||||
|
|
||||||
|
h5:after {
|
||||||
|
content: "";
|
||||||
|
display: block;
|
||||||
|
width: 50%;
|
||||||
|
padding-top: 8px;
|
||||||
|
border-bottom: 1px solid $dark-border-color;
|
||||||
|
}
|
||||||
|
|
||||||
|
.open {
|
||||||
|
transform: rotate(180deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.animated {
|
||||||
|
transition: all 0.2s $easing-in;
|
||||||
|
}
|
||||||
|
</style>
|
@@ -19,6 +19,19 @@
|
|||||||
</div>
|
</div>
|
||||||
<p v-if="showURI && twoFAStatus == false" class="text-break mt-2">{{ uri }}</p>
|
<p v-if="showURI && twoFAStatus == false" class="text-break mt-2">{{ uri }}</p>
|
||||||
|
|
||||||
|
<div v-if="!(uri && twoFAStatus == false)" class="mb-3">
|
||||||
|
<label for="current-password" class="form-label">
|
||||||
|
{{ $t("Current Password") }}
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
id="current-password"
|
||||||
|
v-model="currentPassword"
|
||||||
|
type="password"
|
||||||
|
class="form-control"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
<button v-if="uri == null && twoFAStatus == false" class="btn btn-primary" type="button" @click="prepare2FA()">
|
<button v-if="uri == null && twoFAStatus == false" class="btn btn-primary" type="button" @click="prepare2FA()">
|
||||||
{{ $t("Enable 2FA") }}
|
{{ $t("Enable 2FA") }}
|
||||||
</button>
|
</button>
|
||||||
@@ -59,11 +72,11 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Modal } from "bootstrap"
|
import { Modal } from "bootstrap";
|
||||||
import Confirm from "./Confirm.vue";
|
import Confirm from "./Confirm.vue";
|
||||||
import VueQrcode from "vue-qrcode"
|
import VueQrcode from "vue-qrcode";
|
||||||
import { useToast } from "vue-toastification"
|
import { useToast } from "vue-toastification";
|
||||||
const toast = useToast()
|
const toast = useToast();
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
@@ -73,35 +86,36 @@ export default {
|
|||||||
props: {},
|
props: {},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
|
currentPassword: "",
|
||||||
processing: false,
|
processing: false,
|
||||||
uri: null,
|
uri: null,
|
||||||
tokenValid: false,
|
tokenValid: false,
|
||||||
twoFAStatus: null,
|
twoFAStatus: null,
|
||||||
token: null,
|
token: null,
|
||||||
showURI: false,
|
showURI: false,
|
||||||
}
|
};
|
||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
this.modal = new Modal(this.$refs.modal)
|
this.modal = new Modal(this.$refs.modal);
|
||||||
this.getStatus();
|
this.getStatus();
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
show() {
|
show() {
|
||||||
this.modal.show()
|
this.modal.show();
|
||||||
},
|
},
|
||||||
|
|
||||||
confirmEnableTwoFA() {
|
confirmEnableTwoFA() {
|
||||||
this.$refs.confirmEnableTwoFA.show()
|
this.$refs.confirmEnableTwoFA.show();
|
||||||
},
|
},
|
||||||
|
|
||||||
confirmDisableTwoFA() {
|
confirmDisableTwoFA() {
|
||||||
this.$refs.confirmDisableTwoFA.show()
|
this.$refs.confirmDisableTwoFA.show();
|
||||||
},
|
},
|
||||||
|
|
||||||
prepare2FA() {
|
prepare2FA() {
|
||||||
this.processing = true;
|
this.processing = true;
|
||||||
|
|
||||||
this.$root.getSocket().emit("prepare2FA", (res) => {
|
this.$root.getSocket().emit("prepare2FA", this.currentPassword, (res) => {
|
||||||
this.processing = false;
|
this.processing = false;
|
||||||
|
|
||||||
if (res.ok) {
|
if (res.ok) {
|
||||||
@@ -109,49 +123,51 @@ export default {
|
|||||||
} else {
|
} else {
|
||||||
toast.error(res.msg);
|
toast.error(res.msg);
|
||||||
}
|
}
|
||||||
})
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
save2FA() {
|
save2FA() {
|
||||||
this.processing = true;
|
this.processing = true;
|
||||||
|
|
||||||
this.$root.getSocket().emit("save2FA", (res) => {
|
this.$root.getSocket().emit("save2FA", this.currentPassword, (res) => {
|
||||||
this.processing = false;
|
this.processing = false;
|
||||||
|
|
||||||
if (res.ok) {
|
if (res.ok) {
|
||||||
this.$root.toastRes(res)
|
this.$root.toastRes(res);
|
||||||
this.getStatus();
|
this.getStatus();
|
||||||
|
this.currentPassword = "";
|
||||||
this.modal.hide();
|
this.modal.hide();
|
||||||
} else {
|
} else {
|
||||||
toast.error(res.msg);
|
toast.error(res.msg);
|
||||||
}
|
}
|
||||||
})
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
disable2FA() {
|
disable2FA() {
|
||||||
this.processing = true;
|
this.processing = true;
|
||||||
|
|
||||||
this.$root.getSocket().emit("disable2FA", (res) => {
|
this.$root.getSocket().emit("disable2FA", this.currentPassword, (res) => {
|
||||||
this.processing = false;
|
this.processing = false;
|
||||||
|
|
||||||
if (res.ok) {
|
if (res.ok) {
|
||||||
this.$root.toastRes(res)
|
this.$root.toastRes(res);
|
||||||
this.getStatus();
|
this.getStatus();
|
||||||
|
this.currentPassword = "";
|
||||||
this.modal.hide();
|
this.modal.hide();
|
||||||
} else {
|
} else {
|
||||||
toast.error(res.msg);
|
toast.error(res.msg);
|
||||||
}
|
}
|
||||||
})
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
verifyToken() {
|
verifyToken() {
|
||||||
this.$root.getSocket().emit("verifyToken", this.token, (res) => {
|
this.$root.getSocket().emit("verifyToken", this.token, this.currentPassword, (res) => {
|
||||||
if (res.ok) {
|
if (res.ok) {
|
||||||
this.tokenValid = res.valid;
|
this.tokenValid = res.valid;
|
||||||
} else {
|
} else {
|
||||||
toast.error(res.msg);
|
toast.error(res.msg);
|
||||||
}
|
}
|
||||||
})
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
getStatus() {
|
getStatus() {
|
||||||
@@ -161,10 +177,10 @@ export default {
|
|||||||
} else {
|
} else {
|
||||||
toast.error(res.msg);
|
toast.error(res.msg);
|
||||||
}
|
}
|
||||||
})
|
});
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
|
@@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<span :class="className">{{ uptime }}</span>
|
<span :class="className" :title="24 + $t('-hour')">{{ uptime }}</span>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
14
src/components/notifications/Alerta.vue
Normal file
14
src/components/notifications/Alerta.vue
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
<template>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="alerta-api-endpoint" class="form-label">{{ $t("alertaApiEndpoint") }}</label>
|
||||||
|
<input id="alerta-api-endpoint" v-model="$parent.notification.alertaApiEndpoint" type="text" class="form-control" required>
|
||||||
|
<label for="alerta-environment" class="form-label">{{ $t("alertaEnvironment") }}</label>
|
||||||
|
<input id="alerta-environment" v-model="$parent.notification.alertaEnvironment" type="text" class="form-control" required>
|
||||||
|
<label for="alerta-api-key" class="form-label">{{ $t("alertaApiKey") }}</label>
|
||||||
|
<input id="alerta-api-key" v-model="$parent.notification.alertaApiKey" type="text" class="form-control" required>
|
||||||
|
<label for="alerta-alert-state" class="form-label">{{ $t("alertaAlertState") }}</label>
|
||||||
|
<input id="alerta-alert-state" v-model="$parent.notification.alertaAlertState" type="text" class="form-control" placeholder="critical" required>
|
||||||
|
<label for="alerta-recover-state" class="form-label">{{ $t("alertaRecoverState") }}</label>
|
||||||
|
<input id="alerta-recover-state" v-model="$parent.notification.alertaRecoverState" type="text" class="form-control" placeholder="cleared" required>
|
||||||
|
</div>
|
||||||
|
</template>
|
@@ -6,7 +6,7 @@
|
|||||||
<label for="secretAccessKey" class="form-label">{{ $t("SecretAccessKey") }}<span style="color: red;"><sup>*</sup></span></label>
|
<label for="secretAccessKey" class="form-label">{{ $t("SecretAccessKey") }}<span style="color: red;"><sup>*</sup></span></label>
|
||||||
<input id="secretAccessKey" v-model="$parent.notification.secretAccessKey" type="text" class="form-control" required>
|
<input id="secretAccessKey" v-model="$parent.notification.secretAccessKey" type="text" class="form-control" required>
|
||||||
|
|
||||||
<label for="phonenumber" class="form-label">{{ $t("Phonenumber") }}<span style="color: red;"><sup>*</sup></span></label>
|
<label for="phonenumber" class="form-label">{{ $t("PhoneNumbers") }}<span style="color: red;"><sup>*</sup></span></label>
|
||||||
<input id="phonenumber" v-model="$parent.notification.phonenumber" type="text" class="form-control" required>
|
<input id="phonenumber" v-model="$parent.notification.phonenumber" type="text" class="form-control" required>
|
||||||
|
|
||||||
<label for="templateCode" class="form-label">{{ $t("TemplateCode") }}<span style="color: red;"><sup>*</sup></span></label>
|
<label for="templateCode" class="form-label">{{ $t("TemplateCode") }}<span style="color: red;"><sup>*</sup></span></label>
|
||||||
@@ -16,7 +16,7 @@
|
|||||||
<input id="signName" v-model="$parent.notification.signName" type="text" class="form-control" required>
|
<input id="signName" v-model="$parent.notification.signName" type="text" class="form-control" required>
|
||||||
|
|
||||||
<div class="form-text">
|
<div class="form-text">
|
||||||
<p>Sms template must contain parameters: <br> <code>${name} ${time} ${status} ${msg}</code></p>
|
<p>{{ $t("Sms template must contain parameters: ") }}<br> <code>${name} ${time} ${status} ${msg}</code></p>
|
||||||
<i18n-t tag="p" keypath="Read more:">
|
<i18n-t tag="p" keypath="Read more:">
|
||||||
<a href="https://help.aliyun.com/document_detail/101414.html" target="_blank">https://help.aliyun.com/document_detail/101414.html</a>
|
<a href="https://help.aliyun.com/document_detail/101414.html" target="_blank">https://help.aliyun.com/document_detail/101414.html</a>
|
||||||
</i18n-t>
|
</i18n-t>
|
||||||
|
@@ -3,7 +3,7 @@
|
|||||||
<label for="clicksendsms-login" class="form-label">API Username</label>
|
<label for="clicksendsms-login" class="form-label">API Username</label>
|
||||||
<div class="form-text">
|
<div class="form-text">
|
||||||
{{ $t("apiCredentials") }}
|
{{ $t("apiCredentials") }}
|
||||||
<a href="http://dashboard.clicksend.com/account/subaccounts" target="_blank">here</a>
|
<a href="http://dashboard.clicksend.com/account/subaccounts" target="_blank">{{ $t("here") }}</a>
|
||||||
</div>
|
</div>
|
||||||
<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">API Key</label>
|
||||||
|
@@ -7,9 +7,9 @@
|
|||||||
<input id="secretKey" v-model="$parent.notification.secretKey" type="text" class="form-control" required>
|
<input id="secretKey" v-model="$parent.notification.secretKey" type="text" class="form-control" required>
|
||||||
|
|
||||||
<div class="form-text">
|
<div class="form-text">
|
||||||
<p>For safety, must use secret key</p>
|
<p>{{ $t("For safety, must use secret key") }}</p>
|
||||||
<i18n-t tag="p" keypath="Read more:">
|
<i18n-t tag="p" keypath="Read more:">
|
||||||
<a href="https://developers.dingtalk.com/document/robots/custom-robot-access" target="_blank">https://developers.dingtalk.com/document/robots/custom-robot-access</a>
|
<a href="https://developers.dingtalk.com/document/robots/custom-robot-access" target="_blank">https://developers.dingtalk.com/document/robots/custom-robot-access</a> <a href="https://open.dingtalk.com/document/robots/customize-robot-security-settings#title-7fs-kgs-36x" target="_blank">https://open.dingtalk.com/document/robots/customize-robot-security-settings#title-7fs-kgs-36x</a>
|
||||||
</i18n-t>
|
</i18n-t>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
13
src/components/notifications/GoogleChat.vue
Normal file
13
src/components/notifications/GoogleChat.vue
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
<template>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="google-chat-webhook-url" class="form-label">{{ $t("Webhook URL") }}<span style="color: red;"><sup>*</sup></span></label>
|
||||||
|
<input id="google-chat-webhook-url" v-model="$parent.notification.googleChatWebhookURL" type="text" class="form-control" required>
|
||||||
|
|
||||||
|
<div class="form-text">
|
||||||
|
<span style="color: red;"><sup>*</sup></span>{{ $t("Required") }}
|
||||||
|
<i18n-t tag="p" keypath="aboutWebhooks" style="margin-top: 8px;">
|
||||||
|
<a href="https://developers.google.com/chat/how-tos/webhooks" target="_blank">https://developers.google.com/chat/how-tos/webhooks</a>
|
||||||
|
</i18n-t>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
51
src/components/notifications/Gorush.vue
Normal file
51
src/components/notifications/Gorush.vue
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
<template>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="gorush-device-token" class="form-label">{{ $t("Device Token") }}</label><span style="color: red;"><sup>*</sup></span>
|
||||||
|
<div class="input-group mb-3">
|
||||||
|
<input id="gorush-device-token" v-model="$parent.notification.gorushDeviceToken" type="text" class="form-control" required>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="gorush-server-url" class="form-label">{{ $t("Server URL") }}</label><span style="color: red;"><sup>*</sup></span>
|
||||||
|
<div class="input-group mb-3">
|
||||||
|
<input id="gorush-server-url" v-model="$parent.notification.gorushServerURL" type="text" class="form-control" required>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="gorush-platform" class="form-label">{{ $t("Platform") }}</label><span style="color: red;"><sup>*</sup></span>
|
||||||
|
<select id="gorush-platform" v-model="$parent.notification.gorushPlatform" class="form-select">
|
||||||
|
<option value="ios">{{ $t("iOS") }}</option>
|
||||||
|
<option value="android">{{ $t("Android") }}</option>
|
||||||
|
<option value="huawei">{{ $t("Huawei") }}</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="gorush-title" class="form-label">{{ $t("Title") }}</label>
|
||||||
|
<input id="gorush-title" v-model="$parent.notification.gorushTitle" type="text" class="form-control">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="gorush-priority" class="form-label">{{ $t("Priority") }}</label>
|
||||||
|
<select id="gorush-priority" v-model="$parent.notification.gorushPriority" class="form-select">
|
||||||
|
<option value="normal">{{ $t("Normal") }}</option>
|
||||||
|
<option value="high">{{ $t("High") }}</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="gorush-retry" class="form-label">{{ $t("Retry") }}</label>
|
||||||
|
<input id="gorush-retry" v-model="$parent.notification.gorushRetry" type="number" class="form-control">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="gorush-topic" class="form-label">{{ $t("Topic") }}</label>
|
||||||
|
<input id="gorush-topic" v-model="$parent.notification.gorushTopic" type="text" class="form-control">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-text">
|
||||||
|
<span style="color: red;"><sup>*</sup></span>{{ $t("Required") }}
|
||||||
|
</div>
|
||||||
|
</template>
|
@@ -1,82 +1,117 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="mb-3">
|
<div>
|
||||||
<label for="hostname" class="form-label">{{ $t("Hostname") }}</label>
|
<div class="mb-3">
|
||||||
<input id="hostname" v-model="$parent.notification.smtpHost" type="text" class="form-control" required>
|
<label for="hostname" class="form-label">{{ $t("Hostname") }}</label>
|
||||||
</div>
|
<input id="hostname" v-model="$parent.notification.smtpHost" type="text" class="form-control" required>
|
||||||
|
|
||||||
<div class="mb-3">
|
|
||||||
<label for="port" class="form-label">{{ $t("Port") }}</label>
|
|
||||||
<input id="port" v-model="$parent.notification.smtpPort" type="number" class="form-control" required min="0" max="65535" step="1">
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="mb-3">
|
|
||||||
<label for="secure" class="form-label">Secure</label>
|
|
||||||
<select id="secure" v-model="$parent.notification.smtpSecure" class="form-select">
|
|
||||||
<option :value="false">{{ $t("secureOptionNone") }}</option>
|
|
||||||
<option :value="true">{{ $t("secureOptionTLS") }}</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="mb-3">
|
|
||||||
<div class="form-check">
|
|
||||||
<input id="ignore-tls-error" v-model="$parent.notification.smtpIgnoreTLSError" class="form-check-input" type="checkbox" value="">
|
|
||||||
<label class="form-check-label" for="ignore-tls-error">
|
|
||||||
{{ $t("Ignore TLS Error") }}
|
|
||||||
</label>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label for="username" class="form-label">{{ $t("Username") }}</label>
|
<label for="port" class="form-label">{{ $t("Port") }}</label>
|
||||||
<input id="username" v-model="$parent.notification.smtpUsername" type="text" class="form-control" autocomplete="false">
|
<input id="port" v-model="$parent.notification.smtpPort" type="number" class="form-control" required min="0" max="65535" step="1">
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="mb-3">
|
|
||||||
<label for="password" class="form-label">{{ $t("Password") }}</label>
|
|
||||||
<HiddenInput id="password" v-model="$parent.notification.smtpPassword" :required="false" autocomplete="one-time-code"></HiddenInput>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="mb-3">
|
|
||||||
<label for="from-email" class="form-label">{{ $t("From Email") }}</label>
|
|
||||||
<input id="from-email" v-model="$parent.notification.smtpFrom" type="text" class="form-control" required autocomplete="false" placeholder=""Uptime Kuma" <example@kuma.pet>">
|
|
||||||
<div class="form-text">
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label for="to-email" class="form-label">{{ $t("To Email") }}</label>
|
<label for="secure" class="form-label">{{ $t("Security") }}</label>
|
||||||
<input id="to-email" v-model="$parent.notification.smtpTo" type="text" class="form-control" autocomplete="false" placeholder="example2@kuma.pet, example3@kuma.pet" :required="!hasRecipient">
|
<select id="secure" v-model="$parent.notification.smtpSecure" class="form-select">
|
||||||
</div>
|
<option :value="false">{{ $t("secureOptionNone") }}</option>
|
||||||
|
<option :value="true">{{ $t("secureOptionTLS") }}</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label for="to-cc" class="form-label">{{ $t("smtpCC") }}</label>
|
<div class="form-check">
|
||||||
<input id="to-cc" v-model="$parent.notification.smtpCC" type="text" class="form-control" autocomplete="false" :required="!hasRecipient">
|
<input id="ignore-tls-error" v-model="$parent.notification.smtpIgnoreTLSError" class="form-check-input" type="checkbox" value="">
|
||||||
</div>
|
<label class="form-check-label" for="ignore-tls-error">
|
||||||
|
{{ $t("Ignore TLS Error") }}
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label for="to-bcc" class="form-label">{{ $t("smtpBCC") }}</label>
|
<label for="username" class="form-label">{{ $t("Username") }}</label>
|
||||||
<input id="to-bcc" v-model="$parent.notification.smtpBCC" type="text" class="form-control" autocomplete="false" :required="!hasRecipient">
|
<input id="username" v-model="$parent.notification.smtpUsername" type="text" class="form-control" autocomplete="false">
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label for="subject-email" class="form-label">{{ $t("emailCustomSubject") }}</label>
|
<label for="password" class="form-label">{{ $t("Password") }}</label>
|
||||||
<input id="subject-email" v-model="$parent.notification.customSubject" type="text" class="form-control" autocomplete="false" placeholder="">
|
<HiddenInput id="password" v-model="$parent.notification.smtpPassword" :required="false" autocomplete="one-time-code"></HiddenInput>
|
||||||
<div v-pre class="form-text">
|
</div>
|
||||||
(leave blank for default one)<br />
|
|
||||||
{{NAME}}: Service Name<br />
|
<div class="mb-3">
|
||||||
{{HOSTNAME_OR_URL}}: Hostname or URL<br />
|
<label for="from-email" class="form-label">{{ $t("From Email") }}</label>
|
||||||
{{URL}}: URL<br />
|
<input id="from-email" v-model="$parent.notification.smtpFrom" type="text" class="form-control" required autocomplete="false" placeholder=""Uptime Kuma" <example@kuma.pet>">
|
||||||
{{STATUS}}: Status<br />
|
<div class="form-text">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="to-email" class="form-label">{{ $t("To Email") }}</label>
|
||||||
|
<input id="to-email" v-model="$parent.notification.smtpTo" type="text" class="form-control" autocomplete="false" placeholder="example2@kuma.pet, example3@kuma.pet" :required="!hasRecipient">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="to-cc" class="form-label">{{ $t("smtpCC") }}</label>
|
||||||
|
<input id="to-cc" v-model="$parent.notification.smtpCC" type="text" class="form-control" autocomplete="false" :required="!hasRecipient">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="to-bcc" class="form-label">{{ $t("smtpBCC") }}</label>
|
||||||
|
<input id="to-bcc" v-model="$parent.notification.smtpBCC" type="text" class="form-control" autocomplete="false" :required="!hasRecipient">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<ToggleSection :heading="$t('smtpDkimSettings')">
|
||||||
|
<i18n-t tag="div" keypath="smtpDkimDesc" class="form-text mb-3">
|
||||||
|
<a href="https://nodemailer.com/dkim/" target="_blank">{{ $t("documentation") }}</a>
|
||||||
|
</i18n-t>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="dkim-domain" class="form-label">{{ $t("smtpDkimDomain") }}</label>
|
||||||
|
<input id="dkim-domain" v-model="$parent.notification.smtpDkimDomain" type="text" class="form-control" autocomplete="false" placeholder="example.com">
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="dkim-key-selector" class="form-label">{{ $t("smtpDkimKeySelector") }}</label>
|
||||||
|
<input id="dkim-key-selector" v-model="$parent.notification.smtpDkimKeySelector" type="text" class="form-control" autocomplete="false" placeholder="2017">
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="dkim-private-key" class="form-label">{{ $t("smtpDkimPrivateKey") }}</label>
|
||||||
|
<textarea id="dkim-private-key" v-model="$parent.notification.smtpDkimPrivateKey" rows="5" type="text" class="form-control" autocomplete="false" placeholder="-----BEGIN PRIVATE KEY-----"></textarea>
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="dkim-hash-algo" class="form-label">{{ $t("smtpDkimHashAlgo") }}</label>
|
||||||
|
<input id="dkim-hash-algo" v-model="$parent.notification.smtpDkimHashAlgo" type="text" class="form-control" autocomplete="false" placeholder="sha256">
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="dkim-header-fields" class="form-label">{{ $t("smtpDkimheaderFieldNames") }}</label>
|
||||||
|
<input id="dkim-header-fields" v-model="$parent.notification.smtpDkimheaderFieldNames" type="text" class="form-control" autocomplete="false" placeholder="message-id:date:from:to">
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="dkim-skip-fields" class="form-label">{{ $t("smtpDkimskipFields") }}</label>
|
||||||
|
<input id="dkim-skip-fields" v-model="$parent.notification.smtpDkimskipFields" type="text" class="form-control" autocomplete="false" placeholder="message-id:date">
|
||||||
|
</div>
|
||||||
|
</ToggleSection>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="subject-email" class="form-label">{{ $t("emailCustomSubject") }}</label>
|
||||||
|
<input id="subject-email" v-model="$parent.notification.customSubject" type="text" class="form-control" autocomplete="false" placeholder="">
|
||||||
|
<div v-pre class="form-text">
|
||||||
|
(leave blank for default one)<br />
|
||||||
|
{{NAME}}: Service Name<br />
|
||||||
|
{{HOSTNAME_OR_URL}}: Hostname or URL<br />
|
||||||
|
{{URL}}: URL<br />
|
||||||
|
{{STATUS}}: Status<br />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import HiddenInput from "../HiddenInput.vue";
|
import HiddenInput from "../HiddenInput.vue";
|
||||||
|
import ToggleSection from "../ToggleSection.vue";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
HiddenInput,
|
HiddenInput,
|
||||||
|
ToggleSection,
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
hasRecipient() {
|
hasRecipient() {
|
||||||
|
28
src/components/notifications/SerwerSMS.vue
Normal file
28
src/components/notifications/SerwerSMS.vue
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
<template>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="serwersms-username" class="form-label">{{ $t('serwersmsAPIUser') }}</label>
|
||||||
|
<input id="serwersms-username" v-model="$parent.notification.serwersmsUsername" type="text" class="form-control" required>
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="serwersms-key" class="form-label">{{ $t('serwersmsAPIPassword') }}</label>
|
||||||
|
<HiddenInput id="serwersms-key" v-model="$parent.notification.serwersmsPassword" :required="true" autocomplete="one-time-code"></HiddenInput>
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="serwersms-phone-number" class="form-label">{{ $t("serwersmsPhoneNumber") }}</label>
|
||||||
|
<input id="serwersms-phone-number" v-model="$parent.notification.serwersmsPhoneNumber" type="text" class="form-control" required>
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="serwersms-sender-name" class="form-label">{{ $t("serwersmsSenderName") }}</label>
|
||||||
|
<input id="serwersms-sender-name" v-model="$parent.notification.serwersmsSenderName" type="text" minlength="3" maxlength="11" class="form-control">
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import HiddenInput from "../HiddenInput.vue";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
components: {
|
||||||
|
HiddenInput,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
13
src/components/notifications/Stackfield.vue
Normal file
13
src/components/notifications/Stackfield.vue
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
<template>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="stackfield-webhook-url" class="form-label">{{ $t("Webhook URL") }}<span style="color: red;"><sup>*</sup></span></label>
|
||||||
|
<input id="stackfield-webhook-url" v-model="$parent.notification.stackfieldwebhookURL" type="text" class="form-control" required>
|
||||||
|
|
||||||
|
<div class="form-text">
|
||||||
|
<span style="color: red;"><sup>*</sup></span>{{ $t("Required") }}
|
||||||
|
<i18n-t tag="p" keypath="aboutWebhooks" style="margin-top: 8px;">
|
||||||
|
<a href="https://www.stackfield.com/developer-api#AnchorAPI2" target="_blank">https://www.stackfield.com/developer-api#AnchorAPI2</a>
|
||||||
|
</i18n-t>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
20
src/components/notifications/TechulusPush.vue
Normal file
20
src/components/notifications/TechulusPush.vue
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
<template>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="push-api-key" class="form-label">API_KEY</label>
|
||||||
|
<HiddenInput id="push-api-key" v-model="$parent.notification.pushAPIKey" :required="true" autocomplete="one-time-code"></HiddenInput>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<i18n-t tag="p" keypath="More info on:" style="margin-top: 8px;">
|
||||||
|
<a href="https://docs.push.techulus.com" target="_blank">https://docs.push.techulus.com</a>
|
||||||
|
</i18n-t>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import HiddenInput from "../HiddenInput.vue";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
components: {
|
||||||
|
HiddenInput,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
@@ -25,13 +25,7 @@
|
|||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p style="margin-top: 8px;">
|
<p style="margin-top: 8px;">
|
||||||
<template v-if="$parent.notification.telegramBotToken">
|
<a :href="telegramGetUpdatesURL('withToken')" target="_blank" style="word-break: break-word;">{{ telegramGetUpdatesURL("masked") }}</a>
|
||||||
<a :href="telegramGetUpdatesURL" target="_blank" style="word-break: break-word;">{{ telegramGetUpdatesURL }}</a>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<template v-else>
|
|
||||||
{{ telegramGetUpdatesURL }}
|
|
||||||
</template>
|
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -40,49 +34,51 @@
|
|||||||
<script>
|
<script>
|
||||||
import HiddenInput from "../HiddenInput.vue";
|
import HiddenInput from "../HiddenInput.vue";
|
||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
import { useToast } from "vue-toastification"
|
import { useToast } from "vue-toastification";
|
||||||
const toast = useToast();
|
const toast = useToast();
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
HiddenInput,
|
HiddenInput,
|
||||||
},
|
},
|
||||||
computed: {
|
methods: {
|
||||||
telegramGetUpdatesURL() {
|
telegramGetUpdatesURL(mode = "masked") {
|
||||||
let token = `<${this.$t("YOUR BOT TOKEN HERE")}>`
|
let token = `<${this.$t("YOUR BOT TOKEN HERE")}>`;
|
||||||
|
|
||||||
if (this.$parent.notification.telegramBotToken) {
|
if (this.$parent.notification.telegramBotToken) {
|
||||||
token = this.$parent.notification.telegramBotToken;
|
if (mode === "withToken") {
|
||||||
|
token = this.$parent.notification.telegramBotToken;
|
||||||
|
} else if (mode === "masked") {
|
||||||
|
token = "*".repeat(this.$parent.notification.telegramBotToken.length);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return `https://api.telegram.org/bot${token}/getUpdates`;
|
return `https://api.telegram.org/bot${token}/getUpdates`;
|
||||||
},
|
},
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
async autoGetTelegramChatID() {
|
async autoGetTelegramChatID() {
|
||||||
try {
|
try {
|
||||||
let res = await axios.get(this.telegramGetUpdatesURL)
|
let res = await axios.get(this.telegramGetUpdatesURL("withToken"));
|
||||||
|
|
||||||
if (res.data.result.length >= 1) {
|
if (res.data.result.length >= 1) {
|
||||||
let update = res.data.result[res.data.result.length - 1]
|
let update = res.data.result[res.data.result.length - 1];
|
||||||
|
|
||||||
if (update.channel_post) {
|
if (update.channel_post) {
|
||||||
this.notification.telegramChatID = update.channel_post.chat.id;
|
this.$parent.notification.telegramChatID = update.channel_post.chat.id;
|
||||||
} else if (update.message) {
|
} else if (update.message) {
|
||||||
this.notification.telegramChatID = update.message.chat.id;
|
this.$parent.notification.telegramChatID = update.message.chat.id;
|
||||||
} else {
|
} else {
|
||||||
throw new Error(this.$t("chatIDNotFound"))
|
throw new Error(this.$t("chatIDNotFound"));
|
||||||
}
|
}
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
throw new Error(this.$t("chatIDNotFound"))
|
throw new Error(this.$t("chatIDNotFound"));
|
||||||
}
|
}
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
toast.error(error.message)
|
toast.error(error.message);
|
||||||
}
|
}
|
||||||
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
</script>
|
</script>
|
||||||
|
12
src/components/notifications/WeCom.vue
Normal file
12
src/components/notifications/WeCom.vue
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
<template>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="WeCom Bot Key" class="form-label">{{ $t("WeCom Bot Key") }}<span style="color: red;"><sup>*</sup></span></label>
|
||||||
|
<input id="WeCom Bot Key" v-model="$parent.notification.weComBotKey" type="text" class="form-control" required>
|
||||||
|
<div class="form-text">
|
||||||
|
<p><span style="color: red;"><sup>*</sup></span>{{ $t("Required") }}</p>
|
||||||
|
</div>
|
||||||
|
<i18n-t tag="p" keypath="Read more:">
|
||||||
|
<a href="https://work.weixin.qq.com/api/doc/90000/90136/91770" target="_blank">https://work.weixin.qq.com/api/doc/90000/90136/91770</a>
|
||||||
|
</i18n-t>
|
||||||
|
</div>
|
||||||
|
</template>
|
@@ -1,4 +1,4 @@
|
|||||||
import STMP from "./SMTP.vue"
|
import STMP from "./SMTP.vue";
|
||||||
import Telegram from "./Telegram.vue";
|
import Telegram from "./Telegram.vue";
|
||||||
import Discord from "./Discord.vue";
|
import Discord from "./Discord.vue";
|
||||||
import Webhook from "./Webhook.vue";
|
import Webhook from "./Webhook.vue";
|
||||||
@@ -9,6 +9,7 @@ import RocketChat from "./RocketChat.vue";
|
|||||||
import Teams from "./Teams.vue";
|
import Teams from "./Teams.vue";
|
||||||
import Pushover from "./Pushover.vue";
|
import Pushover from "./Pushover.vue";
|
||||||
import Pushy from "./Pushy.vue";
|
import Pushy from "./Pushy.vue";
|
||||||
|
import TechulusPush from "./TechulusPush.vue";
|
||||||
import Octopush from "./Octopush.vue";
|
import Octopush from "./Octopush.vue";
|
||||||
import PromoSMS from "./PromoSMS.vue";
|
import PromoSMS from "./PromoSMS.vue";
|
||||||
import ClickSendSMS from "./ClickSendSMS.vue";
|
import ClickSendSMS from "./ClickSendSMS.vue";
|
||||||
@@ -22,6 +23,12 @@ import Matrix from "./Matrix.vue";
|
|||||||
import AliyunSMS from "./AliyunSms.vue";
|
import AliyunSMS from "./AliyunSms.vue";
|
||||||
import DingDing from "./DingDing.vue";
|
import DingDing from "./DingDing.vue";
|
||||||
import Bark from "./Bark.vue";
|
import Bark from "./Bark.vue";
|
||||||
|
import SerwerSMS from "./SerwerSMS.vue";
|
||||||
|
import Stackfield from './Stackfield.vue';
|
||||||
|
import WeCom from "./WeCom.vue";
|
||||||
|
import GoogleChat from "./GoogleChat.vue";
|
||||||
|
import Gorush from "./Gorush.vue";
|
||||||
|
import Alerta from "./Alerta.vue";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Manage all notification form.
|
* Manage all notification form.
|
||||||
@@ -40,6 +47,7 @@ const NotificationFormList = {
|
|||||||
"rocket.chat": RocketChat,
|
"rocket.chat": RocketChat,
|
||||||
"pushover": Pushover,
|
"pushover": Pushover,
|
||||||
"pushy": Pushy,
|
"pushy": Pushy,
|
||||||
|
"PushByTechulus": TechulusPush,
|
||||||
"octopush": Octopush,
|
"octopush": Octopush,
|
||||||
"promosms": PromoSMS,
|
"promosms": PromoSMS,
|
||||||
"clicksendsms": ClickSendSMS,
|
"clicksendsms": ClickSendSMS,
|
||||||
@@ -52,7 +60,13 @@ const NotificationFormList = {
|
|||||||
"mattermost": Mattermost,
|
"mattermost": Mattermost,
|
||||||
"matrix": Matrix,
|
"matrix": Matrix,
|
||||||
"DingDing": DingDing,
|
"DingDing": DingDing,
|
||||||
"Bark": Bark
|
"Bark": Bark,
|
||||||
}
|
"serwersms": SerwerSMS,
|
||||||
|
"stackfield": Stackfield,
|
||||||
|
"WeCom": WeCom,
|
||||||
|
"GoogleChat": GoogleChat,
|
||||||
|
"gorush": Gorush,
|
||||||
|
"alerta": Alerta,
|
||||||
|
};
|
||||||
|
|
||||||
export default NotificationFormList
|
export default NotificationFormList;
|
||||||
|
50
src/components/settings/About.vue
Normal file
50
src/components/settings/About.vue
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
<template>
|
||||||
|
<div class="d-flex justify-content-center align-items-center">
|
||||||
|
<div class="logo d-flex flex-column justify-content-center align-items-center">
|
||||||
|
<object class="my-4" width="200" height="200" data="/icon.svg" />
|
||||||
|
<div class="fs-4 fw-bold">Uptime Kuma</div>
|
||||||
|
<div>{{ $t("Version") }}: {{ $root.info.version }}</div>
|
||||||
|
|
||||||
|
<div class="my-3 update-link"><a href="https://github.com/louislam/uptime-kuma/releases" target="_blank" rel="noopener">{{ $t("Check Update On GitHub") }}</a></div>
|
||||||
|
|
||||||
|
<div class="mt-1">
|
||||||
|
<div class="form-check">
|
||||||
|
<label><input v-model="settings.checkUpdate" type="checkbox" @change="saveSettings()" /> Show update if available</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-check">
|
||||||
|
<label><input v-model="settings.checkBeta" type="checkbox" :disabled="!settings.checkUpdate" @change="saveSettings()" /> Also check beta release</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
computed: {
|
||||||
|
settings() {
|
||||||
|
return this.$parent.$parent.$parent.settings;
|
||||||
|
},
|
||||||
|
saveSettings() {
|
||||||
|
return this.$parent.$parent.$parent.saveSettings;
|
||||||
|
},
|
||||||
|
settingsLoaded() {
|
||||||
|
return this.$parent.$parent.$parent.settingsLoaded;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
watch: {
|
||||||
|
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.logo {
|
||||||
|
margin: 4em 1em;
|
||||||
|
}
|
||||||
|
.update-link {
|
||||||
|
font-size: 0.9em;
|
||||||
|
}
|
||||||
|
</style>
|
143
src/components/settings/Appearance.vue
Normal file
143
src/components/settings/Appearance.vue
Normal file
@@ -0,0 +1,143 @@
|
|||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<div class="my-4">
|
||||||
|
<label for="language" class="form-label">
|
||||||
|
{{ $t("Language") }}
|
||||||
|
</label>
|
||||||
|
<select id="language" v-model="$root.language" class="form-select">
|
||||||
|
<option
|
||||||
|
v-for="(lang, i) in $i18n.availableLocales"
|
||||||
|
:key="`Lang${i}`"
|
||||||
|
:value="lang"
|
||||||
|
>
|
||||||
|
{{ $i18n.messages[lang].languageName }}
|
||||||
|
</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="my-4">
|
||||||
|
<label for="timezone" class="form-label">{{ $t("Theme") }}</label>
|
||||||
|
<div>
|
||||||
|
<div
|
||||||
|
class="btn-group"
|
||||||
|
role="group"
|
||||||
|
aria-label="Basic checkbox toggle button group"
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
id="btncheck1"
|
||||||
|
v-model="$root.userTheme"
|
||||||
|
type="radio"
|
||||||
|
class="btn-check"
|
||||||
|
name="theme"
|
||||||
|
autocomplete="off"
|
||||||
|
value="light"
|
||||||
|
/>
|
||||||
|
<label class="btn btn-outline-primary" for="btncheck1">
|
||||||
|
{{ $t("Light") }}
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<input
|
||||||
|
id="btncheck2"
|
||||||
|
v-model="$root.userTheme"
|
||||||
|
type="radio"
|
||||||
|
class="btn-check"
|
||||||
|
name="theme"
|
||||||
|
autocomplete="off"
|
||||||
|
value="dark"
|
||||||
|
/>
|
||||||
|
<label class="btn btn-outline-primary" for="btncheck2">
|
||||||
|
{{ $t("Dark") }}
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<input
|
||||||
|
id="btncheck3"
|
||||||
|
v-model="$root.userTheme"
|
||||||
|
type="radio"
|
||||||
|
class="btn-check"
|
||||||
|
name="theme"
|
||||||
|
autocomplete="off"
|
||||||
|
value="auto"
|
||||||
|
/>
|
||||||
|
<label class="btn btn-outline-primary" for="btncheck3">
|
||||||
|
{{ $t("Auto") }}
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="my-4">
|
||||||
|
<label class="form-label">{{ $t("Theme - Heartbeat Bar") }}</label>
|
||||||
|
<div>
|
||||||
|
<div
|
||||||
|
class="btn-group"
|
||||||
|
role="group"
|
||||||
|
aria-label="Basic checkbox toggle button group"
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
id="btncheck4"
|
||||||
|
v-model="$root.userHeartbeatBar"
|
||||||
|
type="radio"
|
||||||
|
class="btn-check"
|
||||||
|
name="heartbeatBarTheme"
|
||||||
|
autocomplete="off"
|
||||||
|
value="normal"
|
||||||
|
/>
|
||||||
|
<label class="btn btn-outline-primary" for="btncheck4">
|
||||||
|
{{ $t("Normal") }}
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<input
|
||||||
|
id="btncheck5"
|
||||||
|
v-model="$root.userHeartbeatBar"
|
||||||
|
type="radio"
|
||||||
|
class="btn-check"
|
||||||
|
name="heartbeatBarTheme"
|
||||||
|
autocomplete="off"
|
||||||
|
value="bottom"
|
||||||
|
/>
|
||||||
|
<label class="btn btn-outline-primary" for="btncheck5">
|
||||||
|
{{ $t("Bottom") }}
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<input
|
||||||
|
id="btncheck6"
|
||||||
|
v-model="$root.userHeartbeatBar"
|
||||||
|
type="radio"
|
||||||
|
class="btn-check"
|
||||||
|
name="heartbeatBarTheme"
|
||||||
|
autocomplete="off"
|
||||||
|
value="none"
|
||||||
|
/>
|
||||||
|
<label class="btn btn-outline-primary" for="btncheck6">
|
||||||
|
{{ $t("None") }}
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
@import "../../assets/vars.scss";
|
||||||
|
|
||||||
|
.btn-check:active + .btn-outline-primary,
|
||||||
|
.btn-check:checked + .btn-outline-primary,
|
||||||
|
.btn-check:hover + .btn-outline-primary {
|
||||||
|
color: #fff;
|
||||||
|
|
||||||
|
.dark & {
|
||||||
|
color: #000;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark {
|
||||||
|
.list-group-item {
|
||||||
|
background-color: $dark-bg2;
|
||||||
|
color: $dark-font-color;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
213
src/components/settings/Backup.vue
Normal file
213
src/components/settings/Backup.vue
Normal file
@@ -0,0 +1,213 @@
|
|||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<div class="my-4">
|
||||||
|
<h4 class="mt-4 mb-2">{{ $t("Export Backup") }}</h4>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
{{ $t("backupDescription") }} <br />
|
||||||
|
({{ $t("backupDescription2") }}) <br />
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div class="mb-2">
|
||||||
|
<button class="btn btn-primary" @click="downloadBackup">
|
||||||
|
{{ $t("Export") }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
<strong>{{ $t("backupDescription3") }}</strong>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="my-4">
|
||||||
|
<h4 class="mt-4 mb-2">{{ $t("Import Backup") }}</h4>
|
||||||
|
|
||||||
|
<label class="form-label">{{ $t("Options") }}:</label>
|
||||||
|
<br />
|
||||||
|
<div class="form-check form-check-inline">
|
||||||
|
<input
|
||||||
|
id="radioKeep"
|
||||||
|
v-model="importHandle"
|
||||||
|
class="form-check-input"
|
||||||
|
type="radio"
|
||||||
|
name="radioImportHandle"
|
||||||
|
value="keep"
|
||||||
|
/>
|
||||||
|
<label class="form-check-label" for="radioKeep">
|
||||||
|
{{ $t("Keep both") }}
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div class="form-check form-check-inline">
|
||||||
|
<input
|
||||||
|
id="radioSkip"
|
||||||
|
v-model="importHandle"
|
||||||
|
class="form-check-input"
|
||||||
|
type="radio"
|
||||||
|
name="radioImportHandle"
|
||||||
|
value="skip"
|
||||||
|
/>
|
||||||
|
<label class="form-check-label" for="radioSkip">
|
||||||
|
{{ $t("Skip existing") }}
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div class="form-check form-check-inline">
|
||||||
|
<input
|
||||||
|
id="radioOverwrite"
|
||||||
|
v-model="importHandle"
|
||||||
|
class="form-check-input"
|
||||||
|
type="radio"
|
||||||
|
name="radioImportHandle"
|
||||||
|
value="overwrite"
|
||||||
|
/>
|
||||||
|
<label class="form-check-label" for="radioOverwrite">
|
||||||
|
{{ $t("Overwrite") }}
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div class="form-text mb-2">
|
||||||
|
{{ $t("importHandleDescription") }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-2">
|
||||||
|
<input
|
||||||
|
id="importBackup"
|
||||||
|
type="file"
|
||||||
|
class="form-control"
|
||||||
|
accept="application/json"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="input-group mb-2 justify-content-end">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="btn btn-outline-primary"
|
||||||
|
:disabled="processing"
|
||||||
|
@click="confirmImport"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
v-if="processing"
|
||||||
|
class="spinner-border spinner-border-sm me-1"
|
||||||
|
></div>
|
||||||
|
{{ $t("Import") }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
v-if="importAlert"
|
||||||
|
class="alert alert-danger mt-3"
|
||||||
|
style="padding: 6px 16px"
|
||||||
|
>
|
||||||
|
{{ importAlert }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Confirm
|
||||||
|
ref="confirmImport"
|
||||||
|
btn-style="btn-danger"
|
||||||
|
:yes-text="$t('Yes')"
|
||||||
|
:no-text="$t('No')"
|
||||||
|
@yes="importBackup"
|
||||||
|
>
|
||||||
|
{{ $t("confirmImportMsg") }}
|
||||||
|
</Confirm>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import Confirm from "../../components/Confirm.vue";
|
||||||
|
import dayjs from "dayjs";
|
||||||
|
import { useToast } from "vue-toastification";
|
||||||
|
|
||||||
|
const toast = useToast();
|
||||||
|
|
||||||
|
export default {
|
||||||
|
components: {
|
||||||
|
Confirm,
|
||||||
|
},
|
||||||
|
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
processing: false,
|
||||||
|
importHandle: "skip",
|
||||||
|
importAlert: null,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
confirmImport() {
|
||||||
|
this.$refs.confirmImport.show();
|
||||||
|
},
|
||||||
|
|
||||||
|
downloadBackup() {
|
||||||
|
let time = dayjs().format("YYYY_MM_DD-hh_mm_ss");
|
||||||
|
let fileName = `Uptime_Kuma_Backup_${time}.json`;
|
||||||
|
let monitorList = Object.values(this.$root.monitorList);
|
||||||
|
let exportData = {
|
||||||
|
version: this.$root.info.version,
|
||||||
|
notificationList: this.$root.notificationList,
|
||||||
|
monitorList: monitorList,
|
||||||
|
};
|
||||||
|
exportData = JSON.stringify(exportData, null, 4);
|
||||||
|
let downloadItem = document.createElement("a");
|
||||||
|
downloadItem.setAttribute(
|
||||||
|
"href",
|
||||||
|
"data:application/json;charset=utf-8," +
|
||||||
|
encodeURIComponent(exportData)
|
||||||
|
);
|
||||||
|
downloadItem.setAttribute("download", fileName);
|
||||||
|
downloadItem.click();
|
||||||
|
},
|
||||||
|
|
||||||
|
importBackup() {
|
||||||
|
this.processing = true;
|
||||||
|
let uploadItem = document.getElementById("importBackup").files;
|
||||||
|
|
||||||
|
if (uploadItem.length <= 0) {
|
||||||
|
this.processing = false;
|
||||||
|
return (this.importAlert = this.$t("alertNoFile"));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (uploadItem.item(0).type !== "application/json") {
|
||||||
|
this.processing = false;
|
||||||
|
return (this.importAlert = this.$t("alertWrongFileType"));
|
||||||
|
}
|
||||||
|
|
||||||
|
let fileReader = new FileReader();
|
||||||
|
fileReader.readAsText(uploadItem.item(0));
|
||||||
|
|
||||||
|
fileReader.onload = (item) => {
|
||||||
|
this.$root.uploadBackup(
|
||||||
|
item.target.result,
|
||||||
|
this.importHandle,
|
||||||
|
(res) => {
|
||||||
|
this.processing = false;
|
||||||
|
|
||||||
|
if (res.ok) {
|
||||||
|
toast.success(res.msg);
|
||||||
|
} else {
|
||||||
|
toast.error(res.msg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
};
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
@import "../../assets/vars.scss";
|
||||||
|
|
||||||
|
.dark {
|
||||||
|
#importBackup {
|
||||||
|
&::file-selector-button {
|
||||||
|
color: $primary;
|
||||||
|
background-color: $dark-bg;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover:not(:disabled):not([readonly])::file-selector-button {
|
||||||
|
color: $dark-font-color2;
|
||||||
|
background-color: $primary;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
192
src/components/settings/General.vue
Normal file
192
src/components/settings/General.vue
Normal file
@@ -0,0 +1,192 @@
|
|||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<form class="my-4" @submit.prevent="saveGeneral">
|
||||||
|
<!-- Timezone -->
|
||||||
|
<div class="mb-4">
|
||||||
|
<label for="timezone" class="form-label">
|
||||||
|
{{ $t("Timezone") }}
|
||||||
|
</label>
|
||||||
|
<select id="timezone" v-model="$root.userTimezone" class="form-select">
|
||||||
|
<option value="auto">
|
||||||
|
{{ $t("Auto") }}: {{ guessTimezone }}
|
||||||
|
</option>
|
||||||
|
<option
|
||||||
|
v-for="(timezone, index) in timezoneList"
|
||||||
|
:key="index"
|
||||||
|
:value="timezone.value"
|
||||||
|
>
|
||||||
|
{{ timezone.name }}
|
||||||
|
</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Search Engine -->
|
||||||
|
<div class="mb-4">
|
||||||
|
<label class="form-label">
|
||||||
|
{{ $t("Search Engine Visibility") }}
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<div class="form-check">
|
||||||
|
<input
|
||||||
|
id="searchEngineIndexYes"
|
||||||
|
v-model="settings.searchEngineIndex"
|
||||||
|
class="form-check-input"
|
||||||
|
type="radio"
|
||||||
|
name="flexRadioDefault"
|
||||||
|
:value="true"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
<label class="form-check-label" for="searchEngineIndexYes">
|
||||||
|
{{ $t("Allow indexing") }}
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div class="form-check">
|
||||||
|
<input
|
||||||
|
id="searchEngineIndexNo"
|
||||||
|
v-model="settings.searchEngineIndex"
|
||||||
|
class="form-check-input"
|
||||||
|
type="radio"
|
||||||
|
name="flexRadioDefault"
|
||||||
|
:value="false"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
<label class="form-check-label" for="searchEngineIndexNo">
|
||||||
|
{{ $t("Discourage search engines from indexing site") }}
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Entry Page -->
|
||||||
|
<div class="mb-4">
|
||||||
|
<label class="form-label">{{ $t("Entry Page") }}</label>
|
||||||
|
|
||||||
|
<div class="form-check">
|
||||||
|
<input
|
||||||
|
id="entryPageDashboard"
|
||||||
|
v-model="settings.entryPage"
|
||||||
|
class="form-check-input"
|
||||||
|
type="radio"
|
||||||
|
name="entryPage"
|
||||||
|
value="dashboard"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
<label class="form-check-label" for="entryPageDashboard">
|
||||||
|
{{ $t("Dashboard") }}
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-for="statusPage in $root.statusPageList" :key="statusPage.id" class="form-check">
|
||||||
|
<input
|
||||||
|
:id="'status-page-' + statusPage.id"
|
||||||
|
v-model="settings.entryPage"
|
||||||
|
class="form-check-input"
|
||||||
|
type="radio"
|
||||||
|
name="entryPage"
|
||||||
|
:value="'statusPage-' + statusPage.slug"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
<label class="form-check-label" :for="'status-page-' + statusPage.id">
|
||||||
|
{{ $t("Status Page") }} - {{ statusPage.title }}
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Primary Base URL -->
|
||||||
|
<div class="mb-4">
|
||||||
|
<label class="form-label" for="primaryBaseURL">
|
||||||
|
{{ $t("Primary Base URL") }}
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<div class="input-group mb-3">
|
||||||
|
<input
|
||||||
|
id="primaryBaseURL"
|
||||||
|
v-model="settings.primaryBaseURL"
|
||||||
|
class="form-control"
|
||||||
|
name="primaryBaseURL"
|
||||||
|
placeholder="https://"
|
||||||
|
pattern="https?://.+"
|
||||||
|
/>
|
||||||
|
<button class="btn btn-outline-primary" type="button" @click="autoGetPrimaryBaseURL">
|
||||||
|
{{ $t("Auto Get") }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-text"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Steam API Key -->
|
||||||
|
<div class="mb-4">
|
||||||
|
<label class="form-label" for="steamAPIKey">
|
||||||
|
{{ $t("Steam API Key") }}
|
||||||
|
</label>
|
||||||
|
<HiddenInput
|
||||||
|
id="steamAPIKey"
|
||||||
|
v-model="settings.steamAPIKey"
|
||||||
|
autocomplete="one-time-code"
|
||||||
|
/>
|
||||||
|
<div class="form-text">
|
||||||
|
{{ $t("steamApiKeyDescription") }}
|
||||||
|
<a href="https://steamcommunity.com/dev" target="_blank">
|
||||||
|
https://steamcommunity.com/dev
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Save Button -->
|
||||||
|
<div>
|
||||||
|
<button class="btn btn-primary" type="submit">
|
||||||
|
{{ $t("Save") }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import HiddenInput from "../../components/HiddenInput.vue";
|
||||||
|
import dayjs from "dayjs";
|
||||||
|
import utc from "dayjs/plugin/utc";
|
||||||
|
import timezone from "dayjs/plugin/timezone";
|
||||||
|
import { timezoneList } from "../../util-frontend";
|
||||||
|
dayjs.extend(utc);
|
||||||
|
dayjs.extend(timezone);
|
||||||
|
|
||||||
|
export default {
|
||||||
|
components: {
|
||||||
|
HiddenInput,
|
||||||
|
},
|
||||||
|
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
timezoneList: timezoneList(),
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
settings() {
|
||||||
|
return this.$parent.$parent.$parent.settings;
|
||||||
|
},
|
||||||
|
saveSettings() {
|
||||||
|
return this.$parent.$parent.$parent.saveSettings;
|
||||||
|
},
|
||||||
|
settingsLoaded() {
|
||||||
|
return this.$parent.$parent.$parent.settingsLoaded;
|
||||||
|
},
|
||||||
|
guessTimezone() {
|
||||||
|
return dayjs.tz.guess();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
saveGeneral() {
|
||||||
|
localStorage.timezone = this.$root.userTimezone;
|
||||||
|
this.saveSettings();
|
||||||
|
},
|
||||||
|
autoGetPrimaryBaseURL() {
|
||||||
|
this.settings.primaryBaseURL = location.protocol + "//" + location.host;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style></style>
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user