mirror of
https://github.com/louislam/uptime-kuma.git
synced 2025-09-11 05:16:55 +08:00
Compare commits
712 Commits
1.9.2
...
1.14.0-bet
Author | SHA1 | Date | |
---|---|---|---|
|
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 | ||
|
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 | ||
|
124c98ce76 | ||
|
61135e8500 | ||
|
08a58dec2b | ||
|
741ed548da | ||
|
52d80d3a5d | ||
|
586c748d44 | ||
|
b5d6e96b1d | ||
|
68b74f07e4 | ||
|
bc615c2dd8 | ||
|
e7104737e7 | ||
|
1dbf1c3dea | ||
|
74688e69aa | ||
|
b32bfb3ff1 | ||
|
24664cde2c | ||
|
348c5ec995 | ||
|
9143b73f84 | ||
|
5e6d945095 | ||
|
ba93129b18 | ||
|
036218f711 | ||
|
69aa60d1fb | ||
|
eaecd6e571 | ||
|
f2a27a2cf1 | ||
|
d4c9431142 | ||
|
d7f7dba13f | ||
|
3e5ae00d25 | ||
|
5311bef3eb | ||
|
c400595f67 | ||
|
e84f7dac60 | ||
|
67a22399bc | ||
|
947fc6001e | ||
|
c87e67ad1b | ||
|
6f92774a8f | ||
|
6e76ab7426 | ||
|
b2fbd7e263 | ||
|
199e6ec82b | ||
|
18a99c2016 | ||
|
e261a27ebe | ||
|
de5cce9d90 | ||
|
b85c9186f9 | ||
|
eb22ad5ffe | ||
|
f5f4835b74 | ||
|
44c1b336dc | ||
|
110ec491ee | ||
|
640b6e5b1c | ||
|
234fba3978 | ||
|
1285ccb537 | ||
|
6c542edfc9 | ||
|
767807dd22 | ||
|
546402f3d2 | ||
|
698a38e773 | ||
|
71884cf42a | ||
|
d676c782bb | ||
|
dd773aa5a2 | ||
|
2852e59ffb | ||
|
cb3da50e7e | ||
|
f25653d778 | ||
|
2e7ad1b7b2 | ||
|
e0e1ab6fa6 | ||
|
8d984881c9 | ||
|
955f9ae20a | ||
|
a9e319517a | ||
|
39ad8b4bb7 | ||
|
8fb8cbdaf3 | ||
|
3f3d8b4eb3 | ||
|
9123e9461f | ||
|
77addfebc8 | ||
|
16846c7c6d | ||
|
d1c4d13903 | ||
|
7cd4bfc11d | ||
|
1d5c0502ab | ||
|
a1cda93ad5 | ||
|
3bd420f0e0 | ||
|
78424b4f2d | ||
|
f8055ed03d | ||
|
fe4724fc53 | ||
|
7f0dda6a44 | ||
|
43791ee97e | ||
|
6362ef6a9c | ||
|
9d3a4e9d1e | ||
|
6c60096f56 | ||
|
ba1e025353 | ||
|
a41a081727 | ||
|
a5f15f2319 | ||
|
e69799f613 | ||
|
3c795bebe3 | ||
|
3a43fec666 | ||
|
24c645e437 | ||
|
9d31da1fe8 | ||
|
2e4c42941a | ||
|
4fc2603818 | ||
|
7bc38d4231 | ||
|
daad63d70b | ||
|
9ddd2c7365 | ||
|
fa5ba12e14 | ||
|
85053f865e | ||
|
1239f6d1a2 | ||
|
fed611d1b9 | ||
|
bc68088350 | ||
|
90200958cd | ||
|
aa13d74d7a | ||
|
d82f305f6e | ||
|
7c63cbfd84 | ||
|
c7e1267779 | ||
|
5d0b54c292 | ||
|
b50b390048 | ||
|
65158cb06b | ||
|
8fe5e4e605 | ||
|
ab5ddae2ee | ||
|
89c64f4ea2 | ||
|
40a1ebecc5 | ||
|
e1793596fe | ||
|
c489058a57 | ||
|
95342ec006 | ||
|
bdebbf8e40 | ||
|
9a9fca67d5 | ||
|
665bae0806 | ||
|
e4be28a9e7 | ||
|
445674aacb | ||
|
2f7b60f5e5 | ||
|
b83c59e308 | ||
|
ce852dfa02 | ||
|
c9549c0de2 | ||
|
957c292307 | ||
|
b5eb17ed93 | ||
|
d578300104 | ||
|
b77b33e790 | ||
|
4becb97a5d | ||
|
85e2b36424 | ||
|
abdf1ae90a | ||
|
606c967985 | ||
|
93c231b4d9 | ||
|
9ad8e5f56a | ||
|
8a481a1be0 | ||
|
657987a013 | ||
|
d74577608b | ||
|
20a399c557 | ||
|
060dde9827 | ||
|
1d1601cf24 | ||
|
ff5f2e8dfb | ||
|
5451fb7672 | ||
|
56094a43d7 | ||
|
68bbe8944a | ||
|
8f1da6aa22 | ||
|
c0d6fe0d76 | ||
|
29e4e41215 | ||
|
7a1bb964e9 | ||
|
3fe0e9bf1e | ||
|
9982887783 | ||
|
c31efc0ef4 | ||
|
6463d4b209 | ||
|
acada8028a | ||
|
d0b0c64b81 | ||
|
cd04ac4557 | ||
|
d7d2f7b7fc | ||
|
5a05d135b8 | ||
|
e03ee593e2 | ||
|
6c1ee70e15 | ||
|
5c3892313e | ||
|
c57c94642c | ||
|
62f168a2a5 | ||
|
c808f78f09 | ||
|
9c80e1c732 | ||
|
acc2995d86 | ||
|
7def9dcec7 | ||
|
a35569481d | ||
|
9ddffc0f7f | ||
|
76e7c8b276 | ||
|
572a5300aa | ||
|
e1f1d4a959 | ||
|
b5857f7c0c | ||
|
6277babf25 | ||
|
300a95d779 | ||
|
23714ab688 | ||
|
16b44001e7 | ||
|
f2f8f33b86 | ||
|
df4682d19b | ||
|
11a1f35cc5 | ||
|
2a3ce15328 | ||
|
7cb25255bf | ||
|
c622f7958f | ||
|
8cb26d2b31 | ||
|
bda481c61e | ||
|
86dcc9bc8f | ||
|
145b722aec | ||
|
79c81395bc | ||
|
89c0f8b734 | ||
|
dc805cff97 | ||
|
dc1de50a02 | ||
|
0e6d7694ce | ||
|
11bcd1e2ed | ||
|
06310423f4 | ||
|
e127e168b6 | ||
|
075535ba46 | ||
|
13cf6891ac |
@@ -1,5 +1,4 @@
|
||||
/.idea
|
||||
/dist
|
||||
/node_modules
|
||||
/data
|
||||
/out
|
||||
|
21
.github/ISSUE_TEMPLATE/ask-for-help.md
vendored
21
.github/ISSUE_TEMPLATE/ask-for-help.md
vendored
@@ -1,21 +0,0 @@
|
||||
---
|
||||
name: Ask for help
|
||||
about: You can ask any question related to Uptime Kuma.
|
||||
title: ''
|
||||
labels: help
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
**Is it a duplicate question?**
|
||||
Please search in Issues without filters: https://github.com/louislam/uptime-kuma/issues?q=
|
||||
|
||||
**Describe your problem**
|
||||
Please describe what you are asking for
|
||||
|
||||
**Info**
|
||||
Uptime Kuma Version:
|
||||
Using Docker?: Yes/No
|
||||
Docker Version:
|
||||
Node.js Version (Without Docker only):
|
||||
OS:
|
||||
Browser:
|
68
.github/ISSUE_TEMPLATE/ask-for-help.yaml
vendored
Normal file
68
.github/ISSUE_TEMPLATE/ask-for-help.yaml
vendored
Normal file
@@ -0,0 +1,68 @@
|
||||
name: "❓ Ask for help"
|
||||
description: "Submit any question related to Uptime Kuma"
|
||||
#title: "[Help] "
|
||||
labels: [help]
|
||||
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
|
||||
id: steps-to-reproduce
|
||||
validations:
|
||||
required: true
|
||||
attributes:
|
||||
label: "📝 Describe your problem"
|
||||
description: "Please walk us through it step by step."
|
||||
placeholder: "Describe what are you asking for..."
|
||||
- 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
|
42
.github/ISSUE_TEMPLATE/bug_report.md
vendored
42
.github/ISSUE_TEMPLATE/bug_report.md
vendored
@@ -1,42 +0,0 @@
|
||||
---
|
||||
name: Bug report
|
||||
about: Create a report to help us improve
|
||||
title: ''
|
||||
labels: bug
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**Is it a duplicate question?**
|
||||
Please search in Issues without filters: https://github.com/louislam/uptime-kuma/issues?q=
|
||||
|
||||
**Describe the bug**
|
||||
A clear and concise description of what the bug is.
|
||||
|
||||
**To Reproduce**
|
||||
Steps to reproduce the behavior:
|
||||
|
||||
1. Go to '...'
|
||||
2. Click on '....'
|
||||
3. Scroll down to '....'
|
||||
4. See error
|
||||
|
||||
**Expected behavior**
|
||||
A clear and concise description of what you expected to happen.
|
||||
|
||||
**Info**
|
||||
Uptime Kuma Version:
|
||||
Using Docker?: Yes/No
|
||||
Docker Version:
|
||||
Node.js Version (Without Docker only):
|
||||
OS:
|
||||
Browser:
|
||||
|
||||
**Screenshots**
|
||||
If applicable, add screenshots to help explain your problem.
|
||||
|
||||
**Error Log**
|
||||
It is easier for us to find out the problem.
|
||||
|
||||
Docker: `docker logs <container id>`
|
||||
PM2: `~/.pm2/logs/` (e.g. `/home/ubuntu/.pm2/logs`)
|
99
.github/ISSUE_TEMPLATE/bug_report.yaml
vendored
Normal file
99
.github/ISSUE_TEMPLATE/bug_report.yaml
vendored
Normal file
@@ -0,0 +1,99 @@
|
||||
name: "🐛 Bug Report"
|
||||
description: "Submit a bug report to help us improve"
|
||||
#title: "[Bug] "
|
||||
labels: [bug]
|
||||
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
|
||||
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
|
22
.github/ISSUE_TEMPLATE/feature_request.md
vendored
22
.github/ISSUE_TEMPLATE/feature_request.md
vendored
@@ -1,22 +0,0 @@
|
||||
---
|
||||
name: Feature request
|
||||
about: Suggest an idea for this project
|
||||
title: ''
|
||||
labels: enhancement
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
**Is it a duplicate question?**
|
||||
Please search in Issues without filters: https://github.com/louislam/uptime-kuma/issues?q=
|
||||
|
||||
**Is your feature request related to a problem? Please describe.**
|
||||
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
|
||||
|
||||
**Describe the solution you'd like**
|
||||
A clear and concise description of what you want to happen.
|
||||
|
||||
**Describe alternatives you've considered**
|
||||
A clear and concise description of any alternative solutions or features you've considered.
|
||||
|
||||
**Additional context**
|
||||
Add any other context or screenshots about the feature request here.
|
59
.github/ISSUE_TEMPLATE/feature_request.yaml
vendored
Normal file
59
.github/ISSUE_TEMPLATE/feature_request.yaml
vendored
Normal file
@@ -0,0 +1,59 @@
|
||||
name: 🚀 Feature Request
|
||||
description: "Submit a proposal for a new feature"
|
||||
#title: "[Feature] "
|
||||
labels: [feature-request]
|
||||
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
|
||||
id: feature-area
|
||||
attributes:
|
||||
label: "🏷️ Feature Request Type"
|
||||
description: "What kind of feature request is this?"
|
||||
multiple: true
|
||||
options:
|
||||
- API
|
||||
- New Notification
|
||||
- New Monitor
|
||||
- UI Feature
|
||||
- Other
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: feature-description
|
||||
validations:
|
||||
required: true
|
||||
attributes:
|
||||
label: "🔖 Feature description"
|
||||
description: "A clear and concise description of what the feature request is."
|
||||
placeholder: "You should add ..."
|
||||
- type: textarea
|
||||
id: solution
|
||||
validations:
|
||||
required: true
|
||||
attributes:
|
||||
label: "✔️ Solution"
|
||||
description: "A clear and concise description of what you want to happen."
|
||||
placeholder: "In my use-case, ..."
|
||||
- type: textarea
|
||||
id: alternatives
|
||||
validations:
|
||||
required: false
|
||||
attributes:
|
||||
label: "❓ Alternatives"
|
||||
description: "A clear and concise description of any alternative solutions or features you've considered."
|
||||
placeholder: "I have considered ..."
|
||||
- type: textarea
|
||||
id: additional-context
|
||||
validations:
|
||||
required: false
|
||||
attributes:
|
||||
label: "📝 Additional Context"
|
||||
description: "Add any other context or screenshots about the feature request here."
|
||||
placeholder: "..."
|
28
.github/PULL_REQUEST_TEMPLATE.md
vendored
Normal file
28
.github/PULL_REQUEST_TEMPLATE.md
vendored
Normal file
@@ -0,0 +1,28 @@
|
||||
# Description
|
||||
|
||||
Fixes #(issue)
|
||||
|
||||
## Type of change
|
||||
|
||||
Please delete any options that are not relevant.
|
||||
|
||||
- Bug fix (non-breaking change which fixes an issue)
|
||||
- User interface (UI)
|
||||
- New feature (non-breaking change which adds functionality)
|
||||
- Breaking change (fix or feature that would cause existing functionality to not work as expected)
|
||||
- Translation update
|
||||
- Other
|
||||
- This change requires a documentation update
|
||||
|
||||
## Checklist
|
||||
|
||||
- [ ] My code follows the style guidelines of this project
|
||||
- [ ] I ran ESLint and other linters for modified files
|
||||
- [ ] I have performed a self-review of my own code and tested it
|
||||
- [ ] I have commented my code, particularly in hard-to-understand areas
|
||||
- [ ] My changes generate no new warnings
|
||||
- [ ] My code needed automated testing. I have added them (this is optional task)
|
||||
|
||||
## 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.
|
2
.github/workflows/auto-test.yml
vendored
2
.github/workflows/auto-test.yml
vendored
@@ -16,7 +16,7 @@ jobs:
|
||||
strategy:
|
||||
matrix:
|
||||
os: [macos-latest, ubuntu-latest, windows-latest]
|
||||
node-version: [14.x, 16.x]
|
||||
node-version: [14.x, 16.x, 17.x]
|
||||
# See supported Node.js release schedule at https://nodejs.org/en/about/releases/
|
||||
|
||||
steps:
|
||||
|
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 }}
|
103
CONTRIBUTING.md
103
CONTRIBUTING.md
@@ -1,8 +1,8 @@
|
||||
# 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.
|
||||
|
||||
@@ -27,33 +27,36 @@ The frontend code build into "dist" directory. The server (express.js) exposes t
|
||||
|
||||
## 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, 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.
|
||||
|
||||
### Pull Request Examples
|
||||
|
||||
#### ✅ High - Medium Priority
|
||||
|
||||
- Add a new notification
|
||||
- Add a chart
|
||||
- Fix a bug
|
||||
✅ Accept:
|
||||
- Bug/Security fix
|
||||
- Translations
|
||||
- Adding notification providers
|
||||
|
||||
#### *️⃣ Requires one more reviewer
|
||||
❌ Avoid:
|
||||
- Large pull requests
|
||||
- New big features
|
||||
|
||||
I do not have such knowledge to test it.
|
||||
My long story here: https://www.reddit.com/r/UptimeKuma/comments/t1t6or/comment/hynyijx/
|
||||
|
||||
- Add k8s supports
|
||||
### Recommended Pull Request Guideline
|
||||
|
||||
#### *️⃣ Low Priority
|
||||
|
||||
It changed my current workflow and require further studies.
|
||||
|
||||
- Change my release approach
|
||||
1. Fork the project
|
||||
1. Clone your fork repo to local
|
||||
1. Create a new branch
|
||||
1. Create an empty commit
|
||||
`git commit -m "[empty commit] pull request for <YOUR TASK NAME>" --allow-empty`
|
||||
1. Push to your fork repo
|
||||
1. Create a pull request: https://github.com/louislam/uptime-kuma/compare
|
||||
1. Write a proper description
|
||||
1. Click "Change to draft"
|
||||
|
||||
#### ❌ Won't Merge
|
||||
|
||||
- Any breaking changes
|
||||
- Duplicated pull request
|
||||
- Buggy
|
||||
- Existing logic is completely modified or deleted
|
||||
@@ -84,7 +87,7 @@ I personally do not like something need to learn so much and need to config so m
|
||||
|
||||
- Node.js >= 14
|
||||
- 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)
|
||||
|
||||
## Install dependencies
|
||||
@@ -111,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)
|
||||
- modules/ (Modified 3rd-party modules)
|
||||
- notification-providers/ (indivdual notification logic)
|
||||
- notification-providers/ (individual notification logic)
|
||||
- routers/ (Express Routers)
|
||||
- scoket-handler (Socket.io Handlers)
|
||||
- socket-handler (Socket.io Handlers)
|
||||
- server.js (Server main logic)
|
||||
|
||||
## How to start the Frontend Dev Server
|
||||
@@ -171,10 +174,64 @@ ncu -u -t patch
|
||||
npm install
|
||||
```
|
||||
|
||||
Since previously updating vite 2.5.10 to 2.6.0 broke the application completely, from now on, it should update patch release version only.
|
||||
Since previously updating Vite 2.5.10 to 2.6.0 broke the application completely, from now on, it should update patch release version only.
|
||||
|
||||
Patch release = the third digit ([Semantic Versioning](https://semver.org/))
|
||||
|
||||
## Translations
|
||||
|
||||
Please read: https://github.com/louislam/uptime-kuma/tree/master/src/languages
|
||||
|
||||
## Wiki
|
||||
|
||||
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
|
||||
|
||||
## Maintainer
|
||||
|
||||
Check the latest issues and pull requests:
|
||||
https://github.com/louislam/uptime-kuma/issues?q=sort%3Aupdated-desc
|
||||
|
||||
### Release Procedures
|
||||
|
||||
1. Draft a release note
|
||||
2. Make sure the repo is cleared
|
||||
3. `npm run release-final with env vars: `VERSION` and `GITHUB_TOKEN`
|
||||
4. Wait until the `Press any key to continue`
|
||||
5. `git push`
|
||||
6. Publish the release note as 1.X.X
|
||||
7. Press any key to continue
|
||||
8. SSH to demo site server and update to 1.X.X
|
||||
|
||||
Checking:
|
||||
|
||||
- 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 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
|
||||
|
||||
#### Setup Repo
|
||||
|
||||
```bash
|
||||
git clone https://github.com/louislam/uptime-kuma-wiki.git
|
||||
cd uptime-kuma-wiki
|
||||
git remote add production https://github.com/louislam/uptime-kuma.wiki.git
|
||||
```
|
||||
|
||||
#### Push to Production Wiki
|
||||
|
||||
```bash
|
||||
git pull
|
||||
git push production master
|
||||
```
|
||||
|
35
README.md
35
README.md
@@ -1,6 +1,7 @@
|
||||
# Uptime Kuma
|
||||
|
||||
<a target="_blank" href="https://github.com/louislam/uptime-kuma"><img src="https://img.shields.io/github/stars/louislam/uptime-kuma" /></a> <a target="_blank" href="https://hub.docker.com/r/louislam/uptime-kuma"><img src="https://img.shields.io/docker/pulls/louislam/uptime-kuma" /></a> <a target="_blank" href="https://hub.docker.com/r/louislam/uptime-kuma"><img src="https://img.shields.io/docker/v/louislam/uptime-kuma/latest?label=docker%20image%20ver." /></a> <a target="_blank" href="https://github.com/louislam/uptime-kuma"><img src="https://img.shields.io/github/last-commit/louislam/uptime-kuma" /></a> <a target="_blank" href="https://opencollective.com/uptime-kuma"><img src="https://opencollective.com/uptime-kuma/total/badge.svg?label=Backers&color=brightgreen" /></a>
|
||||
<a target="_blank" href="https://github.com/louislam/uptime-kuma"><img src="https://img.shields.io/github/stars/louislam/uptime-kuma" /></a> <a target="_blank" href="https://hub.docker.com/r/louislam/uptime-kuma"><img src="https://img.shields.io/docker/pulls/louislam/uptime-kuma" /></a> <a target="_blank" href="https://hub.docker.com/r/louislam/uptime-kuma"><img src="https://img.shields.io/docker/v/louislam/uptime-kuma/latest?label=docker%20image%20ver." /></a> <a target="_blank" href="https://github.com/louislam/uptime-kuma"><img src="https://img.shields.io/github/last-commit/louislam/uptime-kuma" /></a> <a target="_blank" href="https://opencollective.com/uptime-kuma"><img src="https://opencollective.com/uptime-kuma/total/badge.svg?label=Open%20Collective%20Backers&color=brightgreen" /></a>
|
||||
[](https://github.com/sponsors/louislam)
|
||||
|
||||
<div align="center" width="100%">
|
||||
<img src="./public/icon.svg" width="128" alt="" />
|
||||
@@ -16,13 +17,13 @@ Try it!
|
||||
|
||||
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!
|
||||
|
||||
## ⭐ 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.
|
||||
* 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.
|
||||
@@ -40,9 +41,11 @@ 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
|
||||
```
|
||||
|
||||
⚠️ Please use a **local volume** only. Other types such as NFS are not supported.
|
||||
|
||||
Browse to http://localhost:3001 after starting.
|
||||
|
||||
### 💪🏻 Without Docker
|
||||
### 💪🏻 Non-Docker
|
||||
|
||||
Required Tools: Node.js >= 14, git and pm2.
|
||||
|
||||
@@ -58,15 +61,21 @@ npm run setup
|
||||
node server/server.js
|
||||
|
||||
# (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:
|
||||
npm install pm2 -g && pm2 install pm2-logrotate
|
||||
|
||||
# Start Server
|
||||
pm2 start server/server.js --name uptime-kuma
|
||||
|
||||
# If you want to see the current console output
|
||||
pm2 monit
|
||||
```
|
||||
|
||||
Browse to http://localhost:3001 after starting.
|
||||
|
||||
### 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
|
||||
|
||||
@@ -86,6 +95,12 @@ Project Plan:
|
||||
|
||||
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
|
||||
|
||||
Light Mode:
|
||||
@@ -106,7 +121,7 @@ Telegram Notification Sample:
|
||||
|
||||
## 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.
|
||||
* Learn Vue 3 and vite.js.
|
||||
* Show the power of Bootstrap 5.
|
||||
@@ -119,7 +134,7 @@ If you love this project, please consider giving me a ⭐.
|
||||
|
||||
### 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
|
||||
|
||||
@@ -131,8 +146,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 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
|
||||
|
||||
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.
|
||||
|
16
SECURITY.md
16
SECURITY.md
@@ -1,5 +1,11 @@
|
||||
# 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
|
||||
|
||||
Use this section to tell people about which versions of your project are
|
||||
@@ -9,8 +15,8 @@ currently being supported with security updates.
|
||||
|
||||
| Version | Supported |
|
||||
| ------- | ------------------ |
|
||||
| 1.8.X | :white_check_mark: |
|
||||
| <= 1.7.X | ❌ |
|
||||
| 1.9.X | :white_check_mark: |
|
||||
| <= 1.8.X | ❌ |
|
||||
|
||||
### Upgradable Docker Tags
|
||||
|
||||
@@ -23,9 +29,3 @@ currently being supported with security updates.
|
||||
| debian | :white_check_mark: |
|
||||
| alpine | :white_check_mark: |
|
||||
| All other tags | ❌ |
|
||||
|
||||
## Reporting a Vulnerability
|
||||
|
||||
Please report security issues to uptime@kuma.pet.
|
||||
|
||||
Do not use the issue tracker or discuss it in the public as it will cause more damage.
|
||||
|
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 = {
|
||||
"launch": {
|
||||
"dumpio": true,
|
||||
"slowMo": 500,
|
||||
"headless": process.env.HEADLESS_TEST || false,
|
||||
"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
|
||||
},
|
||||
"testRegex": "./test/e2e.spec.js",
|
||||
"testEnvironment": "./config/jest-debug-env.js",
|
||||
"rootDir": "..",
|
||||
"testTimeout": 30000,
|
||||
};
|
||||
|
7
db/patch-2fa-invalidate-used-token.sql
Normal file
7
db/patch-2fa-invalidate-used-token.sql
Normal file
@@ -0,0 +1,7 @@
|
||||
-- You should not modify if this have pushed to Github, unless it does serious wrong with the db.
|
||||
BEGIN TRANSACTION;
|
||||
|
||||
ALTER TABLE user
|
||||
ADD twofa_last_token VARCHAR(6);
|
||||
|
||||
COMMIT;
|
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;
|
18
db/patch-notification_sent_history.sql
Normal file
18
db/patch-notification_sent_history.sql
Normal file
@@ -0,0 +1,18 @@
|
||||
-- You should not modify if this have pushed to Github, unless it does serious wrong with the db.
|
||||
BEGIN TRANSACTION;
|
||||
|
||||
CREATE TABLE [notification_sent_history] (
|
||||
[id] INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
|
||||
[type] VARCHAR(50) NOT NULL,
|
||||
[monitor_id] INTEGER NOT NULL,
|
||||
[days] INTEGER NOT NULL,
|
||||
UNIQUE([type], [monitor_id], [days])
|
||||
);
|
||||
|
||||
CREATE INDEX [good_index] ON [notification_sent_history] (
|
||||
[type],
|
||||
[monitor_id],
|
||||
[days]
|
||||
);
|
||||
|
||||
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.
|
||||
FROM node:14-alpine3.12
|
||||
FROM node:16-alpine3.12
|
||||
WORKDIR /app
|
||||
|
||||
# Install apprise, iputils for non-root ping, setpriv
|
||||
RUN apk add --no-cache iputils setpriv dumb-init python3 py3-cryptography py3-pip py3-six py3-yaml py3-click py3-markdown py3-requests py3-requests-oauthlib && \
|
||||
pip3 --no-cache-dir install apprise && \
|
||||
pip3 --no-cache-dir install apprise==0.9.7 && \
|
||||
rm -rf /root/.cache
|
||||
|
@@ -1,12 +1,26 @@
|
||||
# DON'T UPDATE TO node:14-bullseye-slim, see #372.
|
||||
# If the image changed, the second stage image should be changed too
|
||||
FROM node:14-buster-slim
|
||||
FROM node:16-buster-slim
|
||||
ARG TARGETPLATFORM
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# Install Curl
|
||||
# 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 && \
|
||||
apt --yes --no-install-recommends install python3 python3-pip python3-cryptography python3-six python3-yaml python3-click python3-markdown python3-requests python3-requests-oauthlib \
|
||||
sqlite3 iputils-ping util-linux dumb-init && \
|
||||
pip3 --no-cache-dir install apprise && \
|
||||
pip3 --no-cache-dir install apprise==0.9.7 && \
|
||||
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
|
||||
|
||||
|
@@ -4,9 +4,7 @@ WORKDIR /app
|
||||
ENV PUPPETEER_SKIP_CHROMIUM_DOWNLOAD=1
|
||||
|
||||
COPY . .
|
||||
RUN npm ci && \
|
||||
npm run build && \
|
||||
npm ci --production && \
|
||||
RUN npm ci --production && \
|
||||
chmod +x /app/extra/entrypoint.sh
|
||||
|
||||
|
||||
@@ -22,23 +20,26 @@ HEALTHCHECK --interval=60s --timeout=30s --start-period=180s --retries=5 CMD nod
|
||||
ENTRYPOINT ["/usr/bin/dumb-init", "--", "extra/entrypoint.sh"]
|
||||
CMD ["node", "server/server.js"]
|
||||
|
||||
|
||||
FROM release AS nightly
|
||||
RUN npm run mark-as-nightly
|
||||
|
||||
|
||||
# Upload the artifact to Github
|
||||
FROM louislam/uptime-kuma:base-debian AS upload-artifact
|
||||
WORKDIR /
|
||||
RUN apt update && \
|
||||
apt --yes install curl file
|
||||
|
||||
COPY --from=build /app /app
|
||||
|
||||
ARG VERSION
|
||||
ARG GITHUB_TOKEN
|
||||
ARG TARGETARCH
|
||||
ARG PLATFORM=debian
|
||||
ARG VERSION=1.9.0
|
||||
ARG FILE=$PLATFORM-$TARGETARCH-$VERSION.tar.gz
|
||||
ARG DIST=dist.tar.gz
|
||||
|
||||
COPY --from=build /app /app
|
||||
RUN chmod +x /app/extra/upload-github-release-asset.sh
|
||||
|
||||
# Full Build
|
||||
|
@@ -4,9 +4,7 @@ WORKDIR /app
|
||||
ENV PUPPETEER_SKIP_CHROMIUM_DOWNLOAD=1
|
||||
|
||||
COPY . .
|
||||
RUN npm ci && \
|
||||
npm run build && \
|
||||
npm ci --production && \
|
||||
RUN npm ci --production && \
|
||||
chmod +x /app/extra/entrypoint.sh
|
||||
|
||||
|
||||
@@ -22,5 +20,6 @@ HEALTHCHECK --interval=60s --timeout=30s --start-period=180s --retries=5 CMD nod
|
||||
ENTRYPOINT ["/usr/bin/dumb-init", "--", "extra/entrypoint.sh"]
|
||||
CMD ["node", "server/server.js"]
|
||||
|
||||
|
||||
FROM release AS nightly
|
||||
RUN npm run mark-as-nightly
|
||||
|
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 fs = require("fs");
|
||||
const rmSync = require("./fs-rmSync.js");
|
||||
const version = packageJSON.version;
|
||||
|
||||
const filename = "dist.tar.gz";
|
||||
@@ -21,7 +22,7 @@ function download(url) {
|
||||
if (fs.existsSync("./dist")) {
|
||||
|
||||
if (fs.existsSync("./dist-backup")) {
|
||||
fs.rmdirSync("./dist-backup", {
|
||||
rmSync("./dist-backup", {
|
||||
recursive: true
|
||||
});
|
||||
}
|
||||
@@ -34,9 +35,11 @@ function download(url) {
|
||||
});
|
||||
|
||||
tarStream.on("close", () => {
|
||||
fs.rmdirSync("./dist-backup", {
|
||||
recursive: true
|
||||
});
|
||||
if (fs.existsSync("./dist-backup")) {
|
||||
rmSync("./dist-backup", {
|
||||
recursive: true
|
||||
});
|
||||
}
|
||||
console.log("Done");
|
||||
});
|
||||
|
||||
@@ -44,7 +47,7 @@ function download(url) {
|
||||
if (fs.existsSync("./dist-backup")) {
|
||||
fs.renameSync("./dist-backup", "./dist");
|
||||
}
|
||||
console.log("Done");
|
||||
console.error("Error from tarStream");
|
||||
});
|
||||
|
||||
response.pipe(tarStream);
|
||||
|
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);
|
20
extra/fs-rmSync.js
Normal file
20
extra/fs-rmSync.js
Normal file
@@ -0,0 +1,20 @@
|
||||
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.
|
||||
* @link https://nodejs.org/docs/latest-v16.x/api/deprecations.html#dep0147-fsrmdirpath--recursive-true-
|
||||
* @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.
|
||||
*/
|
||||
const { FBSD } = require("../server/util-server");
|
||||
|
||||
process.env.NODE_TLS_REJECT_UNAUTHORIZED = "0";
|
||||
|
||||
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");
|
||||
} else {
|
||||
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 = {
|
||||
host: process.env.HOST || "127.0.0.1",
|
||||
port: parseInt(process.env.PORT) || 3001,
|
||||
host: hostname || "127.0.0.1",
|
||||
port: port,
|
||||
timeout: 28 * 1000,
|
||||
};
|
||||
|
||||
let request = client.request(options, (res) => {
|
||||
console.log(`Health Check OK [Res Code: ${res.statusCode}]`);
|
||||
if (res.statusCode === 200) {
|
||||
if (res.statusCode === 302) {
|
||||
process.exit(0);
|
||||
} else {
|
||||
process.exit(1);
|
||||
|
@@ -189,7 +189,7 @@ if (type == "local") {
|
||||
bash("check=$(pm2 --version)");
|
||||
if (check == "") {
|
||||
println("Installing PM2");
|
||||
bash("npm install pm2 -g");
|
||||
bash("npm install pm2 -g && pm2 install pm2-logrotate");
|
||||
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,
|
||||
};
|
@@ -3,6 +3,7 @@
|
||||
import fs from "fs";
|
||||
import path from "path";
|
||||
import util from "util";
|
||||
import rmSync from "../fs-rmSync.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";
|
||||
console.log("Base Lang: " + baseLangCode);
|
||||
if (fs.existsSync("./languages")) {
|
||||
fs.rmdirSync("./languages", { recursive: true });
|
||||
rmSync("./languages", { recursive: true });
|
||||
}
|
||||
copyRecursiveSync("../../src/languages", "./languages");
|
||||
|
||||
@@ -40,7 +41,7 @@ const files = fs.readdirSync("./languages");
|
||||
console.log("Files:", files);
|
||||
|
||||
for (const file of files) {
|
||||
if (!file.endsWith(".js")) {
|
||||
if (! file.endsWith(".js")) {
|
||||
console.log("Skipping " + file);
|
||||
continue;
|
||||
}
|
||||
@@ -82,5 +83,5 @@ for (const file of files) {
|
||||
fs.writeFileSync(`../../src/languages/${file}`, code);
|
||||
}
|
||||
|
||||
fs.rmdirSync("./languages", { recursive: true });
|
||||
rmSync("./languages", { recursive: true });
|
||||
console.log("Done. Fixing formatting by ESLint...");
|
||||
|
@@ -1,14 +1,13 @@
|
||||
const pkg = require("../package.json");
|
||||
const fs = require("fs");
|
||||
const rmSync = require("./fs-rmSync.js");
|
||||
const child_process = require("child_process");
|
||||
const util = require("../src/util");
|
||||
|
||||
util.polyfill();
|
||||
|
||||
const oldVersion = pkg.version;
|
||||
const newVersion = process.argv[2];
|
||||
const newVersion = process.env.VERSION;
|
||||
|
||||
console.log("Old Version: " + oldVersion);
|
||||
console.log("New Version: " + newVersion);
|
||||
|
||||
if (! newVersion) {
|
||||
@@ -22,23 +21,20 @@ if (! exists) {
|
||||
|
||||
// Process package.json
|
||||
pkg.version = newVersion;
|
||||
pkg.scripts.setup = pkg.scripts.setup.replaceAll(oldVersion, newVersion);
|
||||
pkg.scripts["build-docker"] = pkg.scripts["build-docker"].replaceAll(oldVersion, newVersion);
|
||||
pkg.scripts["build-docker-alpine"] = pkg.scripts["build-docker-alpine"].replaceAll(oldVersion, newVersion);
|
||||
pkg.scripts["build-docker-debian"] = pkg.scripts["build-docker-debian"].replaceAll(oldVersion, newVersion);
|
||||
|
||||
// Replace the version: https://regex101.com/r/hmj2Bc/1
|
||||
pkg.scripts.setup = pkg.scripts.setup.replace(/(git checkout )([^\s]+)/, `$1${newVersion}`);
|
||||
fs.writeFileSync("package.json", JSON.stringify(pkg, null, 4) + "\n");
|
||||
|
||||
commit(newVersion);
|
||||
tag(newVersion);
|
||||
|
||||
updateWiki(oldVersion, newVersion);
|
||||
|
||||
} else {
|
||||
console.log("version exists");
|
||||
}
|
||||
|
||||
function commit(version) {
|
||||
let msg = "update to " + version;
|
||||
let msg = "Update to " + version;
|
||||
|
||||
let res = child_process.spawnSync("git", ["commit", "-m", msg, "-a"]);
|
||||
let stdout = res.stdout.toString().trim();
|
||||
@@ -63,38 +59,3 @@ function tagExists(version) {
|
||||
|
||||
return res.stdout.toString().trim() === version;
|
||||
}
|
||||
|
||||
function updateWiki(oldVersion, newVersion) {
|
||||
const wikiDir = "./tmp/wiki";
|
||||
const howToUpdateFilename = "./tmp/wiki/🆙-How-to-Update.md";
|
||||
|
||||
safeDelete(wikiDir);
|
||||
|
||||
child_process.spawnSync("git", ["clone", "https://github.com/louislam/uptime-kuma.wiki.git", wikiDir]);
|
||||
let content = fs.readFileSync(howToUpdateFilename).toString();
|
||||
content = content.replaceAll(`git checkout ${oldVersion}`, `git checkout ${newVersion}`);
|
||||
fs.writeFileSync(howToUpdateFilename, content);
|
||||
|
||||
child_process.spawnSync("git", ["add", "-A"], {
|
||||
cwd: wikiDir,
|
||||
});
|
||||
|
||||
child_process.spawnSync("git", ["commit", "-m", `Update to ${newVersion} from ${oldVersion}`], {
|
||||
cwd: wikiDir,
|
||||
});
|
||||
|
||||
console.log("Pushing to Github");
|
||||
child_process.spawnSync("git", ["push"], {
|
||||
cwd: wikiDir,
|
||||
});
|
||||
|
||||
safeDelete(wikiDir);
|
||||
}
|
||||
|
||||
function safeDelete(dir) {
|
||||
if (fs.existsSync(dir)) {
|
||||
fs.rmdirSync(dir, {
|
||||
recursive: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
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,
|
||||
});
|
||||
}
|
||||
}
|
@@ -5,7 +5,7 @@
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png">
|
||||
<link rel="icon" type="image/svg+xml" href="/icon.svg" />
|
||||
<link rel="manifest" href="manifest.json" />
|
||||
<link rel="manifest" href="/manifest.json" />
|
||||
<meta name="theme-color" id="theme-color" content="" />
|
||||
<meta name="description" content="Uptime Kuma monitoring tool" />
|
||||
<title>Uptime Kuma</title>
|
||||
|
@@ -159,7 +159,7 @@ fi
|
||||
check=$(pm2 --version)
|
||||
if [ "$check" == "" ]; then
|
||||
"echo" "-e" "Installing PM2"
|
||||
npm install pm2 -g
|
||||
npm install pm2 -g && pm2 install pm2-logrotate
|
||||
pm2 startup
|
||||
fi
|
||||
mkdir -p $installPath
|
||||
|
18917
package-lock.json
generated
18917
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
102
package.json
102
package.json
@@ -1,13 +1,13 @@
|
||||
{
|
||||
"name": "uptime-kuma",
|
||||
"version": "1.9.2",
|
||||
"version": "1.14.0-beta.1",
|
||||
"license": "MIT",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/louislam/uptime-kuma.git"
|
||||
},
|
||||
"engines": {
|
||||
"node": "14.*"
|
||||
"node": "14.* || >=16.*"
|
||||
},
|
||||
"scripts": {
|
||||
"install-legacy": "npm install --legacy-peer-deps",
|
||||
@@ -22,25 +22,25 @@
|
||||
"build": "vite build --config ./config/vite.config.js",
|
||||
"test": "node test/prepare-test-server.js && node server/server.js --port=3002 --data-dir=./data/test/ --test",
|
||||
"test-with-build": "npm run build && npm test",
|
||||
"jest": "node test/prepare-jest.js && npm run jest-frontend && npm run jest-backend && jest --config=./config/jest.config.js",
|
||||
"jest": "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-backend": "cross-env TEST_BACKEND=1 jest --config=./config/jest-backend.config.js",
|
||||
"tsc": "tsc",
|
||||
"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-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.9.2-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.9.2 -t louislam/uptime-kuma:debian -t louislam/uptime-kuma:1-debian -t louislam/uptime-kuma:1.9.2-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-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": "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": "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-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",
|
||||
"setup": "git checkout 1.9.2 && npm ci --production && npm run download-dist",
|
||||
"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.13.1 && npm ci --production && npm run download-dist",
|
||||
"download-dist": "node extra/download-dist.js",
|
||||
"update-version": "node extra/update-version.js",
|
||||
"mark-as-nightly": "node extra/mark-as-nightly.js",
|
||||
"reset-password": "node extra/reset-password.js",
|
||||
"remove-2fa": "node extra/remove-2fa.js",
|
||||
"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-alpine3": "npm run compile-install-script && docker build --progress plain -f test/test_install_script/alpine3.dockerfile .",
|
||||
@@ -49,80 +49,94 @@
|
||||
"test-nodejs16": "docker build --progress plain -f test/ubuntu-nodejs16.dockerfile .",
|
||||
"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": "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": {
|
||||
"@fortawesome/fontawesome-svg-core": "~1.2.36",
|
||||
"@fortawesome/free-regular-svg-icons": "~5.15.4",
|
||||
"@fortawesome/free-solid-svg-icons": "~5.15.4",
|
||||
"@fortawesome/vue-fontawesome": "~3.0.0-4",
|
||||
"@louislam/sqlite3": "~6.0.0",
|
||||
"@fortawesome/vue-fontawesome": "~3.0.0-5",
|
||||
"@louislam/sqlite3": "~6.0.1",
|
||||
"@popperjs/core": "~2.10.2",
|
||||
"args-parser": "~1.3.0",
|
||||
"axios": "~0.21.4",
|
||||
"axios": "~0.26.1",
|
||||
"bcryptjs": "~2.4.3",
|
||||
"bootstrap": "~5.1.1",
|
||||
"bootstrap": "5.1.3",
|
||||
"bree": "~7.1.5",
|
||||
"chardet": "^1.3.0",
|
||||
"bree": "~6.3.1",
|
||||
"chart.js": "~3.5.1",
|
||||
"chart.js": "~3.6.2",
|
||||
"chartjs-adapter-dayjs": "~1.0.0",
|
||||
"check-password-strength": "^2.0.5",
|
||||
"command-exists": "~1.2.9",
|
||||
"compare-versions": "~3.6.0",
|
||||
"dayjs": "~1.10.7",
|
||||
"express": "~4.17.1",
|
||||
"express-basic-auth": "~1.2.0",
|
||||
"dayjs": "~1.10.8",
|
||||
"express": "~4.17.3",
|
||||
"express-basic-auth": "~1.2.1",
|
||||
"favico.js": "^0.3.10",
|
||||
"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",
|
||||
"jsonwebtoken": "~8.5.1",
|
||||
"jwt-decode": "^3.1.2",
|
||||
"limiter": "^2.1.0",
|
||||
"node-cloudflared-tunnel": "~1.0.9",
|
||||
"nodemailer": "~6.6.5",
|
||||
"notp": "~2.0.3",
|
||||
"password-hash": "~1.2.2",
|
||||
"postcss-rtlcss": "~3.4.1",
|
||||
"postcss-scss": "~4.0.1",
|
||||
"postcss-scss": "~4.0.3",
|
||||
"prom-client": "~13.2.0",
|
||||
"prometheus-api-metrics": "~3.2.0",
|
||||
"qrcode": "~1.4.4",
|
||||
"redbean-node": "0.1.2",
|
||||
"socket.io": "~4.2.0",
|
||||
"socket.io-client": "~4.2.0",
|
||||
"prometheus-api-metrics": "~3.2.1",
|
||||
"qrcode": "~1.5.0",
|
||||
"redbean-node": "0.1.3",
|
||||
"socket.io": "~4.4.1",
|
||||
"socket.io-client": "~4.4.1",
|
||||
"socks-proxy-agent": "^6.1.1",
|
||||
"tar": "^6.1.11",
|
||||
"tcp-ping": "~0.1.1",
|
||||
"thirty-two": "~1.0.2",
|
||||
"timezones-list": "~3.0.1",
|
||||
"v-pagination-3": "~0.1.6",
|
||||
"v-pagination-3": "~0.1.7",
|
||||
"vue": "next",
|
||||
"vue-chart-3": "~0.5.8",
|
||||
"vue-chart-3": "3.0.9",
|
||||
"vue-confirm-dialog": "~1.0.2",
|
||||
"vue-contenteditable": "~3.0.4",
|
||||
"vue-i18n": "~9.1.9",
|
||||
"vue-image-crop-upload": "~3.0.3",
|
||||
"vue-multiselect": "~3.0.0-alpha.2",
|
||||
"vue-qrcode": "~1.0.0",
|
||||
"vue-router": "~4.0.11",
|
||||
"vue-toastification": "~2.0.0-rc.1",
|
||||
"vue-router": "~4.0.14",
|
||||
"vue-toastification": "~2.0.0-rc.5",
|
||||
"vuedraggable": "~4.1.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/eslint-parser": "~7.15.7",
|
||||
"@actions/github": "~5.0.1",
|
||||
"@babel/eslint-parser": "~7.15.8",
|
||||
"@babel/preset-env": "^7.15.8",
|
||||
"@types/bootstrap": "~5.1.6",
|
||||
"@vitejs/plugin-legacy": "~1.6.1",
|
||||
"@vitejs/plugin-vue": "~1.9.2",
|
||||
"@vue/compiler-sfc": "~3.2.19",
|
||||
"@types/bootstrap": "~5.1.9",
|
||||
"@vitejs/plugin-legacy": "~1.6.4",
|
||||
"@vitejs/plugin-vue": "~1.9.4",
|
||||
"@vue/compiler-sfc": "~3.2.31",
|
||||
"babel-plugin-rewire": "~1.2.0",
|
||||
"core-js": "~3.18.1",
|
||||
"core-js": "~3.18.3",
|
||||
"cross-env": "~7.0.3",
|
||||
"dns2": "~2.0.1",
|
||||
"eslint": "~7.32.0",
|
||||
"eslint-plugin-vue": "~7.18.0",
|
||||
"jest": "~27.2.4",
|
||||
"jest-puppeteer": "~6.0.0",
|
||||
"puppeteer": "~10.4.0",
|
||||
"jest": "~27.2.5",
|
||||
"jest-puppeteer": "~6.0.3",
|
||||
"npm-check-updates": "^12.5.5",
|
||||
"puppeteer": "~13.1.3",
|
||||
"sass": "~1.42.1",
|
||||
"stylelint": "~13.13.1",
|
||||
"stylelint-config-standard": "~22.0.0",
|
||||
"typescript": "~4.4.3",
|
||||
"vite": "~2.6.4"
|
||||
"stylelint": "~14.2.0",
|
||||
"stylelint-config-standard": "~24.0.0",
|
||||
"typescript": "~4.4.4",
|
||||
"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;
|
@@ -1,8 +1,9 @@
|
||||
const basicAuth = require("express-basic-auth")
|
||||
const basicAuth = require("express-basic-auth");
|
||||
const passwordHash = require("./password-hash");
|
||||
const { R } = require("redbean-node");
|
||||
const { setting } = require("./util-server");
|
||||
const { debug } = require("../src/util");
|
||||
const { loginRateLimiter } = require("./rate-limiter");
|
||||
|
||||
/**
|
||||
*
|
||||
@@ -11,9 +12,13 @@ const { debug } = require("../src/util");
|
||||
* @returns {Promise<Bean|null>}
|
||||
*/
|
||||
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 ", [
|
||||
username,
|
||||
])
|
||||
]);
|
||||
|
||||
if (user && passwordHash.verify(password, user.password)) {
|
||||
// Upgrade the hash to bcrypt
|
||||
@@ -27,25 +32,37 @@ exports.login = async function (username, password) {
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
function myAuthorizer(username, password, callback) {
|
||||
|
||||
setting("disableAuth").then((result) => {
|
||||
|
||||
if (result) {
|
||||
callback(null, true)
|
||||
} else {
|
||||
// Login Rate Limit
|
||||
loginRateLimiter.pass(null, 0).then((pass) => {
|
||||
if (pass) {
|
||||
exports.login(username, password).then((user) => {
|
||||
callback(null, user != null)
|
||||
})
|
||||
}
|
||||
})
|
||||
callback(null, user != null);
|
||||
|
||||
if (user == null) {
|
||||
loginRateLimiter.removeTokens(1);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
callback(null, false);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
exports.basicAuth = basicAuth({
|
||||
authorizer: myAuthorizer,
|
||||
authorizeAsync: true,
|
||||
challenge: true,
|
||||
});
|
||||
exports.basicAuth = async function (req, res, next) {
|
||||
const middleware = basicAuth({
|
||||
authorizer: myAuthorizer,
|
||||
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 compareVersions = require("compare-versions");
|
||||
|
||||
exports.version = require("../package.json").version;
|
||||
exports.latestVersion = null;
|
||||
@@ -9,18 +10,30 @@ let interval;
|
||||
exports.startInterval = () => {
|
||||
let check = async () => {
|
||||
try {
|
||||
const res = await axios.get("https://raw.githubusercontent.com/louislam/uptime-kuma/master/package.json");
|
||||
|
||||
if (typeof res.data === "string") {
|
||||
res.data = JSON.parse(res.data);
|
||||
}
|
||||
const res = await axios.get("https://uptime.kuma.pet/version");
|
||||
|
||||
// For debug
|
||||
if (process.env.TEST_CHECK_VERSION === "1") {
|
||||
res.data.version = "1000.0.0";
|
||||
res.data.slow = "1000.0.0";
|
||||
}
|
||||
|
||||
if (!await setting("checkUpdate")) {
|
||||
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) {
|
||||
exports.latestVersion = res.data.slow;
|
||||
}
|
||||
|
||||
exports.latestVersion = res.data.version;
|
||||
} catch (_) { }
|
||||
|
||||
};
|
||||
|
@@ -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) {
|
||||
socket.emit("info", {
|
||||
version: checkVersion.version,
|
||||
@@ -95,6 +112,6 @@ module.exports = {
|
||||
sendNotificationList,
|
||||
sendImportantHeartbeatList,
|
||||
sendHeartbeatList,
|
||||
sendInfo
|
||||
sendProxyList,
|
||||
sendInfo,
|
||||
};
|
||||
|
||||
|
@@ -50,6 +50,11 @@ class Database {
|
||||
"patch-group-table.sql": true,
|
||||
"patch-monitor-push_token.sql": true,
|
||||
"patch-http-monitor-method-body-and-headers.sql": true,
|
||||
"patch-2fa-invalidate-used-token.sql": true,
|
||||
"patch-notification_sent_history.sql": true,
|
||||
"patch-monitor-basic-auth.sql": true,
|
||||
"patch-status-page.sql": true,
|
||||
"patch-proxy.sql": true,
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -77,7 +82,7 @@ class Database {
|
||||
console.log(`Data Dir: ${Database.dataDir}`);
|
||||
}
|
||||
|
||||
static async connect() {
|
||||
static async connect(testMode = false) {
|
||||
const acquireConnectionTimeout = 120 * 1000;
|
||||
|
||||
const Dialect = require("knex/lib/dialects/sqlite3/index.js");
|
||||
@@ -110,9 +115,15 @@ class Database {
|
||||
await R.autoloadModels("./server/model");
|
||||
|
||||
await R.exec("PRAGMA foreign_keys = ON");
|
||||
// Change to WAL
|
||||
await R.exec("PRAGMA journal_mode = WAL");
|
||||
if (testMode) {
|
||||
// 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 auto_vacuum = FULL");
|
||||
|
||||
console.log("SQLite config:");
|
||||
console.log(await R.getAll("PRAGMA journal_mode"));
|
||||
@@ -131,7 +142,7 @@ class Database {
|
||||
console.info("Latest database version: " + this.latestVersion);
|
||||
|
||||
if (version === this.latestVersion) {
|
||||
console.info("Database no need to patch");
|
||||
console.info("Database patch not needed");
|
||||
} else if (version > this.latestVersion) {
|
||||
console.info("Warning: Database version is newer than expected");
|
||||
} else {
|
||||
@@ -152,8 +163,8 @@ class Database {
|
||||
await Database.close();
|
||||
|
||||
console.error(ex);
|
||||
console.error("Start Uptime-Kuma failed due to patch db failed");
|
||||
console.error("Please submit the bug report if you still encounter the problem after restart: https://github.com/louislam/uptime-kuma/issues");
|
||||
console.error("Start Uptime-Kuma failed due to issue patching the database");
|
||||
console.error("Please submit a bug report if you still encounter the problem after restart: https://github.com/louislam/uptime-kuma/issues");
|
||||
|
||||
this.restore();
|
||||
process.exit(1);
|
||||
@@ -161,6 +172,7 @@ class Database {
|
||||
}
|
||||
|
||||
await this.patch2();
|
||||
await this.migrateNewStatusPage();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -191,7 +203,7 @@ class Database {
|
||||
await Database.close();
|
||||
|
||||
console.error(ex);
|
||||
console.error("Start Uptime-Kuma failed due to patch db failed");
|
||||
console.error("Start Uptime-Kuma failed due to issue patching the database");
|
||||
console.error("Please submit the bug report if you still encounter the problem after restart: https://github.com/louislam/uptime-kuma/issues");
|
||||
|
||||
this.restore();
|
||||
@@ -202,6 +214,74 @@ class Database {
|
||||
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
|
||||
* @param sqlFilename
|
||||
@@ -232,7 +312,7 @@ class Database {
|
||||
this.patched = true;
|
||||
await this.importSQLFile("./db/" + sqlFilename);
|
||||
databasePatchedFiles[sqlFilename] = true;
|
||||
console.log(sqlFilename + " is patched successfully");
|
||||
console.log(sqlFilename + " was patched successfully");
|
||||
|
||||
} else {
|
||||
debug(sqlFilename + " is already patched, skip");
|
||||
@@ -287,7 +367,7 @@ class Database {
|
||||
};
|
||||
process.addListener("unhandledRejection", listener);
|
||||
|
||||
console.log("Closing DB");
|
||||
console.log("Closing the database");
|
||||
|
||||
while (true) {
|
||||
Database.noReject = true;
|
||||
@@ -297,7 +377,7 @@ class Database {
|
||||
if (Database.noReject) {
|
||||
break;
|
||||
} else {
|
||||
console.log("Waiting to close the db");
|
||||
console.log("Waiting to close the database");
|
||||
}
|
||||
}
|
||||
console.log("SQLite closed");
|
||||
@@ -312,7 +392,7 @@ class Database {
|
||||
*/
|
||||
static backup(version) {
|
||||
if (! this.backupPath) {
|
||||
console.info("Backup the db");
|
||||
console.info("Backing up the database");
|
||||
this.backupPath = this.dataDir + "kuma.db.bak" + version;
|
||||
fs.copyFileSync(Database.path, this.backupPath);
|
||||
|
||||
@@ -335,7 +415,7 @@ class Database {
|
||||
*/
|
||||
static restore() {
|
||||
if (this.backupPath) {
|
||||
console.error("Patch db failed!!! Restoring the backup");
|
||||
console.error("Patching the database failed!!! Restoring the backup");
|
||||
|
||||
const shmPath = Database.path + "-shm";
|
||||
const walPath = Database.path + "-wal";
|
||||
@@ -354,7 +434,7 @@ class Database {
|
||||
fs.unlinkSync(walPath);
|
||||
}
|
||||
} catch (e) {
|
||||
console.log("Restore failed, you may need to restore the backup manually");
|
||||
console.log("Restore failed; you may need to restore the backup manually");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
@@ -373,6 +453,17 @@ class Database {
|
||||
console.log("Nothing to restore");
|
||||
}
|
||||
}
|
||||
|
||||
static getSize() {
|
||||
debug("Database.getSize()");
|
||||
let stats = fs.statSync(Database.path);
|
||||
debug(stats);
|
||||
return stats.size;
|
||||
}
|
||||
|
||||
static async shrink() {
|
||||
await R.exec("VACUUM");
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Database;
|
||||
|
@@ -6,7 +6,7 @@ const jobs = [
|
||||
{
|
||||
name: "clear-old-data",
|
||||
interval: "at 03:14",
|
||||
}
|
||||
},
|
||||
];
|
||||
|
||||
const initBackgroundJobs = function (args) {
|
||||
|
@@ -3,12 +3,12 @@ const { R } = require("redbean-node");
|
||||
|
||||
class Group extends BeanModel {
|
||||
|
||||
async toPublicJSON() {
|
||||
async toPublicJSON(showTags = false) {
|
||||
let monitorBeanList = await this.getMonitorList();
|
||||
let monitorList = [];
|
||||
|
||||
for (let bean of monitorBeanList) {
|
||||
monitorList.push(await bean.toPublicJSON());
|
||||
monitorList.push(await bean.toPublicJSON(showTags));
|
||||
}
|
||||
|
||||
return {
|
||||
|
@@ -7,10 +7,11 @@ dayjs.extend(timezone);
|
||||
const axios = require("axios");
|
||||
const { Prometheus } = require("../prometheus");
|
||||
const { debug, UP, DOWN, PENDING, flipStatus, TimeLogger } = require("../../src/util");
|
||||
const { tcping, ping, dnsResolve, checkCertificate, checkStatusCode, getTotalClientInRoom, setting } = require("../util-server");
|
||||
const { tcping, ping, dnsResolve, checkCertificate, checkStatusCode, getTotalClientInRoom, setting, errorLog } = require("../util-server");
|
||||
const { R } = require("redbean-node");
|
||||
const { BeanModel } = require("redbean-node/dist/bean-model");
|
||||
const { Notification } = require("../notification");
|
||||
const { Proxy } = require("../proxy");
|
||||
const { demoMode } = require("../config");
|
||||
const version = require("../../package.json").version;
|
||||
const apicache = require("../modules/apicache");
|
||||
@@ -24,18 +25,22 @@ const apicache = require("../modules/apicache");
|
||||
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
|
||||
*/
|
||||
async toPublicJSON() {
|
||||
return {
|
||||
async toPublicJSON(showTags = false) {
|
||||
let obj = {
|
||||
id: this.id,
|
||||
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() {
|
||||
|
||||
@@ -49,7 +54,7 @@ class Monitor extends BeanModel {
|
||||
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 {
|
||||
id: this.id,
|
||||
@@ -58,6 +63,8 @@ class Monitor extends BeanModel {
|
||||
method: this.method,
|
||||
body: this.body,
|
||||
headers: this.headers,
|
||||
basic_auth_user: this.basic_auth_user,
|
||||
basic_auth_pass: this.basic_auth_pass,
|
||||
hostname: this.hostname,
|
||||
port: this.port,
|
||||
maxretries: this.maxretries,
|
||||
@@ -75,11 +82,25 @@ class Monitor extends BeanModel {
|
||||
dns_resolve_server: this.dns_resolve_server,
|
||||
dns_last_result: this.dns_last_result,
|
||||
pushToken: this.pushToken,
|
||||
proxyId: this.proxy_id,
|
||||
notificationIDList,
|
||||
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");
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse to boolean
|
||||
* @returns {boolean}
|
||||
@@ -108,6 +129,19 @@ class Monitor extends BeanModel {
|
||||
|
||||
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
|
||||
// undefined if not https
|
||||
let tlsInfo = undefined;
|
||||
@@ -141,25 +175,59 @@ class Monitor extends BeanModel {
|
||||
// Do not do any queries/high loading things before the "bean.ping"
|
||||
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 = {
|
||||
url: this.url,
|
||||
method: (this.method || "get").toLowerCase(),
|
||||
...(this.body ? { data: JSON.parse(this.body) } : {}),
|
||||
timeout: this.interval * 1000 * 0.8,
|
||||
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,
|
||||
...(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,
|
||||
validateStatus: (status) => {
|
||||
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);
|
||||
bean.msg = `${res.status} - ${res.statusText}`;
|
||||
bean.ping = dayjs().valueOf() - startTime;
|
||||
@@ -167,8 +235,16 @@ class Monitor extends BeanModel {
|
||||
// Check certificate if https is used
|
||||
let certInfoStartTime = dayjs().valueOf();
|
||||
if (this.getUrl()?.protocol === "https:") {
|
||||
debug(`[${this.name}] Check cert`);
|
||||
try {
|
||||
tlsInfo = await this.updateTlsInfo(checkCertificate(res));
|
||||
let tlsInfoObject = checkCertificate(res);
|
||||
tlsInfo = await this.updateTlsInfo(tlsInfoObject);
|
||||
|
||||
if (!this.getIgnoreTls()) {
|
||||
debug(`[${this.name}] call sendCertNotification`);
|
||||
await this.sendCertNotification(tlsInfoObject);
|
||||
}
|
||||
|
||||
} catch (e) {
|
||||
if (e.message !== "No TLS certificate in response") {
|
||||
console.error(e.message);
|
||||
@@ -264,11 +340,14 @@ class Monitor extends BeanModel {
|
||||
debug("heartbeatCount" + heartbeatCount + " " + time);
|
||||
|
||||
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");
|
||||
} else {
|
||||
// No need to insert successful heartbeat for push type, so end here
|
||||
retries = 0;
|
||||
this.heartbeatInterval = setTimeout(beat, this.interval * 1000);
|
||||
this.heartbeatInterval = setTimeout(beat, beatInterval * 1000);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -342,15 +421,21 @@ class Monitor extends BeanModel {
|
||||
}
|
||||
}
|
||||
|
||||
let beatInterval = this.interval;
|
||||
|
||||
debug(`[${this.name}] Check isImportant`);
|
||||
let isImportant = Monitor.isImportantBeat(isFirstBeat, previousBeat?.status, bean.status);
|
||||
|
||||
// Mark as important if status changed, ignore pending pings,
|
||||
// Don't notify if disrupted changes to up
|
||||
if (isImportant) {
|
||||
bean.important = true;
|
||||
|
||||
debug(`[${this.name}] sendNotification`);
|
||||
await Monitor.sendNotification(isFirstBeat, this, bean);
|
||||
|
||||
// Clear Status Page Cache
|
||||
debug(`[${this.name}] apicache clear`);
|
||||
apicache.clear();
|
||||
|
||||
} else {
|
||||
bean.important = false;
|
||||
}
|
||||
@@ -366,41 +451,61 @@ class Monitor extends BeanModel {
|
||||
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());
|
||||
Monitor.sendStats(io, this.id, this.user_id);
|
||||
|
||||
debug(`[${this.name}] Store`);
|
||||
await R.store(bean);
|
||||
|
||||
debug(`[${this.name}] prometheus.update`);
|
||||
prometheus.update(bean, tlsInfo);
|
||||
|
||||
previousBeat = bean;
|
||||
|
||||
if (! this.isStop) {
|
||||
|
||||
if (demoMode) {
|
||||
if (beatInterval < 20) {
|
||||
console.log("beat interval too low, reset to 20s");
|
||||
beatInterval = 20;
|
||||
}
|
||||
}
|
||||
|
||||
this.heartbeatInterval = setTimeout(beat, beatInterval * 1000);
|
||||
debug(`[${this.name}] SetTimeout for next check.`);
|
||||
this.heartbeatInterval = setTimeout(safeBeat, beatInterval * 1000);
|
||||
} else {
|
||||
console.log(`[${this.name}] isStop = true, no next check.`);
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
const safeBeat = async () => {
|
||||
try {
|
||||
await beat();
|
||||
} catch (e) {
|
||||
console.trace(e);
|
||||
errorLog(e, false);
|
||||
console.error("Please report to https://github.com/louislam/uptime-kuma/issues");
|
||||
|
||||
if (! this.isStop) {
|
||||
console.log("Try to restart the monitor");
|
||||
this.heartbeatInterval = setTimeout(safeBeat, this.interval * 1000);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Delay Push Type
|
||||
if (this.type === "push") {
|
||||
setTimeout(() => {
|
||||
beat();
|
||||
safeBeat();
|
||||
}, this.interval * 1000);
|
||||
} else {
|
||||
beat();
|
||||
safeBeat();
|
||||
}
|
||||
}
|
||||
|
||||
stop() {
|
||||
clearTimeout(this.heartbeatInterval);
|
||||
this.isStop = true;
|
||||
|
||||
this.prometheus().remove();
|
||||
}
|
||||
|
||||
prometheus() {
|
||||
return new Prometheus(this);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -426,10 +531,36 @@ class Monitor extends BeanModel {
|
||||
let tls_info_bean = await R.findOne("monitor_tls_info", "monitor_id = ?", [
|
||||
this.id,
|
||||
]);
|
||||
|
||||
if (tls_info_bean == null) {
|
||||
tls_info_bean = R.dispense("monitor_tls_info");
|
||||
tls_info_bean.monitor_id = this.id;
|
||||
} else {
|
||||
|
||||
// Clear sent history if the cert changed.
|
||||
try {
|
||||
let oldCertInfo = JSON.parse(tls_info_bean.info_json);
|
||||
|
||||
let isValidObjects = oldCertInfo && oldCertInfo.certInfo && checkCertificateResult && checkCertificateResult.certInfo;
|
||||
|
||||
if (isValidObjects) {
|
||||
if (oldCertInfo.certInfo.fingerprint256 !== checkCertificateResult.certInfo.fingerprint256) {
|
||||
debug("Resetting sent_history");
|
||||
await R.exec("DELETE FROM notification_sent_history WHERE type = 'certificate' AND monitor_id = ?", [
|
||||
this.id
|
||||
]);
|
||||
} else {
|
||||
debug("No need to reset sent_history");
|
||||
debug(oldCertInfo.certInfo.fingerprint256);
|
||||
debug(checkCertificateResult.certInfo.fingerprint256);
|
||||
}
|
||||
} else {
|
||||
debug("Not valid object");
|
||||
}
|
||||
} catch (e) { }
|
||||
|
||||
}
|
||||
|
||||
tls_info_bean.info_json = JSON.stringify(checkCertificateResult);
|
||||
await R.store(tls_info_bean);
|
||||
|
||||
@@ -577,9 +708,7 @@ class Monitor extends BeanModel {
|
||||
|
||||
static async sendNotification(isFirstBeat, monitor, bean) {
|
||||
if (!isFirstBeat || bean.status === DOWN) {
|
||||
let notificationList = await R.getAll("SELECT notification.* FROM notification, monitor_notification WHERE monitor_id = ? AND monitor_notification.notification_id = notification.id ", [
|
||||
monitor.id,
|
||||
]);
|
||||
const notificationList = await Monitor.getNotificationList(monitor);
|
||||
|
||||
let text;
|
||||
if (bean.status === UP) {
|
||||
@@ -598,12 +727,82 @@ class Monitor extends BeanModel {
|
||||
console.log(e);
|
||||
}
|
||||
}
|
||||
|
||||
// Clear Status Page Cache
|
||||
apicache.clear();
|
||||
}
|
||||
}
|
||||
|
||||
static async getNotificationList(monitor) {
|
||||
let notificationList = await R.getAll("SELECT notification.* FROM notification, monitor_notification WHERE monitor_id = ? AND monitor_notification.notification_id = notification.id ", [
|
||||
monitor.id,
|
||||
]);
|
||||
return notificationList;
|
||||
}
|
||||
|
||||
async sendCertNotification(tlsInfoObject) {
|
||||
if (tlsInfoObject && tlsInfoObject.certInfo && tlsInfoObject.certInfo.daysRemaining) {
|
||||
const notificationList = await Monitor.getNotificationList(this);
|
||||
|
||||
debug("call sendCertNotificationByTargetDays");
|
||||
await this.sendCertNotificationByTargetDays(tlsInfoObject.certInfo.daysRemaining, 21, notificationList);
|
||||
await this.sendCertNotificationByTargetDays(tlsInfoObject.certInfo.daysRemaining, 14, notificationList);
|
||||
await this.sendCertNotificationByTargetDays(tlsInfoObject.certInfo.daysRemaining, 7, notificationList);
|
||||
}
|
||||
}
|
||||
|
||||
async sendCertNotificationByTargetDays(daysRemaining, targetDays, notificationList) {
|
||||
|
||||
if (daysRemaining > targetDays) {
|
||||
debug(`No need to send cert notification. ${daysRemaining} > ${targetDays}`);
|
||||
return;
|
||||
}
|
||||
|
||||
if (notificationList.length > 0) {
|
||||
|
||||
let row = await R.getRow("SELECT * FROM notification_sent_history WHERE type = ? AND monitor_id = ? AND days = ?", [
|
||||
"certificate",
|
||||
this.id,
|
||||
targetDays,
|
||||
]);
|
||||
|
||||
// Sent already, no need to send again
|
||||
if (row) {
|
||||
debug("Sent already, no need to send again");
|
||||
return;
|
||||
}
|
||||
|
||||
let sent = false;
|
||||
debug("Send certificate notification");
|
||||
|
||||
for (let notification of notificationList) {
|
||||
try {
|
||||
debug("Sending to " + notification.name);
|
||||
await Notification.send(JSON.parse(notification.config), `[${this.name}][${this.url}] Certificate will be expired in ${daysRemaining} days`);
|
||||
sent = true;
|
||||
} catch (e) {
|
||||
console.error("Cannot send cert notification to " + notification.name);
|
||||
console.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
if (sent) {
|
||||
await R.exec("INSERT INTO notification_sent_history (type, monitor_id, days) VALUES(?, ?, ?)", [
|
||||
"certificate",
|
||||
this.id,
|
||||
targetDays,
|
||||
]);
|
||||
}
|
||||
} else {
|
||||
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;
|
||||
|
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;
|
60
server/model/status_page.js
Normal file
60
server/model/status_page.js
Normal file
@@ -0,0 +1,60 @@
|
||||
const { BeanModel } = require("redbean-node/dist/bean-model");
|
||||
const { R } = require("redbean-node");
|
||||
|
||||
class StatusPage extends BeanModel {
|
||||
|
||||
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 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,
|
||||
};
|
||||
}
|
||||
|
||||
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;
|
89
server/notification-providers/bark.js
Normal file
89
server/notification-providers/bark.js
Normal file
@@ -0,0 +1,89 @@
|
||||
//
|
||||
// bark.js
|
||||
// UptimeKuma
|
||||
//
|
||||
// Created by Lakr Aream on 2021/10/24.
|
||||
// Copyright © 2021 Lakr Aream. All rights reserved.
|
||||
//
|
||||
|
||||
const NotificationProvider = require("./notification-provider");
|
||||
const { DOWN, UP } = require("../../src/util");
|
||||
const { default: axios } = require("axios");
|
||||
|
||||
// bark is an APN bridge that sends notifications to Apple devices.
|
||||
|
||||
const barkNotificationGroup = "UptimeKuma";
|
||||
const barkNotificationAvatar = "https://github.com/louislam/uptime-kuma/raw/master/public/icon.png";
|
||||
const barkNotificationSound = "telegraph";
|
||||
const successMessage = "Successes!";
|
||||
|
||||
class Bark extends NotificationProvider {
|
||||
name = "Bark";
|
||||
|
||||
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
|
||||
try {
|
||||
var barkEndpoint = notification.barkEndpoint;
|
||||
|
||||
// check if the endpoint has a "/" suffix, if so, delete it first
|
||||
if (barkEndpoint.endsWith("/")) {
|
||||
barkEndpoint = barkEndpoint.substring(0, barkEndpoint.length - 1);
|
||||
}
|
||||
|
||||
if (msg != null && heartbeatJSON != null && heartbeatJSON["status"] == UP) {
|
||||
let title = "UptimeKuma Monitor Up";
|
||||
return await this.postNotification(title, msg, barkEndpoint);
|
||||
}
|
||||
|
||||
if (msg != null && heartbeatJSON != null && heartbeatJSON["status"] == DOWN) {
|
||||
let title = "UptimeKuma Monitor Down";
|
||||
return await this.postNotification(title, msg, barkEndpoint);
|
||||
}
|
||||
|
||||
if (msg != null) {
|
||||
let title = "UptimeKuma Message";
|
||||
return await this.postNotification(title, msg, barkEndpoint);
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// add additional parameter for better on device styles (iOS 15 optimized)
|
||||
appendAdditionalParameters(postUrl) {
|
||||
// grouping all our notifications
|
||||
postUrl += "?group=" + barkNotificationGroup;
|
||||
// set icon to uptime kuma icon, 11kb should be fine
|
||||
postUrl += "&icon=" + barkNotificationAvatar;
|
||||
// picked a sound, this should follow system's mute status when arrival
|
||||
postUrl += "&sound=" + barkNotificationSound;
|
||||
return postUrl;
|
||||
}
|
||||
|
||||
// thrown if failed to check result, result code should be in range 2xx
|
||||
checkResult(result) {
|
||||
if (result.status == null) {
|
||||
throw new Error("Bark notification failed with invalid response!");
|
||||
}
|
||||
if (result.status < 200 || result.status >= 300) {
|
||||
throw new Error("Bark notification failed with status code " + result.status);
|
||||
}
|
||||
}
|
||||
|
||||
async postNotification(title, subtitle, endpoint) {
|
||||
// url encode title and subtitle
|
||||
title = encodeURIComponent(title);
|
||||
subtitle = encodeURIComponent(subtitle);
|
||||
let postUrl = endpoint + "/" + title + "/" + subtitle;
|
||||
postUrl = this.appendAdditionalParameters(postUrl);
|
||||
let result = await axios.get(postUrl);
|
||||
this.checkResult(result);
|
||||
if (result.statusText != null) {
|
||||
return "Bark notification succeed: " + result.statusText;
|
||||
}
|
||||
// because returned in range 200 ..< 300
|
||||
return successMessage;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Bark;
|
42
server/notification-providers/clicksendsms.js
Normal file
42
server/notification-providers/clicksendsms.js
Normal file
@@ -0,0 +1,42 @@
|
||||
const NotificationProvider = require("./notification-provider");
|
||||
const axios = require("axios");
|
||||
|
||||
class ClickSendSMS extends NotificationProvider {
|
||||
|
||||
name = "clicksendsms";
|
||||
|
||||
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
|
||||
let okMsg = "Sent Successfully.";
|
||||
try {
|
||||
console.log({ notification });
|
||||
let config = {
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"Authorization": "Basic " + Buffer.from(notification.clicksendsmsLogin + ":" + notification.clicksendsmsPassword).toString('base64'),
|
||||
"Accept": "text/json",
|
||||
}
|
||||
};
|
||||
let data = {
|
||||
messages: [
|
||||
{
|
||||
"body": msg.replace(/[^\x00-\x7F]/g, ""),
|
||||
"to": notification.clicksendsmsToNumber,
|
||||
"source": "uptime-kuma",
|
||||
"from": notification.clicksendsmsSenderName,
|
||||
}
|
||||
]
|
||||
};
|
||||
let resp = await axios.post("https://rest.clicksend.com/v3/sms/send", data, config);
|
||||
if (resp.data.data.messages[0].status !== "SUCCESS") {
|
||||
let error = "Something gone wrong. Api returned " + resp.data.data.messages[0].status + ".";
|
||||
this.throwGeneralAxiosError(error);
|
||||
}
|
||||
|
||||
return okMsg;
|
||||
} catch (error) {
|
||||
this.throwGeneralAxiosError(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = ClickSendSMS;
|
@@ -14,8 +14,8 @@ class DingDing extends NotificationProvider {
|
||||
let params = {
|
||||
msgtype: "markdown",
|
||||
markdown: {
|
||||
title: monitorJSON["name"],
|
||||
text: `## [${this.statusToString(heartbeatJSON["status"])}] \n > ${heartbeatJSON["msg"]} \n > Time(UTC):${heartbeatJSON["time"]}`,
|
||||
title: `[${this.statusToString(heartbeatJSON["status"])}] ${monitorJSON["name"]}`,
|
||||
text: `## [${this.statusToString(heartbeatJSON["status"])}] ${monitorJSON["name"]} \n > ${heartbeatJSON["msg"]} \n > Time(UTC):${heartbeatJSON["time"]}`,
|
||||
}
|
||||
};
|
||||
if (this.sendToDingDing(notification, params)) {
|
||||
|
@@ -27,7 +27,7 @@ class Feishu extends NotificationProvider {
|
||||
content: {
|
||||
post: {
|
||||
zh_cn: {
|
||||
title: "UptimeKuma Alert: " + monitorJSON["name"],
|
||||
title: "UptimeKuma Alert: [Down] " + monitorJSON["name"],
|
||||
content: [
|
||||
[
|
||||
{
|
||||
@@ -54,7 +54,7 @@ class Feishu extends NotificationProvider {
|
||||
content: {
|
||||
post: {
|
||||
zh_cn: {
|
||||
title: "UptimeKuma Alert: " + monitorJSON["name"],
|
||||
title: "UptimeKuma Alert: [Up] " + monitorJSON["name"],
|
||||
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;
|
@@ -20,7 +20,7 @@ class Mattermost extends NotificationProvider {
|
||||
return okMsg;
|
||||
}
|
||||
|
||||
const mattermostChannel = notification.mattermostchannel;
|
||||
const mattermostChannel = notification.mattermostchannel.toLowerCase();
|
||||
const mattermostIconEmoji = notification.mattermosticonemo;
|
||||
const mattermostIconUrl = notification.mattermosticonurl;
|
||||
|
||||
|
@@ -7,40 +7,35 @@ class Pushover extends NotificationProvider {
|
||||
|
||||
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
|
||||
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 {
|
||||
if (heartbeatJSON == null) {
|
||||
let data = {
|
||||
"message": "<b>Uptime Kuma Pushover testing successful.</b>",
|
||||
"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)
|
||||
await axios.post(pushoverlink, data);
|
||||
return okMsg;
|
||||
} else {
|
||||
data.message += "\n<b>Time (UTC)</b>:" + heartbeatJSON["time"];
|
||||
await axios.post(pushoverlink, data);
|
||||
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) {
|
||||
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;
|
@@ -12,8 +12,23 @@ class SMTP extends NotificationProvider {
|
||||
host: notification.smtpHost,
|
||||
port: notification.smtpPort,
|
||||
secure: notification.smtpSecure,
|
||||
tls: {
|
||||
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
|
||||
if (notification.smtpUsername || notification.smtpPassword) {
|
||||
config.auth = {
|
||||
@@ -87,9 +102,6 @@ class SMTP extends NotificationProvider {
|
||||
to: notification.smtpTo,
|
||||
subject: subject,
|
||||
text: bodyTextContent,
|
||||
tls: {
|
||||
rejectUnauthorized: notification.smtpIgnoreTLSError || false,
|
||||
},
|
||||
});
|
||||
|
||||
return "Sent Successfully.";
|
||||
|
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;
|
@@ -8,9 +8,11 @@ const Mattermost = require("./notification-providers/mattermost");
|
||||
const Matrix = require("./notification-providers/matrix");
|
||||
const Octopush = require("./notification-providers/octopush");
|
||||
const PromoSMS = require("./notification-providers/promosms");
|
||||
const ClickSendSMS = require("./notification-providers/clicksendsms");
|
||||
const Pushbullet = require("./notification-providers/pushbullet");
|
||||
const Pushover = require("./notification-providers/pushover");
|
||||
const Pushy = require("./notification-providers/pushy");
|
||||
const TechulusPush = require("./notification-providers/techulus-push");
|
||||
const RocketChat = require("./notification-providers/rocket-chat");
|
||||
const Signal = require("./notification-providers/signal");
|
||||
const Slack = require("./notification-providers/slack");
|
||||
@@ -21,6 +23,13 @@ const Webhook = require("./notification-providers/webhook");
|
||||
const Feishu = require("./notification-providers/feishu");
|
||||
const AliyunSms = require("./notification-providers/aliyun-sms");
|
||||
const DingDing = require("./notification-providers/dingding");
|
||||
const Bark = require("./notification-providers/bark");
|
||||
const 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 {
|
||||
|
||||
@@ -45,15 +54,24 @@ class Notification {
|
||||
new Matrix(),
|
||||
new Octopush(),
|
||||
new PromoSMS(),
|
||||
new ClickSendSMS(),
|
||||
new Pushbullet(),
|
||||
new Pushover(),
|
||||
new Pushy(),
|
||||
new TechulusPush(),
|
||||
new RocketChat(),
|
||||
new Signal(),
|
||||
new Slack(),
|
||||
new SMTP(),
|
||||
new Telegram(),
|
||||
new Webhook(),
|
||||
new Bark(),
|
||||
new SerwerSMS(),
|
||||
new Stackfield(),
|
||||
new WeCom(),
|
||||
new GoogleChat(),
|
||||
new Gorush(),
|
||||
new Alerta(),
|
||||
];
|
||||
|
||||
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._regmatch = /=([0-9.]+?) ms/;
|
||||
|
||||
} else if (util.FBSD) {
|
||||
} else if (util.BSD) {
|
||||
this._bin = "/sbin/ping";
|
||||
|
||||
const defaultArgs = [ "-n", "-t", timeout, "-c", "1", host ];
|
||||
|
@@ -60,7 +60,9 @@ class Prometheus {
|
||||
}
|
||||
|
||||
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) {
|
||||
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 = {
|
||||
|
170
server/proxy.js
Normal file
170
server/proxy.js
Normal file
@@ -0,0 +1,170 @@
|
||||
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");
|
||||
|
||||
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
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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,
|
||||
};
|
47
server/rate-limiter.js
Normal file
47
server/rate-limiter.js
Normal file
@@ -0,0 +1,47 @@
|
||||
const { RateLimiter } = require("limiter");
|
||||
const { debug } = require("../src/util");
|
||||
|
||||
class KumaRateLimiter {
|
||||
constructor(config) {
|
||||
this.errorMessage = config.errorMessage;
|
||||
this.rateLimiter = new RateLimiter(config);
|
||||
}
|
||||
|
||||
async pass(callback, num = 1) {
|
||||
const remainingRequests = await this.removeTokens(num);
|
||||
debug("Rate Limit (remainingRequests):" + remainingRequests);
|
||||
if (remainingRequests < 0) {
|
||||
if (callback) {
|
||||
callback({
|
||||
ok: false,
|
||||
msg: this.errorMessage,
|
||||
});
|
||||
}
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
async removeTokens(num = 1) {
|
||||
return await this.rateLimiter.removeTokens(num);
|
||||
}
|
||||
}
|
||||
|
||||
const loginRateLimiter = new KumaRateLimiter({
|
||||
tokensPerInterval: 20,
|
||||
interval: "minute",
|
||||
fireImmediately: true,
|
||||
errorMessage: "Too frequently, try again later."
|
||||
});
|
||||
|
||||
const twoFaRateLimiter = new KumaRateLimiter({
|
||||
tokensPerInterval: 30,
|
||||
interval: "minute",
|
||||
fireImmediately: true,
|
||||
errorMessage: "Too frequently, try again later."
|
||||
});
|
||||
|
||||
module.exports = {
|
||||
loginRateLimiter,
|
||||
twoFaRateLimiter,
|
||||
};
|
@@ -6,6 +6,7 @@ const apicache = require("../modules/apicache");
|
||||
const Monitor = require("../model/monitor");
|
||||
const dayjs = require("dayjs");
|
||||
const { UP, flipStatus, debug } = require("../../src/util");
|
||||
const StatusPage = require("../model/status_page");
|
||||
let router = express.Router();
|
||||
|
||||
let cache = apicache.middleware;
|
||||
@@ -31,12 +32,7 @@ router.get("/api/push/:pushToken", async (request, response) => {
|
||||
throw new Error("Monitor not found or not active.");
|
||||
}
|
||||
|
||||
const previousHeartbeat = await R.getRow(`
|
||||
SELECT status, time FROM heartbeat
|
||||
WHERE id = (select MAX(id) from heartbeat where monitor_id = ?)
|
||||
`, [
|
||||
monitor.id
|
||||
]);
|
||||
const previousHeartbeat = await Monitor.getPreviousHeartbeat(monitor.id);
|
||||
|
||||
let status = UP;
|
||||
if (monitor.isUpsideDown()) {
|
||||
@@ -87,88 +83,80 @@ router.get("/api/push/:pushToken", async (request, response) => {
|
||||
}
|
||||
});
|
||||
|
||||
// Status Page Config
|
||||
router.get("/api/status-page/config", async (_request, response) => {
|
||||
// Status page config, incident, monitor list
|
||||
router.get("/api/status-page/:slug", cache("5 minutes"), async (request, response) => {
|
||||
allowDevAllOrigin(response);
|
||||
let slug = request.params.slug;
|
||||
|
||||
let config = await getSettings("statusPage");
|
||||
// Get Status Page
|
||||
let statusPage = await R.findOne("status_page", " slug = ? ", [
|
||||
slug
|
||||
]);
|
||||
|
||||
if (! config.statusPageTheme) {
|
||||
config.statusPageTheme = "light";
|
||||
if (!statusPage) {
|
||||
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 {
|
||||
await checkPublished();
|
||||
|
||||
let incident = await R.findOne("incident", " pin = 1 AND active = 1");
|
||||
// Incident
|
||||
let incident = await R.findOne("incident", " pin = 1 AND active = 1 AND status_page_id = ? ", [
|
||||
statusPage.id,
|
||||
]);
|
||||
|
||||
if (incident) {
|
||||
incident = incident.toPublicJSON();
|
||||
}
|
||||
|
||||
// Public Group List
|
||||
const publicGroupList = [];
|
||||
const showTags = !!statusPage.show_tags;
|
||||
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({
|
||||
ok: true,
|
||||
config: await statusPage.toPublicJSON(),
|
||||
incident,
|
||||
publicGroupList
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
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
|
||||
// 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);
|
||||
|
||||
try {
|
||||
await checkPublished();
|
||||
|
||||
let heartbeatList = {};
|
||||
let uptimeList = {};
|
||||
|
||||
let slug = request.params.slug;
|
||||
let statusPageID = await StatusPage.slugToID(slug);
|
||||
|
||||
let monitorIDList = await R.getCol(`
|
||||
SELECT monitor_group.monitor_id FROM monitor_group, \`group\`
|
||||
WHERE monitor_group.group_id = \`group\`.id
|
||||
AND public = 1
|
||||
`);
|
||||
AND \`group\`.status_page_id = ?
|
||||
`, [
|
||||
statusPageID
|
||||
]);
|
||||
|
||||
for (let monitorID of monitorIDList) {
|
||||
let list = await R.getAll(`
|
||||
@@ -197,22 +185,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
|
||||
* @returns {Promise<boolean>}
|
||||
*/
|
||||
async function isPublished() {
|
||||
const value = await setting("statusPagePublished");
|
||||
if (value === null) {
|
||||
return true;
|
||||
}
|
||||
return value;
|
||||
return true;
|
||||
}
|
||||
|
||||
function send403(res, msg = "") {
|
||||
|
351
server/server.js
351
server/server.js
@@ -1,4 +1,15 @@
|
||||
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 { sleep, debug, getRandomInt, genSecret } = require("../src/util");
|
||||
const config = require("./config");
|
||||
@@ -31,6 +42,7 @@ debug("Importing prometheus-api-metrics");
|
||||
const prometheusAPIMetrics = require("prometheus-api-metrics");
|
||||
debug("Importing compare-versions");
|
||||
const compareVersions = require("compare-versions");
|
||||
const { passwordStrength } = require("check-password-strength");
|
||||
|
||||
debug("Importing 2FA Modules");
|
||||
const notp = require("notp");
|
||||
@@ -40,17 +52,21 @@ console.log("Importing this project modules");
|
||||
debug("Importing Monitor");
|
||||
const Monitor = require("./model/monitor");
|
||||
debug("Importing Settings");
|
||||
const { getSettings, setSettings, setting, initJWTSecret, checkLogin, startUnitTest, FBSD } = require("./util-server");
|
||||
const { getSettings, setSettings, setting, initJWTSecret, checkLogin, startUnitTest, FBSD, errorLog, doubleCheckPassword } = require("./util-server");
|
||||
|
||||
debug("Importing Notification");
|
||||
const { Notification } = require("./notification");
|
||||
Notification.init();
|
||||
|
||||
debug("Importing Proxy");
|
||||
const { Proxy } = require("./proxy");
|
||||
|
||||
debug("Importing Database");
|
||||
const Database = require("./database");
|
||||
|
||||
debug("Importing Background Jobs");
|
||||
const { initBackgroundJobs } = require("./jobs");
|
||||
const { loginRateLimiter, twoFaRateLimiter } = require("./rate-limiter");
|
||||
|
||||
const { basicAuth } = require("./auth");
|
||||
const { login } = require("./auth");
|
||||
@@ -77,6 +93,8 @@ const port = parseInt(process.env.UPTIME_KUMA_PORT || process.env.PORT || args.p
|
||||
// SSL
|
||||
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 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
|
||||
const twofa_verification_opts = {
|
||||
@@ -114,11 +132,24 @@ const io = new Server(server);
|
||||
module.exports.io = io;
|
||||
|
||||
// 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 databaseSocketHandler = require("./socket-handlers/database-socket-handler");
|
||||
const TwoFA = require("./2fa");
|
||||
const StatusPage = require("./model/status_page");
|
||||
const { cloudflaredSocketHandler, autoStart: cloudflaredAutoStart } = require("./socket-handlers/cloudflared-socket-handler");
|
||||
|
||||
app.use(express.json());
|
||||
|
||||
// Global Middleware
|
||||
app.use(function (req, res, next) {
|
||||
if (!disableFrameSameOrigin) {
|
||||
res.setHeader("X-Frame-Options", "SAMEORIGIN");
|
||||
}
|
||||
res.removeHeader("X-Powered-By");
|
||||
next();
|
||||
});
|
||||
|
||||
/**
|
||||
* Total WebSocket client connected to server currently, no actual use
|
||||
* @type {number}
|
||||
@@ -147,13 +178,23 @@ let needSetup = false;
|
||||
* Cache Index HTML
|
||||
* @type {string}
|
||||
*/
|
||||
let indexHTML = fs.readFileSync("./dist/index.html").toString();
|
||||
let indexHTML = "";
|
||||
|
||||
try {
|
||||
indexHTML = fs.readFileSync("./dist/index.html").toString();
|
||||
} catch (e) {
|
||||
// "dist/index.html" is not necessary for development
|
||||
if (process.env.NODE_ENV !== "development") {
|
||||
console.error("Error: Cannot find 'dist/index.html', did you install correctly?");
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
exports.entryPage = "dashboard";
|
||||
|
||||
(async () => {
|
||||
Database.init(args);
|
||||
await initDatabase();
|
||||
await initDatabase(testMode);
|
||||
|
||||
exports.entryPage = await setting("entryPage");
|
||||
|
||||
@@ -163,6 +204,15 @@ exports.entryPage = "dashboard";
|
||||
// Normal Router here
|
||||
// ***************************
|
||||
|
||||
// Entry Page
|
||||
app.get("/", async (_request, response) => {
|
||||
if (exports.entryPage && exports.entryPage.startsWith("statusPage-")) {
|
||||
response.redirect("/status/" + exports.entryPage.replace("statusPage-", ""));
|
||||
} else {
|
||||
response.redirect("/dashboard");
|
||||
}
|
||||
});
|
||||
|
||||
// Robots.txt
|
||||
app.get("/robots.txt", async (_request, response) => {
|
||||
let txt = "User-agent: *\nDisallow:";
|
||||
@@ -192,7 +242,7 @@ exports.entryPage = "dashboard";
|
||||
const apiRouter = require("./routers/api-router");
|
||||
app.use(apiRouter);
|
||||
|
||||
// Universal Route Handler, must be at the end of all express route.
|
||||
// Universal Route Handler, must be at the end of all express routes.
|
||||
app.get("*", async (_request, response) => {
|
||||
if (_request.originalUrl.startsWith("/upload/")) {
|
||||
response.status(404).send("File not found.");
|
||||
@@ -260,10 +310,24 @@ exports.entryPage = "dashboard";
|
||||
socket.on("login", async (data, callback) => {
|
||||
console.log("Login");
|
||||
|
||||
// Checking
|
||||
if (typeof callback !== "function") {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!data) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Login Rate Limit
|
||||
if (! await loginRateLimiter.pass(callback)) {
|
||||
return;
|
||||
}
|
||||
|
||||
let user = await login(data.username, data.password);
|
||||
|
||||
if (user) {
|
||||
if (user.twofaStatus == 0) {
|
||||
if (user.twofa_status == 0) {
|
||||
afterLogin(socket, user);
|
||||
callback({
|
||||
ok: true,
|
||||
@@ -273,7 +337,7 @@ exports.entryPage = "dashboard";
|
||||
});
|
||||
}
|
||||
|
||||
if (user.twofaStatus == 1 && !data.token) {
|
||||
if (user.twofa_status == 1 && !data.token) {
|
||||
callback({
|
||||
tokenRequired: true,
|
||||
});
|
||||
@@ -282,8 +346,14 @@ exports.entryPage = "dashboard";
|
||||
if (data.token) {
|
||||
let verify = notp.totp.verify(data.token, user.twofa_secret, twofa_verification_opts);
|
||||
|
||||
if (verify && verify.delta == 0) {
|
||||
if (user.twofa_last_token !== data.token && verify) {
|
||||
afterLogin(socket, user);
|
||||
|
||||
await R.exec("UPDATE `user` SET twofa_last_token = ? WHERE id = ? ", [
|
||||
data.token,
|
||||
socket.userID,
|
||||
]);
|
||||
|
||||
callback({
|
||||
ok: true,
|
||||
token: jwt.sign({
|
||||
@@ -307,21 +377,34 @@ exports.entryPage = "dashboard";
|
||||
});
|
||||
|
||||
socket.on("logout", async (callback) => {
|
||||
// Rate Limit
|
||||
if (! await loginRateLimiter.pass(callback)) {
|
||||
return;
|
||||
}
|
||||
|
||||
socket.leave(socket.userID);
|
||||
socket.userID = null;
|
||||
callback();
|
||||
|
||||
if (typeof callback === "function") {
|
||||
callback();
|
||||
}
|
||||
});
|
||||
|
||||
socket.on("prepare2FA", async (callback) => {
|
||||
socket.on("prepare2FA", async (currentPassword, callback) => {
|
||||
try {
|
||||
if (! await twoFaRateLimiter.pass(callback)) {
|
||||
return;
|
||||
}
|
||||
|
||||
checkLogin(socket);
|
||||
await doubleCheckPassword(socket, currentPassword);
|
||||
|
||||
let user = await R.findOne("user", " id = ? AND active = 1 ", [
|
||||
socket.userID,
|
||||
]);
|
||||
|
||||
if (user.twofa_status == 0) {
|
||||
let newSecret = await genSecret();
|
||||
let newSecret = genSecret();
|
||||
let encodedSecret = base32.encode(newSecret);
|
||||
|
||||
// Google authenticator doesn't like equal signs
|
||||
@@ -349,14 +432,19 @@ exports.entryPage = "dashboard";
|
||||
} catch (error) {
|
||||
callback({
|
||||
ok: false,
|
||||
msg: "Error while trying to prepare 2FA.",
|
||||
msg: error.message,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
socket.on("save2FA", async (callback) => {
|
||||
socket.on("save2FA", async (currentPassword, callback) => {
|
||||
try {
|
||||
if (! await twoFaRateLimiter.pass(callback)) {
|
||||
return;
|
||||
}
|
||||
|
||||
checkLogin(socket);
|
||||
await doubleCheckPassword(socket, currentPassword);
|
||||
|
||||
await R.exec("UPDATE `user` SET twofa_status = 1 WHERE id = ? ", [
|
||||
socket.userID,
|
||||
@@ -369,18 +457,20 @@ exports.entryPage = "dashboard";
|
||||
} catch (error) {
|
||||
callback({
|
||||
ok: false,
|
||||
msg: "Error while trying to change 2FA.",
|
||||
msg: error.message,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
socket.on("disable2FA", async (callback) => {
|
||||
socket.on("disable2FA", async (currentPassword, callback) => {
|
||||
try {
|
||||
checkLogin(socket);
|
||||
if (! await twoFaRateLimiter.pass(callback)) {
|
||||
return;
|
||||
}
|
||||
|
||||
await R.exec("UPDATE `user` SET twofa_status = 0 WHERE id = ? ", [
|
||||
socket.userID,
|
||||
]);
|
||||
checkLogin(socket);
|
||||
await doubleCheckPassword(socket, currentPassword);
|
||||
await TwoFA.disable2FA(socket.userID);
|
||||
|
||||
callback({
|
||||
ok: true,
|
||||
@@ -389,36 +479,47 @@ exports.entryPage = "dashboard";
|
||||
} catch (error) {
|
||||
callback({
|
||||
ok: false,
|
||||
msg: "Error while trying to change 2FA.",
|
||||
msg: error.message,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
socket.on("verifyToken", async (token, callback) => {
|
||||
let user = await R.findOne("user", " id = ? AND active = 1 ", [
|
||||
socket.userID,
|
||||
]);
|
||||
socket.on("verifyToken", async (token, currentPassword, callback) => {
|
||||
try {
|
||||
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 (verify && verify.delta == 0) {
|
||||
callback({
|
||||
ok: true,
|
||||
valid: true,
|
||||
});
|
||||
} else {
|
||||
let verify = notp.totp.verify(token, user.twofa_secret, twofa_verification_opts);
|
||||
|
||||
if (user.twofa_last_token !== token && verify) {
|
||||
callback({
|
||||
ok: true,
|
||||
valid: true,
|
||||
});
|
||||
} else {
|
||||
callback({
|
||||
ok: false,
|
||||
msg: "Invalid Token.",
|
||||
valid: false,
|
||||
});
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
callback({
|
||||
ok: false,
|
||||
msg: "Invalid Token.",
|
||||
valid: false,
|
||||
msg: error.message,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
socket.on("twoFAStatus", async (callback) => {
|
||||
checkLogin(socket);
|
||||
|
||||
try {
|
||||
checkLogin(socket);
|
||||
|
||||
let user = await R.findOne("user", " id = ? AND active = 1 ", [
|
||||
socket.userID,
|
||||
]);
|
||||
@@ -435,9 +536,10 @@ exports.entryPage = "dashboard";
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
callback({
|
||||
ok: false,
|
||||
msg: "Error while trying to get 2FA status.",
|
||||
msg: error.message,
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -448,8 +550,12 @@ exports.entryPage = "dashboard";
|
||||
|
||||
socket.on("setup", async (username, password, callback) => {
|
||||
try {
|
||||
if (passwordStrength(password).value === "Too weak") {
|
||||
throw new Error("Password is too weak. It should contain alphabetic and numeric characters. It must be at least 6 characters in length.");
|
||||
}
|
||||
|
||||
if ((await R.count("user")) !== 0) {
|
||||
throw new Error("Uptime Kuma has been setup. If you want to setup again, please delete the database.");
|
||||
throw new Error("Uptime Kuma has been initialized. If you want to run setup again, please delete the database.");
|
||||
}
|
||||
|
||||
let user = R.dispense("user");
|
||||
@@ -494,8 +600,8 @@ exports.entryPage = "dashboard";
|
||||
|
||||
await updateMonitorNotification(bean.id, notificationIDList);
|
||||
|
||||
await startMonitor(socket.userID, bean.id);
|
||||
await sendMonitorList(socket);
|
||||
await startMonitor(socket.userID, bean.id);
|
||||
|
||||
callback({
|
||||
ok: true,
|
||||
@@ -522,12 +628,17 @@ exports.entryPage = "dashboard";
|
||||
throw new Error("Permission denied.");
|
||||
}
|
||||
|
||||
// Reset Prometheus labels
|
||||
monitorList[monitor.id]?.prometheus()?.remove();
|
||||
|
||||
bean.name = monitor.name;
|
||||
bean.type = monitor.type;
|
||||
bean.url = monitor.url;
|
||||
bean.method = monitor.method;
|
||||
bean.body = monitor.body;
|
||||
bean.headers = monitor.headers;
|
||||
bean.basic_auth_user = monitor.basic_auth_user;
|
||||
bean.basic_auth_pass = monitor.basic_auth_pass;
|
||||
bean.interval = monitor.interval;
|
||||
bean.retryInterval = monitor.retryInterval;
|
||||
bean.hostname = monitor.hostname;
|
||||
@@ -541,6 +652,7 @@ exports.entryPage = "dashboard";
|
||||
bean.dns_resolve_type = monitor.dns_resolve_type;
|
||||
bean.dns_resolve_server = monitor.dns_resolve_server;
|
||||
bean.pushToken = monitor.pushToken;
|
||||
bean.proxyId = Number.isInteger(monitor.proxyId) ? monitor.proxyId : null;
|
||||
|
||||
await R.store(bean);
|
||||
|
||||
@@ -607,6 +719,38 @@ exports.entryPage = "dashboard";
|
||||
}
|
||||
});
|
||||
|
||||
socket.on("getMonitorBeats", async (monitorID, period, callback) => {
|
||||
try {
|
||||
checkLogin(socket);
|
||||
|
||||
console.log(`Get Monitor Beats: ${monitorID} User ID: ${socket.userID}`);
|
||||
|
||||
if (period == null) {
|
||||
throw new Error("Invalid period.");
|
||||
}
|
||||
|
||||
let list = await R.getAll(`
|
||||
SELECT * FROM heartbeat
|
||||
WHERE monitor_id = ? AND
|
||||
time > DATETIME('now', '-' || ? || ' hours')
|
||||
ORDER BY time ASC
|
||||
`, [
|
||||
monitorID,
|
||||
period,
|
||||
]);
|
||||
|
||||
callback({
|
||||
ok: true,
|
||||
data: list,
|
||||
});
|
||||
} catch (e) {
|
||||
callback({
|
||||
ok: false,
|
||||
msg: e.message,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Start or Resume the monitor
|
||||
socket.on("resumeMonitor", async (monitorID, callback) => {
|
||||
try {
|
||||
@@ -837,26 +981,22 @@ exports.entryPage = "dashboard";
|
||||
try {
|
||||
checkLogin(socket);
|
||||
|
||||
if (! password.currentPassword) {
|
||||
if (! password.newPassword) {
|
||||
throw new Error("Invalid new password");
|
||||
}
|
||||
|
||||
let user = await R.findOne("user", " id = ? AND active = 1 ", [
|
||||
socket.userID,
|
||||
]);
|
||||
|
||||
if (user && passwordHash.verify(password.currentPassword, user.password)) {
|
||||
|
||||
user.resetPassword(password.newPassword);
|
||||
|
||||
callback({
|
||||
ok: true,
|
||||
msg: "Password has been updated successfully.",
|
||||
});
|
||||
} else {
|
||||
throw new Error("Incorrect current password");
|
||||
if (passwordStrength(password.newPassword).value === "Too weak") {
|
||||
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 doubleCheckPassword(socket, password.currentPassword);
|
||||
await user.resetPassword(password.newPassword);
|
||||
|
||||
callback({
|
||||
ok: true,
|
||||
msg: "Password has been updated successfully.",
|
||||
});
|
||||
|
||||
} catch (e) {
|
||||
callback({
|
||||
ok: false,
|
||||
@@ -882,10 +1022,14 @@ exports.entryPage = "dashboard";
|
||||
}
|
||||
});
|
||||
|
||||
socket.on("setSettings", async (data, callback) => {
|
||||
socket.on("setSettings", async (data, currentPassword, callback) => {
|
||||
try {
|
||||
checkLogin(socket);
|
||||
|
||||
if (data.disableAuth) {
|
||||
await doubleCheckPassword(socket, currentPassword);
|
||||
}
|
||||
|
||||
await setSettings("general", data);
|
||||
exports.entryPage = data.entryPage;
|
||||
|
||||
@@ -967,6 +1111,52 @@ exports.entryPage = "dashboard";
|
||||
}
|
||||
});
|
||||
|
||||
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 restartMonitors(socket.userID);
|
||||
}
|
||||
|
||||
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 restartMonitors(socket.userID);
|
||||
|
||||
callback({
|
||||
ok: true,
|
||||
msg: "Deleted",
|
||||
});
|
||||
|
||||
} catch (e) {
|
||||
callback({
|
||||
ok: false,
|
||||
msg: e.message,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
socket.on("checkApprise", async (callback) => {
|
||||
try {
|
||||
checkLogin(socket);
|
||||
@@ -985,6 +1175,7 @@ exports.entryPage = "dashboard";
|
||||
console.log(`Importing Backup, User ID: ${socket.userID}, Version: ${backupData.version}`);
|
||||
|
||||
let notificationListData = backupData.notificationList;
|
||||
let proxyListData = backupData.proxyList;
|
||||
let monitorListData = backupData.monitorList;
|
||||
|
||||
let version17x = compareVersions.compare(backupData.version, "1.7.0", ">=");
|
||||
@@ -1003,6 +1194,7 @@ exports.entryPage = "dashboard";
|
||||
await R.exec("DELETE FROM monitor_tag");
|
||||
await R.exec("DELETE FROM tag");
|
||||
await R.exec("DELETE FROM monitor");
|
||||
await R.exec("DELETE FROM proxy");
|
||||
}
|
||||
|
||||
// Only starts importing if the backup file contains at least one notification
|
||||
@@ -1022,6 +1214,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
|
||||
if (monitorListData.length >= 1) {
|
||||
// Get every existing monitor name and puts them in one simple string
|
||||
@@ -1056,6 +1266,8 @@ exports.entryPage = "dashboard";
|
||||
method: monitorListData[i].method || "GET",
|
||||
body: monitorListData[i].body,
|
||||
headers: monitorListData[i].headers,
|
||||
basic_auth_user: monitorListData[i].basic_auth_user,
|
||||
basic_auth_pass: monitorListData[i].basic_auth_pass,
|
||||
interval: monitorListData[i].interval,
|
||||
retryInterval: retryInterval,
|
||||
hostname: monitorListData[i].hostname,
|
||||
@@ -1069,6 +1281,7 @@ exports.entryPage = "dashboard";
|
||||
dns_resolve_type: monitorListData[i].dns_resolve_type,
|
||||
dns_resolve_server: monitorListData[i].dns_resolve_server,
|
||||
notificationIDList: {},
|
||||
proxy_id: monitorListData[i].proxy_id || null,
|
||||
};
|
||||
|
||||
if (monitorListData[i].pushToken) {
|
||||
@@ -1222,6 +1435,8 @@ exports.entryPage = "dashboard";
|
||||
|
||||
// Status Page Socket Handler for admin only
|
||||
statusPageSocketHandler(socket);
|
||||
cloudflaredSocketHandler(socket);
|
||||
databaseSocketHandler(socket);
|
||||
|
||||
debug("added all socket handlers");
|
||||
|
||||
@@ -1263,6 +1478,9 @@ exports.entryPage = "dashboard";
|
||||
|
||||
initBackgroundJobs(args);
|
||||
|
||||
// Start cloudflared at the end if configured
|
||||
await cloudflaredAutoStart(cloudflaredToken);
|
||||
|
||||
})();
|
||||
|
||||
async function updateMonitorNotification(monitorID, notificationIDList) {
|
||||
@@ -1303,9 +1521,12 @@ async function afterLogin(socket, user) {
|
||||
|
||||
let monitorList = await sendMonitorList(socket);
|
||||
sendNotificationList(socket);
|
||||
sendProxyList(socket);
|
||||
|
||||
await sleep(500);
|
||||
|
||||
await StatusPage.sendStatusPageList(io, socket);
|
||||
|
||||
for (let monitorID in monitorList) {
|
||||
await sendHeartbeatList(socket, monitorID);
|
||||
}
|
||||
@@ -1333,14 +1554,14 @@ async function getMonitorJSONList(userID) {
|
||||
return result;
|
||||
}
|
||||
|
||||
async function initDatabase() {
|
||||
async function initDatabase(testMode = false) {
|
||||
if (! fs.existsSync(Database.path)) {
|
||||
console.log("Copying Database");
|
||||
fs.copyFileSync(Database.templatePath, Database.path);
|
||||
}
|
||||
|
||||
console.log("Connecting to Database");
|
||||
await Database.connect();
|
||||
console.log("Connecting to the Database");
|
||||
await Database.connect(testMode);
|
||||
console.log("Connected");
|
||||
|
||||
// Patch the database
|
||||
@@ -1393,6 +1614,19 @@ async function restartMonitor(userID, monitorID) {
|
||||
return await startMonitor(userID, monitorID);
|
||||
}
|
||||
|
||||
async function restartMonitors(userID) {
|
||||
// Fetch all active monitors for user
|
||||
const monitors = await R.getAll("SELECT id FROM monitor WHERE active = 1 AND user_id = ?", [userID]);
|
||||
|
||||
for (const monitor of monitors) {
|
||||
// Start updated monitor
|
||||
await startMonitor(userID, monitor.id);
|
||||
|
||||
// Give some delays, so all monitors won't make request at the same moment when just start the server.
|
||||
await sleep(getRandomInt(300, 1000));
|
||||
}
|
||||
}
|
||||
|
||||
async function pauseMonitor(userID, monitorID) {
|
||||
await checkOwner(userID, monitorID);
|
||||
|
||||
@@ -1439,7 +1673,7 @@ async function shutdownFunction(signal) {
|
||||
}
|
||||
|
||||
function finalFunction() {
|
||||
console.log("Graceful shutdown successfully!");
|
||||
console.log("Graceful shutdown successful!");
|
||||
}
|
||||
|
||||
gracefulShutdown(server, {
|
||||
@@ -1454,5 +1688,6 @@ gracefulShutdown(server, {
|
||||
// Catch unexpected errors here
|
||||
process.addListener("unhandledRejection", (error, promise) => {
|
||||
console.trace(error);
|
||||
errorLog(error, false);
|
||||
console.error("If you keep encountering errors, please report to https://github.com/louislam/uptime-kuma/issues");
|
||||
});
|
||||
|
85
server/socket-handlers/cloudflared-socket-handler.js
Normal file
85
server/socket-handlers/cloudflared-socket-handler.js
Normal file
@@ -0,0 +1,85 @@
|
||||
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();
|
||||
}
|
||||
};
|
37
server/socket-handlers/database-socket-handler.js
Normal file
37
server/socket-handlers/database-socket-handler.js
Normal file
@@ -0,0 +1,37 @@
|
||||
const { checkLogin } = require("../util-server");
|
||||
const Database = require("../database");
|
||||
|
||||
module.exports = (socket) => {
|
||||
|
||||
// Post or edit incident
|
||||
socket.on("getDatabaseSize", async (callback) => {
|
||||
try {
|
||||
checkLogin(socket);
|
||||
callback({
|
||||
ok: true,
|
||||
size: Database.getSize(),
|
||||
});
|
||||
} catch (error) {
|
||||
callback({
|
||||
ok: false,
|
||||
msg: error.message,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
socket.on("shrinkDatabase", async (callback) => {
|
||||
try {
|
||||
checkLogin(socket);
|
||||
Database.shrink();
|
||||
callback({
|
||||
ok: true,
|
||||
});
|
||||
} catch (error) {
|
||||
callback({
|
||||
ok: false,
|
||||
msg: error.message,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
};
|
@@ -1,25 +1,36 @@
|
||||
const { R } = require("redbean-node");
|
||||
const { checkLogin, setSettings } = require("../util-server");
|
||||
const { checkLogin, setSettings, setSetting } = require("../util-server");
|
||||
const dayjs = require("dayjs");
|
||||
const { debug } = require("../../src/util");
|
||||
const ImageDataURI = require("../image-data-uri");
|
||||
const Database = require("../database");
|
||||
const apicache = require("../modules/apicache");
|
||||
const StatusPage = require("../model/status_page");
|
||||
const server = require("../server");
|
||||
|
||||
module.exports.statusPageSocketHandler = (socket) => {
|
||||
|
||||
// Post or edit incident
|
||||
socket.on("postIncident", async (incident, callback) => {
|
||||
socket.on("postIncident", async (slug, incident, callback) => {
|
||||
try {
|
||||
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;
|
||||
|
||||
if (incident.id) {
|
||||
incidentBean = await R.findOne("incident", " id = ?", [
|
||||
incident.id
|
||||
incidentBean = await R.findOne("incident", " id = ? AND status_page_id = ? ", [
|
||||
incident.id,
|
||||
statusPageID
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -31,6 +42,7 @@ module.exports.statusPageSocketHandler = (socket) => {
|
||||
incidentBean.content = incident.content;
|
||||
incidentBean.style = incident.style;
|
||||
incidentBean.pin = true;
|
||||
incidentBean.status_page_id = statusPageID;
|
||||
|
||||
if (incident.id) {
|
||||
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 {
|
||||
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({
|
||||
ok: true,
|
||||
@@ -71,13 +87,23 @@ module.exports.statusPageSocketHandler = (socket) => {
|
||||
|
||||
// Save Status Page
|
||||
// imgDataUrl Only Accept PNG!
|
||||
socket.on("saveStatusPage", async (config, imgDataUrl, publicGroupList, callback) => {
|
||||
socket.on("saveStatusPage", async (slug, config, imgDataUrl, publicGroupList, callback) => {
|
||||
|
||||
try {
|
||||
checkLogin(socket);
|
||||
checkSlug(config.slug);
|
||||
|
||||
checkLogin(socket);
|
||||
apicache.clear();
|
||||
|
||||
// Save Config
|
||||
let statusPage = await R.findOne("status_page", " slug = ? ", [
|
||||
slug
|
||||
]);
|
||||
|
||||
if (!statusPage) {
|
||||
throw new Error("No slug?");
|
||||
}
|
||||
|
||||
const header = "data:image/png;base64,";
|
||||
|
||||
// Check logo format
|
||||
@@ -88,16 +114,28 @@ module.exports.statusPageSocketHandler = (socket) => {
|
||||
throw new Error("Only allowed PNG logo.");
|
||||
}
|
||||
|
||||
const filename = `logo${statusPage.id}.png`;
|
||||
|
||||
// Convert to file
|
||||
await ImageDataURI.outputFile(imgDataUrl, Database.uploadDir + "logo.png");
|
||||
config.logo = "/upload/logo.png?t=" + Date.now();
|
||||
await ImageDataURI.outputFile(imgDataUrl, Database.uploadDir + filename);
|
||||
config.logo = `/upload/${filename}?t=` + Date.now();
|
||||
|
||||
} else {
|
||||
config.icon = imgDataUrl;
|
||||
}
|
||||
|
||||
// Save Config
|
||||
await setSettings("statusPage", config);
|
||||
statusPage.slug = config.slug;
|
||||
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);
|
||||
|
||||
// Save Public Group List
|
||||
const groupIDList = [];
|
||||
@@ -106,13 +144,15 @@ module.exports.statusPageSocketHandler = (socket) => {
|
||||
for (let group of publicGroupList) {
|
||||
let groupBean;
|
||||
if (group.id) {
|
||||
groupBean = await R.findOne("group", " id = ? AND public = 1 ", [
|
||||
group.id
|
||||
groupBean = await R.findOne("group", " id = ? AND public = 1 AND status_page_id = ? ", [
|
||||
group.id,
|
||||
statusPage.id
|
||||
]);
|
||||
} else {
|
||||
groupBean = R.dispense("group");
|
||||
}
|
||||
|
||||
groupBean.status_page_id = statusPage.id;
|
||||
groupBean.name = group.name;
|
||||
groupBean.public = true;
|
||||
groupBean.weight = groupOrder++;
|
||||
@@ -124,7 +164,6 @@ module.exports.statusPageSocketHandler = (socket) => {
|
||||
]);
|
||||
|
||||
let monitorOrder = 1;
|
||||
console.log(group.monitorList);
|
||||
|
||||
for (let monitor of group.monitorList) {
|
||||
let relationBean = R.dispense("monitor_group");
|
||||
@@ -141,7 +180,18 @@ module.exports.statusPageSocketHandler = (socket) => {
|
||||
// Delete groups that not in the list
|
||||
debug("Delete groups that not in the list");
|
||||
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");
|
||||
}
|
||||
|
||||
callback({
|
||||
ok: true,
|
||||
@@ -149,7 +199,7 @@ module.exports.statusPageSocketHandler = (socket) => {
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
console.error(error);
|
||||
|
||||
callback({
|
||||
ok: false,
|
||||
@@ -158,4 +208,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,19 +1,21 @@
|
||||
const tcpp = require("tcp-ping");
|
||||
const Ping = require("./ping-lite");
|
||||
const { R } = require("redbean-node");
|
||||
const { debug } = require("../src/util");
|
||||
const { debug, genSecret } = require("../src/util");
|
||||
const passwordHash = require("./password-hash");
|
||||
const dayjs = require("dayjs");
|
||||
const { Resolver } = require("dns");
|
||||
const child_process = require("child_process");
|
||||
const iconv = require("iconv-lite");
|
||||
const chardet = require("chardet");
|
||||
const fs = require("fs");
|
||||
const nodeJsUtil = require("util");
|
||||
|
||||
// From ping-lite
|
||||
exports.WIN = /^win/.test(process.platform);
|
||||
exports.LIN = /^linux/.test(process.platform);
|
||||
exports.MAC = /^darwin/.test(process.platform);
|
||||
exports.FBSD = /^freebsd/.test(process.platform);
|
||||
exports.BSD = /bsd$/.test(process.platform);
|
||||
|
||||
/**
|
||||
* Init or reset JWT secret
|
||||
@@ -29,7 +31,7 @@ exports.initJWTSecret = async () => {
|
||||
jwtSecretBean.key = "jwtSecret";
|
||||
}
|
||||
|
||||
jwtSecretBean.value = passwordHash.generate(dayjs() + "");
|
||||
jwtSecretBean.value = passwordHash.generate(genSecret());
|
||||
await R.store(jwtSecretBean);
|
||||
return jwtSecretBean;
|
||||
};
|
||||
@@ -199,8 +201,13 @@ const getDaysRemaining = (validFrom, validTo) => {
|
||||
// param: info - the chain obtained from getPeerCertificate()
|
||||
const parseCertificateInfo = function (info) {
|
||||
let link = info;
|
||||
let i = 0;
|
||||
|
||||
const existingList = {};
|
||||
|
||||
while (link) {
|
||||
debug(`[${i}] ${link.fingerprint}`);
|
||||
|
||||
if (!link.valid_from || !link.valid_to) {
|
||||
break;
|
||||
}
|
||||
@@ -208,15 +215,24 @@ const parseCertificateInfo = function (info) {
|
||||
link.validFor = link.subjectaltname?.replace(/DNS:|IP Address:/g, "").split(", ");
|
||||
link.daysRemaining = getDaysRemaining(new Date(), link.validTo);
|
||||
|
||||
existingList[link.fingerprint] = true;
|
||||
|
||||
// Move up the chain until loop is encountered
|
||||
if (link.issuerCertificate == null) {
|
||||
break;
|
||||
} else if (link.fingerprint == link.issuerCertificate.fingerprint) {
|
||||
} else if (link.issuerCertificate.fingerprint in existingList) {
|
||||
debug(`[Last] ${link.issuerCertificate.fingerprint}`);
|
||||
link.issuerCertificate = null;
|
||||
break;
|
||||
} else {
|
||||
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;
|
||||
@@ -226,6 +242,7 @@ exports.checkCertificate = function (res) {
|
||||
const info = res.request.res.socket.getPeerCertificate(true);
|
||||
const valid = res.request.res.socket.authorized || false;
|
||||
|
||||
debug("Parsing Certificate Info");
|
||||
const parsedInfo = parseCertificateInfo(info);
|
||||
|
||||
return {
|
||||
@@ -303,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 () => {
|
||||
console.log("Starting unit test...");
|
||||
const npm = /^win/.test(process.platform) ? "npm.cmd" : "npm";
|
||||
@@ -332,3 +371,24 @@ exports.convertToUTF8 = (body) => {
|
||||
const str = iconv.decode(body, guessEncoding);
|
||||
return str.toString();
|
||||
};
|
||||
|
||||
let logFile;
|
||||
|
||||
try {
|
||||
logFile = fs.createWriteStream("./data/error.log", {
|
||||
flags: "a"
|
||||
});
|
||||
} catch (_) { }
|
||||
|
||||
exports.errorLog = (error, outputToConsole = true) => {
|
||||
try {
|
||||
if (logFile) {
|
||||
const dateTime = R.isoDateTime();
|
||||
logFile.write(`[${dateTime}] ` + nodeJsUtil.format(error) + "\n");
|
||||
|
||||
if (outputToConsole) {
|
||||
console.error(error);
|
||||
}
|
||||
}
|
||||
} catch (_) { }
|
||||
};
|
||||
|
@@ -92,6 +92,10 @@ textarea.form-control {
|
||||
}
|
||||
}
|
||||
|
||||
.btn-dark {
|
||||
background-color: #161B22;
|
||||
}
|
||||
|
||||
@media (max-width: 550px) {
|
||||
.table-shadow-box {
|
||||
padding: 10px !important;
|
||||
@@ -144,6 +148,10 @@ textarea.form-control {
|
||||
background-color: #090c10;
|
||||
color: $dark-font-color;
|
||||
|
||||
mark, .mark {
|
||||
background-color: #b6ad86;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-thumb, ::-webkit-scrollbar-thumb {
|
||||
background: $dark-border-color;
|
||||
}
|
||||
@@ -156,13 +164,24 @@ textarea.form-control {
|
||||
|
||||
.form-check-input {
|
||||
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 {
|
||||
background-color: #232f3b;
|
||||
}
|
||||
|
||||
a,
|
||||
a:not(.btn),
|
||||
.table,
|
||||
.nav-link {
|
||||
color: $dark-font-color;
|
||||
@@ -189,7 +208,7 @@ textarea.form-control {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.table-hover > tbody > tr:hover {
|
||||
.table-hover > tbody > tr:hover > * {
|
||||
--bs-table-accent-bg: #070a10;
|
||||
color: $dark-font-color;
|
||||
}
|
||||
@@ -313,13 +332,24 @@ textarea.form-control {
|
||||
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 {
|
||||
&.scrollbar {
|
||||
min-height: calc(100vh - 240px);
|
||||
max-height: calc(100vh - 30px);
|
||||
overflow-y: auto;
|
||||
position: sticky;
|
||||
top: 10px;
|
||||
height: calc(100% - 65px);
|
||||
}
|
||||
|
||||
.item {
|
||||
@@ -346,6 +376,10 @@ textarea.form-control {
|
||||
&.active {
|
||||
background-color: #cdf8f4;
|
||||
}
|
||||
.tags {
|
||||
// Removes margin to line up tags list with uptime percentage
|
||||
margin-left: -0.25rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -415,6 +449,10 @@ textarea.form-control {
|
||||
border-radius: 10px !important;
|
||||
}
|
||||
|
||||
.spinner {
|
||||
color: $primary;
|
||||
}
|
||||
|
||||
// Localization
|
||||
|
||||
@import "localization.scss";
|
||||
|
@@ -12,6 +12,7 @@ $dark-font-color2: #020b05;
|
||||
$dark-bg: #0d1117;
|
||||
$dark-bg2: #070a10;
|
||||
$dark-border-color: #1d2634;
|
||||
$dark-header-bg: #161b22;
|
||||
|
||||
$easing-in: cubic-bezier(0.54, 0.78, 0.55, 0.97);
|
||||
$easing-out: cubic-bezier(0.25, 0.46, 0.45, 0.94);
|
||||
|
@@ -167,7 +167,7 @@ export default {
|
||||
},
|
||||
|
||||
getBeatTitle(beat) {
|
||||
return `${this.$root.datetime(beat.time)} - ${beat.msg}`;
|
||||
return `${this.$root.datetime(beat.time)}` + ((beat.msg) ? ` - ${beat.msg}` : ``);
|
||||
}
|
||||
},
|
||||
}
|
||||
|
@@ -16,8 +16,8 @@
|
||||
|
||||
<div v-if="tokenRequired">
|
||||
<div class="form-floating mt-3">
|
||||
<input id="floatingToken" v-model="token" type="text" maxlength="6" class="form-control" placeholder="123456">
|
||||
<label for="floatingToken">{{ $t("Token") }}</label>
|
||||
<input id="otp" v-model="token" type="text" maxlength="6" class="form-control" placeholder="123456">
|
||||
<label for="otp">{{ $t("Token") }}</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div class="shadow-box mb-3">
|
||||
<div class="shadow-box mb-3" :style="boxStyle">
|
||||
<div class="list-header">
|
||||
<div class="placeholder"></div>
|
||||
<div class="search-wrapper">
|
||||
@@ -9,7 +9,9 @@
|
||||
<a v-if="searchText != ''" class="search-icon" @click="clearSearchText">
|
||||
<font-awesome-icon icon="times" />
|
||||
</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 class="monitor-list" :class="{ scrollbar: scrollbar }">
|
||||
@@ -63,9 +65,16 @@ export default {
|
||||
data() {
|
||||
return {
|
||||
searchText: "",
|
||||
windowTop: 0,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
boxStyle() {
|
||||
return {
|
||||
height: `calc(100vh - 160px + ${this.windowTop}px)`,
|
||||
};
|
||||
},
|
||||
|
||||
sortedMonitorList() {
|
||||
let result = Object.values(this.$root.monitorList);
|
||||
|
||||
@@ -108,7 +117,20 @@ export default {
|
||||
return result;
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
window.addEventListener("scroll", this.onScroll);
|
||||
},
|
||||
beforeUnmount() {
|
||||
window.removeEventListener("scroll", this.onScroll);
|
||||
},
|
||||
methods: {
|
||||
onScroll() {
|
||||
if (window.top.scrollY <= 133) {
|
||||
this.windowTop = window.top.scrollY;
|
||||
} else {
|
||||
this.windowTop = 133;
|
||||
}
|
||||
},
|
||||
monitorURL(id) {
|
||||
return getMonitorRelativeURL(id);
|
||||
},
|
||||
@@ -122,6 +144,12 @@ export default {
|
||||
<style lang="scss" scoped>
|
||||
@import "../assets/vars.scss";
|
||||
|
||||
.shadow-box {
|
||||
height: calc(100vh - 150px);
|
||||
position: sticky;
|
||||
top: 10px;
|
||||
}
|
||||
|
||||
.small-padding {
|
||||
padding-left: 5px !important;
|
||||
padding-right: 5px !important;
|
||||
@@ -137,11 +165,17 @@ export default {
|
||||
justify-content: space-between;
|
||||
|
||||
.dark & {
|
||||
background-color: #161b22;
|
||||
background-color: $dark-header-bg;
|
||||
border-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.dark {
|
||||
.footer {
|
||||
// background-color: $dark-bg;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 770px) {
|
||||
.list-header {
|
||||
margin: -20px;
|
||||
|
@@ -85,7 +85,9 @@ export default {
|
||||
model: null,
|
||||
processing: false,
|
||||
id: null,
|
||||
notificationTypes: Object.keys(NotificationFormList),
|
||||
notificationTypes: Object.keys(NotificationFormList).sort((a, b) => {
|
||||
return a.toLowerCase().localeCompare(b.toLowerCase());
|
||||
}),
|
||||
notification: {
|
||||
name: "",
|
||||
/** @type { null | keyof NotificationFormList } */
|
||||
@@ -143,12 +145,9 @@ export default {
|
||||
this.id = null;
|
||||
this.notification = {
|
||||
name: "",
|
||||
type: null,
|
||||
type: "telegram",
|
||||
isDefault: false,
|
||||
};
|
||||
|
||||
// Set Default value here
|
||||
this.notification.type = this.notificationTypes[0];
|
||||
}
|
||||
|
||||
this.modal.show();
|
||||
|
@@ -1,16 +1,34 @@
|
||||
<template>
|
||||
<LineChart :chart-data="chartData" :options="chartOptions" />
|
||||
<div>
|
||||
<div class="period-options">
|
||||
<button type="button" class="btn btn-light dropdown-toggle btn-period-toggle" data-bs-toggle="dropdown" aria-expanded="false">
|
||||
{{ chartPeriodOptions[chartPeriodHrs] }}
|
||||
</button>
|
||||
<ul class="dropdown-menu dropdown-menu-end">
|
||||
<li v-for="(item, key) in chartPeriodOptions" :key="key">
|
||||
<a class="dropdown-item" :class="{ active: chartPeriodHrs == key }" href="#" @click="chartPeriodHrs = key">{{ item }}</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="chart-wrapper" :class="{ loading : loading}">
|
||||
<LineChart :chart-data="chartData" :options="chartOptions" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
<script lang="ts">
|
||||
import { BarController, BarElement, Chart, Filler, LinearScale, LineController, LineElement, PointElement, TimeScale, Tooltip } from "chart.js";
|
||||
import dayjs from "dayjs";
|
||||
import utc from "dayjs/plugin/utc";
|
||||
import timezone from "dayjs/plugin/timezone";
|
||||
import "chartjs-adapter-dayjs";
|
||||
import { LineChart } from "vue-chart-3";
|
||||
import { useToast } from "vue-toastification";
|
||||
import { UP, DOWN, PENDING } from "../util.ts";
|
||||
|
||||
dayjs.extend(utc);
|
||||
dayjs.extend(timezone);
|
||||
const toast = useToast();
|
||||
|
||||
Chart.register(LineController, BarController, LineElement, PointElement, TimeScale, BarElement, LinearScale, Tooltip, Filler);
|
||||
|
||||
@@ -24,8 +42,23 @@ export default {
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
|
||||
loading: false,
|
||||
|
||||
// Configurable filtering on top of the returned data
|
||||
chartPeriodHrs: 6,
|
||||
chartPeriodHrs: 0,
|
||||
|
||||
chartPeriodOptions: {
|
||||
0: this.$t("recent"),
|
||||
3: "3h",
|
||||
6: "6h",
|
||||
24: "24h",
|
||||
168: "1w",
|
||||
},
|
||||
|
||||
// A heartbeatList for 3h, 6h, 24h, 1w
|
||||
// Uses the $root.heartbeatList when value is null
|
||||
heartbeatList: null
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
@@ -117,7 +150,7 @@ export default {
|
||||
},
|
||||
callbacks: {
|
||||
label: (context) => {
|
||||
return ` ${new Intl.NumberFormat().format(context.parsed.y)} ms`
|
||||
return ` ${new Intl.NumberFormat().format(context.parsed.y)} ms`;
|
||||
},
|
||||
}
|
||||
},
|
||||
@@ -125,27 +158,36 @@ export default {
|
||||
display: false,
|
||||
},
|
||||
},
|
||||
}
|
||||
};
|
||||
},
|
||||
chartData() {
|
||||
let pingData = []; // Ping Data for Line Chart, y-axis contains ping time
|
||||
let downData = []; // Down Data for Bar Chart, y-axis is 1 if target is down, 0 if target is up
|
||||
if (this.monitorId in this.$root.heartbeatList) {
|
||||
this.$root.heartbeatList[this.monitorId]
|
||||
.filter(
|
||||
(beat) => dayjs.utc(beat.time).tz(this.$root.timezone).isAfter(dayjs().subtract(this.chartPeriodHrs, "hours")))
|
||||
.map((beat) => {
|
||||
const x = this.$root.datetime(beat.time);
|
||||
pingData.push({
|
||||
x,
|
||||
y: beat.ping,
|
||||
});
|
||||
downData.push({
|
||||
x,
|
||||
y: beat.status === 0 ? 1 : 0,
|
||||
})
|
||||
|
||||
let heartbeatList = this.heartbeatList ||
|
||||
(this.monitorId in this.$root.heartbeatList && this.$root.heartbeatList[this.monitorId]) ||
|
||||
[];
|
||||
|
||||
heartbeatList
|
||||
.filter(
|
||||
// Filtering as data gets appended
|
||||
// not the most efficient, but works for now
|
||||
(beat) => dayjs.utc(beat.time).tz(this.$root.timezone).isAfter(
|
||||
dayjs().subtract(Math.max(this.chartPeriodHrs, 6), "hours")
|
||||
)
|
||||
)
|
||||
.map((beat) => {
|
||||
const x = this.$root.datetime(beat.time);
|
||||
pingData.push({
|
||||
x,
|
||||
y: beat.ping,
|
||||
});
|
||||
}
|
||||
downData.push({
|
||||
x,
|
||||
y: beat.status === DOWN ? 1 : 0,
|
||||
});
|
||||
});
|
||||
|
||||
return {
|
||||
datasets: [
|
||||
{
|
||||
@@ -172,5 +214,110 @@ export default {
|
||||
};
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
// Update chart data when the selected chart period changes
|
||||
chartPeriodHrs: function (newPeriod) {
|
||||
if (newPeriod == "0") {
|
||||
newPeriod = null;
|
||||
this.heartbeatList = null;
|
||||
} else {
|
||||
this.loading = true;
|
||||
|
||||
this.$root.getMonitorBeats(this.monitorId, newPeriod, (res) => {
|
||||
if (!res.ok) {
|
||||
toast.error(res.msg);
|
||||
} else {
|
||||
this.heartbeatList = res.data;
|
||||
}
|
||||
this.loading = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
created() {
|
||||
// Setup Watcher on the root heartbeatList,
|
||||
// And mirror latest change to this.heartbeatList
|
||||
this.$watch(() => this.$root.heartbeatList[this.monitorId],
|
||||
(heartbeatList) => {
|
||||
if (this.chartPeriodHrs != 0) {
|
||||
const newBeat = heartbeatList.at(-1);
|
||||
if (newBeat && dayjs.utc(newBeat.time) > dayjs.utc(this.heartbeatList.at(-1)?.time)) {
|
||||
this.heartbeatList.push(heartbeatList.at(-1));
|
||||
}
|
||||
}
|
||||
},
|
||||
{ deep: true }
|
||||
);
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import "../assets/vars.scss";
|
||||
|
||||
.form-select {
|
||||
width: unset;
|
||||
display: inline-flex;
|
||||
}
|
||||
|
||||
.period-options {
|
||||
padding: 0.1em 1em;
|
||||
margin-bottom: -1.2em;
|
||||
float: right;
|
||||
position: relative;
|
||||
z-index: 10;
|
||||
|
||||
.dropdown-menu {
|
||||
padding: 0;
|
||||
min-width: 50px;
|
||||
font-size: 0.9em;
|
||||
|
||||
.dark & {
|
||||
background: $dark-bg;
|
||||
}
|
||||
|
||||
.dropdown-item {
|
||||
border-radius: 0.3rem;
|
||||
padding: 2px 16px 4px 16px;
|
||||
|
||||
.dark & {
|
||||
background: $dark-bg;
|
||||
}
|
||||
|
||||
.dark &:hover {
|
||||
background: $dark-font-color;
|
||||
}
|
||||
}
|
||||
|
||||
.dark & .dropdown-item.active {
|
||||
background: $primary;
|
||||
color: $dark-font-color2;
|
||||
}
|
||||
}
|
||||
|
||||
.btn-period-toggle {
|
||||
padding: 2px 15px;
|
||||
background: transparent;
|
||||
border: 0;
|
||||
color: $link-color;
|
||||
opacity: 0.7;
|
||||
font-size: 0.9em;
|
||||
|
||||
&::after {
|
||||
vertical-align: 0.155em;
|
||||
}
|
||||
|
||||
.dark & {
|
||||
color: $dark-font-color;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.chart-wrapper {
|
||||
margin-bottom: 0.5em;
|
||||
|
||||
&.loading {
|
||||
filter: blur(10px);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
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" />
|
||||
{{ monitor.element.name }}
|
||||
</div>
|
||||
<div v-if="showTags" class="tags">
|
||||
<Tag v-for="tag in monitor.element.tags" :key="tag" :item="tag" :size="'sm'" />
|
||||
</div>
|
||||
</div>
|
||||
<div :key="$root.userHeartbeatBar" class="col-3 col-md-4">
|
||||
<HeartbeatBar size="small" :monitor-id="monitor.element.id" />
|
||||
@@ -59,18 +62,23 @@
|
||||
import Draggable from "vuedraggable";
|
||||
import HeartbeatBar from "./HeartbeatBar.vue";
|
||||
import Uptime from "./Uptime.vue";
|
||||
import Tag from "./Tag.vue";
|
||||
|
||||
export default {
|
||||
components: {
|
||||
Draggable,
|
||||
HeartbeatBar,
|
||||
Uptime,
|
||||
Tag,
|
||||
},
|
||||
props: {
|
||||
editMode: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
},
|
||||
showTags: {
|
||||
type: Boolean,
|
||||
}
|
||||
},
|
||||
data() {
|
||||
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>
|
||||
<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()">
|
||||
{{ $t("Enable 2FA") }}
|
||||
</button>
|
||||
@@ -59,11 +72,11 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { Modal } from "bootstrap"
|
||||
import { Modal } from "bootstrap";
|
||||
import Confirm from "./Confirm.vue";
|
||||
import VueQrcode from "vue-qrcode"
|
||||
import { useToast } from "vue-toastification"
|
||||
const toast = useToast()
|
||||
import VueQrcode from "vue-qrcode";
|
||||
import { useToast } from "vue-toastification";
|
||||
const toast = useToast();
|
||||
|
||||
export default {
|
||||
components: {
|
||||
@@ -73,35 +86,36 @@ export default {
|
||||
props: {},
|
||||
data() {
|
||||
return {
|
||||
currentPassword: "",
|
||||
processing: false,
|
||||
uri: null,
|
||||
tokenValid: false,
|
||||
twoFAStatus: null,
|
||||
token: null,
|
||||
showURI: false,
|
||||
}
|
||||
};
|
||||
},
|
||||
mounted() {
|
||||
this.modal = new Modal(this.$refs.modal)
|
||||
this.modal = new Modal(this.$refs.modal);
|
||||
this.getStatus();
|
||||
},
|
||||
methods: {
|
||||
show() {
|
||||
this.modal.show()
|
||||
this.modal.show();
|
||||
},
|
||||
|
||||
confirmEnableTwoFA() {
|
||||
this.$refs.confirmEnableTwoFA.show()
|
||||
this.$refs.confirmEnableTwoFA.show();
|
||||
},
|
||||
|
||||
confirmDisableTwoFA() {
|
||||
this.$refs.confirmDisableTwoFA.show()
|
||||
this.$refs.confirmDisableTwoFA.show();
|
||||
},
|
||||
|
||||
prepare2FA() {
|
||||
this.processing = true;
|
||||
|
||||
this.$root.getSocket().emit("prepare2FA", (res) => {
|
||||
this.$root.getSocket().emit("prepare2FA", this.currentPassword, (res) => {
|
||||
this.processing = false;
|
||||
|
||||
if (res.ok) {
|
||||
@@ -109,49 +123,51 @@ export default {
|
||||
} else {
|
||||
toast.error(res.msg);
|
||||
}
|
||||
})
|
||||
});
|
||||
},
|
||||
|
||||
save2FA() {
|
||||
this.processing = true;
|
||||
|
||||
this.$root.getSocket().emit("save2FA", (res) => {
|
||||
this.$root.getSocket().emit("save2FA", this.currentPassword, (res) => {
|
||||
this.processing = false;
|
||||
|
||||
if (res.ok) {
|
||||
this.$root.toastRes(res)
|
||||
this.$root.toastRes(res);
|
||||
this.getStatus();
|
||||
this.currentPassword = "";
|
||||
this.modal.hide();
|
||||
} else {
|
||||
toast.error(res.msg);
|
||||
}
|
||||
})
|
||||
});
|
||||
},
|
||||
|
||||
disable2FA() {
|
||||
this.processing = true;
|
||||
|
||||
this.$root.getSocket().emit("disable2FA", (res) => {
|
||||
this.$root.getSocket().emit("disable2FA", this.currentPassword, (res) => {
|
||||
this.processing = false;
|
||||
|
||||
if (res.ok) {
|
||||
this.$root.toastRes(res)
|
||||
this.$root.toastRes(res);
|
||||
this.getStatus();
|
||||
this.currentPassword = "";
|
||||
this.modal.hide();
|
||||
} else {
|
||||
toast.error(res.msg);
|
||||
}
|
||||
})
|
||||
});
|
||||
},
|
||||
|
||||
verifyToken() {
|
||||
this.$root.getSocket().emit("verifyToken", this.token, (res) => {
|
||||
this.$root.getSocket().emit("verifyToken", this.token, this.currentPassword, (res) => {
|
||||
if (res.ok) {
|
||||
this.tokenValid = res.valid;
|
||||
} else {
|
||||
toast.error(res.msg);
|
||||
}
|
||||
})
|
||||
});
|
||||
},
|
||||
|
||||
getStatus() {
|
||||
@@ -161,10 +177,10 @@ export default {
|
||||
} else {
|
||||
toast.error(res.msg);
|
||||
}
|
||||
})
|
||||
});
|
||||
},
|
||||
},
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<span :class="className">{{ uptime }}</span>
|
||||
<span :class="className" :title="24 + $t('-hour')">{{ uptime }}</span>
|
||||
</template>
|
||||
|
||||
<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>
|
||||
<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>
|
||||
|
||||
<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>
|
||||
|
||||
<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:">
|
||||
<a href="https://help.aliyun.com/document_detail/101414.html" target="_blank">https://help.aliyun.com/document_detail/101414.html</a>
|
||||
</i18n-t>
|
||||
|
15
src/components/notifications/Bark.vue
Normal file
15
src/components/notifications/Bark.vue
Normal file
@@ -0,0 +1,15 @@
|
||||
<template>
|
||||
<div class="mb-3">
|
||||
<label for="Bark Endpoint" class="form-label">{{ $t("Bark Endpoint") }}<span style="color: red;"><sup>*</sup></span></label>
|
||||
<input id="Bark Endpoint" v-model="$parent.notification.barkEndpoint" type="text" class="form-control" required>
|
||||
<div class="form-text">
|
||||
<p><span style="color: red;"><sup>*</sup></span>{{ $t("Required") }}</p>
|
||||
</div>
|
||||
<i18n-t tag="div" keypath="wayToGetTeamsURL" class="form-text">
|
||||
<a
|
||||
href="https://github.com/Finb/Bark"
|
||||
target="_blank"
|
||||
>{{ $t("here") }}</a>
|
||||
</i18n-t>
|
||||
</div>
|
||||
</template>
|
38
src/components/notifications/ClickSendSMS.vue
Normal file
38
src/components/notifications/ClickSendSMS.vue
Normal file
@@ -0,0 +1,38 @@
|
||||
<template>
|
||||
<div class="mb-3">
|
||||
<label for="clicksendsms-login" class="form-label">API Username</label>
|
||||
<div class="form-text">
|
||||
{{ $t("apiCredentials") }}
|
||||
<a href="http://dashboard.clicksend.com/account/subaccounts" target="_blank">{{ $t("here") }}</a>
|
||||
</div>
|
||||
<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>
|
||||
<HiddenInput id="clicksendsms-key" v-model="$parent.notification.clicksendsmsPassword" :required="true" autocomplete="one-time-code"></HiddenInput>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<div class="form-text">
|
||||
{{ $t("checkPrice", [$t("clicksendsms")]) }}
|
||||
<a href="https://www.clicksend.com/us/pricing" target="_blank">https://clicksend.com/us/pricing</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="clicksendsms-to-number" class="form-label">Recipient Number</label>
|
||||
<input id="clicksendsms-to-number" v-model="$parent.notification.clicksendsmsToNumber" type="text" minlength="8" maxlength="14" class="form-control" required>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="clicksendsms-sender-name" class="form-label">From Name/Number -
|
||||
<a href="https://help.clicksend.com/article/4kgj7krx00-what-is-a-sender-id-or-sender-number" target="_blank">More Info</a>
|
||||
</label>
|
||||
<input id="clicksendsms-sender-name" v-model="$parent.notification.clicksendsmsSenderName" type="text" minlength="3" maxlength="11" class="form-control">
|
||||
<div class="form-text">Leave blank to use a shared sender number.</div>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import HiddenInput from "../HiddenInput.vue";
|
||||
|
||||
export default {
|
||||
components: {
|
||||
HiddenInput,
|
||||
},
|
||||
};
|
||||
</script>
|
@@ -7,9 +7,9 @@
|
||||
<input id="secretKey" v-model="$parent.notification.secretKey" type="text" class="form-control" required>
|
||||
|
||||
<div class="form-text">
|
||||
<p>For safety, must use secret key</p>
|
||||
<p>{{ $t("For safety, must use secret key") }}</p>
|
||||
<i18n-t tag="p" keypath="Read more:">
|
||||
<a href="https://developers.dingtalk.com/document/robots/custom-robot-access" target="_blank">https://developers.dingtalk.com/document/robots/custom-robot-access</a>
|
||||
<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>
|
||||
</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>
|
@@ -11,7 +11,7 @@
|
||||
<div class="form-text">
|
||||
<span style="color: red;"><sup>*</sup></span>{{ $t("Required") }}
|
||||
<i18n-t tag="p" keypath="aboutWebhooks" style="margin-top: 8px;">
|
||||
<a href="https://docs.rocket.chat/guides/administration/administration/integrations" target="_blank">https://api.slack.com/messaging/webhooks</a>
|
||||
<a href="https://docs.rocket.chat/guides/administration/administration/integrations" target="_blank">https://docs.rocket.chat/guides/administration/administration/integrations</a>
|
||||
</i18n-t>
|
||||
<p style="margin-top: 8px;">
|
||||
{{ $t("aboutChannelName", [$t("rocket.chat")]) }}
|
||||
|
@@ -1,82 +1,117 @@
|
||||
<template>
|
||||
<div class="mb-3">
|
||||
<label for="hostname" class="form-label">{{ $t("Hostname") }}</label>
|
||||
<input id="hostname" v-model="$parent.notification.smtpHost" type="text" class="form-control" required>
|
||||
</div>
|
||||
|
||||
<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 class="mb-3">
|
||||
<label for="hostname" class="form-label">{{ $t("Hostname") }}</label>
|
||||
<input id="hostname" v-model="$parent.notification.smtpHost" type="text" class="form-control" required>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="username" class="form-label">{{ $t("Username") }}</label>
|
||||
<input id="username" v-model="$parent.notification.smtpUsername" type="text" class="form-control" autocomplete="false">
|
||||
</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 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>
|
||||
|
||||
<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="secure" class="form-label">{{ $t("Security") }}</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">
|
||||
<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">
|
||||
<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 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>
|
||||
<div class="mb-3">
|
||||
<label for="username" class="form-label">{{ $t("Username") }}</label>
|
||||
<input id="username" v-model="$parent.notification.smtpUsername" type="text" class="form-control" autocomplete="false">
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="subject-email" class="form-label">{{ $t("emailCustomSubject") }}</label>
|
||||
<input id="subject-email" v-model="$parent.notification.customSubject" type="text" class="form-control" autocomplete="false" placeholder="">
|
||||
<div v-pre class="form-text">
|
||||
(leave blank for default one)<br />
|
||||
{{NAME}}: Service Name<br />
|
||||
{{HOSTNAME_OR_URL}}: Hostname or URL<br />
|
||||
{{URL}}: URL<br />
|
||||
{{STATUS}}: Status<br />
|
||||
<div 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 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>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import HiddenInput from "../HiddenInput.vue";
|
||||
import ToggleSection from "../ToggleSection.vue";
|
||||
|
||||
export default {
|
||||
components: {
|
||||
HiddenInput,
|
||||
ToggleSection,
|
||||
},
|
||||
computed: {
|
||||
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>
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user