mirror of
https://github.com/louislam/uptime-kuma.git
synced 2025-09-12 06:26:59 +08:00
Compare commits
1563 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
16b2cf0e89 | ||
|
c538983b87 | ||
|
0686757160 | ||
|
25ea99a436 | ||
|
c2c3f981bc | ||
|
2c237e9c03 | ||
|
3e85893bdd | ||
|
543a74ecab | ||
|
62ad2f9bb4 | ||
|
7672057319 | ||
|
0f99d49a27 | ||
|
d93f7b33be | ||
|
894aeaea0a | ||
|
97dc8eba13 | ||
|
d39a4770e0 | ||
|
f6ac09b751 | ||
|
9c1ad4f8c6 | ||
|
8595824b5d | ||
|
da34685019 | ||
|
0cf28c2025 | ||
|
ed7bc0e6d1 | ||
|
6a3eccf6a6 | ||
|
97de3959cd | ||
|
63e408f4f2 | ||
|
a3b1123e82 | ||
|
2e54dee817 | ||
|
ceeb47bf82 | ||
|
929d238106 | ||
|
bef9cb6a5f | ||
|
4157c7d546 | ||
|
c3eef28443 | ||
|
443235b20b | ||
|
35810e299d | ||
|
b03624b7e3 | ||
|
dcbd9c12cf | ||
|
83ca74eba7 | ||
|
c6cf600722 | ||
|
22ef8ff751 | ||
|
565e9233fe | ||
|
9a7c2d562a | ||
|
c4cb825fef | ||
|
3193533a60 | ||
|
1f1825dbff | ||
|
e4e47c3976 | ||
|
1c4e97439c | ||
|
d23085cddc | ||
|
f96bad1629 | ||
|
38c45a3fe3 | ||
|
e815e51608 | ||
|
bec3b0d2dc | ||
|
2d5096317f | ||
|
1c3da995e3 | ||
|
db6fdf5e26 | ||
|
fce175cad6 | ||
|
b673cfbe94 | ||
|
0ae8010156 | ||
|
527e479f2d | ||
|
d63022676a | ||
|
08fdbeaa75 | ||
|
0dd858d516 | ||
|
104d521633 | ||
|
839183aa85 | ||
|
e83ff0d679 | ||
|
80c1054877 | ||
|
f503488618 | ||
|
7577477ae8 | ||
|
3ea57600ba | ||
|
b22176d218 | ||
|
7f9e291206 | ||
|
197d44981f | ||
|
97e9bc7705 | ||
|
87b72191e5 | ||
|
8827176390 | ||
|
42969d11ee | ||
|
7b8f9c7655 | ||
|
e90a4f1f34 | ||
|
1e5376d80b | ||
|
dad2ec1164 | ||
|
676e64c77d | ||
|
0244507a07 | ||
|
6601e9bbba | ||
|
9589fcfdef | ||
|
ce3fe9f0a6 | ||
|
995276badc | ||
|
9e62a6ec7d | ||
|
75deab2cc5 | ||
|
50f7b39672 | ||
|
6a802bf68c | ||
|
be8caa0d1e | ||
|
d1aa9cfbcc | ||
|
808efb267f | ||
|
252d6ea9c9 | ||
|
7d12cd0d42 | ||
|
cf10e26aff | ||
|
53135641f3 | ||
|
c0fe2d54f9 | ||
|
d8303f1f4d | ||
|
ee14ab6751 | ||
|
fd2df562b1 | ||
|
87e45b21fa | ||
|
626accedee | ||
|
b890812411 | ||
|
cbc0b9c553 | ||
|
c2472bf750 | ||
|
e0cdc3e7c5 | ||
|
84fad93555 | ||
|
b9b00050dd | ||
|
064fe50e38 | ||
|
a8ea76e8a1 | ||
|
2975204a0a | ||
|
31150642cd | ||
|
3c5c49c16d | ||
|
584d52517a | ||
|
82dd9a7c16 | ||
|
d44663c57c | ||
|
e557545c97 | ||
|
055948d1b9 | ||
|
4ac80cfc02 | ||
|
af89c4d8ae | ||
|
40b9d9ed17 | ||
|
65e6921a41 | ||
|
04fc124928 | ||
|
5c25354682 | ||
|
2aad2510b7 | ||
|
fac2f1cbc6 | ||
|
8bc3651a7d | ||
|
684d0a7eb8 | ||
|
b3712ee1cc | ||
|
af94424283 | ||
|
728e811969 | ||
|
a6007adce3 | ||
|
30b72d81cf | ||
|
de6e1e7ddd | ||
|
2af754b5e8 | ||
|
3b3763351b | ||
|
9a488d6968 | ||
|
aca395cea1 | ||
|
a49faf09b9 | ||
|
d0d1e0de28 | ||
|
70aa8fe453 | ||
|
19d8761305 | ||
|
d6a113396a | ||
|
fb3fe17c28 | ||
|
71d62ee151 | ||
|
82b9bfc5a0 | ||
|
f016caa513 | ||
|
6e29feffd3 | ||
|
2389b604fe | ||
|
a3b1612938 | ||
|
a07f54f35b | ||
|
b777c0c3e4 | ||
|
bea8679788 | ||
|
f091e92c70 | ||
|
a0843745f9 | ||
|
16d6885a88 | ||
|
4d975a5bd5 | ||
|
96ec46765b | ||
|
96971f6776 | ||
|
ffb1a948fe | ||
|
4e4156285a | ||
|
1223b56205 | ||
|
8ced61697a | ||
|
f3322398e5 | ||
|
b76ca59dfe | ||
|
554b0d2bc3 | ||
|
4575f31094 | ||
|
df7f0b078d | ||
|
d8253405b4 | ||
|
56c6c0c6f1 | ||
|
b16cb6a337 | ||
|
694b4cadb3 | ||
|
75f6ff8b58 | ||
|
1062e629c5 | ||
|
13f7db655b | ||
|
60e7824ff0 | ||
|
fb3b407577 | ||
|
d54c652d26 | ||
|
f1bcecb0c6 | ||
|
88afd662db | ||
|
e356d5f623 | ||
|
0d098b0958 | ||
|
239611a016 | ||
|
2ccf1fe41b | ||
|
77340cf0d2 | ||
|
c412c66aeb | ||
|
c4a2ce4e78 | ||
|
a382f811f4 | ||
|
986c03aecd | ||
|
31c388a6e3 | ||
|
9a8b484ee8 | ||
|
17ed051401 | ||
|
8f7b7e74c9 | ||
|
9cd060c6c3 | ||
|
1999541802 | ||
|
65d71e5db0 | ||
|
2073f0c284 | ||
|
25d711e683 | ||
|
77a7801992 | ||
|
525607f49e | ||
|
8613d3ffa9 | ||
|
d44d984a46 | ||
|
d362372b05 | ||
|
3fa5dfc873 | ||
|
6ce012c9a1 | ||
|
f33b6de157 | ||
|
e0a2ed2523 | ||
|
a78cb7ab42 | ||
|
278d9f5689 | ||
|
2ad79a68b9 | ||
|
d29955f3ba | ||
|
c4125a8334 | ||
|
0a368ff553 | ||
|
27dbc021b4 | ||
|
1b120f8a6f | ||
|
6f57c4195a | ||
|
baa592bce3 | ||
|
42e30de209 | ||
|
624678826d | ||
|
e8c3807594 | ||
|
4d8e755400 | ||
|
9b92a02968 | ||
|
6418f99f1a | ||
|
93ac4e1b96 | ||
|
f65bef686c | ||
|
2f26864892 | ||
|
a802f7ebed | ||
|
e5e8db6c38 | ||
|
303738b7c2 | ||
|
dddd2c0042 | ||
|
515095ecfb | ||
|
83284b6d2c | ||
|
1af6d33fcd | ||
|
e36b65c2df | ||
|
8542e6cbb9 | ||
|
5dd197374d | ||
|
f84ae82983 | ||
|
9650418ef7 | ||
|
b546c846ae | ||
|
b5cbc6f5f6 | ||
|
f8def5aa6f | ||
|
6f01a448ad | ||
|
5dd3d32d77 | ||
|
ff8bba6863 | ||
|
ed1f88a852 | ||
|
0ecaa2cbd7 | ||
|
3c3dc05621 | ||
|
1f5466a3e8 | ||
|
d5da5af174 | ||
|
c36d9a4b8b | ||
|
a7063b8aca | ||
|
0a8046c98e | ||
|
7ba717ee55 | ||
|
ea400ac35f | ||
|
15db2c060d | ||
|
89717495dc | ||
|
7b710af12c | ||
|
5b278ca500 | ||
|
f1d24782f8 | ||
|
b97019eea8 | ||
|
d65abe5b8c | ||
|
c4e2d67d17 | ||
|
bcd616a4d0 | ||
|
7533041696 | ||
|
7b0deb5e20 | ||
|
b4a4171178 | ||
|
012be23509 | ||
|
dd09351c8e | ||
|
ffad990ca4 | ||
|
47e82ed83a | ||
|
e1f766756f | ||
|
4b2a465c94 | ||
|
edcdedcaae | ||
|
f7afe121e3 | ||
|
945288f0c0 | ||
|
ac27e6e2af | ||
|
869a040011 | ||
|
42848bcd2e | ||
|
a3b94aa532 | ||
|
fdbdf83a0d | ||
|
81d5360520 | ||
|
8f1e193de3 | ||
|
ac449ec1c2 | ||
|
da91317760 | ||
|
bef0febede | ||
|
7d63b700e1 | ||
|
0223f86a2a | ||
|
c170b1edd0 | ||
|
5943514a92 | ||
|
62acd2edb1 | ||
|
483cbfb636 | ||
|
660005b143 | ||
|
98f3c126e5 | ||
|
6995a29980 | ||
|
cf2ca71dee | ||
|
0bd1c42080 | ||
|
9b21b86e70 | ||
|
f723930d11 | ||
|
e425e408a2 | ||
|
c690d1c3a1 | ||
|
8abbc9fd15 | ||
|
af7c905b44 | ||
|
0e8f6d2f85 | ||
|
d16be6fb7d | ||
|
f25ca96308 | ||
|
6682839ec8 | ||
|
817f6db4fd | ||
|
dc2302244f | ||
|
7a27d3752a | ||
|
f0e8f34aeb | ||
|
54b9698a05 | ||
|
69273a6c41 | ||
|
6424fe77ab | ||
|
fa60672cce | ||
|
6e43ef1dd3 | ||
|
f4f2b8ddb8 | ||
|
436bc13aeb | ||
|
b72a279361 | ||
|
a28ef56553 | ||
|
7f432bd916 | ||
|
f570d41142 | ||
|
1c4ddaeddf | ||
|
d4485fe62f | ||
|
e1681ce370 | ||
|
69d6633e6d | ||
|
55a6e5af42 | ||
|
252709ff49 | ||
|
774fe58ddc | ||
|
5f347b10ba | ||
|
f442507cab | ||
|
a23ab9d1de | ||
|
404923b7c8 | ||
|
a41023ca2a | ||
|
817c941489 | ||
|
5f6347d277 | ||
|
fbfa5a33ed | ||
|
04e22f17a9 | ||
|
aa398948da | ||
|
11243a6ca1 | ||
|
54548e34ed | ||
|
87428231ad | ||
|
a68d945cdc | ||
|
2c0180f323 | ||
|
4fdaa1abb6 | ||
|
6ee7b3696a | ||
|
cc258dce14 | ||
|
fb420fa1b1 | ||
|
a707b51053 | ||
|
a927f5cd15 | ||
|
0e28707307 | ||
|
c94dcf1533 | ||
|
b0476cfb5b | ||
|
2170229031 | ||
|
213aca4fc3 | ||
|
2b42c3c828 | ||
|
d939d03690 | ||
|
07888e43f1 | ||
|
c6c1bb5b5c | ||
|
3210264e28 | ||
|
54e948c2ca | ||
|
80094ec4e1 | ||
|
091158cfe7 | ||
|
abb6ce2366 | ||
|
e4ad8cbfc8 | ||
|
a674caa520 | ||
|
179e3569b5 | ||
|
43527f2f40 | ||
|
26ff6f45a0 | ||
|
c095767f4a | ||
|
ffb7ba176c | ||
|
857e88b27e | ||
|
90fe25e8ad | ||
|
46a593534b | ||
|
7a4b54f4ee | ||
|
ea10d89f51 | ||
|
7f46223d68 | ||
|
df4ce811d9 | ||
|
30858ab038 | ||
|
e25d406fa5 | ||
|
10e16782b1 | ||
|
107a44885c | ||
|
ef9f66fad9 | ||
|
e9e78c26e5 | ||
|
46dae99695 | ||
|
edd9bf3887 | ||
|
ab4edf2092 | ||
|
334cb57fed | ||
|
cfa5b551a5 | ||
|
46ee149b70 | ||
|
0a8c922abf | ||
|
058e5442af | ||
|
ea1725737f | ||
|
5566b038c8 | ||
|
5830f1e0b5 | ||
|
35b8e89457 | ||
|
d892b2c549 | ||
|
f23baf9c22 | ||
|
54184350a4 | ||
|
14dbe7c334 | ||
|
122e6a842b | ||
|
77ef22bdb4 | ||
|
59f983d506 | ||
|
71f031c14e | ||
|
6ae2a48584 | ||
|
7373747906 | ||
|
9d87f8d390 | ||
|
73b965c867 | ||
|
751e5ac477 | ||
|
93e5023ead | ||
|
32cfd411f8 | ||
|
a9f3142cee | ||
|
b7ba6330db | ||
|
4c3aa20eb0 | ||
|
f779c6286a | ||
|
da99a57560 | ||
|
42d68edab0 | ||
|
019d638767 | ||
|
9fc5a3329f | ||
|
23c4ece2a5 | ||
|
175556f9fc | ||
|
398219f847 | ||
|
7a50f0e3f3 | ||
|
4178b003a2 | ||
|
8ede6d888f | ||
|
cec0521834 | ||
|
73b603dd10 | ||
|
92a43e1f30 | ||
|
0cf395dfc3 | ||
|
749ca6f4a8 | ||
|
ef73af391f | ||
|
44f6fca945 | ||
|
23ce7c6623 | ||
|
c346ea7864 | ||
|
f0ad32a252 | ||
|
5720017fb4 | ||
|
b7dc8e3ef8 | ||
|
5bba19f866 | ||
|
e198f2f1ab | ||
|
f91e5b98f9 | ||
|
87f933df4f | ||
|
332b9ab248 | ||
|
7cc89979f0 | ||
|
398ecb7666 | ||
|
668e97c5a9 | ||
|
90473e7924 | ||
|
fdd781b081 | ||
|
373bd9b962 | ||
|
66971deaf4 | ||
|
59be9bb971 | ||
|
8077744c60 | ||
|
c5faf709b8 | ||
|
7da9f139c1 | ||
|
7acbfd2474 | ||
|
9f493bbec7 | ||
|
5bf58cc6c4 | ||
|
d344914ca0 | ||
|
201a25c659 | ||
|
b680371746 | ||
|
e488e2dc0a | ||
|
4e3258579d | ||
|
aa8ea6d398 | ||
|
cd3fbc80b4 | ||
|
bb7d67f717 | ||
|
8b0813ceff | ||
|
91178ce6a5 | ||
|
429ad384d0 | ||
|
24e52726b2 | ||
|
e0a0a5db4c | ||
|
93050208bb | ||
|
98ee9caf2c | ||
|
8e99cbf426 | ||
|
cbfecab850 | ||
|
25cc54bf72 | ||
|
3700b16c5b | ||
|
4b9dc2890d | ||
|
f9004bcbed | ||
|
bc174c3325 | ||
|
4c2753af46 | ||
|
c6ba5b621c | ||
|
96536ae391 | ||
|
ba46544772 | ||
|
5c852db1cf | ||
|
069d3765f0 | ||
|
15820c6937 | ||
|
000cbeb0ce | ||
|
e118d59ac8 | ||
|
39aa0a7f07 | ||
|
a12dffd1bc | ||
|
410805052e | ||
|
02a8147f22 | ||
|
d962ab7a1c | ||
|
63c8d24d6f | ||
|
254a6bfd36 | ||
|
29f3cbe8c6 | ||
|
53b98ad3e4 | ||
|
dbd7c087e0 | ||
|
d0546afe71 | ||
|
272956025c | ||
|
db50ba91cc | ||
|
42ea3fb412 | ||
|
9f8b3151d8 | ||
|
73e38a13d2 | ||
|
f4515ad8c5 | ||
|
369477b4b9 | ||
|
2347a01f7c | ||
|
c114c053d6 | ||
|
ae2c49a729 | ||
|
b9e72b9645 | ||
|
5a069b278d | ||
|
65ea2e6aeb | ||
|
e82fc1df61 | ||
|
7dd5f5ea0d | ||
|
45da7c5431 | ||
|
26230a3d3a | ||
|
82aa52b330 | ||
|
fa7d15cf64 | ||
|
d7f16908d8 | ||
|
bddd5de22b | ||
|
6333231f1b | ||
|
60538036c6 | ||
|
0ba5d031d0 | ||
|
66e4c89897 | ||
|
d210548ae8 | ||
|
023db1450d | ||
|
824c16a07c | ||
|
09fdef9bdc | ||
|
7078b06272 | ||
|
d3bd2976c5 | ||
|
db646aa40b | ||
|
3c01e8732c | ||
|
b50f1bb7e8 | ||
|
a3baa3c149 | ||
|
2adb142ae2 | ||
|
752415dae6 | ||
|
1687de163c | ||
|
245b13d3c8 | ||
|
d6c3fdb6fb | ||
|
372bf57e9f | ||
|
39df4eea92 | ||
|
03e6f0a6c8 | ||
|
dcec53a755 | ||
|
ce17ed163e | ||
|
3019d5dd64 | ||
|
dcdbb7be8b | ||
|
b874ea8b28 | ||
|
1e595eaa76 | ||
|
5fbfacf5ce | ||
|
d39dc94496 | ||
|
94ada36dfa | ||
|
4114f43b48 | ||
|
4c8da89c36 | ||
|
db3ef3805b | ||
|
751924b335 | ||
|
edec1024b5 | ||
|
5f9f29f527 | ||
|
13a3dd91bb | ||
|
d1a3cd047a | ||
|
d9c5a7812c | ||
|
484d4a20ab | ||
|
3582e99770 | ||
|
2197b98444 | ||
|
b641c8a878 | ||
|
9130b3762c | ||
|
587faecf87 | ||
|
46da5e51be | ||
|
1eecdec2d9 | ||
|
e6a1719ab4 | ||
|
7d5e7a577d | ||
|
64a33d7455 | ||
|
09e61d9d63 | ||
|
9996ba1636 | ||
|
c2f6c5b42e | ||
|
0083485d4c | ||
|
630bb03d9c | ||
|
7ed8ae9f7c | ||
|
4ddbf71920 | ||
|
068b920553 | ||
|
f1c83bb838 | ||
|
303a226ab7 | ||
|
c7ec9a07e2 | ||
|
052fde5a24 | ||
|
d6b591a513 | ||
|
3d04befc1f | ||
|
d3f0bdb440 | ||
|
6d22ebedca | ||
|
19933bbd99 | ||
|
60f8ab7285 | ||
|
b7e2489d22 | ||
|
e56ac7b03b | ||
|
aafcbaf098 | ||
|
4d4d04adbd | ||
|
03b2d8d521 | ||
|
f8c9472ea2 | ||
|
4e28ad4ac2 | ||
|
06f326e49e | ||
|
07c0801ad5 | ||
|
8cefc96c78 | ||
|
b326a69838 | ||
|
e103ac8335 | ||
|
a391576285 | ||
|
e0966e55c8 | ||
|
59d9891105 | ||
|
f8f19d8dc5 | ||
|
a3d79a93e9 | ||
|
bdc23a3f57 | ||
|
7c13b1b6cb | ||
|
10f6a3c4f5 | ||
|
cd7c2beca6 | ||
|
8ee99760ec | ||
|
cb55e23718 | ||
|
200fdfb808 | ||
|
29d2d95c71 | ||
|
b782b25e17 | ||
|
919393cac9 | ||
|
18925293fb | ||
|
17d4003e5c | ||
|
cefb5bb60a | ||
|
9bf3b3a0f4 | ||
|
e2c45f93bf | ||
|
addf75daa7 | ||
|
359a490ae3 | ||
|
cd38dd3f68 | ||
|
f712fe85e5 | ||
|
c28b90feb4 | ||
|
ceba096f3e | ||
|
2a248ad73f | ||
|
5fa62a888c | ||
|
e6a8a84278 | ||
|
47c72192e1 | ||
|
d71c086447 | ||
|
46e1a628a7 | ||
|
cd3dfd3146 | ||
|
572f2b9838 | ||
|
8eb83394f7 | ||
|
1bc01d1077 | ||
|
45f44b183d | ||
|
5a209c74e1 | ||
|
4e6ddc8880 | ||
|
07c474db0b | ||
|
8d8c38b1a8 | ||
|
03bcf5c766 | ||
|
136fdf3768 | ||
|
e34420368b | ||
|
60c63cc18e | ||
|
566133e350 | ||
|
30e113755e | ||
|
083e8355b7 | ||
|
64a0e1aa9b | ||
|
b1c7915bc1 | ||
|
6fb66728e6 | ||
|
a680331dd7 | ||
|
b7aebceaab | ||
|
0302fdbc96 | ||
|
84a50f058f | ||
|
9ec652639b | ||
|
0c40e32d75 | ||
|
288ed1e3ca | ||
|
6f99d7577b | ||
|
1417b6eacf | ||
|
1baee42cf5 | ||
|
fb0064082e | ||
|
93c51504f9 | ||
|
fb059f5e91 | ||
|
2e3414135f | ||
|
e44699216e | ||
|
fd8cba1dad | ||
|
03dd02fd38 | ||
|
d0b5f147e2 | ||
|
ddf8a7a692 | ||
|
bd9df09f87 | ||
|
4656ab3d57 | ||
|
0a5db0cecb | ||
|
60b44c2cdd | ||
|
8c8eeaf627 | ||
|
b893d50e45 | ||
|
16b61dba27 | ||
|
2b0c184a88 | ||
|
2642e70fc8 | ||
|
8d0446dc38 | ||
|
3436e26ed4 | ||
|
649f3106e1 | ||
|
6f72ca481f | ||
|
670ea415b2 | ||
|
17dcf6d3a2 | ||
|
e9ce1433cd | ||
|
f5d006add8 | ||
|
361e44ad6a | ||
|
4df147786d | ||
|
27861f0d14 | ||
|
5aa747a301 | ||
|
81de2eedfb | ||
|
4a6d7207ef | ||
|
4053b9db1f | ||
|
772d009f43 | ||
|
ae54d9c011 | ||
|
5ca606fe99 | ||
|
6179f6c982 | ||
|
94770cf865 | ||
|
9ec29c1bc4 | ||
|
279e2eb3f6 | ||
|
1ba92d803e | ||
|
45ca3085b2 | ||
|
0765f05090 | ||
|
2638d68c97 | ||
|
e38742a2d0 | ||
|
a0d1ae2cce | ||
|
1b1e0f6dd9 | ||
|
0961c6d9b3 | ||
|
ce7d8c38c5 | ||
|
f030487f7d | ||
|
316e65d35a | ||
|
df5ba02f3f | ||
|
c9fa183712 | ||
|
0b9b5102ec | ||
|
c399984b7f | ||
|
0afa0be5c2 | ||
|
6a30dbd71a | ||
|
a2d9474e85 | ||
|
8479e772cd | ||
|
2e50ef0e8f | ||
|
4fb2c69dd1 | ||
|
c08910a65c | ||
|
943c904256 | ||
|
25b5edea7f | ||
|
7bbaeffd3e | ||
|
008dc27f52 | ||
|
5027fcd320 | ||
|
d5e68f8453 | ||
|
fcb577097b | ||
|
082c2dd32d | ||
|
e89356b283 | ||
|
6014b9534f | ||
|
8b45a95cc3 | ||
|
02becfd113 | ||
|
8ad992eac8 | ||
|
c4e74c9943 | ||
|
fee88b32e3 | ||
|
ffc5bca51d | ||
|
511b9dd425 | ||
|
e9dd64b6f0 | ||
|
355aec46dc | ||
|
c9deea9fdf | ||
|
70311f7a5a | ||
|
4b99160b1f | ||
|
48d679234a | ||
|
d8b32d652f | ||
|
d3d1656625 | ||
|
8e78e62eee | ||
|
706d6cee07 | ||
|
af44b0beab | ||
|
84a0b24448 | ||
|
43eed45bae | ||
|
19b7e2ba5e | ||
|
99042e6991 | ||
|
f54084c888 | ||
|
87d3853b8e | ||
|
4738581c66 | ||
|
3218a0eee8 | ||
|
87ee3c20bd | ||
|
38e6e846bf | ||
|
92ab2b12d0 | ||
|
a4be651118 | ||
|
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 | ||
|
d8013f31e8 | ||
|
91366ff565 | ||
|
7fd5b61bab | ||
|
96289fe014 | ||
|
381605aca1 | ||
|
742c6bcaa3 | ||
|
88604845e6 | ||
|
be88351eb3 | ||
|
34a0b54b93 | ||
|
e11ea7b061 | ||
|
12237dec6e | ||
|
f6272155af | ||
|
630b441a2d | ||
|
1ecd2e45d0 | ||
|
5922771909 | ||
|
a7e1a78ea9 | ||
|
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 | ||
|
454c1687cf | ||
|
986ddd92ff | ||
|
c75c6c5640 | ||
|
5aed36b470 | ||
|
76b9fb967f | ||
|
b58120d258 | ||
|
79f99ce215 | ||
|
e7e30bf497 | ||
|
efaa55ad1f | ||
|
32a898bee5 | ||
|
561a0a3c9a | ||
|
daac9ddffc | ||
|
6bd2ee8c69 | ||
|
244a7b3671 | ||
|
45dca072b2 | ||
|
7d8b72c6c0 | ||
|
40cc885eb8 | ||
|
742ad083e5 | ||
|
27f4f5ee0b | ||
|
41f1686147 | ||
|
faab1ead92 | ||
|
f1007ad42f | ||
|
dd28ecaa2d | ||
|
ffa585376d | ||
|
c1c1e2ba5b | ||
|
2f7e24191a | ||
|
0fce1b4b9b | ||
|
11c2e86bfe | ||
|
1bbf17f3da | ||
|
39f8b30b36 | ||
|
ffb2c2996b | ||
|
65896ed035 | ||
|
b13b20bd95 | ||
|
8febff9282 | ||
|
90f2497548 | ||
|
a9df7b4a14 | ||
|
28be32fc68 | ||
|
cefe43800f | ||
|
97a5b400db | ||
|
ca89f84b9a | ||
|
eaf370637e | ||
|
ee90d2713f | ||
|
d446a57d42 | ||
|
855b12f435 | ||
|
f390a8caf1 | ||
|
30ce53f57c | ||
|
8c4ab9d652 | ||
|
f931e709e6 | ||
|
11e9eee09d | ||
|
65fc71e485 | ||
|
b69a8b8493 | ||
|
1ac904d6d6 | ||
|
dd3992063e | ||
|
29df70949d | ||
|
0313acd4c5 | ||
|
cd19b9fc49 | ||
|
c57b2c4d28 | ||
|
3dda5938f2 | ||
|
23796723dd | ||
|
51b7a2badb | ||
|
74c584f544 | ||
|
0345719e53 | ||
|
22256dfcd2 | ||
|
c3c4db52ec | ||
|
aba6cb2c52 | ||
|
ff0e85737f | ||
|
4713820da7 | ||
|
a99e87c02c | ||
|
3f8ca82434 | ||
|
60f1eb7b45 | ||
|
55a593f75d | ||
|
a0d51a15cf | ||
|
5a08b42e4f | ||
|
6961af005e | ||
|
847a19afc1 | ||
|
7532e7fd3e | ||
|
63a3704836 | ||
|
3e87eb596f | ||
|
c679613f7e | ||
|
bd8fa17887 | ||
|
d1a99b0a22 | ||
|
4818bb67d6 | ||
|
3b9fac2942 | ||
|
812e80030b | ||
|
b89efa49aa | ||
|
6490ef3787 | ||
|
9619d31a05 | ||
|
c5cc42272f | ||
|
329c8cbc2d | ||
|
b0259b5592 | ||
|
2bf9764cec | ||
|
c116754360 | ||
|
227bbdea2f | ||
|
6272514820 | ||
|
1c8407a433 | ||
|
32ec4beda0 | ||
|
2c7a701c84 | ||
|
9462646ad3 | ||
|
482b3f9233 | ||
|
6014ed1156 | ||
|
bfee63452d | ||
|
fd1fce0143 | ||
|
076d6bdbb6 | ||
|
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 | ||
|
0bbe157099 | ||
|
0053a29d10 | ||
|
2c8d5d28e9 | ||
|
2304c53c8d | ||
|
1bbd744d02 | ||
|
2e0e35a1ee | ||
|
1e92487f30 | ||
|
edd2534a1b | ||
|
f6ef390c76 | ||
|
d4b86dc472 | ||
|
46fa6a56fa | ||
|
ec5037f30d | ||
|
81a194d826 | ||
|
64b3e04d3f | ||
|
4ee829ab25 | ||
|
bcc3cec7d6 | ||
|
f8c5015e3f | ||
|
f00ec4dfef | ||
|
43f8fc701c | ||
|
499042504f | ||
|
faf6719e7c | ||
|
a9d264ccfc | ||
|
df8f93f0c2 | ||
|
8f3ec33591 | ||
|
c5fe3a64c2 | ||
|
2a1456cfd0 | ||
|
69dfc0c0d2 | ||
|
28c0e16a0c | ||
|
6acc9546a0 | ||
|
6d11289257 | ||
|
f455e3a454 | ||
|
7abbf421d0 | ||
|
3625915a85 | ||
|
d74404e106 | ||
|
1c5bce8afa | ||
|
8b5997691e | ||
|
35360e2069 | ||
|
590859a95b | ||
|
f9c0ff1841 | ||
|
a8566acbaa | ||
|
4b07ec23fe | ||
|
0e50b71290 | ||
|
390b50353f | ||
|
d7cb4fa331 | ||
|
e18d4b6ad0 | ||
|
f6fc3737fc | ||
|
4005856ba6 | ||
|
72a59ce7a4 | ||
|
40b70277c7 | ||
|
3d002b3ce9 | ||
|
3d6c52fbea | ||
|
9ee591417d | ||
|
a2bc74c4fd | ||
|
4118de6d53 | ||
|
a48176bd48 | ||
|
7cfc5c64b7 | ||
|
3a12e209da | ||
|
2c2a824f97 | ||
|
931ca6a3ef | ||
|
624cd862a5 | ||
|
d3c90df8a8 | ||
|
0ca68f791f | ||
|
6127eab517 | ||
|
0de7fb69f6 | ||
|
a42932a43e | ||
|
a6072a0e30 | ||
|
475a466c7e | ||
|
5bc68d7f3b | ||
|
000703837b | ||
|
b10cecb362 | ||
|
6d6cb2ad49 | ||
|
cb76801b85 | ||
|
aa92727a61 | ||
|
56dfa05642 | ||
|
8ad6bd31d4 | ||
|
a71569379e | ||
|
8398466860 | ||
|
8050cb8e99 | ||
|
71492aeb3a | ||
|
5ee5ea909d | ||
|
a09b97f778 | ||
|
e0a08e6b5d | ||
|
6f5cbbdf69 | ||
|
34ee342d3e | ||
|
38f8a8ac2f | ||
|
f793aa5264 | ||
|
728485d686 | ||
|
cb3429d3c7 | ||
|
807519d07d | ||
|
0d69b4426e | ||
|
8bb8b0a53c | ||
|
a4841eb8aa | ||
|
2ef2a42e87 | ||
|
9473cd6919 | ||
|
74f18a2b3f | ||
|
f9cd0eb084 | ||
|
6a845bd937 | ||
|
c91f517121 | ||
|
7899707582 | ||
|
12215af2f4 | ||
|
d4bfe57b79 | ||
|
dcc91d6c72 | ||
|
e684712a77 | ||
|
a041a7964a | ||
|
76611ecaca | ||
|
5afc6a41e3 | ||
|
f802154456 | ||
|
9fb461976d | ||
|
c8e364911f | ||
|
88bc08e7b7 | ||
|
03aeab0421 | ||
|
f331f1a63e | ||
|
d645e29455 | ||
|
b4507f9706 | ||
|
fc6d0d1fca | ||
|
b62f1475ee | ||
|
d47d8517a8 | ||
|
19d2db6c8c | ||
|
dcc7856b5d | ||
|
5a8162747c | ||
|
220108ebc6 | ||
|
984a3704e0 | ||
|
909412c87e | ||
|
481fd3a05f | ||
|
5434e2da4f | ||
|
b3d348dcea | ||
|
0aca0455ab | ||
|
8f3ef734bc | ||
|
120eb0d85f | ||
|
4aaed0837e | ||
|
60657132c0 | ||
|
76cbef85d5 | ||
|
e17ef02008 | ||
|
f33d55c92d | ||
|
67849a9e84 | ||
|
ee79a34148 | ||
|
d2f0480889 | ||
|
c36190bba6 | ||
|
4b3fae53d4 | ||
|
4dd60cba3d | ||
|
4bc84d2122 | ||
|
a796f80018 | ||
|
c9b0a81cdc | ||
|
2f97f44086 | ||
|
a13bdaac84 | ||
|
40cb22e671 | ||
|
d95258e7db | ||
|
baae4b5a5e | ||
|
c1b118a0f6 | ||
|
9c5466890e | ||
|
bf8dbd78b3 | ||
|
6cd130de38 | ||
|
a864b72e03 | ||
|
5070927478 | ||
|
bedc1f8617 | ||
|
077f3837d9 | ||
|
aea128a85b | ||
|
c50b2b636a | ||
|
a284703d9e | ||
|
64ec766423 | ||
|
186c11540f | ||
|
4d947d9374 | ||
|
4888c97d86 | ||
|
50593f3edf | ||
|
c1267e9b3b | ||
|
2ca7a5b962 | ||
|
9f0c66d775 | ||
|
a1f9a82537 | ||
|
37e6ca8d77 | ||
|
e3745da986 | ||
|
0b0fd6609d | ||
|
3a32fd6f42 | ||
|
97cb060cf5 | ||
|
5afb29f8f9 | ||
|
f9b8dbf4db | ||
|
92a5f18bf5 | ||
|
dce908a07b | ||
|
4155f84eec | ||
|
94ffeeeab6 | ||
|
3d222ac5f5 | ||
|
c811c1ccde | ||
|
bd3d34400d | ||
|
35da8c78f4 | ||
|
7179c6cc4c | ||
|
ed96757b24 | ||
|
3306f4a8e0 | ||
|
5d3bf68123 | ||
|
a2de9e4e36 | ||
|
1f77526210 | ||
|
88ed965d69 | ||
|
7f4d5a0f76 | ||
|
df813fbdee | ||
|
07742799ed | ||
|
f65cc655c0 | ||
|
1a218aaa17 | ||
|
369cad90c1 | ||
|
f9bb48de13 | ||
|
3f5133d1ba | ||
|
74d2b38cb6 | ||
|
7bba4fe2d0 | ||
|
be3a791e6e | ||
|
9747048890 | ||
|
6f2dcc6dd7 | ||
|
57bed4d672 | ||
|
df36a4bb3c | ||
|
e5913c5abc | ||
|
d5d957b748 | ||
|
5cdb5edeb3 | ||
|
73c18b6ff0 | ||
|
567ea346fe | ||
|
453f6fbadf | ||
|
dd79042128 | ||
|
583e6bf978 | ||
|
b1fca7c1a7 | ||
|
19dd11d624 | ||
|
42ce34b6c7 | ||
|
b7a9d1474f | ||
|
31fa67452e | ||
|
9ef3727c91 | ||
|
ed39485af9 | ||
|
daef238a70 | ||
|
d21f7971b5 | ||
|
bdcdf47e52 | ||
|
4cc433166e | ||
|
28f530394e | ||
|
b0615d347b | ||
|
be19336149 | ||
|
94508cae2f | ||
|
265cca9ed1 | ||
|
267654c987 | ||
|
2c85491ee0 | ||
|
5d836cf05d | ||
|
ba46fb6b1c | ||
|
f55350bebc | ||
|
3721d11259 | ||
|
5df34cd137 | ||
|
bf64095cea | ||
|
2333d1c7a7 | ||
|
95bae8289d | ||
|
45f7c647a6 | ||
|
dff1056bb1 | ||
|
62222c0336 | ||
|
733d0af75f | ||
|
b88e74fad8 | ||
|
734762b773 | ||
|
0275d7a42b | ||
|
41a6d1b701 | ||
|
34d8984e3a | ||
|
c92153c97e | ||
|
ad82ab0305 | ||
|
f952d283c6 | ||
|
e164fabf81 | ||
|
bc69a331ee | ||
|
e4506963d9 | ||
|
222540898b | ||
|
baf3612ece | ||
|
8f44b9f618 | ||
|
210566c7af | ||
|
149015556b | ||
|
2bcbeba384 | ||
|
d5d07da4ee | ||
|
2d802585ff | ||
|
0481a241f3 | ||
|
179ca232bc | ||
|
0dcb7aed21 | ||
|
23736549f9 | ||
|
665c263c03 | ||
|
c5e6628803 | ||
|
3a1d8ddc11 | ||
|
bc5f61b3ec | ||
|
6828e8ef6d | ||
|
670754b697 | ||
|
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 | ||
|
cf548df15f | ||
|
caa2a34177 | ||
|
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,7 +1,7 @@
|
|||||||
/.idea
|
/.idea
|
||||||
/dist
|
|
||||||
/node_modules
|
/node_modules
|
||||||
/data
|
/data
|
||||||
|
/cypress
|
||||||
/out
|
/out
|
||||||
/test
|
/test
|
||||||
/kubernetes
|
/kubernetes
|
||||||
@@ -29,6 +29,8 @@ SECURITY.md
|
|||||||
tsconfig.json
|
tsconfig.json
|
||||||
.env
|
.env
|
||||||
/tmp
|
/tmp
|
||||||
|
/babel.config.js
|
||||||
|
/ecosystem.config.js
|
||||||
|
|
||||||
### .gitignore content (commented rules are duplicated)
|
### .gitignore content (commented rules are duplicated)
|
||||||
|
|
||||||
@@ -43,4 +45,6 @@ dist-ssr
|
|||||||
#!/data/.gitkeep
|
#!/data/.gitkeep
|
||||||
#.vscode
|
#.vscode
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
### End of .gitignore content
|
### End of .gitignore content
|
||||||
|
60
.eslintrc.js
60
.eslintrc.js
@@ -1,4 +1,9 @@
|
|||||||
module.exports = {
|
module.exports = {
|
||||||
|
ignorePatterns: [
|
||||||
|
"test/*",
|
||||||
|
"server/modules/apicache/*",
|
||||||
|
"src/util.js"
|
||||||
|
],
|
||||||
root: true,
|
root: true,
|
||||||
env: {
|
env: {
|
||||||
browser: true,
|
browser: true,
|
||||||
@@ -17,39 +22,48 @@ module.exports = {
|
|||||||
requireConfigFile: false,
|
requireConfigFile: false,
|
||||||
},
|
},
|
||||||
rules: {
|
rules: {
|
||||||
"linebreak-style": ["error", "unix"],
|
"yoda": "error",
|
||||||
"camelcase": ["warn", {
|
eqeqeq: [ "warn", "smart" ],
|
||||||
|
"linebreak-style": [ "error", "unix" ],
|
||||||
|
"camelcase": [ "warn", {
|
||||||
"properties": "never",
|
"properties": "never",
|
||||||
"ignoreImports": true
|
"ignoreImports": true
|
||||||
}],
|
}],
|
||||||
// override/add rules settings here, such as:
|
"no-unused-vars": [ "warn", {
|
||||||
// 'vue/no-unused-vars': 'error'
|
"args": "none"
|
||||||
"no-unused-vars": "warn",
|
}],
|
||||||
indent: [
|
indent: [
|
||||||
"error",
|
"error",
|
||||||
4,
|
4,
|
||||||
{
|
{
|
||||||
ignoredNodes: ["TemplateLiteral"],
|
ignoredNodes: [ "TemplateLiteral" ],
|
||||||
SwitchCase: 1,
|
SwitchCase: 1,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
quotes: ["warn", "double"],
|
quotes: [ "error", "double" ],
|
||||||
semi: "warn",
|
semi: "error",
|
||||||
"vue/html-indent": ["warn", 4], // default: 2
|
"vue/html-indent": [ "error", 4 ], // default: 2
|
||||||
"vue/max-attributes-per-line": "off",
|
"vue/max-attributes-per-line": "off",
|
||||||
"vue/singleline-html-element-content-newline": "off",
|
"vue/singleline-html-element-content-newline": "off",
|
||||||
"vue/html-self-closing": "off",
|
"vue/html-self-closing": "off",
|
||||||
|
"vue/require-component-is": "off", // not allow is="style" https://github.com/vuejs/eslint-plugin-vue/issues/462#issuecomment-430234675
|
||||||
"vue/attribute-hyphenation": "off", // This change noNL to "no-n-l" unexpectedly
|
"vue/attribute-hyphenation": "off", // This change noNL to "no-n-l" unexpectedly
|
||||||
"no-multi-spaces": ["error", {
|
"vue/multi-word-component-names": "off",
|
||||||
|
"no-multi-spaces": [ "error", {
|
||||||
ignoreEOLComments: true,
|
ignoreEOLComments: true,
|
||||||
}],
|
}],
|
||||||
"space-before-function-paren": ["error", {
|
"array-bracket-spacing": [ "warn", "always", {
|
||||||
|
"singleValue": true,
|
||||||
|
"objectsInArrays": false,
|
||||||
|
"arraysInArrays": false
|
||||||
|
}],
|
||||||
|
"space-before-function-paren": [ "error", {
|
||||||
"anonymous": "always",
|
"anonymous": "always",
|
||||||
"named": "never",
|
"named": "never",
|
||||||
"asyncArrow": "always"
|
"asyncArrow": "always"
|
||||||
}],
|
}],
|
||||||
"curly": "error",
|
"curly": "error",
|
||||||
"object-curly-spacing": ["error", "always"],
|
"object-curly-spacing": [ "error", "always" ],
|
||||||
"object-curly-newline": "off",
|
"object-curly-newline": "off",
|
||||||
"object-property-newline": "error",
|
"object-property-newline": "error",
|
||||||
"comma-spacing": "error",
|
"comma-spacing": "error",
|
||||||
@@ -59,37 +73,37 @@ module.exports = {
|
|||||||
"keyword-spacing": "warn",
|
"keyword-spacing": "warn",
|
||||||
"space-infix-ops": "warn",
|
"space-infix-ops": "warn",
|
||||||
"arrow-spacing": "warn",
|
"arrow-spacing": "warn",
|
||||||
"no-trailing-spaces": "warn",
|
"no-trailing-spaces": "error",
|
||||||
"no-constant-condition": ["error", {
|
"no-constant-condition": [ "error", {
|
||||||
"checkLoops": false,
|
"checkLoops": false,
|
||||||
}],
|
}],
|
||||||
"space-before-blocks": "warn",
|
"space-before-blocks": "warn",
|
||||||
//'no-console': 'warn',
|
//'no-console': 'warn',
|
||||||
"no-extra-boolean-cast": "off",
|
"no-extra-boolean-cast": "off",
|
||||||
"no-multiple-empty-lines": ["warn", {
|
"no-multiple-empty-lines": [ "warn", {
|
||||||
"max": 1,
|
"max": 1,
|
||||||
"maxBOF": 0,
|
"maxBOF": 0,
|
||||||
}],
|
}],
|
||||||
"lines-between-class-members": ["warn", "always", {
|
"lines-between-class-members": [ "warn", "always", {
|
||||||
exceptAfterSingleLine: true,
|
exceptAfterSingleLine: true,
|
||||||
}],
|
}],
|
||||||
"no-unneeded-ternary": "error",
|
"no-unneeded-ternary": "error",
|
||||||
"array-bracket-newline": ["error", "consistent"],
|
"array-bracket-newline": [ "error", "consistent" ],
|
||||||
"eol-last": ["error", "always"],
|
"eol-last": [ "error", "always" ],
|
||||||
//'prefer-template': 'error',
|
//'prefer-template': 'error',
|
||||||
"comma-dangle": ["warn", "only-multiline"],
|
"comma-dangle": [ "warn", "only-multiline" ],
|
||||||
"no-empty": ["error", {
|
"no-empty": [ "error", {
|
||||||
"allowEmptyCatch": true
|
"allowEmptyCatch": true
|
||||||
}],
|
}],
|
||||||
"no-control-regex": "off",
|
"no-control-regex": "off",
|
||||||
"one-var": ["error", "never"],
|
"one-var": [ "error", "never" ],
|
||||||
"max-statements-per-line": ["error", { "max": 1 }]
|
"max-statements-per-line": [ "error", { "max": 1 }]
|
||||||
},
|
},
|
||||||
"overrides": [
|
"overrides": [
|
||||||
{
|
{
|
||||||
"files": [ "src/languages/*.js", "src/icon.js" ],
|
"files": [ "src/languages/*.js", "src/icon.js" ],
|
||||||
"rules": {
|
"rules": {
|
||||||
"comma-dangle": ["error", "always-multiline"],
|
"comma-dangle": [ "error", "always-multiline" ],
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
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: "..."
|
35
.github/PULL_REQUEST_TEMPLATE.md
vendored
Normal file
35
.github/PULL_REQUEST_TEMPLATE.md
vendored
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
⚠️⚠️⚠️ Since we do not accept all types of pull requests and do not want to waste your time. Please be sure that you have read pull request rules:
|
||||||
|
https://github.com/louislam/uptime-kuma/blob/master/CONTRIBUTING.md#can-i-create-a-pull-request-for-uptime-kuma
|
||||||
|
|
||||||
|
Tick the checkbox if you understand [x]:
|
||||||
|
- [ ] I have read and understand the pull request rules.
|
||||||
|
|
||||||
|
# Description
|
||||||
|
|
||||||
|
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
|
||||||
|
(including JSDoc for methods)
|
||||||
|
- [ ] 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.
|
45
.github/workflows/auto-test.yml
vendored
45
.github/workflows/auto-test.yml
vendored
@@ -11,25 +11,58 @@ on:
|
|||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
auto-test:
|
auto-test:
|
||||||
|
needs: [ check-linters ]
|
||||||
runs-on: ${{ matrix.os }}
|
runs-on: ${{ matrix.os }}
|
||||||
|
timeout-minutes: 15
|
||||||
|
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
os: [macos-latest, ubuntu-latest, windows-latest]
|
os: [macos-latest, ubuntu-latest, windows-latest]
|
||||||
node-version: [14.x, 16.x]
|
node: [ 14, 16, 17, 18 ]
|
||||||
# See supported Node.js release schedule at https://nodejs.org/en/about/releases/
|
# See supported Node.js release schedule at https://nodejs.org/en/about/releases/
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- run: git config --global core.autocrlf false # Mainly for Windows
|
||||||
|
- uses: actions/checkout@v3
|
||||||
|
|
||||||
- name: Use Node.js ${{ matrix.node-version }}
|
- name: Use Node.js ${{ matrix.node }}
|
||||||
uses: actions/setup-node@v2
|
uses: actions/setup-node@v3
|
||||||
with:
|
with:
|
||||||
node-version: ${{ matrix.node-version }}
|
node-version: ${{ matrix.node }}
|
||||||
cache: 'npm'
|
cache: 'npm'
|
||||||
- run: npm run install-legacy
|
- run: npm install
|
||||||
- run: npm run build
|
- run: npm run build
|
||||||
- run: npm test
|
- run: npm test
|
||||||
env:
|
env:
|
||||||
HEADLESS_TEST: 1
|
HEADLESS_TEST: 1
|
||||||
JUST_FOR_TEST: ${{ secrets.JUST_FOR_TEST }}
|
JUST_FOR_TEST: ${{ secrets.JUST_FOR_TEST }}
|
||||||
|
check-linters:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- run: git config --global core.autocrlf false # Mainly for Windows
|
||||||
|
- uses: actions/checkout@v3
|
||||||
|
|
||||||
|
- name: Use Node.js 14
|
||||||
|
uses: actions/setup-node@v3
|
||||||
|
with:
|
||||||
|
node-version: 14
|
||||||
|
cache: 'npm'
|
||||||
|
- run: npm install
|
||||||
|
- run: npm run lint
|
||||||
|
|
||||||
|
e2e-tests:
|
||||||
|
needs: [ check-linters ]
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- run: git config --global core.autocrlf false # Mainly for Windows
|
||||||
|
- uses: actions/checkout@v3
|
||||||
|
|
||||||
|
- name: Use Node.js 14
|
||||||
|
uses: actions/setup-node@v3
|
||||||
|
with:
|
||||||
|
node-version: 14
|
||||||
|
cache: 'npm'
|
||||||
|
- run: npm install
|
||||||
|
- run: npm run build
|
||||||
|
- run: npm run cy:test
|
||||||
|
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 }}
|
24
.github/workflows/stale-bot.yml
vendored
Normal file
24
.github/workflows/stale-bot.yml
vendored
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
name: 'Automatically close stale issues and PRs'
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
schedule:
|
||||||
|
- cron: '0 */6 * * *'
|
||||||
|
#Run every 6 hours
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
stale:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/stale@v5
|
||||||
|
with:
|
||||||
|
stale-issue-message: 'We are clearing up our old issues and your ticket has been open for 3 months with no activity. Remove stale label or comment or this will be closed in 2 days.'
|
||||||
|
stale-pr-message: 'We are clearing up our old Pull Requests and yours has been open for 3 months with no activity. Remove stale label or comment or this will be closed in 2 days.'
|
||||||
|
close-issue-message: 'This issue was closed because it has been stalled for 2 days with no activity.'
|
||||||
|
close-pr-message: 'This PR was closed because it has been stalled for 2 days with no activity.'
|
||||||
|
days-before-stale: 90
|
||||||
|
days-before-close: 2
|
||||||
|
exempt-issue-labels: 'News,Medium,High,discussion,bug,doc,feature-request'
|
||||||
|
exempt-pr-labels: 'awaiting-approval,work-in-progress,enhancement,feature-request'
|
||||||
|
exempt-issue-assignees: 'louislam'
|
||||||
|
exempt-pr-assignees: 'louislam'
|
||||||
|
operations-per-run: 200
|
3
.gitignore
vendored
3
.gitignore
vendored
@@ -13,3 +13,6 @@ dist-ssr
|
|||||||
/out
|
/out
|
||||||
/tmp
|
/tmp
|
||||||
.env
|
.env
|
||||||
|
|
||||||
|
cypress/videos
|
||||||
|
cypress/screenshots
|
||||||
|
@@ -1,9 +1,14 @@
|
|||||||
{
|
{
|
||||||
"extends": "stylelint-config-standard",
|
"extends": "stylelint-config-standard",
|
||||||
|
"customSyntax": "postcss-html",
|
||||||
"rules": {
|
"rules": {
|
||||||
"indentation": 4,
|
"indentation": 4,
|
||||||
"no-descending-specificity": null,
|
"no-descending-specificity": null,
|
||||||
"selector-list-comma-newline-after": null,
|
"selector-list-comma-newline-after": null,
|
||||||
"declaration-empty-line-before": null
|
"declaration-empty-line-before": null,
|
||||||
|
"alpha-value-notation": "number",
|
||||||
|
"color-function-notation": "legacy",
|
||||||
|
"shorthand-property-no-redundant-values": null,
|
||||||
|
"color-hex-length": null,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
181
CONTRIBUTING.md
181
CONTRIBUTING.md
@@ -1,8 +1,8 @@
|
|||||||
# Project Info
|
# Project Info
|
||||||
|
|
||||||
First of all, thank you everyone who made pull requests for Uptime Kuma, I never thought GitHub Community can be that nice! And also because of this, I also never thought other people actually read my code and edit my code. It is not structed and commented so well, lol. Sorry about that.
|
First of all, thank you everyone who made pull requests for Uptime Kuma, I never thought GitHub Community can be that nice! And also because of this, I also never thought other people actually read my code and edit my code. It is not structured and commented so well, lol. Sorry about that.
|
||||||
|
|
||||||
The project was created with vite.js (vue3). Then I created a sub-directory called "server" for server part. Both frontend and backend share the same package.json.
|
The project was created with vite.js (vue3). Then I created a subdirectory called "server" for server part. Both frontend and backend share the same package.json.
|
||||||
|
|
||||||
The frontend code build into "dist" directory. The server (express.js) exposes the "dist" directory as root of the endpoint. This is how production is working.
|
The frontend code build into "dist" directory. The server (express.js) exposes the "dist" directory as root of the endpoint. This is how production is working.
|
||||||
|
|
||||||
@@ -27,37 +27,48 @@ The frontend code build into "dist" directory. The server (express.js) exposes t
|
|||||||
|
|
||||||
## Can I create a pull request for Uptime Kuma?
|
## Can I create a pull request for Uptime Kuma?
|
||||||
|
|
||||||
Generally, if the pull request is working fine and it do not affect any existing logic, workflow and perfomance, I will merge into the master branch once it is tested.
|
Yes or no, it depends on what you will try to do. Since I don't want to waste your time, be sure to **create an empty draft pull request or open an issue, so we can discuss first**. Especially for a large pull request or you don't know it will be merged or not.
|
||||||
|
|
||||||
If you are not sure, feel free to create an empty pull request draft first.
|
Here are some references:
|
||||||
|
|
||||||
### Pull Request Examples
|
✅ Usually Accept:
|
||||||
|
- Bug/Security fix
|
||||||
#### ✅ High - Medium Priority
|
|
||||||
|
|
||||||
- Add a new notification
|
|
||||||
- Add a chart
|
|
||||||
- Fix a bug
|
|
||||||
- Translations
|
- Translations
|
||||||
|
- Adding notification providers
|
||||||
|
|
||||||
#### *️⃣ Requires one more reviewer
|
⚠️ Discussion First
|
||||||
|
- Large pull requests
|
||||||
I do not have such knowledge to test it.
|
- New features
|
||||||
|
|
||||||
- Add k8s supports
|
|
||||||
|
|
||||||
#### *️⃣ Low Priority
|
|
||||||
|
|
||||||
It changed my current workflow and require further studies.
|
|
||||||
|
|
||||||
- Change my release approach
|
|
||||||
|
|
||||||
#### ❌ Won't Merge
|
|
||||||
|
|
||||||
|
❌ Won't Merge
|
||||||
|
- Do not pass auto test
|
||||||
|
- Any breaking changes
|
||||||
- Duplicated pull request
|
- Duplicated pull request
|
||||||
- Buggy
|
- Buggy
|
||||||
- Existing logic is completely modified or deleted
|
- UI/UX is not close to Uptime Kuma
|
||||||
|
- Existing logic is completely modified or deleted for no reason
|
||||||
- A function that is completely out of scope
|
- A function that is completely out of scope
|
||||||
|
- Unnecessary large code changes (Hard to review, causes code conflicts to other pull requests)
|
||||||
|
|
||||||
|
I will mark your pull request in the [milestones](https://github.com/louislam/uptime-kuma/milestones), if I am plan to review and merge it.
|
||||||
|
|
||||||
|
Also, please don't rush or ask for ETA, because I have to understand the pull request, make sure it is no breaking changes and stick to my vision of this project, especially for large pull requests.
|
||||||
|
|
||||||
|
|
||||||
|
### Recommended Pull Request Guideline
|
||||||
|
|
||||||
|
Before deep into coding, discussion first is preferred. Creating an empty pull request for discussion would be recommended.
|
||||||
|
|
||||||
|
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"
|
||||||
|
1. Discussion
|
||||||
|
|
||||||
## Project Styles
|
## Project Styles
|
||||||
|
|
||||||
@@ -65,27 +76,30 @@ I personally do not like something need to learn so much and need to config so m
|
|||||||
|
|
||||||
- Easy to install for non-Docker users, no native build dependency is needed (at least for x86_64), no extra config, no extra effort to get it run
|
- Easy to install for non-Docker users, no native build dependency is needed (at least for x86_64), no extra config, no extra effort to get it run
|
||||||
- Single container for Docker users, no very complex docker-compose file. Just map the volume and expose the port, then good to go
|
- Single container for Docker users, no very complex docker-compose file. Just map the volume and expose the port, then good to go
|
||||||
- Settings should be configurable in the frontend. Env var is not encouraged.
|
- Settings should be configurable in the frontend. Environment variable is not encouraged, unless it is related to startup such as `DATA_DIR`.
|
||||||
- Easy to use
|
- Easy to use
|
||||||
|
- The web UI styling should be consistent and nice.
|
||||||
|
|
||||||
## Coding Styles
|
## Coding Styles
|
||||||
|
|
||||||
- 4 spaces indentation
|
- 4 spaces indentation
|
||||||
- Follow `.editorconfig`
|
- Follow `.editorconfig`
|
||||||
- Follow ESLint
|
- Follow ESLint
|
||||||
|
- Methods and functions should be documented with JSDoc
|
||||||
|
|
||||||
## Name convention
|
## Name convention
|
||||||
|
|
||||||
- Javascript/Typescript: camelCaseType
|
- Javascript/Typescript: camelCaseType
|
||||||
- SQLite: underscore_type
|
- SQLite: snake_case (Underscore)
|
||||||
- CSS/SCSS: dash-type
|
- CSS/SCSS: kebab-case (Dash)
|
||||||
|
|
||||||
## Tools
|
## Tools
|
||||||
|
|
||||||
- Node.js >= 14
|
- Node.js >= 14
|
||||||
|
- NPM >= 8.5
|
||||||
- Git
|
- Git
|
||||||
- IDE that supports ESLint and EditorConfig (I am using Intellji Idea)
|
- IDE that supports ESLint and EditorConfig (I am using IntelliJ IDEA)
|
||||||
- A SQLite tool (SQLite Expert Personal is suggested)
|
- A SQLite GUI tool (SQLite Expert Personal is suggested)
|
||||||
|
|
||||||
## Install dependencies
|
## Install dependencies
|
||||||
|
|
||||||
@@ -93,39 +107,45 @@ I personally do not like something need to learn so much and need to config so m
|
|||||||
npm ci
|
npm ci
|
||||||
```
|
```
|
||||||
|
|
||||||
## How to start the Backend Dev Server
|
## Dev Server
|
||||||
|
|
||||||
(2021-09-23 Update)
|
(2022-04-26 Update)
|
||||||
|
|
||||||
|
We can start the frontend dev server and the backend dev server in one command.
|
||||||
|
|
||||||
|
Port `3000` and port `3001` will be used.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
npm run start-server-dev
|
npm run dev
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Backend Server
|
||||||
|
|
||||||
It binds to `0.0.0.0:3001` by default.
|
It binds to `0.0.0.0:3001` by default.
|
||||||
|
|
||||||
### Backend Details
|
|
||||||
|
|
||||||
It is mainly a socket.io app + express.js.
|
It is mainly a socket.io app + express.js.
|
||||||
|
|
||||||
express.js is just used for serving the frontend built files (index.html, .js and .css etc.)
|
express.js is used for:
|
||||||
|
- entry point such as redirecting to a status page or the dashboard
|
||||||
|
- serving the frontend built files (index.html, .js and .css etc.)
|
||||||
|
- serving internal APIs of status page
|
||||||
|
|
||||||
|
|
||||||
|
### Structure in /server/
|
||||||
|
|
||||||
- model/ (Object model, auto mapping to the database table name)
|
- model/ (Object model, auto mapping to the database table name)
|
||||||
- modules/ (Modified 3rd-party modules)
|
- modules/ (Modified 3rd-party modules)
|
||||||
- notification-providers/ (indivdual notification logic)
|
- notification-providers/ (individual notification logic)
|
||||||
- routers/ (Express Routers)
|
- routers/ (Express Routers)
|
||||||
- scoket-handler (Socket.io Handlers)
|
- socket-handler (Socket.io Handlers)
|
||||||
- server.js (Server main logic)
|
- server.js (Server entry point and main logic)
|
||||||
|
|
||||||
## How to start the Frontend Dev Server
|
## Frontend Dev Server
|
||||||
|
|
||||||
1. Set the env var `NODE_ENV` to "development".
|
It binds to `0.0.0.0:3000` by default. Frontend dev server is used for development only.
|
||||||
2. Start the frontend dev server by the following command.
|
|
||||||
|
|
||||||
```bash
|
For production, it is not used. It will be compiled to `dist` directory instead.
|
||||||
npm run dev
|
|
||||||
```
|
|
||||||
|
|
||||||
It binds to `0.0.0.0:3000` by default.
|
|
||||||
|
|
||||||
You can use Vue.js devtools Chrome extension for debugging.
|
You can use Vue.js devtools Chrome extension for debugging.
|
||||||
|
|
||||||
@@ -161,7 +181,18 @@ npm test
|
|||||||
|
|
||||||
By default, the Chromium window will be shown up during the test. Specifying `HEADLESS_TEST=1` for terminal environments.
|
By default, the Chromium window will be shown up during the test. Specifying `HEADLESS_TEST=1` for terminal environments.
|
||||||
|
|
||||||
## Update Dependencies
|
## Dependencies
|
||||||
|
|
||||||
|
Both frontend and backend share the same package.json. However, the frontend dependencies are eventually not used in the production environment, because it is usually also baked into dist files. So:
|
||||||
|
|
||||||
|
- Frontend dependencies = "devDependencies"
|
||||||
|
- Examples: vue, chart.js
|
||||||
|
- Backend dependencies = "dependencies"
|
||||||
|
- Examples: socket.io, sqlite3
|
||||||
|
- Development dependencies = "devDependencies"
|
||||||
|
- Examples: eslint, sass
|
||||||
|
|
||||||
|
### Update Dependencies
|
||||||
|
|
||||||
Install `ncu`
|
Install `ncu`
|
||||||
https://github.com/raineorshine/npm-check-updates
|
https://github.com/raineorshine/npm-check-updates
|
||||||
@@ -171,10 +202,64 @@ ncu -u -t patch
|
|||||||
npm install
|
npm install
|
||||||
```
|
```
|
||||||
|
|
||||||
Since previously updating vite 2.5.10 to 2.6.0 broke the application completely, from now on, it should update patch release version only.
|
Since previously updating Vite 2.5.10 to 2.6.0 broke the application completely, from now on, it should update patch release version only.
|
||||||
|
|
||||||
Patch release = the third digit ([Semantic Versioning](https://semver.org/))
|
Patch release = the third digit ([Semantic Versioning](https://semver.org/))
|
||||||
|
|
||||||
## Translations
|
## Translations
|
||||||
|
|
||||||
Please read: https://github.com/louislam/uptime-kuma/tree/master/src/languages
|
Please read: https://github.com/louislam/uptime-kuma/tree/master/src/languages
|
||||||
|
|
||||||
|
## Wiki
|
||||||
|
|
||||||
|
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
|
||||||
|
```
|
||||||
|
84
README.md
84
README.md
@@ -1,6 +1,7 @@
|
|||||||
# Uptime Kuma
|
# 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%">
|
<div align="center" width="100%">
|
||||||
<img src="./public/icon.svg" width="128" alt="" />
|
<img src="./public/icon.svg" width="128" alt="" />
|
||||||
@@ -16,35 +17,42 @@ Try it!
|
|||||||
|
|
||||||
https://demo.uptime.kuma.pet
|
https://demo.uptime.kuma.pet
|
||||||
|
|
||||||
It is a temporary live demo, all data will be deleted after 10 minutes. The server is located at Tokyo, so if you live far from there it may affect your experience. I suggest that you should install and try it out for the best demo experience.
|
It is a temporary live demo, all data will be deleted after 10 minutes. The server is located in Tokyo, so if you live far from there, it may affect your experience. I suggest that you should install and try it out for the best demo experience.
|
||||||
|
|
||||||
VPS is sponsored by Uptime Kuma sponsors on [Open Collective](https://opencollective.com/uptime-kuma)! Thank you so much!
|
VPS is sponsored by Uptime Kuma sponsors on [Open Collective](https://opencollective.com/uptime-kuma)! Thank you so much!
|
||||||
|
|
||||||
## ⭐ Features
|
## ⭐ Features
|
||||||
|
|
||||||
* Monitoring uptime for HTTP(s) / TCP / Ping / DNS Record / Push.
|
* Monitoring uptime for HTTP(s) / TCP / HTTP(s) Keyword / Ping / DNS Record / Push / Steam Game Server / Docker Containers.
|
||||||
* Fancy, Reactive, Fast UI/UX.
|
* Fancy, Reactive, Fast UI/UX.
|
||||||
* Notifications via Telegram, Discord, Gotify, Slack, Pushover, Email (SMTP), and [70+ notification services, click here for the full list](https://github.com/louislam/uptime-kuma/tree/master/src/components/notifications).
|
* Notifications via Telegram, Discord, Gotify, Slack, Pushover, Email (SMTP), and [90+ notification services, click here for the full list](https://github.com/louislam/uptime-kuma/tree/master/src/components/notifications).
|
||||||
* 20 second intervals.
|
* 20 second intervals.
|
||||||
* [Multi Languages](https://github.com/louislam/uptime-kuma/tree/master/src/languages)
|
* [Multi Languages](https://github.com/louislam/uptime-kuma/tree/master/src/languages)
|
||||||
* Simple Status Page
|
* Multiple Status Pages
|
||||||
|
* Map Status Page to Domain
|
||||||
* Ping Chart
|
* Ping Chart
|
||||||
* Certificate Info
|
* Certificate Info
|
||||||
|
* Proxy Support
|
||||||
|
* 2FA available
|
||||||
|
|
||||||
## 🔧 How to Install
|
## 🔧 How to Install
|
||||||
|
|
||||||
### 🐳 Docker
|
### 🐳 Docker
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
docker volume create uptime-kuma
|
|
||||||
docker run -d --restart=always -p 3001:3001 -v uptime-kuma:/app/data --name uptime-kuma louislam/uptime-kuma:1
|
docker run -d --restart=always -p 3001:3001 -v uptime-kuma:/app/data --name uptime-kuma louislam/uptime-kuma:1
|
||||||
```
|
```
|
||||||
|
|
||||||
|
⚠️ Please use a **local volume** only. Other types such as NFS are not supported.
|
||||||
|
|
||||||
Browse to http://localhost:3001 after starting.
|
Browse to http://localhost:3001 after starting.
|
||||||
|
|
||||||
### 💪🏻 Without Docker
|
### 💪🏻 Non-Docker
|
||||||
|
|
||||||
Required Tools: Node.js >= 14, git and pm2.
|
Required Tools:
|
||||||
|
- [Node.js](https://nodejs.org/en/download/) >= 14
|
||||||
|
- [Git](https://git-scm.com/downloads)
|
||||||
|
- [pm2](https://pm2.keymetrics.io/) - For run in background
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Update your npm to the latest version
|
# Update your npm to the latest version
|
||||||
@@ -58,15 +66,29 @@ npm run setup
|
|||||||
node server/server.js
|
node server/server.js
|
||||||
|
|
||||||
# (Recommended) Option 2. Run in background using PM2
|
# (Recommended) Option 2. Run in background using PM2
|
||||||
# Install PM2 if you don't have it: npm install pm2 -g
|
# Install PM2 if you don't have it:
|
||||||
pm2 start server/server.js --name uptime-kuma
|
npm install pm2 -g && pm2 install pm2-logrotate
|
||||||
```
|
|
||||||
|
|
||||||
|
# Start Server
|
||||||
|
pm2 start server/server.js --name uptime-kuma
|
||||||
|
|
||||||
|
|
||||||
|
```
|
||||||
Browse to http://localhost:3001 after starting.
|
Browse to http://localhost:3001 after starting.
|
||||||
|
|
||||||
|
More useful PM2 Commands
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# If you want to see the current console output
|
||||||
|
pm2 monit
|
||||||
|
|
||||||
|
# If you want to add it to startup
|
||||||
|
pm2 save && pm2 startup
|
||||||
|
```
|
||||||
|
|
||||||
### Advanced Installation
|
### Advanced Installation
|
||||||
|
|
||||||
If you need more options or need to browse via a reserve proxy, please read:
|
If you need more options or need to browse via a reverse proxy, please read:
|
||||||
|
|
||||||
https://github.com/louislam/uptime-kuma/wiki/%F0%9F%94%A7-How-to-Install
|
https://github.com/louislam/uptime-kuma/wiki/%F0%9F%94%A7-How-to-Install
|
||||||
|
|
||||||
@@ -84,7 +106,13 @@ https://github.com/louislam/uptime-kuma/milestones
|
|||||||
|
|
||||||
Project Plan:
|
Project Plan:
|
||||||
|
|
||||||
https://github.com/louislam/uptime-kuma/projects/1
|
https://github.com/users/louislam/projects/4/views/1
|
||||||
|
|
||||||
|
## ❤️ Sponsors
|
||||||
|
|
||||||
|
Thank you so much! (GitHub Sponsors will be updated manually. OpenCollective sponsors will be updated automatically, the list will be cached by GitHub though. It may need some time to be updated)
|
||||||
|
|
||||||
|
<img src="https://uptime.kuma.pet/sponsors?v=6" alt />
|
||||||
|
|
||||||
## 🖼 More Screenshots
|
## 🖼 More Screenshots
|
||||||
|
|
||||||
@@ -106,7 +134,7 @@ Telegram Notification Sample:
|
|||||||
|
|
||||||
## Motivation
|
## Motivation
|
||||||
|
|
||||||
* I was looking for a self-hosted monitoring tool like "Uptime Robot", but it is hard to find a suitable one. One of the close ones is statping. Unfortunately, it is not stable and unmaintained.
|
* I was looking for a self-hosted monitoring tool like "Uptime Robot", but it is hard to find a suitable one. One of the close ones is statping. Unfortunately, it is not stable and no longer maintained.
|
||||||
* Want to build a fancy UI.
|
* Want to build a fancy UI.
|
||||||
* Learn Vue 3 and vite.js.
|
* Learn Vue 3 and vite.js.
|
||||||
* Show the power of Bootstrap 5.
|
* Show the power of Bootstrap 5.
|
||||||
@@ -119,20 +147,34 @@ If you love this project, please consider giving me a ⭐.
|
|||||||
|
|
||||||
### Issues Page
|
### Issues Page
|
||||||
|
|
||||||
You can discuss or ask for help in [Issues](https://github.com/louislam/uptime-kuma/issues).
|
You can discuss or ask for help in [issues](https://github.com/louislam/uptime-kuma/issues).
|
||||||
|
|
||||||
### Subreddit
|
### Subreddit
|
||||||
|
|
||||||
My Reddit account: louislamlam
|
My Reddit account: [u/louislamlam](https://reddit.com/u/louislamlam).
|
||||||
You can mention me if you ask a question on Reddit.
|
You can mention me if you ask a question on Reddit.
|
||||||
https://www.reddit.com/r/UptimeKuma/
|
[r/Uptime kuma](https://www.reddit.com/r/UptimeKuma/)
|
||||||
|
|
||||||
## Contribute
|
## Contribute
|
||||||
|
|
||||||
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).
|
### Test Pull Requests
|
||||||
|
|
||||||
If you want to translate Uptime Kuma into your langauge, please read: https://github.com/louislam/uptime-kuma/tree/master/src/languages
|
There are a lot of pull requests right now, but I don't have time to test them all.
|
||||||
|
|
||||||
If you want to modify Uptime Kuma, this guideline may be useful for you: https://github.com/louislam/uptime-kuma/blob/master/CONTRIBUTING.md
|
If you want to help, you can check this:
|
||||||
|
https://github.com/louislam/uptime-kuma/wiki/Test-Pull-Requests
|
||||||
|
|
||||||
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.
|
### Test Beta Version
|
||||||
|
|
||||||
|
Check out the latest beta release here: https://github.com/louislam/uptime-kuma/releases
|
||||||
|
|
||||||
|
### Bug Reports / Feature Requests
|
||||||
|
If you want to report a bug or request a new feature, feel free to open a [new issue](https://github.com/louislam/uptime-kuma/issues).
|
||||||
|
|
||||||
|
### Translations
|
||||||
|
If you want to translate Uptime Kuma into your language, please read: https://github.com/louislam/uptime-kuma/tree/master/src/languages
|
||||||
|
|
||||||
|
Feel free to correct my grammar in this README, source code, or wiki, as my mother language is not English and my grammar is not that great.
|
||||||
|
|
||||||
|
### Create Pull Requests
|
||||||
|
If you want to modify Uptime Kuma, please read this guide and follow the rules here: https://github.com/louislam/uptime-kuma/blob/master/CONTRIBUTING.md
|
||||||
|
24
SECURITY.md
24
SECURITY.md
@@ -1,16 +1,16 @@
|
|||||||
# Security Policy
|
# Security Policy
|
||||||
|
|
||||||
|
## Reporting a Vulnerability
|
||||||
|
|
||||||
|
Please report security issues to uptime@kuma.pet.
|
||||||
|
|
||||||
|
Do not use the issue tracker or discuss it in the public as it will cause more damage.
|
||||||
|
|
||||||
## Supported Versions
|
## Supported Versions
|
||||||
|
|
||||||
Use this section to tell people about which versions of your project are
|
|
||||||
currently being supported with security updates.
|
|
||||||
|
|
||||||
### Uptime Kuma Versions
|
### Uptime Kuma Versions
|
||||||
|
|
||||||
| Version | Supported |
|
You should use or upgrade to the latest version of Uptime Kuma. All `1.X.X` versions are upgradable to the lastest version.
|
||||||
| ------- | ------------------ |
|
|
||||||
| 1.8.X | :white_check_mark: |
|
|
||||||
| <= 1.7.X | ❌ |
|
|
||||||
|
|
||||||
### Upgradable Docker Tags
|
### Upgradable Docker Tags
|
||||||
|
|
||||||
@@ -18,14 +18,8 @@ currently being supported with security updates.
|
|||||||
| ------- | ------------------ |
|
| ------- | ------------------ |
|
||||||
| 1 | :white_check_mark: |
|
| 1 | :white_check_mark: |
|
||||||
| 1-debian | :white_check_mark: |
|
| 1-debian | :white_check_mark: |
|
||||||
| 1-alpine | :white_check_mark: |
|
|
||||||
| latest | :white_check_mark: |
|
| latest | :white_check_mark: |
|
||||||
| debian | :white_check_mark: |
|
| debian | :white_check_mark: |
|
||||||
| alpine | :white_check_mark: |
|
| 1-alpine | ⚠️ Deprecated |
|
||||||
|
| alpine | ⚠️ Deprecated |
|
||||||
| All other tags | ❌ |
|
| All other tags | ❌ |
|
||||||
|
|
||||||
## 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.
|
|
||||||
|
@@ -1,11 +1,11 @@
|
|||||||
const config = {};
|
const config = {};
|
||||||
|
|
||||||
if (process.env.TEST_FRONTEND) {
|
if (process.env.TEST_FRONTEND) {
|
||||||
config.presets = ["@babel/preset-env"];
|
config.presets = [ "@babel/preset-env" ];
|
||||||
}
|
}
|
||||||
|
|
||||||
if (process.env.TEST_BACKEND) {
|
if (process.env.TEST_BACKEND) {
|
||||||
config.plugins = ["babel-plugin-rewire"];
|
config.plugins = [ "babel-plugin-rewire" ];
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = config;
|
module.exports = config;
|
||||||
|
33
config/jest-debug-env.js
Normal file
33
config/jest-debug-env.js
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
const PuppeteerEnvironment = require("jest-environment-puppeteer");
|
||||||
|
const util = require("util");
|
||||||
|
|
||||||
|
class DebugEnv extends PuppeteerEnvironment {
|
||||||
|
async handleTestEvent(event, state) {
|
||||||
|
const ignoredEvents = [
|
||||||
|
"setup",
|
||||||
|
"add_hook",
|
||||||
|
"start_describe_definition",
|
||||||
|
"add_test",
|
||||||
|
"finish_describe_definition",
|
||||||
|
"run_start",
|
||||||
|
"run_describe_start",
|
||||||
|
"test_start",
|
||||||
|
"hook_start",
|
||||||
|
"hook_success",
|
||||||
|
"test_fn_start",
|
||||||
|
"test_fn_success",
|
||||||
|
"test_done",
|
||||||
|
"run_describe_finish",
|
||||||
|
"run_finish",
|
||||||
|
"teardown",
|
||||||
|
"test_fn_failure",
|
||||||
|
];
|
||||||
|
if (!ignoredEvents.includes(event.name)) {
|
||||||
|
console.log(
|
||||||
|
new Date().toString() + ` Unhandled event [${event.name}] ` + util.inspect(event)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = DebugEnv;
|
@@ -1,6 +1,20 @@
|
|||||||
module.exports = {
|
module.exports = {
|
||||||
"launch": {
|
"launch": {
|
||||||
|
"dumpio": true,
|
||||||
|
"slowMo": 500,
|
||||||
"headless": process.env.HEADLESS_TEST || false,
|
"headless": process.env.HEADLESS_TEST || false,
|
||||||
"userDataDir": "./data/test-chrome-profile",
|
"userDataDir": "./data/test-chrome-profile",
|
||||||
|
args: [
|
||||||
|
"--disable-setuid-sandbox",
|
||||||
|
"--disable-gpu",
|
||||||
|
"--disable-dev-shm-usage",
|
||||||
|
"--no-default-browser-check",
|
||||||
|
"--no-experiments",
|
||||||
|
"--no-first-run",
|
||||||
|
"--no-pings",
|
||||||
|
"--no-sandbox",
|
||||||
|
"--no-zygote",
|
||||||
|
"--single-process",
|
||||||
|
],
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@@ -5,6 +5,7 @@ module.exports = {
|
|||||||
"__DEV__": true
|
"__DEV__": true
|
||||||
},
|
},
|
||||||
"testRegex": "./test/e2e.spec.js",
|
"testRegex": "./test/e2e.spec.js",
|
||||||
|
"testEnvironment": "./config/jest-debug-env.js",
|
||||||
"rootDir": "..",
|
"rootDir": "..",
|
||||||
"testTimeout": 30000,
|
"testTimeout": 30000,
|
||||||
};
|
};
|
||||||
|
@@ -1,24 +1,53 @@
|
|||||||
import legacy from "@vitejs/plugin-legacy";
|
import legacy from "@vitejs/plugin-legacy";
|
||||||
import vue from "@vitejs/plugin-vue";
|
import vue from "@vitejs/plugin-vue";
|
||||||
import { defineConfig } from "vite";
|
import { defineConfig } from "vite";
|
||||||
|
import visualizer from "rollup-plugin-visualizer";
|
||||||
|
import viteCompression from "vite-plugin-compression";
|
||||||
|
|
||||||
const postCssScss = require("postcss-scss");
|
const postCssScss = require("postcss-scss");
|
||||||
const postcssRTLCSS = require("postcss-rtlcss");
|
const postcssRTLCSS = require("postcss-rtlcss");
|
||||||
|
|
||||||
|
const viteCompressionFilter = /\.(js|mjs|json|css|html|svg)$/i;
|
||||||
|
|
||||||
// https://vitejs.dev/config/
|
// https://vitejs.dev/config/
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
|
server: {
|
||||||
|
port: 3000,
|
||||||
|
},
|
||||||
|
define: {
|
||||||
|
"FRONTEND_VERSION": JSON.stringify(process.env.npm_package_version),
|
||||||
|
},
|
||||||
plugins: [
|
plugins: [
|
||||||
vue(),
|
vue(),
|
||||||
legacy({
|
legacy({
|
||||||
targets: ["ie > 11"],
|
targets: [ "since 2015" ],
|
||||||
additionalLegacyPolyfills: ["regenerator-runtime/runtime"]
|
}),
|
||||||
})
|
visualizer({
|
||||||
|
filename: "tmp/dist-stats.html"
|
||||||
|
}),
|
||||||
|
viteCompression({
|
||||||
|
algorithm: "gzip",
|
||||||
|
filter: viteCompressionFilter,
|
||||||
|
}),
|
||||||
|
viteCompression({
|
||||||
|
algorithm: "brotliCompress",
|
||||||
|
filter: viteCompressionFilter,
|
||||||
|
}),
|
||||||
],
|
],
|
||||||
css: {
|
css: {
|
||||||
postcss: {
|
postcss: {
|
||||||
"parser": postCssScss,
|
"parser": postCssScss,
|
||||||
"map": false,
|
"map": false,
|
||||||
"plugins": [postcssRTLCSS]
|
"plugins": [ postcssRTLCSS ]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
build: {
|
||||||
|
rollupOptions: {
|
||||||
|
output: {
|
||||||
|
manualChunks(id, { getModuleInfo, getModuleIds }) {
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
15
cypress.config.ts
Normal file
15
cypress.config.ts
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
import { defineConfig } from "cypress";
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
e2e: {
|
||||||
|
baseUrl: "http://localhost:3002",
|
||||||
|
defaultCommandTimeout: 10000,
|
||||||
|
pageLoadTimeout: 60000,
|
||||||
|
viewportWidth: 1920,
|
||||||
|
viewportHeight: 1080,
|
||||||
|
specPattern: ["cypress/e2e/setup.cy.ts", "cypress/e2e/**/*.ts"],
|
||||||
|
},
|
||||||
|
env: {
|
||||||
|
baseUrl: "http://localhost:3002",
|
||||||
|
},
|
||||||
|
});
|
24
cypress/e2e/setup.cy.ts
Normal file
24
cypress/e2e/setup.cy.ts
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
import { actor } from "../support/actors/actor";
|
||||||
|
import { DEFAULT_USER_DATA } from "../support/const/user-data";
|
||||||
|
import { DashboardPage } from "../support/pages/dasboard-page";
|
||||||
|
import { SetupPage } from "../support/pages/setup-page";
|
||||||
|
|
||||||
|
describe("user can create a new account on setup page", () => {
|
||||||
|
before(() => {
|
||||||
|
cy.visit("/setup");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("user can create new account", () => {
|
||||||
|
cy.url().should("be.equal", SetupPage.url);
|
||||||
|
actor.setupTask.fillAndSubmitSetupForm(
|
||||||
|
DEFAULT_USER_DATA.username,
|
||||||
|
DEFAULT_USER_DATA.password,
|
||||||
|
DEFAULT_USER_DATA.password
|
||||||
|
);
|
||||||
|
|
||||||
|
cy.url().should("be.equal", DashboardPage.url);
|
||||||
|
cy.get('[role="alert"]')
|
||||||
|
.should("be.visible")
|
||||||
|
.and("contain.text", "Added Successfully.");
|
||||||
|
});
|
||||||
|
});
|
0
cypress/plugins/index.js
Normal file
0
cypress/plugins/index.js
Normal file
8
cypress/support/actors/actor.ts
Normal file
8
cypress/support/actors/actor.ts
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
import { SetupTask } from "../tasks/setup-task";
|
||||||
|
|
||||||
|
class Actor {
|
||||||
|
setupTask: SetupTask = new SetupTask();
|
||||||
|
}
|
||||||
|
|
||||||
|
const actor = new Actor();
|
||||||
|
export { actor };
|
0
cypress/support/commands.ts
Normal file
0
cypress/support/commands.ts
Normal file
4
cypress/support/const/user-data.ts
Normal file
4
cypress/support/const/user-data.ts
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
export const DEFAULT_USER_DATA = {
|
||||||
|
username: "testuser",
|
||||||
|
password: "testuser123",
|
||||||
|
};
|
1
cypress/support/e2e.ts
Normal file
1
cypress/support/e2e.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
import "./commands";
|
3
cypress/support/pages/dasboard-page.ts
Normal file
3
cypress/support/pages/dasboard-page.ts
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
export const DashboardPage = {
|
||||||
|
url: Cypress.env("baseUrl") + "/dashboard",
|
||||||
|
};
|
7
cypress/support/pages/setup-page.ts
Normal file
7
cypress/support/pages/setup-page.ts
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
export const SetupPage = {
|
||||||
|
url: Cypress.env("baseUrl") + "/setup",
|
||||||
|
usernameInput: '[data-cy="username-input"]',
|
||||||
|
passWordInput: '[data-cy="password-input"]',
|
||||||
|
passwordRepeatInput: '[data-cy="password-repeat-input"]',
|
||||||
|
submitSetupForm: '[data-cy="submit-setup-form"]',
|
||||||
|
};
|
15
cypress/support/tasks/setup-task.ts
Normal file
15
cypress/support/tasks/setup-task.ts
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
import { SetupPage } from "../pages/setup-page";
|
||||||
|
|
||||||
|
export class SetupTask {
|
||||||
|
fillAndSubmitSetupForm(
|
||||||
|
username: string,
|
||||||
|
password: string,
|
||||||
|
passwordRepeat: string
|
||||||
|
) {
|
||||||
|
cy.get(SetupPage.usernameInput).type(username);
|
||||||
|
cy.get(SetupPage.passWordInput).type(password);
|
||||||
|
cy.get(SetupPage.passwordRepeatInput).type(passwordRepeat);
|
||||||
|
|
||||||
|
cy.get(SetupPage.submitSetupForm).click();
|
||||||
|
}
|
||||||
|
}
|
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;
|
5
db/patch-add-clickable-status-page-link.sql
Normal file
5
db/patch-add-clickable-status-page-link.sql
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
-- You should not modify if this have pushed to Github, unless it does serious wrong with the db.
|
||||||
|
BEGIN TRANSACTION;
|
||||||
|
ALTER TABLE monitor_group
|
||||||
|
ADD send_url BOOLEAN DEFAULT 0 NOT NULL;
|
||||||
|
COMMIT;
|
18
db/patch-add-docker-columns.sql
Normal file
18
db/patch-add-docker-columns.sql
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
-- You should not modify if this have pushed to Github, unless it does serious wrong with the db.
|
||||||
|
BEGIN TRANSACTION;
|
||||||
|
|
||||||
|
CREATE TABLE docker_host (
|
||||||
|
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
|
||||||
|
user_id INT NOT NULL,
|
||||||
|
docker_daemon VARCHAR(255),
|
||||||
|
docker_type VARCHAR(255),
|
||||||
|
name VARCHAR(255)
|
||||||
|
);
|
||||||
|
|
||||||
|
ALTER TABLE monitor
|
||||||
|
ADD docker_host INTEGER REFERENCES docker_host(id);
|
||||||
|
|
||||||
|
ALTER TABLE monitor
|
||||||
|
ADD docker_container VARCHAR(255);
|
||||||
|
|
||||||
|
COMMIT;
|
18
db/patch-add-other-auth.sql
Normal file
18
db/patch-add-other-auth.sql
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
BEGIN TRANSACTION;
|
||||||
|
|
||||||
|
ALTER TABLE monitor
|
||||||
|
ADD auth_method VARCHAR(250);
|
||||||
|
|
||||||
|
ALTER TABLE monitor
|
||||||
|
ADD auth_domain TEXT;
|
||||||
|
ALTER TABLE monitor
|
||||||
|
|
||||||
|
ADD auth_workstation TEXT;
|
||||||
|
|
||||||
|
COMMIT;
|
||||||
|
|
||||||
|
BEGIN TRANSACTION;
|
||||||
|
UPDATE monitor
|
||||||
|
SET auth_method = 'basic'
|
||||||
|
WHERE basic_auth_user is not null;
|
||||||
|
COMMIT;
|
18
db/patch-add-radius-monitor.sql
Normal file
18
db/patch-add-radius-monitor.sql
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
BEGIN TRANSACTION;
|
||||||
|
|
||||||
|
ALTER TABLE monitor
|
||||||
|
ADD radius_username VARCHAR(255);
|
||||||
|
|
||||||
|
ALTER TABLE monitor
|
||||||
|
ADD radius_password VARCHAR(255);
|
||||||
|
|
||||||
|
ALTER TABLE monitor
|
||||||
|
ADD radius_calling_station_id VARCHAR(50);
|
||||||
|
|
||||||
|
ALTER TABLE monitor
|
||||||
|
ADD radius_called_station_id VARCHAR(50);
|
||||||
|
|
||||||
|
ALTER TABLE monitor
|
||||||
|
ADD radius_secret VARCHAR(255);
|
||||||
|
|
||||||
|
COMMIT
|
10
db/patch-add-sqlserver-monitor.sql
Normal file
10
db/patch-add-sqlserver-monitor.sql
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
BEGIN TRANSACTION;
|
||||||
|
|
||||||
|
ALTER TABLE monitor
|
||||||
|
ADD database_connection_string VARCHAR(2000);
|
||||||
|
|
||||||
|
ALTER TABLE monitor
|
||||||
|
ADD database_query TEXT;
|
||||||
|
|
||||||
|
|
||||||
|
COMMIT
|
16
db/patch-added-mqtt-monitor.sql
Normal file
16
db/patch-added-mqtt-monitor.sql
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
-- You should not modify if this have pushed to Github, unless it does serious wrong with the db.
|
||||||
|
BEGIN TRANSACTION;
|
||||||
|
|
||||||
|
ALTER TABLE monitor
|
||||||
|
ADD mqtt_topic TEXT;
|
||||||
|
|
||||||
|
ALTER TABLE monitor
|
||||||
|
ADD mqtt_success_message VARCHAR(255);
|
||||||
|
|
||||||
|
ALTER TABLE monitor
|
||||||
|
ADD mqtt_username VARCHAR(255);
|
||||||
|
|
||||||
|
ALTER TABLE monitor
|
||||||
|
ADD mqtt_password VARCHAR(255);
|
||||||
|
|
||||||
|
COMMIT;
|
10
db/patch-monitor-add-resend-interval.sql
Normal file
10
db/patch-monitor-add-resend-interval.sql
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
-- You should not modify if this have pushed to Github, unless it does serious wrong with the db.
|
||||||
|
BEGIN TRANSACTION;
|
||||||
|
|
||||||
|
ALTER TABLE monitor
|
||||||
|
ADD resend_interval INTEGER default 0 not null;
|
||||||
|
|
||||||
|
ALTER TABLE heartbeat
|
||||||
|
ADD down_count INTEGER default 0 not null;
|
||||||
|
|
||||||
|
COMMIT;
|
10
db/patch-monitor-basic-auth.sql
Normal file
10
db/patch-monitor-basic-auth.sql
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
-- You should not modify if this have pushed to Github, unless it does serious wrong with the db.
|
||||||
|
BEGIN TRANSACTION;
|
||||||
|
|
||||||
|
ALTER TABLE monitor
|
||||||
|
ADD basic_auth_user TEXT default null;
|
||||||
|
|
||||||
|
ALTER TABLE monitor
|
||||||
|
ADD basic_auth_pass TEXT default null;
|
||||||
|
|
||||||
|
COMMIT;
|
7
db/patch-monitor-expiry-notification.sql
Normal file
7
db/patch-monitor-expiry-notification.sql
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
-- You should not modify if this have pushed to Github, unless it does serious wrong with the db.
|
||||||
|
BEGIN TRANSACTION;
|
||||||
|
|
||||||
|
ALTER TABLE monitor
|
||||||
|
ADD expiry_notification BOOLEAN default 1;
|
||||||
|
|
||||||
|
COMMIT;
|
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;
|
6
db/patch-status-page-footer-css.sql
Normal file
6
db/patch-status-page-footer-css.sql
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
-- You should not modify if this have pushed to Github, unless it does serious wrong with the db.
|
||||||
|
BEGIN TRANSACTION;
|
||||||
|
ALTER TABLE status_page ADD footer_text TEXT;
|
||||||
|
ALTER TABLE status_page ADD custom_css TEXT;
|
||||||
|
ALTER TABLE status_page ADD show_powered_by BOOLEAN NOT NULL DEFAULT 1;
|
||||||
|
COMMIT;
|
31
db/patch-status-page.sql
Normal file
31
db/patch-status-page.sql
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
-- You should not modify if this have pushed to Github, unless it does serious wrong with the db.
|
||||||
|
BEGIN TRANSACTION;
|
||||||
|
|
||||||
|
CREATE TABLE [status_page](
|
||||||
|
[id] INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
|
||||||
|
[slug] VARCHAR(255) NOT NULL UNIQUE,
|
||||||
|
[title] VARCHAR(255) NOT NULL,
|
||||||
|
[description] TEXT,
|
||||||
|
[icon] VARCHAR(255) NOT NULL,
|
||||||
|
[theme] VARCHAR(30) NOT NULL,
|
||||||
|
[published] BOOLEAN NOT NULL DEFAULT 1,
|
||||||
|
[search_engine_index] BOOLEAN NOT NULL DEFAULT 1,
|
||||||
|
[show_tags] BOOLEAN NOT NULL DEFAULT 0,
|
||||||
|
[password] VARCHAR,
|
||||||
|
[created_date] DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
[modified_date] DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE UNIQUE INDEX [slug] ON [status_page]([slug]);
|
||||||
|
|
||||||
|
|
||||||
|
CREATE TABLE [status_page_cname](
|
||||||
|
[id] INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
|
||||||
|
[status_page_id] INTEGER NOT NULL REFERENCES [status_page]([id]) ON DELETE CASCADE ON UPDATE CASCADE,
|
||||||
|
[domain] VARCHAR NOT NULL UNIQUE
|
||||||
|
);
|
||||||
|
|
||||||
|
ALTER TABLE incident ADD status_page_id INTEGER;
|
||||||
|
ALTER TABLE [group] ADD status_page_id INTEGER;
|
||||||
|
|
||||||
|
COMMIT;
|
@@ -1,8 +1,8 @@
|
|||||||
# DON'T UPDATE TO alpine3.13, 1.14, see #41.
|
# DON'T UPDATE TO alpine3.13, 1.14, see #41.
|
||||||
FROM node:14-alpine3.12
|
FROM node:16-alpine3.12
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
# Install apprise, iputils for non-root ping, setpriv
|
# Install apprise, iputils for non-root ping, setpriv
|
||||||
RUN apk add --no-cache iputils setpriv dumb-init python3 py3-cryptography py3-pip py3-six py3-yaml py3-click py3-markdown py3-requests py3-requests-oauthlib && \
|
RUN apk add --no-cache iputils setpriv dumb-init python3 py3-cryptography py3-pip py3-six py3-yaml py3-click py3-markdown py3-requests py3-requests-oauthlib && \
|
||||||
pip3 --no-cache-dir install apprise && \
|
pip3 --no-cache-dir install apprise==1.0.0 && \
|
||||||
rm -rf /root/.cache
|
rm -rf /root/.cache
|
||||||
|
@@ -1,12 +1,28 @@
|
|||||||
# DON'T UPDATE TO node:14-bullseye-slim, see #372.
|
# DON'T UPDATE TO node:14-bullseye-slim, see #372.
|
||||||
# If the image changed, the second stage image should be changed too
|
# If the image changed, the second stage image should be changed too
|
||||||
FROM node:14-buster-slim
|
FROM node:16-buster-slim
|
||||||
|
ARG TARGETPLATFORM
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Install Curl
|
||||||
# Install Apprise, add sqlite3 cli for debugging in the future, iputils-ping for ping, util-linux for setpriv
|
# Install Apprise, add sqlite3 cli for debugging in the future, iputils-ping for ping, util-linux for setpriv
|
||||||
# Stupid python3 and python3-pip actually install a lot of useless things into Debian, specific --no-install-recommends to skip them, make the base even smaller than alpine!
|
# Stupid python3 and python3-pip actually install a lot of useless things into Debian, specify --no-install-recommends to skip them, make the base even smaller than alpine!
|
||||||
RUN apt update && \
|
RUN apt update && \
|
||||||
apt --yes --no-install-recommends install python3 python3-pip python3-cryptography python3-six python3-yaml python3-click python3-markdown python3-requests python3-requests-oauthlib \
|
apt --yes --no-install-recommends install python3 python3-pip python3-cryptography python3-six python3-yaml python3-click python3-markdown python3-requests python3-requests-oauthlib \
|
||||||
sqlite3 iputils-ping util-linux dumb-init && \
|
sqlite3 iputils-ping util-linux dumb-init && \
|
||||||
pip3 --no-cache-dir install apprise && \
|
pip3 --no-cache-dir install apprise==1.0.0 && \
|
||||||
rm -rf /var/lib/apt/lists/*
|
rm -rf /var/lib/apt/lists/* && \
|
||||||
|
apt --yes autoremove
|
||||||
|
|
||||||
|
# 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 && \
|
||||||
|
apt --yes autoremove
|
||||||
|
|
||||||
|
@@ -1,13 +1,14 @@
|
|||||||
# Simple docker-composer.yml
|
# Simple docker-compose.yml
|
||||||
# You can change your port or volume location
|
# You can change your port or volume location
|
||||||
|
|
||||||
version: '3.3'
|
version: '3.3'
|
||||||
|
|
||||||
services:
|
services:
|
||||||
uptime-kuma:
|
uptime-kuma:
|
||||||
image: louislam/uptime-kuma
|
image: louislam/uptime-kuma:1
|
||||||
container_name: uptime-kuma
|
container_name: uptime-kuma
|
||||||
volumes:
|
volumes:
|
||||||
- ./uptime-kuma:/app/data
|
- ./uptime-kuma-data:/app/data
|
||||||
ports:
|
ports:
|
||||||
- 3001:3001
|
- 3001:3001 # <Host Port>:<Container Port>
|
||||||
|
restart: always
|
||||||
|
@@ -4,9 +4,7 @@ WORKDIR /app
|
|||||||
ENV PUPPETEER_SKIP_CHROMIUM_DOWNLOAD=1
|
ENV PUPPETEER_SKIP_CHROMIUM_DOWNLOAD=1
|
||||||
|
|
||||||
COPY . .
|
COPY . .
|
||||||
RUN npm ci && \
|
RUN npm ci --production && \
|
||||||
npm run build && \
|
|
||||||
npm ci --production && \
|
|
||||||
chmod +x /app/extra/entrypoint.sh
|
chmod +x /app/extra/entrypoint.sh
|
||||||
|
|
||||||
|
|
||||||
@@ -22,23 +20,56 @@ HEALTHCHECK --interval=60s --timeout=30s --start-period=180s --retries=5 CMD nod
|
|||||||
ENTRYPOINT ["/usr/bin/dumb-init", "--", "extra/entrypoint.sh"]
|
ENTRYPOINT ["/usr/bin/dumb-init", "--", "extra/entrypoint.sh"]
|
||||||
CMD ["node", "server/server.js"]
|
CMD ["node", "server/server.js"]
|
||||||
|
|
||||||
|
|
||||||
FROM release AS nightly
|
FROM release AS nightly
|
||||||
RUN npm run mark-as-nightly
|
RUN npm run mark-as-nightly
|
||||||
|
|
||||||
|
# Build an image for testing pr
|
||||||
|
FROM louislam/uptime-kuma:base-debian AS pr-test
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
ENV PUPPETEER_SKIP_CHROMIUM_DOWNLOAD=1
|
||||||
|
|
||||||
|
## Install Git
|
||||||
|
RUN apt update \
|
||||||
|
&& apt --yes --no-install-recommends install curl \
|
||||||
|
&& curl -fsSL https://cli.github.com/packages/githubcli-archive-keyring.gpg | dd of=/usr/share/keyrings/githubcli-archive-keyring.gpg \
|
||||||
|
&& chmod go+r /usr/share/keyrings/githubcli-archive-keyring.gpg \
|
||||||
|
&& echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/githubcli-archive-keyring.gpg] https://cli.github.com/packages stable main" | tee /etc/apt/sources.list.d/github-cli.list > /dev/null \
|
||||||
|
&& apt update \
|
||||||
|
&& apt --yes --no-install-recommends install git
|
||||||
|
|
||||||
|
## Empty the directory, because we have to clone the Git repo.
|
||||||
|
RUN rm -rf ./* && chown node /app
|
||||||
|
|
||||||
|
USER node
|
||||||
|
RUN git config --global user.email "no-reply@no-reply.com"
|
||||||
|
RUN git config --global user.name "PR Tester"
|
||||||
|
RUN git clone https://github.com/louislam/uptime-kuma.git .
|
||||||
|
RUN npm ci
|
||||||
|
|
||||||
|
EXPOSE 3000 3001
|
||||||
|
VOLUME ["/app/data"]
|
||||||
|
HEALTHCHECK --interval=60s --timeout=30s --start-period=180s --retries=5 CMD node extra/healthcheck.js
|
||||||
|
CMD ["npm", "run", "start-pr-test"]
|
||||||
|
|
||||||
|
|
||||||
# Upload the artifact to Github
|
# Upload the artifact to Github
|
||||||
FROM louislam/uptime-kuma:base-debian AS upload-artifact
|
FROM louislam/uptime-kuma:base-debian AS upload-artifact
|
||||||
WORKDIR /
|
WORKDIR /
|
||||||
RUN apt update && \
|
RUN apt update && \
|
||||||
apt --yes install curl file
|
apt --yes install curl file
|
||||||
|
|
||||||
|
COPY --from=build /app /app
|
||||||
|
|
||||||
|
ARG VERSION
|
||||||
ARG GITHUB_TOKEN
|
ARG GITHUB_TOKEN
|
||||||
ARG TARGETARCH
|
ARG TARGETARCH
|
||||||
ARG PLATFORM=debian
|
ARG PLATFORM=debian
|
||||||
ARG VERSION=1.9.0
|
|
||||||
ARG FILE=$PLATFORM-$TARGETARCH-$VERSION.tar.gz
|
ARG FILE=$PLATFORM-$TARGETARCH-$VERSION.tar.gz
|
||||||
ARG DIST=dist.tar.gz
|
ARG DIST=dist.tar.gz
|
||||||
|
|
||||||
COPY --from=build /app /app
|
|
||||||
RUN chmod +x /app/extra/upload-github-release-asset.sh
|
RUN chmod +x /app/extra/upload-github-release-asset.sh
|
||||||
|
|
||||||
# Full Build
|
# Full Build
|
||||||
|
@@ -4,9 +4,7 @@ WORKDIR /app
|
|||||||
ENV PUPPETEER_SKIP_CHROMIUM_DOWNLOAD=1
|
ENV PUPPETEER_SKIP_CHROMIUM_DOWNLOAD=1
|
||||||
|
|
||||||
COPY . .
|
COPY . .
|
||||||
RUN npm ci && \
|
RUN npm ci --production && \
|
||||||
npm run build && \
|
|
||||||
npm ci --production && \
|
|
||||||
chmod +x /app/extra/entrypoint.sh
|
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"]
|
ENTRYPOINT ["/usr/bin/dumb-init", "--", "extra/entrypoint.sh"]
|
||||||
CMD ["node", "server/server.js"]
|
CMD ["node", "server/server.js"]
|
||||||
|
|
||||||
|
|
||||||
FROM release AS nightly
|
FROM release AS nightly
|
||||||
RUN npm run mark-as-nightly
|
RUN npm run mark-as-nightly
|
||||||
|
@@ -1,6 +1,6 @@
|
|||||||
module.exports = {
|
module.exports = {
|
||||||
apps: [{
|
apps: [{
|
||||||
name: "uptime-kuma",
|
name: "uptime-kuma",
|
||||||
script: "./server/server.js",
|
script: "./server/server.js",
|
||||||
}]
|
}]
|
||||||
}
|
};
|
||||||
|
66
extra/beta/update-version.js
Normal file
66
extra/beta/update-version.js
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
const pkg = require("../../package.json");
|
||||||
|
const fs = require("fs");
|
||||||
|
const childProcess = require("child_process");
|
||||||
|
const util = require("../../src/util");
|
||||||
|
|
||||||
|
util.polyfill();
|
||||||
|
|
||||||
|
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");
|
||||||
|
|
||||||
|
// Also update package-lock.json
|
||||||
|
childProcess.spawnSync("npm", [ "install" ]);
|
||||||
|
|
||||||
|
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 = childProcess.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 = childProcess.spawnSync("git", [ "push", "origin", "master" ]);
|
||||||
|
console.log(res.stdout.toString().trim());
|
||||||
|
}
|
||||||
|
|
||||||
|
function tag(version) {
|
||||||
|
let res = childProcess.spawnSync("git", [ "tag", version ]);
|
||||||
|
console.log(res.stdout.toString().trim());
|
||||||
|
|
||||||
|
res = childProcess.spawnSync("git", [ "push", "origin", version ]);
|
||||||
|
console.log(res.stdout.toString().trim());
|
||||||
|
}
|
||||||
|
|
||||||
|
function tagExists(version) {
|
||||||
|
if (! version) {
|
||||||
|
throw new Error("invalid version");
|
||||||
|
}
|
||||||
|
|
||||||
|
let res = childProcess.spawnSync("git", [ "tag", "-l", version ]);
|
||||||
|
|
||||||
|
return res.stdout.toString().trim() === version;
|
||||||
|
}
|
33
extra/checkout-pr.js
Normal file
33
extra/checkout-pr.js
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
const childProcess = require("child_process");
|
||||||
|
|
||||||
|
if (!process.env.UPTIME_KUMA_GH_REPO) {
|
||||||
|
console.error("Please set a repo to the environment variable 'UPTIME_KUMA_GH_REPO' (e.g. mhkarimi1383:goalert-notification)");
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
let inputArray = process.env.UPTIME_KUMA_GH_REPO.split(":");
|
||||||
|
|
||||||
|
if (inputArray.length !== 2) {
|
||||||
|
console.error("Invalid format. Please set a repo to the environment variable 'UPTIME_KUMA_GH_REPO' (e.g. mhkarimi1383:goalert-notification)");
|
||||||
|
}
|
||||||
|
|
||||||
|
let name = inputArray[0];
|
||||||
|
let branch = inputArray[1];
|
||||||
|
|
||||||
|
console.log("Checkout pr");
|
||||||
|
|
||||||
|
// Checkout the pr
|
||||||
|
let result = childProcess.spawnSync("git", [ "remote", "add", name, `https://github.com/${name}/uptime-kuma` ]);
|
||||||
|
|
||||||
|
console.log(result.stdout.toString());
|
||||||
|
console.error(result.stderr.toString());
|
||||||
|
|
||||||
|
result = childProcess.spawnSync("git", [ "fetch", name, branch ]);
|
||||||
|
|
||||||
|
console.log(result.stdout.toString());
|
||||||
|
console.error(result.stderr.toString());
|
||||||
|
|
||||||
|
result = childProcess.spawnSync("git", [ "checkout", `${name}/${branch}`, "--force" ]);
|
||||||
|
|
||||||
|
console.log(result.stdout.toString());
|
||||||
|
console.error(result.stderr.toString());
|
57
extra/close-incorrect-issue.js
Normal file
57
extra/close-incorrect-issue.js
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
const github = require("@actions/github");
|
||||||
|
|
||||||
|
(async () => {
|
||||||
|
try {
|
||||||
|
const token = process.argv[2];
|
||||||
|
const issueNumber = process.argv[3];
|
||||||
|
const username = process.argv[4];
|
||||||
|
|
||||||
|
const client = github.getOctokit(token).rest;
|
||||||
|
|
||||||
|
const issue = {
|
||||||
|
owner: "louislam",
|
||||||
|
repo: "uptime-kuma",
|
||||||
|
number: issueNumber,
|
||||||
|
};
|
||||||
|
|
||||||
|
const labels = (
|
||||||
|
await client.issues.listLabelsOnIssue({
|
||||||
|
owner: issue.owner,
|
||||||
|
repo: issue.repo,
|
||||||
|
issue_number: issue.number
|
||||||
|
})
|
||||||
|
).data.map(({ name }) => name);
|
||||||
|
|
||||||
|
if (labels.length === 0) {
|
||||||
|
console.log("Bad format here");
|
||||||
|
|
||||||
|
await client.issues.addLabels({
|
||||||
|
owner: issue.owner,
|
||||||
|
repo: issue.repo,
|
||||||
|
issue_number: issue.number,
|
||||||
|
labels: [ "invalid-format" ]
|
||||||
|
});
|
||||||
|
|
||||||
|
// Add the issue closing comment
|
||||||
|
await client.issues.createComment({
|
||||||
|
owner: issue.owner,
|
||||||
|
repo: issue.repo,
|
||||||
|
issue_number: issue.number,
|
||||||
|
body: `@${username}: Hello! :wave:\n\nThis issue is being automatically closed because it does not follow the issue template. Please DO NOT open a blank issue.`
|
||||||
|
});
|
||||||
|
|
||||||
|
// Close the issue
|
||||||
|
await client.issues.update({
|
||||||
|
owner: issue.owner,
|
||||||
|
repo: issue.repo,
|
||||||
|
issue_number: issue.number,
|
||||||
|
state: "closed"
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
console.log("Pass!");
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.log(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
})();
|
44
extra/download-cloudflared.js
Normal file
44
extra/download-cloudflared.js
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
//
|
||||||
|
|
||||||
|
const http = require("https"); // or 'https' for https:// URLs
|
||||||
|
const fs = require("fs");
|
||||||
|
|
||||||
|
const platform = process.argv[2];
|
||||||
|
|
||||||
|
if (!platform) {
|
||||||
|
console.error("No platform??");
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
let arch = null;
|
||||||
|
|
||||||
|
if (platform === "linux/amd64") {
|
||||||
|
arch = "amd64";
|
||||||
|
} else if (platform === "linux/arm64") {
|
||||||
|
arch = "arm64";
|
||||||
|
} else if (platform === "linux/arm/v7") {
|
||||||
|
arch = "arm";
|
||||||
|
} else {
|
||||||
|
console.error("Invalid platform?? " + platform);
|
||||||
|
}
|
||||||
|
|
||||||
|
const file = fs.createWriteStream("cloudflared.deb");
|
||||||
|
get("https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-" + arch + ".deb");
|
||||||
|
|
||||||
|
function get(url) {
|
||||||
|
http.get(url, function (res) {
|
||||||
|
if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
|
||||||
|
console.log("Redirect to " + res.headers.location);
|
||||||
|
get(res.headers.location);
|
||||||
|
} else if (res.statusCode >= 200 && res.statusCode < 300) {
|
||||||
|
res.pipe(file);
|
||||||
|
|
||||||
|
res.on("end", function () {
|
||||||
|
console.log("Downloaded");
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
console.error(res.statusCode);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
@@ -4,6 +4,7 @@ const tar = require("tar");
|
|||||||
|
|
||||||
const packageJSON = require("../package.json");
|
const packageJSON = require("../package.json");
|
||||||
const fs = require("fs");
|
const fs = require("fs");
|
||||||
|
const rmSync = require("./fs-rmSync.js");
|
||||||
const version = packageJSON.version;
|
const version = packageJSON.version;
|
||||||
|
|
||||||
const filename = "dist.tar.gz";
|
const filename = "dist.tar.gz";
|
||||||
@@ -11,6 +12,12 @@ const filename = "dist.tar.gz";
|
|||||||
const url = `https://github.com/louislam/uptime-kuma/releases/download/${version}/${filename}`;
|
const url = `https://github.com/louislam/uptime-kuma/releases/download/${version}/${filename}`;
|
||||||
download(url);
|
download(url);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Downloads the latest version of the dist from a GitHub release.
|
||||||
|
* @param {string} url The URL to download from.
|
||||||
|
*
|
||||||
|
* Generated by Trelent
|
||||||
|
*/
|
||||||
function download(url) {
|
function download(url) {
|
||||||
console.log(url);
|
console.log(url);
|
||||||
|
|
||||||
@@ -21,7 +28,7 @@ function download(url) {
|
|||||||
if (fs.existsSync("./dist")) {
|
if (fs.existsSync("./dist")) {
|
||||||
|
|
||||||
if (fs.existsSync("./dist-backup")) {
|
if (fs.existsSync("./dist-backup")) {
|
||||||
fs.rmdirSync("./dist-backup", {
|
rmSync("./dist-backup", {
|
||||||
recursive: true
|
recursive: true
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -34,9 +41,11 @@ function download(url) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
tarStream.on("close", () => {
|
tarStream.on("close", () => {
|
||||||
fs.rmdirSync("./dist-backup", {
|
if (fs.existsSync("./dist-backup")) {
|
||||||
recursive: true
|
rmSync("./dist-backup", {
|
||||||
});
|
recursive: true
|
||||||
|
});
|
||||||
|
}
|
||||||
console.log("Done");
|
console.log("Done");
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -44,7 +53,7 @@ function download(url) {
|
|||||||
if (fs.existsSync("./dist-backup")) {
|
if (fs.existsSync("./dist-backup")) {
|
||||||
fs.renameSync("./dist-backup", "./dist");
|
fs.renameSync("./dist-backup", "./dist");
|
||||||
}
|
}
|
||||||
console.log("Done");
|
console.error("Error from tarStream");
|
||||||
});
|
});
|
||||||
|
|
||||||
response.pipe(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);
|
23
extra/fs-rmSync.js
Normal file
23
extra/fs-rmSync.js
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
const fs = require("fs");
|
||||||
|
/**
|
||||||
|
* Detect if `fs.rmSync` is available
|
||||||
|
* to avoid the runtime deprecation warning triggered for using `fs.rmdirSync` with `{ recursive: true }` in Node.js v16,
|
||||||
|
* or the `recursive` property removing completely in the future Node.js version.
|
||||||
|
* See the link below.
|
||||||
|
*
|
||||||
|
* @todo Once we drop the support for Node.js v14 (or at least versions before v14.14.0), we can safely replace this function with `fs.rmSync`, since `fs.rmSync` was add in Node.js v14.14.0 and currently we supports all the Node.js v14 versions that include the versions before the v14.14.0, and this function have almost the same signature with `fs.rmSync`.
|
||||||
|
* @link https://nodejs.org/docs/latest-v16.x/api/deprecations.html#dep0147-fsrmdirpath--recursive-true- the deprecation infomation of `fs.rmdirSync`
|
||||||
|
* @link https://nodejs.org/docs/latest-v16.x/api/fs.html#fsrmsyncpath-options the document of `fs.rmSync`
|
||||||
|
* @param {fs.PathLike} path Valid types for path values in "fs".
|
||||||
|
* @param {fs.RmDirOptions} [options] options for `fs.rmdirSync`, if `fs.rmSync` is available and property `recursive` is true, it will automatically have property `force` with value `true`.
|
||||||
|
*/
|
||||||
|
const rmSync = (path, options) => {
|
||||||
|
if (typeof fs.rmSync === "function") {
|
||||||
|
if (options.recursive) {
|
||||||
|
options.force = true;
|
||||||
|
}
|
||||||
|
return fs.rmSync(path, options);
|
||||||
|
}
|
||||||
|
return fs.rmdirSync(path, options);
|
||||||
|
};
|
||||||
|
module.exports = rmSync;
|
@@ -1,25 +1,41 @@
|
|||||||
/*
|
/*
|
||||||
* This script should be run after a period of time (180s), because the server may need some time to prepare.
|
* This script should be run after a period of time (180s), because the server may need some time to prepare.
|
||||||
*/
|
*/
|
||||||
|
const { FBSD } = require("../server/util-server");
|
||||||
|
|
||||||
process.env.NODE_TLS_REJECT_UNAUTHORIZED = "0";
|
process.env.NODE_TLS_REJECT_UNAUTHORIZED = "0";
|
||||||
|
|
||||||
let client;
|
let client;
|
||||||
|
|
||||||
if (process.env.SSL_KEY && process.env.SSL_CERT) {
|
const sslKey = process.env.UPTIME_KUMA_SSL_KEY || process.env.SSL_KEY || undefined;
|
||||||
|
const sslCert = process.env.UPTIME_KUMA_SSL_CERT || process.env.SSL_CERT || undefined;
|
||||||
|
|
||||||
|
if (sslKey && sslCert) {
|
||||||
client = require("https");
|
client = require("https");
|
||||||
} else {
|
} else {
|
||||||
client = require("http");
|
client = require("http");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If host is omitted, the server will accept connections on the unspecified IPv6 address (::) when IPv6 is available and the unspecified IPv4 address (0.0.0.0) otherwise.
|
||||||
|
// Dual-stack support for (::)
|
||||||
|
let hostname = process.env.UPTIME_KUMA_HOST;
|
||||||
|
|
||||||
|
// Also read HOST if not *BSD, as HOST is a system environment variable in FreeBSD
|
||||||
|
if (!hostname && !FBSD) {
|
||||||
|
hostname = process.env.HOST;
|
||||||
|
}
|
||||||
|
|
||||||
|
const port = parseInt(process.env.UPTIME_KUMA_PORT || process.env.PORT || 3001);
|
||||||
|
|
||||||
let options = {
|
let options = {
|
||||||
host: process.env.HOST || "127.0.0.1",
|
host: hostname || "127.0.0.1",
|
||||||
port: parseInt(process.env.PORT) || 3001,
|
port: port,
|
||||||
timeout: 28 * 1000,
|
timeout: 28 * 1000,
|
||||||
};
|
};
|
||||||
|
|
||||||
let request = client.request(options, (res) => {
|
let request = client.request(options, (res) => {
|
||||||
console.log(`Health Check OK [Res Code: ${res.statusCode}]`);
|
console.log(`Health Check OK [Res Code: ${res.statusCode}]`);
|
||||||
if (res.statusCode === 200) {
|
if (res.statusCode === 302) {
|
||||||
process.exit(0);
|
process.exit(0);
|
||||||
} else {
|
} else {
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
|
@@ -189,7 +189,7 @@ if (type == "local") {
|
|||||||
bash("check=$(pm2 --version)");
|
bash("check=$(pm2 --version)");
|
||||||
if (check == "") {
|
if (check == "") {
|
||||||
println("Installing PM2");
|
println("Installing PM2");
|
||||||
bash("npm install pm2 -g");
|
bash("npm install pm2 -g && pm2 install pm2-logrotate");
|
||||||
bash("pm2 startup");
|
bash("pm2 startup");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -4,21 +4,21 @@ const util = require("../src/util");
|
|||||||
|
|
||||||
util.polyfill();
|
util.polyfill();
|
||||||
|
|
||||||
const oldVersion = pkg.version
|
const oldVersion = pkg.version;
|
||||||
const newVersion = oldVersion + "-nightly"
|
const newVersion = oldVersion + "-nightly";
|
||||||
|
|
||||||
console.log("Old Version: " + oldVersion)
|
console.log("Old Version: " + oldVersion);
|
||||||
console.log("New Version: " + newVersion)
|
console.log("New Version: " + newVersion);
|
||||||
|
|
||||||
if (newVersion) {
|
if (newVersion) {
|
||||||
// Process package.json
|
// Process package.json
|
||||||
pkg.version = newVersion
|
pkg.version = newVersion;
|
||||||
pkg.scripts.setup = pkg.scripts.setup.replaceAll(oldVersion, 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"] = pkg.scripts["build-docker"].replaceAll(oldVersion, newVersion);
|
||||||
fs.writeFileSync("package.json", JSON.stringify(pkg, null, 4) + "\n")
|
fs.writeFileSync("package.json", JSON.stringify(pkg, null, 4) + "\n");
|
||||||
|
|
||||||
// Process README.md
|
// Process README.md
|
||||||
if (fs.existsSync("README.md")) {
|
if (fs.existsSync("README.md")) {
|
||||||
fs.writeFileSync("README.md", fs.readFileSync("README.md", "utf8").replaceAll(oldVersion, newVersion))
|
fs.writeFileSync("README.md", fs.readFileSync("README.md", "utf8").replaceAll(oldVersion, newVersion));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
6
extra/press-any-key.js
Normal file
6
extra/press-any-key.js
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
console.log("Git Push and Publish the release note on github, then press any key to continue");
|
||||||
|
|
||||||
|
process.stdin.setRawMode(true);
|
||||||
|
process.stdin.resume();
|
||||||
|
process.stdin.on("data", process.exit.bind(process, 0));
|
||||||
|
|
60
extra/remove-2fa.js
Normal file
60
extra/remove-2fa.js
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
console.log("== Uptime Kuma Remove 2FA Tool ==");
|
||||||
|
console.log("Loading the database");
|
||||||
|
|
||||||
|
const Database = require("../server/database");
|
||||||
|
const { R } = require("redbean-node");
|
||||||
|
const readline = require("readline");
|
||||||
|
const TwoFA = require("../server/2fa");
|
||||||
|
const args = require("args-parser")(process.argv);
|
||||||
|
const rl = readline.createInterface({
|
||||||
|
input: process.stdin,
|
||||||
|
output: process.stdout
|
||||||
|
});
|
||||||
|
|
||||||
|
const main = async () => {
|
||||||
|
Database.init(args);
|
||||||
|
await Database.connect();
|
||||||
|
|
||||||
|
try {
|
||||||
|
// No need to actually reset the password for testing, just make sure no connection problem. It is ok for now.
|
||||||
|
if (!process.env.TEST_BACKEND) {
|
||||||
|
const user = await R.findOne("user");
|
||||||
|
if (! user) {
|
||||||
|
throw new Error("user not found, have you installed?");
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("Found user: " + user.username);
|
||||||
|
|
||||||
|
let ans = await question("Are you sure want to remove 2FA? [y/N]");
|
||||||
|
|
||||||
|
if (ans.toLowerCase() === "y") {
|
||||||
|
await TwoFA.disable2FA(user.id);
|
||||||
|
console.log("2FA has been removed successfully.");
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error("Error: " + e.message);
|
||||||
|
}
|
||||||
|
|
||||||
|
await Database.close();
|
||||||
|
rl.close();
|
||||||
|
|
||||||
|
console.log("Finished.");
|
||||||
|
};
|
||||||
|
|
||||||
|
function question(question) {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
rl.question(question, (answer) => {
|
||||||
|
resolve(answer);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!process.env.TEST_BACKEND) {
|
||||||
|
main();
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
main,
|
||||||
|
};
|
@@ -1,11 +1,10 @@
|
|||||||
console.log("== Uptime Kuma Reset Password Tool ==");
|
console.log("== Uptime Kuma Reset Password Tool ==");
|
||||||
|
|
||||||
console.log("Loading the database");
|
|
||||||
|
|
||||||
const Database = require("../server/database");
|
const Database = require("../server/database");
|
||||||
const { R } = require("redbean-node");
|
const { R } = require("redbean-node");
|
||||||
const readline = require("readline");
|
const readline = require("readline");
|
||||||
const { initJWTSecret } = require("../server/util-server");
|
const { initJWTSecret } = require("../server/util-server");
|
||||||
|
const User = require("../server/model/user");
|
||||||
const args = require("args-parser")(process.argv);
|
const args = require("args-parser")(process.argv);
|
||||||
const rl = readline.createInterface({
|
const rl = readline.createInterface({
|
||||||
input: process.stdin,
|
input: process.stdin,
|
||||||
@@ -13,8 +12,9 @@ const rl = readline.createInterface({
|
|||||||
});
|
});
|
||||||
|
|
||||||
const main = async () => {
|
const main = async () => {
|
||||||
|
console.log("Connecting the database");
|
||||||
Database.init(args);
|
Database.init(args);
|
||||||
await Database.connect();
|
await Database.connect(false, false, true);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// No need to actually reset the password for testing, just make sure no connection problem. It is ok for now.
|
// No need to actually reset the password for testing, just make sure no connection problem. It is ok for now.
|
||||||
@@ -31,7 +31,7 @@ const main = async () => {
|
|||||||
let confirmPassword = await question("Confirm New Password: ");
|
let confirmPassword = await question("Confirm New Password: ");
|
||||||
|
|
||||||
if (password === confirmPassword) {
|
if (password === confirmPassword) {
|
||||||
await user.resetPassword(password);
|
await User.resetPassword(user.id, password);
|
||||||
|
|
||||||
// Reset all sessions by reset jwt secret
|
// Reset all sessions by reset jwt secret
|
||||||
await initJWTSecret();
|
await initJWTSecret();
|
||||||
|
@@ -26,7 +26,7 @@ server.on("request", (request, send, rinfo) => {
|
|||||||
ttl: 300,
|
ttl: 300,
|
||||||
address: "1.2.3.4"
|
address: "1.2.3.4"
|
||||||
});
|
});
|
||||||
} if (question.type === Packet.TYPE.AAAA) {
|
} else if (question.type === Packet.TYPE.AAAA) {
|
||||||
response.answers.push({
|
response.answers.push({
|
||||||
name: question.name,
|
name: question.name,
|
||||||
type: question.type,
|
type: question.type,
|
||||||
|
50
extra/simple-mqtt-server.js
Normal file
50
extra/simple-mqtt-server.js
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
const { log } = require("../src/util");
|
||||||
|
|
||||||
|
const mqttUsername = "louis1";
|
||||||
|
const mqttPassword = "!@#$LLam";
|
||||||
|
|
||||||
|
class SimpleMqttServer {
|
||||||
|
aedes = require("aedes")();
|
||||||
|
server = require("net").createServer(this.aedes.handle);
|
||||||
|
|
||||||
|
constructor(port) {
|
||||||
|
this.port = port;
|
||||||
|
}
|
||||||
|
|
||||||
|
start() {
|
||||||
|
this.server.listen(this.port, () => {
|
||||||
|
console.log("server started and listening on port ", this.port);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let server1 = new SimpleMqttServer(10000);
|
||||||
|
|
||||||
|
server1.aedes.authenticate = function (client, username, password, callback) {
|
||||||
|
if (username && password) {
|
||||||
|
console.log(password.toString("utf-8"));
|
||||||
|
callback(null, username === mqttUsername && password.toString("utf-8") === mqttPassword);
|
||||||
|
} else {
|
||||||
|
callback(null, false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
server1.aedes.on("subscribe", (subscriptions, client) => {
|
||||||
|
console.log(subscriptions);
|
||||||
|
|
||||||
|
for (let s of subscriptions) {
|
||||||
|
if (s.topic === "test") {
|
||||||
|
server1.aedes.publish({
|
||||||
|
topic: "test",
|
||||||
|
payload: Buffer.from("ok"),
|
||||||
|
}, (error) => {
|
||||||
|
if (error) {
|
||||||
|
log.error("mqtt_server", error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
server1.start();
|
@@ -3,6 +3,7 @@
|
|||||||
import fs from "fs";
|
import fs from "fs";
|
||||||
import path from "path";
|
import path from "path";
|
||||||
import util from "util";
|
import util from "util";
|
||||||
|
import rmSync from "../fs-rmSync.js";
|
||||||
|
|
||||||
// https://stackoverflow.com/questions/13786160/copy-folder-recursively-in-node-js
|
// https://stackoverflow.com/questions/13786160/copy-folder-recursively-in-node-js
|
||||||
/**
|
/**
|
||||||
@@ -30,7 +31,7 @@ console.log("Arguments:", process.argv);
|
|||||||
const baseLangCode = process.argv[2] || "en";
|
const baseLangCode = process.argv[2] || "en";
|
||||||
console.log("Base Lang: " + baseLangCode);
|
console.log("Base Lang: " + baseLangCode);
|
||||||
if (fs.existsSync("./languages")) {
|
if (fs.existsSync("./languages")) {
|
||||||
fs.rmdirSync("./languages", { recursive: true });
|
rmSync("./languages", { recursive: true });
|
||||||
}
|
}
|
||||||
copyRecursiveSync("../../src/languages", "./languages");
|
copyRecursiveSync("../../src/languages", "./languages");
|
||||||
|
|
||||||
@@ -40,7 +41,7 @@ const files = fs.readdirSync("./languages");
|
|||||||
console.log("Files:", files);
|
console.log("Files:", files);
|
||||||
|
|
||||||
for (const file of files) {
|
for (const file of files) {
|
||||||
if (!file.endsWith(".js")) {
|
if (! file.endsWith(".js")) {
|
||||||
console.log("Skipping " + file);
|
console.log("Skipping " + file);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -82,5 +83,5 @@ for (const file of files) {
|
|||||||
fs.writeFileSync(`../../src/languages/${file}`, code);
|
fs.writeFileSync(`../../src/languages/${file}`, code);
|
||||||
}
|
}
|
||||||
|
|
||||||
fs.rmdirSync("./languages", { recursive: true });
|
rmSync("./languages", { recursive: true });
|
||||||
console.log("Done. Fixing formatting by ESLint...");
|
console.log("Done. Fixing formatting by ESLint...");
|
||||||
|
@@ -1,14 +1,12 @@
|
|||||||
const pkg = require("../package.json");
|
const pkg = require("../package.json");
|
||||||
const fs = require("fs");
|
const fs = require("fs");
|
||||||
const child_process = require("child_process");
|
const childProcess = require("child_process");
|
||||||
const util = require("../src/util");
|
const util = require("../src/util");
|
||||||
|
|
||||||
util.polyfill();
|
util.polyfill();
|
||||||
|
|
||||||
const oldVersion = pkg.version;
|
const newVersion = process.env.VERSION;
|
||||||
const newVersion = process.argv[2];
|
|
||||||
|
|
||||||
console.log("Old Version: " + oldVersion);
|
|
||||||
console.log("New Version: " + newVersion);
|
console.log("New Version: " + newVersion);
|
||||||
|
|
||||||
if (! newVersion) {
|
if (! newVersion) {
|
||||||
@@ -22,25 +20,31 @@ if (! exists) {
|
|||||||
|
|
||||||
// Process package.json
|
// Process package.json
|
||||||
pkg.version = newVersion;
|
pkg.version = newVersion;
|
||||||
pkg.scripts.setup = pkg.scripts.setup.replaceAll(oldVersion, newVersion);
|
|
||||||
pkg.scripts["build-docker"] = pkg.scripts["build-docker"].replaceAll(oldVersion, newVersion);
|
// Replace the version: https://regex101.com/r/hmj2Bc/1
|
||||||
pkg.scripts["build-docker-alpine"] = pkg.scripts["build-docker-alpine"].replaceAll(oldVersion, newVersion);
|
pkg.scripts.setup = pkg.scripts.setup.replace(/(git checkout )([^\s]+)/, `$1${newVersion}`);
|
||||||
pkg.scripts["build-docker-debian"] = pkg.scripts["build-docker-debian"].replaceAll(oldVersion, newVersion);
|
|
||||||
fs.writeFileSync("package.json", JSON.stringify(pkg, null, 4) + "\n");
|
fs.writeFileSync("package.json", JSON.stringify(pkg, null, 4) + "\n");
|
||||||
|
|
||||||
|
// Also update package-lock.json
|
||||||
|
childProcess.spawnSync("npm", [ "install" ]);
|
||||||
|
|
||||||
commit(newVersion);
|
commit(newVersion);
|
||||||
tag(newVersion);
|
tag(newVersion);
|
||||||
|
|
||||||
updateWiki(oldVersion, newVersion);
|
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
console.log("version exists");
|
console.log("version exists");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates the version number in package.json and commits it to git.
|
||||||
|
* @param {string} version - The new version number
|
||||||
|
*
|
||||||
|
* Generated by Trelent
|
||||||
|
*/
|
||||||
function commit(version) {
|
function commit(version) {
|
||||||
let msg = "update to " + version;
|
let msg = "Update to " + version;
|
||||||
|
|
||||||
let res = child_process.spawnSync("git", ["commit", "-m", msg, "-a"]);
|
let res = childProcess.spawnSync("git", [ "commit", "-m", msg, "-a" ]);
|
||||||
let stdout = res.stdout.toString().trim();
|
let stdout = res.stdout.toString().trim();
|
||||||
console.log(stdout);
|
console.log(stdout);
|
||||||
|
|
||||||
@@ -50,51 +54,22 @@ function commit(version) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function tag(version) {
|
function tag(version) {
|
||||||
let res = child_process.spawnSync("git", ["tag", version]);
|
let res = childProcess.spawnSync("git", [ "tag", version ]);
|
||||||
console.log(res.stdout.toString().trim());
|
console.log(res.stdout.toString().trim());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if a given version is already tagged in the git repository.
|
||||||
|
* @param {string} version - The version to check for.
|
||||||
|
*
|
||||||
|
* Generated by Trelent
|
||||||
|
*/
|
||||||
function tagExists(version) {
|
function tagExists(version) {
|
||||||
if (! version) {
|
if (! version) {
|
||||||
throw new Error("invalid version");
|
throw new Error("invalid version");
|
||||||
}
|
}
|
||||||
|
|
||||||
let res = child_process.spawnSync("git", ["tag", "-l", version]);
|
let res = childProcess.spawnSync("git", [ "tag", "-l", version ]);
|
||||||
|
|
||||||
return res.stdout.toString().trim() === version;
|
return res.stdout.toString().trim() === version;
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateWiki(oldVersion, newVersion) {
|
|
||||||
const wikiDir = "./tmp/wiki";
|
|
||||||
const howToUpdateFilename = "./tmp/wiki/🆙-How-to-Update.md";
|
|
||||||
|
|
||||||
safeDelete(wikiDir);
|
|
||||||
|
|
||||||
child_process.spawnSync("git", ["clone", "https://github.com/louislam/uptime-kuma.wiki.git", wikiDir]);
|
|
||||||
let content = fs.readFileSync(howToUpdateFilename).toString();
|
|
||||||
content = content.replaceAll(`git checkout ${oldVersion}`, `git checkout ${newVersion}`);
|
|
||||||
fs.writeFileSync(howToUpdateFilename, content);
|
|
||||||
|
|
||||||
child_process.spawnSync("git", ["add", "-A"], {
|
|
||||||
cwd: wikiDir,
|
|
||||||
});
|
|
||||||
|
|
||||||
child_process.spawnSync("git", ["commit", "-m", `Update to ${newVersion} from ${oldVersion}`], {
|
|
||||||
cwd: wikiDir,
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log("Pushing to Github");
|
|
||||||
child_process.spawnSync("git", ["push"], {
|
|
||||||
cwd: wikiDir,
|
|
||||||
});
|
|
||||||
|
|
||||||
safeDelete(wikiDir);
|
|
||||||
}
|
|
||||||
|
|
||||||
function safeDelete(dir) {
|
|
||||||
if (fs.existsSync(dir)) {
|
|
||||||
fs.rmdirSync(dir, {
|
|
||||||
recursive: true,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
48
extra/update-wiki-version.js
Normal file
48
extra/update-wiki-version.js
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
const childProcess = 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);
|
||||||
|
|
||||||
|
childProcess.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);
|
||||||
|
|
||||||
|
childProcess.spawnSync("git", [ "add", "-A" ], {
|
||||||
|
cwd: wikiDir,
|
||||||
|
});
|
||||||
|
|
||||||
|
childProcess.spawnSync("git", [ "commit", "-m", `Update to ${newVersion}` ], {
|
||||||
|
cwd: wikiDir,
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log("Pushing to Github");
|
||||||
|
childProcess.spawnSync("git", [ "push" ], {
|
||||||
|
cwd: wikiDir,
|
||||||
|
});
|
||||||
|
|
||||||
|
safeDelete(wikiDir);
|
||||||
|
}
|
||||||
|
|
||||||
|
function safeDelete(dir) {
|
||||||
|
if (fs.existsSync(dir)) {
|
||||||
|
fs.rm(dir, {
|
||||||
|
recursive: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
@@ -5,7 +5,7 @@
|
|||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<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="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png">
|
||||||
<link rel="icon" type="image/svg+xml" href="/icon.svg" />
|
<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="theme-color" id="theme-color" content="" />
|
||||||
<meta name="description" content="Uptime Kuma monitoring tool" />
|
<meta name="description" content="Uptime Kuma monitoring tool" />
|
||||||
<title>Uptime Kuma</title>
|
<title>Uptime Kuma</title>
|
||||||
|
@@ -159,7 +159,7 @@ fi
|
|||||||
check=$(pm2 --version)
|
check=$(pm2 --version)
|
||||||
if [ "$check" == "" ]; then
|
if [ "$check" == "" ]; then
|
||||||
"echo" "-e" "Installing PM2"
|
"echo" "-e" "Installing PM2"
|
||||||
npm install pm2 -g
|
npm install pm2 -g && pm2 install pm2-logrotate
|
||||||
pm2 startup
|
pm2 startup
|
||||||
fi
|
fi
|
||||||
mkdir -p $installPath
|
mkdir -p $installPath
|
||||||
|
22750
package-lock.json
generated
22750
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
173
package.json
173
package.json
@@ -1,46 +1,50 @@
|
|||||||
{
|
{
|
||||||
"name": "uptime-kuma",
|
"name": "uptime-kuma",
|
||||||
"version": "1.9.1",
|
"version": "1.18.2",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://github.com/louislam/uptime-kuma.git"
|
"url": "https://github.com/louislam/uptime-kuma.git"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": "14.*"
|
"node": "14.* || >=16.*"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"install-legacy": "npm install --legacy-peer-deps",
|
"install-legacy": "npm install",
|
||||||
"update-legacy": "npm update --legacy-peer-deps",
|
"update-legacy": "npm update",
|
||||||
"lint:js": "eslint --ext \".js,.vue\" --ignore-path .gitignore .",
|
"lint:js": "eslint --ext \".js,.vue\" --ignore-path .gitignore .",
|
||||||
|
"lint-fix:js": "eslint --ext \".js,.vue\" --fix --ignore-path .gitignore .",
|
||||||
"lint:style": "stylelint \"**/*.{vue,css,scss}\" --ignore-path .gitignore",
|
"lint:style": "stylelint \"**/*.{vue,css,scss}\" --ignore-path .gitignore",
|
||||||
|
"lint-fix:style": "stylelint \"**/*.{vue,css,scss}\" --fix --ignore-path .gitignore",
|
||||||
"lint": "npm run lint:js && npm run lint:style",
|
"lint": "npm run lint:js && npm run lint:style",
|
||||||
"dev": "vite --host --config ./config/vite.config.js",
|
"dev": "concurrently -k -r \"wait-on tcp:3000 && npm run start-server-dev \" \"npm run start-frontend-dev\"",
|
||||||
|
"start-frontend-dev": "cross-env NODE_ENV=development vite --host --config ./config/vite.config.js",
|
||||||
"start": "npm run start-server",
|
"start": "npm run start-server",
|
||||||
"start-server": "node server/server.js",
|
"start-server": "node server/server.js",
|
||||||
"start-server-dev": "cross-env NODE_ENV=development node server/server.js",
|
"start-server-dev": "cross-env NODE_ENV=development node server/server.js",
|
||||||
"build": "vite build --config ./config/vite.config.js",
|
"build": "vite build --config ./config/vite.config.js",
|
||||||
"test": "node test/prepare-test-server.js && node server/server.js --port=3002 --data-dir=./data/test/ --test",
|
"test": "node test/prepare-test-server.js && node server/server.js --port=3002 --data-dir=./data/test/ --test",
|
||||||
"test-with-build": "npm run build && npm test",
|
"test-with-build": "npm run build && npm test",
|
||||||
"jest": "node test/prepare-jest.js && npm run jest-frontend && npm run jest-backend && jest --config=./config/jest.config.js",
|
"jest": "node test/prepare-jest.js && npm run jest-frontend && npm run jest-backend",
|
||||||
"jest-frontend": "cross-env TEST_FRONTEND=1 jest --config=./config/jest-frontend.config.js",
|
"jest-frontend": "cross-env TEST_FRONTEND=1 jest --config=./config/jest-frontend.config.js",
|
||||||
"jest-backend": "cross-env TEST_BACKEND=1 jest --config=./config/jest-backend.config.js",
|
"jest-backend": "cross-env TEST_BACKEND=1 jest --config=./config/jest-backend.config.js",
|
||||||
"tsc": "tsc",
|
"tsc": "tsc",
|
||||||
"vite-preview-dist": "vite preview --host --config ./config/vite.config.js",
|
"vite-preview-dist": "vite preview --host --config ./config/vite.config.js",
|
||||||
"build-docker": "npm run build-docker-debian && npm run build-docker-alpine",
|
"build-docker": "npm run build && npm run build-docker-debian && npm run build-docker-alpine",
|
||||||
"build-docker-alpine-base": "docker buildx build -f docker/alpine-base.dockerfile --platform linux/amd64,linux/arm64,linux/arm/v7 -t louislam/uptime-kuma:base-alpine . --push",
|
"build-docker-alpine-base": "docker buildx build -f docker/alpine-base.dockerfile --platform linux/amd64,linux/arm64,linux/arm/v7 -t louislam/uptime-kuma:base-alpine . --push",
|
||||||
"build-docker-debian-base": "docker buildx build -f docker/debian-base.dockerfile --platform linux/amd64,linux/arm64,linux/arm/v7 -t louislam/uptime-kuma:base-debian . --push",
|
"build-docker-debian-base": "docker buildx build -f docker/debian-base.dockerfile --platform linux/amd64,linux/arm64,linux/arm/v7 -t louislam/uptime-kuma:base-debian . --push",
|
||||||
"build-docker-alpine": "docker buildx build -f docker/dockerfile-alpine --platform linux/amd64,linux/arm64,linux/arm/v7 -t louislam/uptime-kuma:alpine -t louislam/uptime-kuma:1-alpine -t louislam/uptime-kuma:1.9.1-alpine --target release . --push",
|
"build-docker-alpine": "node ./extra/env2arg.js docker buildx build -f docker/dockerfile-alpine --platform linux/amd64,linux/arm64,linux/arm/v7 -t louislam/uptime-kuma:alpine -t louislam/uptime-kuma:1-alpine -t louislam/uptime-kuma:$VERSION-alpine --target release . --push",
|
||||||
"build-docker-debian": "docker buildx build -f docker/dockerfile --platform linux/amd64,linux/arm64,linux/arm/v7 -t louislam/uptime-kuma -t louislam/uptime-kuma:1 -t louislam/uptime-kuma:1.9.1 -t louislam/uptime-kuma:debian -t louislam/uptime-kuma:1-debian -t louislam/uptime-kuma:1.9.1-debian --target release . --push",
|
"build-docker-debian": "node ./extra/env2arg.js docker buildx build -f docker/dockerfile --platform linux/amd64,linux/arm64,linux/arm/v7 -t louislam/uptime-kuma -t louislam/uptime-kuma:1 -t louislam/uptime-kuma:$VERSION -t louislam/uptime-kuma:debian -t louislam/uptime-kuma:1-debian -t louislam/uptime-kuma:$VERSION-debian --target release . --push",
|
||||||
"build-docker-nightly": "docker buildx build -f docker/dockerfile --platform linux/amd64,linux/arm64,linux/arm/v7 -t louislam/uptime-kuma:nightly --target nightly . --push",
|
"build-docker-nightly": "npm run build && docker buildx build -f docker/dockerfile --platform linux/amd64,linux/arm64,linux/arm/v7 -t louislam/uptime-kuma:nightly --target nightly . --push",
|
||||||
"build-docker-nightly-alpine": "docker buildx build -f docker/dockerfile-alpine --platform linux/amd64,linux/arm64,linux/arm/v7 -t louislam/uptime-kuma:nightly-alpine --target nightly . --push",
|
"build-docker-nightly-alpine": "docker buildx build -f docker/dockerfile-alpine --platform linux/amd64,linux/arm64,linux/arm/v7 -t louislam/uptime-kuma:nightly-alpine --target nightly . --push",
|
||||||
"build-docker-nightly-amd64": "docker buildx build -f docker/dockerfile --platform linux/amd64 -t louislam/uptime-kuma:nightly-amd64 --target nightly . --push --progress plain",
|
"build-docker-nightly-amd64": "docker buildx build -f docker/dockerfile --platform linux/amd64 -t louislam/uptime-kuma:nightly-amd64 --target nightly . --push --progress plain",
|
||||||
"upload-artifacts": "docker buildx build -f docker/dockerfile --platform linux/amd64 -t louislam/uptime-kuma:upload-artifact --build-arg GITHUB_TOKEN --target upload-artifact . --progress plain",
|
"build-docker-pr-test": "docker buildx build -f docker/dockerfile --platform linux/amd64,linux/arm64 -t louislam/uptime-kuma:pr-test --target pr-test . --push",
|
||||||
"setup": "git checkout 1.9.1 && 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.18.2 && npm ci --production && npm run download-dist",
|
||||||
"download-dist": "node extra/download-dist.js",
|
"download-dist": "node extra/download-dist.js",
|
||||||
"update-version": "node extra/update-version.js",
|
|
||||||
"mark-as-nightly": "node extra/mark-as-nightly.js",
|
"mark-as-nightly": "node extra/mark-as-nightly.js",
|
||||||
"reset-password": "node extra/reset-password.js",
|
"reset-password": "node extra/reset-password.js",
|
||||||
|
"remove-2fa": "node extra/remove-2fa.js",
|
||||||
"compile-install-script": "@powershell -NoProfile -ExecutionPolicy Unrestricted -Command ./extra/compile-install-script.ps1",
|
"compile-install-script": "@powershell -NoProfile -ExecutionPolicy Unrestricted -Command ./extra/compile-install-script.ps1",
|
||||||
"test-install-script-centos7": "npm run compile-install-script && docker build --progress plain -f test/test_install_script/centos7.dockerfile .",
|
"test-install-script-centos7": "npm run compile-install-script && docker build --progress plain -f test/test_install_script/centos7.dockerfile .",
|
||||||
"test-install-script-alpine3": "npm run compile-install-script && docker build --progress plain -f test/test_install_script/alpine3.dockerfile .",
|
"test-install-script-alpine3": "npm run compile-install-script && docker build --progress plain -f test/test_install_script/alpine3.dockerfile .",
|
||||||
@@ -48,81 +52,122 @@
|
|||||||
"test-install-script-ubuntu1604": "npm run compile-install-script && docker build --progress plain -f test/test_install_script/ubuntu1604.dockerfile .",
|
"test-install-script-ubuntu1604": "npm run compile-install-script && docker build --progress plain -f test/test_install_script/ubuntu1604.dockerfile .",
|
||||||
"test-nodejs16": "docker build --progress plain -f test/ubuntu-nodejs16.dockerfile .",
|
"test-nodejs16": "docker build --progress plain -f test/ubuntu-nodejs16.dockerfile .",
|
||||||
"simple-dns-server": "node extra/simple-dns-server.js",
|
"simple-dns-server": "node extra/simple-dns-server.js",
|
||||||
|
"simple-mqtt-server": "node extra/simple-mqtt-server.js",
|
||||||
"update-language-files-with-base-lang": "cd extra/update-language-files && node index.js %npm_config_base_lang% && eslint ../../src/languages/**.js --fix",
|
"update-language-files-with-base-lang": "cd extra/update-language-files && node index.js %npm_config_base_lang% && eslint ../../src/languages/**.js --fix",
|
||||||
"update-language-files": "cd extra/update-language-files && node index.js && eslint ../../src/languages/**.js --fix"
|
"update-language-files": "cd extra/update-language-files && node index.js && eslint ../../src/languages/**.js --fix",
|
||||||
|
"ncu-patch": "npm-check-updates -u -t patch",
|
||||||
|
"release-final": "node extra/update-version.js && npm run build-docker && node ./extra/press-any-key.js && npm run upload-artifacts && node ./extra/update-wiki-version.js",
|
||||||
|
"release-beta": "node extra/beta/update-version.js && npm run build && node ./extra/env2arg.js docker buildx build -f docker/dockerfile --platform linux/amd64,linux/arm64,linux/arm/v7 -t louislam/uptime-kuma:$VERSION -t louislam/uptime-kuma:beta . --target release --push && node ./extra/press-any-key.js && npm run upload-artifacts",
|
||||||
|
"git-remove-tag": "git tag -d",
|
||||||
|
"build-dist-and-restart": "npm run build && npm run start-server-dev",
|
||||||
|
"start-pr-test": "node extra/checkout-pr.js && npm install && npm run dev",
|
||||||
|
"cy:test": "node test/prepare-test-server.js && node server/server.js --port=3002 --data-dir=./data/test/ --e2e",
|
||||||
|
"cy:run": "npx cypress run --browser chrome --headless"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@fortawesome/fontawesome-svg-core": "~1.2.36",
|
"@louislam/sqlite3": "~15.0.6",
|
||||||
"@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",
|
|
||||||
"@popperjs/core": "~2.10.2",
|
|
||||||
"args-parser": "~1.3.0",
|
"args-parser": "~1.3.0",
|
||||||
"axios": "~0.21.4",
|
"axios": "~0.27.0",
|
||||||
|
"axios-ntlm": "^1.3.0",
|
||||||
|
"badge-maker": "^3.3.1",
|
||||||
"bcryptjs": "~2.4.3",
|
"bcryptjs": "~2.4.3",
|
||||||
"bootstrap": "~5.1.1",
|
"bree": "~7.1.5",
|
||||||
|
"cacheable-lookup": "~6.0.4",
|
||||||
"chardet": "^1.3.0",
|
"chardet": "^1.3.0",
|
||||||
"bree": "~6.3.1",
|
"check-password-strength": "^2.0.5",
|
||||||
"chart.js": "~3.5.1",
|
"cheerio": "^1.0.0-rc.10",
|
||||||
"chartjs-adapter-dayjs": "~1.0.0",
|
"chroma-js": "^2.1.2",
|
||||||
"command-exists": "~1.2.9",
|
"command-exists": "~1.2.9",
|
||||||
"compare-versions": "~3.6.0",
|
"compare-versions": "~3.6.0",
|
||||||
"dayjs": "~1.10.7",
|
"compression": "^1.7.4",
|
||||||
"express": "~4.17.1",
|
"dayjs": "^1.11.0",
|
||||||
"express-basic-auth": "~1.2.0",
|
"express": "~4.17.3",
|
||||||
|
"express-basic-auth": "~1.2.1",
|
||||||
|
"express-static-gzip": "^2.1.7",
|
||||||
"form-data": "~4.0.0",
|
"form-data": "~4.0.0",
|
||||||
"http-graceful-shutdown": "~3.1.4",
|
"http-graceful-shutdown": "~3.1.7",
|
||||||
|
"http-proxy-agent": "^5.0.0",
|
||||||
|
"https-proxy-agent": "^5.0.0",
|
||||||
"iconv-lite": "^0.6.3",
|
"iconv-lite": "^0.6.3",
|
||||||
"jsonwebtoken": "~8.5.1",
|
"jsonwebtoken": "~8.5.1",
|
||||||
|
"jwt-decode": "^3.1.2",
|
||||||
|
"limiter": "^2.1.0",
|
||||||
|
"mqtt": "^4.2.8",
|
||||||
|
"mssql": "^8.1.0",
|
||||||
|
"node-cloudflared-tunnel": "~1.0.9",
|
||||||
|
"node-radius-client": "^1.0.0",
|
||||||
"nodemailer": "~6.6.5",
|
"nodemailer": "~6.6.5",
|
||||||
"notp": "~2.0.3",
|
"notp": "~2.0.3",
|
||||||
"password-hash": "~1.2.2",
|
"password-hash": "~1.2.2",
|
||||||
"postcss-rtlcss": "~3.4.1",
|
"pg": "^8.7.3",
|
||||||
"postcss-scss": "~4.0.1",
|
"pg-connection-string": "^2.5.0",
|
||||||
"prom-client": "~13.2.0",
|
"prom-client": "~13.2.0",
|
||||||
"prometheus-api-metrics": "~3.2.0",
|
"prometheus-api-metrics": "~3.2.1",
|
||||||
"qrcode": "~1.4.4",
|
"redbean-node": "0.1.4",
|
||||||
"redbean-node": "0.1.2",
|
"socket.io": "~4.4.1",
|
||||||
"socket.io": "~4.2.0",
|
"socket.io-client": "~4.4.1",
|
||||||
"socket.io-client": "~4.2.0",
|
"socks-proxy-agent": "6.1.1",
|
||||||
"tar": "^6.1.11",
|
"tar": "^6.1.11",
|
||||||
"tcp-ping": "~0.1.1",
|
"tcp-ping": "~0.1.1",
|
||||||
"thirty-two": "~1.0.2",
|
"thirty-two": "~1.0.2"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@actions/github": "~5.0.1",
|
||||||
|
"@babel/eslint-parser": "~7.17.0",
|
||||||
|
"@babel/preset-env": "^7.15.8",
|
||||||
|
"@fortawesome/fontawesome-svg-core": "~1.2.36",
|
||||||
|
"@fortawesome/free-regular-svg-icons": "~5.15.4",
|
||||||
|
"@fortawesome/free-solid-svg-icons": "~5.15.4",
|
||||||
|
"@fortawesome/vue-fontawesome": "~3.0.0-5",
|
||||||
|
"@popperjs/core": "~2.10.2",
|
||||||
|
"@types/bootstrap": "~5.1.9",
|
||||||
|
"@vitejs/plugin-legacy": "~2.1.0",
|
||||||
|
"@vitejs/plugin-vue": "~3.1.0",
|
||||||
|
"@vue/compiler-sfc": "~3.2.36",
|
||||||
|
"aedes": "^0.46.3",
|
||||||
|
"babel-plugin-rewire": "~1.2.0",
|
||||||
|
"bootstrap": "5.1.3",
|
||||||
|
"chart.js": "~3.6.2",
|
||||||
|
"chartjs-adapter-dayjs": "~1.0.0",
|
||||||
|
"concurrently": "^7.1.0",
|
||||||
|
"core-js": "~3.18.3",
|
||||||
|
"cross-env": "~7.0.3",
|
||||||
|
"cypress": "^10.1.0",
|
||||||
|
"delay": "^5.0.0",
|
||||||
|
"dns2": "~2.0.1",
|
||||||
|
"eslint": "~8.14.0",
|
||||||
|
"eslint-plugin-vue": "~8.7.1",
|
||||||
|
"favico.js": "^0.3.10",
|
||||||
|
"jest": "~27.2.5",
|
||||||
|
"jest-puppeteer": "~6.0.3",
|
||||||
|
"postcss-html": "~1.5.0",
|
||||||
|
"postcss-rtlcss": "~3.7.2",
|
||||||
|
"postcss-scss": "~4.0.4",
|
||||||
|
"prismjs": "^1.27.0",
|
||||||
|
"puppeteer": "~13.1.3",
|
||||||
|
"qrcode": "~1.5.0",
|
||||||
|
"rollup-plugin-visualizer": "^5.6.0",
|
||||||
|
"sass": "~1.42.1",
|
||||||
|
"stylelint": "~14.7.1",
|
||||||
|
"stylelint-config-standard": "~25.0.0",
|
||||||
|
"terser": "^5.15.0",
|
||||||
"timezones-list": "~3.0.1",
|
"timezones-list": "~3.0.1",
|
||||||
"v-pagination-3": "~0.1.6",
|
"typescript": "~4.4.4",
|
||||||
|
"v-pagination-3": "~0.1.7",
|
||||||
|
"vite": "~3.1.0",
|
||||||
|
"vite-plugin-compression": "^0.5.1",
|
||||||
"vue": "next",
|
"vue": "next",
|
||||||
"vue-chart-3": "~0.5.8",
|
"vue-chart-3": "3.0.9",
|
||||||
"vue-confirm-dialog": "~1.0.2",
|
"vue-confirm-dialog": "~1.0.2",
|
||||||
"vue-contenteditable": "~3.0.4",
|
"vue-contenteditable": "~3.0.4",
|
||||||
"vue-i18n": "~9.1.9",
|
"vue-i18n": "~9.1.9",
|
||||||
"vue-image-crop-upload": "~3.0.3",
|
"vue-image-crop-upload": "~3.0.3",
|
||||||
"vue-multiselect": "~3.0.0-alpha.2",
|
"vue-multiselect": "~3.0.0-alpha.2",
|
||||||
|
"vue-prism-editor": "^2.0.0-alpha.2",
|
||||||
"vue-qrcode": "~1.0.0",
|
"vue-qrcode": "~1.0.0",
|
||||||
"vue-router": "~4.0.11",
|
"vue-router": "~4.0.14",
|
||||||
"vue-toastification": "~2.0.0-rc.1",
|
"vue-toastification": "~2.0.0-rc.5",
|
||||||
"vuedraggable": "~4.1.0"
|
"vuedraggable": "~4.1.0",
|
||||||
},
|
"wait-on": "^6.0.1"
|
||||||
"devDependencies": {
|
|
||||||
"@babel/eslint-parser": "~7.15.7",
|
|
||||||
"@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",
|
|
||||||
"babel-plugin-rewire": "~1.2.0",
|
|
||||||
"core-js": "~3.18.1",
|
|
||||||
"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",
|
|
||||||
"sass": "~1.42.1",
|
|
||||||
"stylelint": "~13.13.1",
|
|
||||||
"stylelint-config-standard": "~22.0.0",
|
|
||||||
"typescript": "~4.4.3",
|
|
||||||
"vite": "~2.6.4"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
File diff suppressed because one or more lines are too long
Before Width: | Height: | Size: 6.4 KiB After Width: | Height: | Size: 893 B |
18
server/2fa.js
Normal file
18
server/2fa.js
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
const { R } = require("redbean-node");
|
||||||
|
|
||||||
|
class TwoFA {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Disable 2FA for specified user
|
||||||
|
* @param {number} userID ID of user to disable
|
||||||
|
* @returns {Promise<void>}
|
||||||
|
*/
|
||||||
|
static async disable2FA(userID) {
|
||||||
|
return await R.exec("UPDATE `user` SET twofa_status = 0 WHERE id = ? ", [
|
||||||
|
userID,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = TwoFA;
|
@@ -1,19 +1,23 @@
|
|||||||
const basicAuth = require("express-basic-auth")
|
const basicAuth = require("express-basic-auth");
|
||||||
const passwordHash = require("./password-hash");
|
const passwordHash = require("./password-hash");
|
||||||
const { R } = require("redbean-node");
|
const { R } = require("redbean-node");
|
||||||
const { setting } = require("./util-server");
|
const { setting } = require("./util-server");
|
||||||
const { debug } = require("../src/util");
|
const { loginRateLimiter } = require("./rate-limiter");
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
* Login to web app
|
||||||
* @param username : string
|
* @param {string} username
|
||||||
* @param password : string
|
* @param {string} password
|
||||||
* @returns {Promise<Bean|null>}
|
* @returns {Promise<(Bean|null)>}
|
||||||
*/
|
*/
|
||||||
exports.login = async function (username, password) {
|
exports.login = async function (username, password) {
|
||||||
|
if (typeof username !== "string" || typeof password !== "string") {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
let user = await R.findOne("user", " username = ? AND active = 1 ", [
|
let user = await R.findOne("user", " username = ? AND active = 1 ", [
|
||||||
username,
|
username,
|
||||||
])
|
]);
|
||||||
|
|
||||||
if (user && passwordHash.verify(password, user.password)) {
|
if (user && passwordHash.verify(password, user.password)) {
|
||||||
// Upgrade the hash to bcrypt
|
// Upgrade the hash to bcrypt
|
||||||
@@ -27,25 +31,50 @@ exports.login = async function (username, password) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Callback for myAuthorizer
|
||||||
|
* @callback myAuthorizerCB
|
||||||
|
* @param {any} err Any error encountered
|
||||||
|
* @param {boolean} authorized Is the client authorized?
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Custom authorizer for express-basic-auth
|
||||||
|
* @param {string} username
|
||||||
|
* @param {string} password
|
||||||
|
* @param {myAuthorizerCB} callback
|
||||||
|
*/
|
||||||
function myAuthorizer(username, password, callback) {
|
function myAuthorizer(username, password, callback) {
|
||||||
|
// Login Rate Limit
|
||||||
setting("disableAuth").then((result) => {
|
loginRateLimiter.pass(null, 0).then((pass) => {
|
||||||
|
if (pass) {
|
||||||
if (result) {
|
|
||||||
callback(null, true)
|
|
||||||
} else {
|
|
||||||
exports.login(username, password).then((user) => {
|
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({
|
exports.basicAuth = async function (req, res, next) {
|
||||||
authorizer: myAuthorizer,
|
const middleware = basicAuth({
|
||||||
authorizeAsync: true,
|
authorizer: myAuthorizer,
|
||||||
challenge: true,
|
authorizeAsync: true,
|
||||||
});
|
challenge: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
const disabledAuth = await setting("disableAuth");
|
||||||
|
|
||||||
|
if (!disabledAuth) {
|
||||||
|
middleware(req, res, next);
|
||||||
|
} else {
|
||||||
|
next();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
54
server/cacheable-dns-http-agent.js
Normal file
54
server/cacheable-dns-http-agent.js
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
const https = require("https");
|
||||||
|
const http = require("http");
|
||||||
|
const CacheableLookup = require("cacheable-lookup");
|
||||||
|
|
||||||
|
class CacheableDnsHttpAgent {
|
||||||
|
|
||||||
|
static cacheable = new CacheableLookup();
|
||||||
|
|
||||||
|
static httpAgentList = {};
|
||||||
|
static httpsAgentList = {};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Register cacheable to global agents
|
||||||
|
*/
|
||||||
|
static registerGlobalAgent() {
|
||||||
|
this.cacheable.install(http.globalAgent);
|
||||||
|
this.cacheable.install(https.globalAgent);
|
||||||
|
}
|
||||||
|
|
||||||
|
static install(agent) {
|
||||||
|
this.cacheable.install(agent);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var {https.AgentOptions} agentOptions
|
||||||
|
* @return {https.Agent}
|
||||||
|
*/
|
||||||
|
static getHttpsAgent(agentOptions) {
|
||||||
|
let key = JSON.stringify(agentOptions);
|
||||||
|
if (!(key in this.httpsAgentList)) {
|
||||||
|
this.httpsAgentList[key] = new https.Agent(agentOptions);
|
||||||
|
this.cacheable.install(this.httpsAgentList[key]);
|
||||||
|
}
|
||||||
|
return this.httpsAgentList[key];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var {http.AgentOptions} agentOptions
|
||||||
|
* @return {https.Agents}
|
||||||
|
*/
|
||||||
|
static getHttpAgent(agentOptions) {
|
||||||
|
let key = JSON.stringify(agentOptions);
|
||||||
|
if (!(key in this.httpAgentList)) {
|
||||||
|
this.httpAgentList[key] = new http.Agent(agentOptions);
|
||||||
|
this.cacheable.install(this.httpAgentList[key]);
|
||||||
|
}
|
||||||
|
return this.httpAgentList[key];
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
CacheableDnsHttpAgent,
|
||||||
|
};
|
@@ -1,26 +1,40 @@
|
|||||||
const { setSetting } = require("./util-server");
|
const { setSetting, setting } = require("./util-server");
|
||||||
const axios = require("axios");
|
const axios = require("axios");
|
||||||
|
const compareVersions = require("compare-versions");
|
||||||
|
|
||||||
exports.version = require("../package.json").version;
|
exports.version = require("../package.json").version;
|
||||||
exports.latestVersion = null;
|
exports.latestVersion = null;
|
||||||
|
|
||||||
let interval;
|
let interval;
|
||||||
|
|
||||||
|
/** Start 48 hour check interval */
|
||||||
exports.startInterval = () => {
|
exports.startInterval = () => {
|
||||||
let check = async () => {
|
let check = async () => {
|
||||||
try {
|
try {
|
||||||
const res = await axios.get("https://raw.githubusercontent.com/louislam/uptime-kuma/master/package.json");
|
const res = await axios.get("https://uptime.kuma.pet/version");
|
||||||
|
|
||||||
if (typeof res.data === "string") {
|
|
||||||
res.data = JSON.parse(res.data);
|
|
||||||
}
|
|
||||||
|
|
||||||
// For debug
|
// For debug
|
||||||
if (process.env.TEST_CHECK_VERSION === "1") {
|
if (process.env.TEST_CHECK_VERSION === "1") {
|
||||||
res.data.version = "1000.0.0";
|
res.data.slow = "1000.0.0";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (await setting("checkUpdate") === false) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let checkBeta = await setting("checkBeta");
|
||||||
|
|
||||||
|
if (checkBeta && res.data.beta) {
|
||||||
|
if (compareVersions.compare(res.data.beta, res.data.beta, ">")) {
|
||||||
|
exports.latestVersion = res.data.beta;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (res.data.slow) {
|
||||||
|
exports.latestVersion = res.data.slow;
|
||||||
}
|
}
|
||||||
|
|
||||||
exports.latestVersion = res.data.version;
|
|
||||||
} catch (_) { }
|
} catch (_) { }
|
||||||
|
|
||||||
};
|
};
|
||||||
@@ -29,6 +43,11 @@ exports.startInterval = () => {
|
|||||||
interval = setInterval(check, 3600 * 1000 * 48);
|
interval = setInterval(check, 3600 * 1000 * 48);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Enable the check update feature
|
||||||
|
* @param {boolean} value Should the check update feature be enabled?
|
||||||
|
* @returns {Promise<void>}
|
||||||
|
*/
|
||||||
exports.enableCheckUpdate = async (value) => {
|
exports.enableCheckUpdate = async (value) => {
|
||||||
await setSetting("checkUpdate", value);
|
await setSetting("checkUpdate", value);
|
||||||
|
|
||||||
|
@@ -3,10 +3,16 @@
|
|||||||
*/
|
*/
|
||||||
const { TimeLogger } = require("../src/util");
|
const { TimeLogger } = require("../src/util");
|
||||||
const { R } = require("redbean-node");
|
const { R } = require("redbean-node");
|
||||||
const { io } = require("./server");
|
const { UptimeKumaServer } = require("./uptime-kuma-server");
|
||||||
|
const io = UptimeKumaServer.getInstance().io;
|
||||||
const { setting } = require("./util-server");
|
const { setting } = require("./util-server");
|
||||||
const checkVersion = require("./check-version");
|
const checkVersion = require("./check-version");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send list of notification providers to client
|
||||||
|
* @param {Socket} socket Socket.io socket instance
|
||||||
|
* @returns {Promise<Bean[]>}
|
||||||
|
*/
|
||||||
async function sendNotificationList(socket) {
|
async function sendNotificationList(socket) {
|
||||||
const timeLogger = new TimeLogger();
|
const timeLogger = new TimeLogger();
|
||||||
|
|
||||||
@@ -16,7 +22,10 @@ async function sendNotificationList(socket) {
|
|||||||
]);
|
]);
|
||||||
|
|
||||||
for (let bean of list) {
|
for (let bean of list) {
|
||||||
result.push(bean.export());
|
let notificationObject = bean.export();
|
||||||
|
notificationObject.isDefault = (notificationObject.isDefault === 1);
|
||||||
|
notificationObject.active = (notificationObject.active === 1);
|
||||||
|
result.push(notificationObject);
|
||||||
}
|
}
|
||||||
|
|
||||||
io.to(socket.userID).emit("notificationList", result);
|
io.to(socket.userID).emit("notificationList", result);
|
||||||
@@ -28,8 +37,11 @@ async function sendNotificationList(socket) {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Send Heartbeat History list to socket
|
* Send Heartbeat History list to socket
|
||||||
* @param toUser True = send to all browsers with the same user id, False = send to the current browser only
|
* @param {Socket} socket Socket.io instance
|
||||||
* @param overwrite Overwrite client-side's heartbeat list
|
* @param {number} monitorID ID of monitor to send heartbeat history
|
||||||
|
* @param {boolean} [toUser=false] True = send to all browsers with the same user id, False = send to the current browser only
|
||||||
|
* @param {boolean} [overwrite=false] Overwrite client-side's heartbeat list
|
||||||
|
* @returns {Promise<void>}
|
||||||
*/
|
*/
|
||||||
async function sendHeartbeatList(socket, monitorID, toUser = false, overwrite = false) {
|
async function sendHeartbeatList(socket, monitorID, toUser = false, overwrite = false) {
|
||||||
const timeLogger = new TimeLogger();
|
const timeLogger = new TimeLogger();
|
||||||
@@ -55,11 +67,12 @@ async function sendHeartbeatList(socket, monitorID, toUser = false, overwrite =
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Important Heart beat list (aka event list)
|
* Important Heart beat list (aka event list)
|
||||||
* @param socket
|
* @param {Socket} socket Socket.io instance
|
||||||
* @param monitorID
|
* @param {number} monitorID ID of monitor to send heartbeat history
|
||||||
* @param toUser True = send to all browsers with the same user id, False = send to the current browser only
|
* @param {boolean} [toUser=false] True = send to all browsers with the same user id, False = send to the current browser only
|
||||||
* @param overwrite Overwrite client-side's heartbeat list
|
* @param {boolean} [overwrite=false] Overwrite client-side's heartbeat list
|
||||||
|
* @returns {Promise<void>}
|
||||||
*/
|
*/
|
||||||
async function sendImportantHeartbeatList(socket, monitorID, toUser = false, overwrite = false) {
|
async function sendImportantHeartbeatList(socket, monitorID, toUser = false, overwrite = false) {
|
||||||
const timeLogger = new TimeLogger();
|
const timeLogger = new TimeLogger();
|
||||||
@@ -83,6 +96,27 @@ async function sendImportantHeartbeatList(socket, monitorID, toUser = false, ove
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Emit proxy list to client
|
||||||
|
* @param {Socket} socket Socket.io socket instance
|
||||||
|
* @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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Emits the version information to the client.
|
||||||
|
* @param {Socket} socket Socket.io socket instance
|
||||||
|
* @returns {Promise<void>}
|
||||||
|
*/
|
||||||
async function sendInfo(socket) {
|
async function sendInfo(socket) {
|
||||||
socket.emit("info", {
|
socket.emit("info", {
|
||||||
version: checkVersion.version,
|
version: checkVersion.version,
|
||||||
@@ -91,10 +125,35 @@ async function sendInfo(socket) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send list of docker hosts to client
|
||||||
|
* @param {Socket} socket Socket.io socket instance
|
||||||
|
* @returns {Promise<Bean[]>}
|
||||||
|
*/
|
||||||
|
async function sendDockerHostList(socket) {
|
||||||
|
const timeLogger = new TimeLogger();
|
||||||
|
|
||||||
|
let result = [];
|
||||||
|
let list = await R.find("docker_host", " user_id = ? ", [
|
||||||
|
socket.userID,
|
||||||
|
]);
|
||||||
|
|
||||||
|
for (let bean of list) {
|
||||||
|
result.push(bean.toJSON());
|
||||||
|
}
|
||||||
|
|
||||||
|
io.to(socket.userID).emit("dockerHostList", result);
|
||||||
|
|
||||||
|
timeLogger.print("Send Docker Host List");
|
||||||
|
|
||||||
|
return list;
|
||||||
|
}
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
sendNotificationList,
|
sendNotificationList,
|
||||||
sendImportantHeartbeatList,
|
sendImportantHeartbeatList,
|
||||||
sendHeartbeatList,
|
sendHeartbeatList,
|
||||||
sendInfo
|
sendProxyList,
|
||||||
|
sendInfo,
|
||||||
|
sendDockerHostList
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@@ -1,7 +1,20 @@
|
|||||||
const args = require("args-parser")(process.argv);
|
const args = require("args-parser")(process.argv);
|
||||||
const demoMode = args["demo"] || false;
|
const demoMode = args["demo"] || false;
|
||||||
|
|
||||||
|
const badgeConstants = {
|
||||||
|
naColor: "#999",
|
||||||
|
defaultUpColor: "#66c20a",
|
||||||
|
defaultDownColor: "#c2290a",
|
||||||
|
defaultPingColor: "blue", // as defined by badge-maker / shields.io
|
||||||
|
defaultStyle: "flat",
|
||||||
|
defaultPingValueSuffix: "ms",
|
||||||
|
defaultPingLabelSuffix: "h",
|
||||||
|
defaultUptimeValueSuffix: "%",
|
||||||
|
defaultUptimeLabelSuffix: "h",
|
||||||
|
};
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
args,
|
args,
|
||||||
demoMode
|
demoMode,
|
||||||
|
badgeConstants,
|
||||||
};
|
};
|
||||||
|
@@ -1,7 +1,7 @@
|
|||||||
const fs = require("fs");
|
const fs = require("fs");
|
||||||
const { R } = require("redbean-node");
|
const { R } = require("redbean-node");
|
||||||
const { setSetting, setting } = require("./util-server");
|
const { setSetting, setting } = require("./util-server");
|
||||||
const { debug, sleep } = require("../src/util");
|
const { log, sleep } = require("../src/util");
|
||||||
const dayjs = require("dayjs");
|
const dayjs = require("dayjs");
|
||||||
const knex = require("knex");
|
const knex = require("knex");
|
||||||
|
|
||||||
@@ -50,7 +50,21 @@ class Database {
|
|||||||
"patch-group-table.sql": true,
|
"patch-group-table.sql": true,
|
||||||
"patch-monitor-push_token.sql": true,
|
"patch-monitor-push_token.sql": true,
|
||||||
"patch-http-monitor-method-body-and-headers.sql": true,
|
"patch-http-monitor-method-body-and-headers.sql": true,
|
||||||
}
|
"patch-2fa-invalidate-used-token.sql": true,
|
||||||
|
"patch-notification_sent_history.sql": true,
|
||||||
|
"patch-monitor-basic-auth.sql": true,
|
||||||
|
"patch-add-docker-columns.sql": true,
|
||||||
|
"patch-status-page.sql": true,
|
||||||
|
"patch-proxy.sql": true,
|
||||||
|
"patch-monitor-expiry-notification.sql": true,
|
||||||
|
"patch-status-page-footer-css.sql": true,
|
||||||
|
"patch-added-mqtt-monitor.sql": true,
|
||||||
|
"patch-add-clickable-status-page-link.sql": true,
|
||||||
|
"patch-add-sqlserver-monitor.sql": true,
|
||||||
|
"patch-add-other-auth.sql": { parents: [ "patch-monitor-basic-auth.sql" ] },
|
||||||
|
"patch-add-radius-monitor.sql": true,
|
||||||
|
"patch-monitor-add-resend-interval.sql": true,
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The final version should be 10 after merged tag feature
|
* The final version should be 10 after merged tag feature
|
||||||
@@ -60,6 +74,10 @@ class Database {
|
|||||||
|
|
||||||
static noReject = true;
|
static noReject = true;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize the database
|
||||||
|
* @param {Object} args Arguments to initialize DB with
|
||||||
|
*/
|
||||||
static init(args) {
|
static init(args) {
|
||||||
// Data Directory (must be end with "/")
|
// Data Directory (must be end with "/")
|
||||||
Database.dataDir = process.env.DATA_DIR || args["data-dir"] || "./data/";
|
Database.dataDir = process.env.DATA_DIR || args["data-dir"] || "./data/";
|
||||||
@@ -74,10 +92,19 @@ class Database {
|
|||||||
fs.mkdirSync(Database.uploadDir, { recursive: true });
|
fs.mkdirSync(Database.uploadDir, { recursive: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(`Data Dir: ${Database.dataDir}`);
|
log.info("db", `Data Dir: ${Database.dataDir}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
static async connect() {
|
/**
|
||||||
|
* Connect to the database
|
||||||
|
* @param {boolean} [testMode=false] Should the connection be
|
||||||
|
* started in test mode?
|
||||||
|
* @param {boolean} [autoloadModels=true] Should models be
|
||||||
|
* automatically loaded?
|
||||||
|
* @param {boolean} [noLog=false] Should logs not be output?
|
||||||
|
* @returns {Promise<void>}
|
||||||
|
*/
|
||||||
|
static async connect(testMode = false, autoloadModels = true, noLog = false) {
|
||||||
const acquireConnectionTimeout = 120 * 1000;
|
const acquireConnectionTimeout = 120 * 1000;
|
||||||
|
|
||||||
const Dialect = require("knex/lib/dialects/sqlite3/index.js");
|
const Dialect = require("knex/lib/dialects/sqlite3/index.js");
|
||||||
@@ -107,19 +134,39 @@ class Database {
|
|||||||
|
|
||||||
// Auto map the model to a bean object
|
// Auto map the model to a bean object
|
||||||
R.freeze(true);
|
R.freeze(true);
|
||||||
await R.autoloadModels("./server/model");
|
|
||||||
|
if (autoloadModels) {
|
||||||
|
await R.autoloadModels("./server/model");
|
||||||
|
}
|
||||||
|
|
||||||
await R.exec("PRAGMA foreign_keys = ON");
|
await R.exec("PRAGMA foreign_keys = ON");
|
||||||
// Change to WAL
|
if (testMode) {
|
||||||
await R.exec("PRAGMA journal_mode = WAL");
|
// Change to MEMORY
|
||||||
|
await R.exec("PRAGMA journal_mode = MEMORY");
|
||||||
|
} else {
|
||||||
|
// Change to WAL
|
||||||
|
await R.exec("PRAGMA journal_mode = WAL");
|
||||||
|
}
|
||||||
await R.exec("PRAGMA cache_size = -12000");
|
await R.exec("PRAGMA cache_size = -12000");
|
||||||
|
await R.exec("PRAGMA auto_vacuum = FULL");
|
||||||
|
|
||||||
console.log("SQLite config:");
|
// Avoid error "SQLITE_BUSY: database is locked" by allowing SQLITE to wait up to 5 seconds to do a write
|
||||||
console.log(await R.getAll("PRAGMA journal_mode"));
|
await R.exec("PRAGMA busy_timeout = 5000");
|
||||||
console.log(await R.getAll("PRAGMA cache_size"));
|
|
||||||
console.log("SQLite Version: " + await R.getCell("SELECT sqlite_version()"));
|
// This ensures that an operating system crash or power failure will not corrupt the database.
|
||||||
|
// FULL synchronous is very safe, but it is also slower.
|
||||||
|
// Read more: https://sqlite.org/pragma.html#pragma_synchronous
|
||||||
|
await R.exec("PRAGMA synchronous = FULL");
|
||||||
|
|
||||||
|
if (!noLog) {
|
||||||
|
log.info("db", "SQLite config:");
|
||||||
|
log.info("db", await R.getAll("PRAGMA journal_mode"));
|
||||||
|
log.info("db", await R.getAll("PRAGMA cache_size"));
|
||||||
|
log.info("db", "SQLite Version: " + await R.getCell("SELECT sqlite_version()"));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Patch the database */
|
||||||
static async patch() {
|
static async patch() {
|
||||||
let version = parseInt(await setting("database_version"));
|
let version = parseInt(await setting("database_version"));
|
||||||
|
|
||||||
@@ -127,33 +174,39 @@ class Database {
|
|||||||
version = 0;
|
version = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
console.info("Your database version: " + version);
|
log.info("db", "Your database version: " + version);
|
||||||
console.info("Latest database version: " + this.latestVersion);
|
log.info("db", "Latest database version: " + this.latestVersion);
|
||||||
|
|
||||||
if (version === this.latestVersion) {
|
if (version === this.latestVersion) {
|
||||||
console.info("Database no need to patch");
|
log.info("db", "Database patch not needed");
|
||||||
} else if (version > this.latestVersion) {
|
} else if (version > this.latestVersion) {
|
||||||
console.info("Warning: Database version is newer than expected");
|
log.info("db", "Warning: Database version is newer than expected");
|
||||||
} else {
|
} else {
|
||||||
console.info("Database patch is needed");
|
log.info("db", "Database patch is needed");
|
||||||
|
|
||||||
this.backup(version);
|
try {
|
||||||
|
this.backup(version);
|
||||||
|
} catch (e) {
|
||||||
|
log.error("db", e);
|
||||||
|
log.error("db", "Unable to create a backup before patching the database. Please make sure you have enough space and permission.");
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
// Try catch anything here, if gone wrong, restore the backup
|
// Try catch anything here, if gone wrong, restore the backup
|
||||||
try {
|
try {
|
||||||
for (let i = version + 1; i <= this.latestVersion; i++) {
|
for (let i = version + 1; i <= this.latestVersion; i++) {
|
||||||
const sqlFile = `./db/patch${i}.sql`;
|
const sqlFile = `./db/patch${i}.sql`;
|
||||||
console.info(`Patching ${sqlFile}`);
|
log.info("db", `Patching ${sqlFile}`);
|
||||||
await Database.importSQLFile(sqlFile);
|
await Database.importSQLFile(sqlFile);
|
||||||
console.info(`Patched ${sqlFile}`);
|
log.info("db", `Patched ${sqlFile}`);
|
||||||
await setSetting("database_version", i);
|
await setSetting("database_version", i);
|
||||||
}
|
}
|
||||||
} catch (ex) {
|
} catch (ex) {
|
||||||
await Database.close();
|
await Database.close();
|
||||||
|
|
||||||
console.error(ex);
|
log.error("db", ex);
|
||||||
console.error("Start Uptime-Kuma failed due to patch db failed");
|
log.error("db", "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");
|
log.error("db", "Please submit a bug report if you still encounter the problem after restart: https://github.com/louislam/uptime-kuma/issues");
|
||||||
|
|
||||||
this.restore();
|
this.restore();
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
@@ -161,22 +214,25 @@ class Database {
|
|||||||
}
|
}
|
||||||
|
|
||||||
await this.patch2();
|
await this.patch2();
|
||||||
|
await this.migrateNewStatusPage();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
* Patch DB using new process
|
||||||
* Call it from patch() only
|
* Call it from patch() only
|
||||||
|
* @private
|
||||||
* @returns {Promise<void>}
|
* @returns {Promise<void>}
|
||||||
*/
|
*/
|
||||||
static async patch2() {
|
static async patch2() {
|
||||||
console.log("Database Patch 2.0 Process");
|
log.info("db", "Database Patch 2.0 Process");
|
||||||
let databasePatchedFiles = await setting("databasePatchedFiles");
|
let databasePatchedFiles = await setting("databasePatchedFiles");
|
||||||
|
|
||||||
if (! databasePatchedFiles) {
|
if (! databasePatchedFiles) {
|
||||||
databasePatchedFiles = {};
|
databasePatchedFiles = {};
|
||||||
}
|
}
|
||||||
|
|
||||||
debug("Patched files:");
|
log.debug("db", "Patched files:");
|
||||||
debug(databasePatchedFiles);
|
log.debug("db", databasePatchedFiles);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
for (let sqlFilename in this.patchList) {
|
for (let sqlFilename in this.patchList) {
|
||||||
@@ -184,15 +240,15 @@ class Database {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (this.patched) {
|
if (this.patched) {
|
||||||
console.log("Database Patched Successfully");
|
log.info("db", "Database Patched Successfully");
|
||||||
}
|
}
|
||||||
|
|
||||||
} catch (ex) {
|
} catch (ex) {
|
||||||
await Database.close();
|
await Database.close();
|
||||||
|
|
||||||
console.error(ex);
|
log.error("db", ex);
|
||||||
console.error("Start Uptime-Kuma failed due to patch db failed");
|
log.error("db", "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");
|
log.error("db", "Please submit the bug report if you still encounter the problem after restart: https://github.com/louislam/uptime-kuma/issues");
|
||||||
|
|
||||||
this.restore();
|
this.restore();
|
||||||
|
|
||||||
@@ -203,24 +259,95 @@ class Database {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
* 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");
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Patch database using new patching process
|
||||||
* Used it patch2() only
|
* Used it patch2() only
|
||||||
|
* @private
|
||||||
* @param sqlFilename
|
* @param sqlFilename
|
||||||
* @param databasePatchedFiles
|
* @param databasePatchedFiles
|
||||||
|
* @returns {Promise<void>}
|
||||||
*/
|
*/
|
||||||
static async patch2Recursion(sqlFilename, databasePatchedFiles) {
|
static async patch2Recursion(sqlFilename, databasePatchedFiles) {
|
||||||
let value = this.patchList[sqlFilename];
|
let value = this.patchList[sqlFilename];
|
||||||
|
|
||||||
if (! value) {
|
if (! value) {
|
||||||
console.log(sqlFilename + " skip");
|
log.info("db", sqlFilename + " skip");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if patched
|
// Check if patched
|
||||||
if (! databasePatchedFiles[sqlFilename]) {
|
if (! databasePatchedFiles[sqlFilename]) {
|
||||||
console.log(sqlFilename + " is not patched");
|
log.info("db", sqlFilename + " is not patched");
|
||||||
|
|
||||||
if (value.parents) {
|
if (value.parents) {
|
||||||
console.log(sqlFilename + " need parents");
|
log.info("db", sqlFilename + " need parents");
|
||||||
for (let parentSQLFilename of value.parents) {
|
for (let parentSQLFilename of value.parents) {
|
||||||
await this.patch2Recursion(parentSQLFilename, databasePatchedFiles);
|
await this.patch2Recursion(parentSQLFilename, databasePatchedFiles);
|
||||||
}
|
}
|
||||||
@@ -228,24 +355,24 @@ class Database {
|
|||||||
|
|
||||||
this.backup(dayjs().format("YYYYMMDDHHmmss"));
|
this.backup(dayjs().format("YYYYMMDDHHmmss"));
|
||||||
|
|
||||||
console.log(sqlFilename + " is patching");
|
log.info("db", sqlFilename + " is patching");
|
||||||
this.patched = true;
|
this.patched = true;
|
||||||
await this.importSQLFile("./db/" + sqlFilename);
|
await this.importSQLFile("./db/" + sqlFilename);
|
||||||
databasePatchedFiles[sqlFilename] = true;
|
databasePatchedFiles[sqlFilename] = true;
|
||||||
console.log(sqlFilename + " is patched successfully");
|
log.info("db", sqlFilename + " was patched successfully");
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
debug(sqlFilename + " is already patched, skip");
|
log.debug("db", sqlFilename + " is already patched, skip");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sadly, multi sql statements is not supported by many sqlite libraries, I have to implement it myself
|
* Load an SQL file and execute it
|
||||||
* @param filename
|
* @param filename Filename of SQL file to import
|
||||||
* @returns {Promise<void>}
|
* @returns {Promise<void>}
|
||||||
*/
|
*/
|
||||||
static async importSQLFile(filename) {
|
static async importSQLFile(filename) {
|
||||||
|
// Sadly, multi sql statements is not supported by many sqlite libraries, I have to implement it myself
|
||||||
await R.getCell("SELECT 1");
|
await R.getCell("SELECT 1");
|
||||||
|
|
||||||
let text = fs.readFileSync(filename).toString();
|
let text = fs.readFileSync(filename).toString();
|
||||||
@@ -273,6 +400,10 @@ class Database {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Aquire a direct connection to database
|
||||||
|
* @returns {any}
|
||||||
|
*/
|
||||||
static getBetterSQLite3Database() {
|
static getBetterSQLite3Database() {
|
||||||
return R.knex.client.acquireConnection();
|
return R.knex.client.acquireConnection();
|
||||||
}
|
}
|
||||||
@@ -287,7 +418,7 @@ class Database {
|
|||||||
};
|
};
|
||||||
process.addListener("unhandledRejection", listener);
|
process.addListener("unhandledRejection", listener);
|
||||||
|
|
||||||
console.log("Closing DB");
|
log.info("db", "Closing the database");
|
||||||
|
|
||||||
while (true) {
|
while (true) {
|
||||||
Database.noReject = true;
|
Database.noReject = true;
|
||||||
@@ -297,10 +428,10 @@ class Database {
|
|||||||
if (Database.noReject) {
|
if (Database.noReject) {
|
||||||
break;
|
break;
|
||||||
} else {
|
} else {
|
||||||
console.log("Waiting to close the db");
|
log.info("db", "Waiting to close the database");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
console.log("SQLite closed");
|
log.info("db", "SQLite closed");
|
||||||
|
|
||||||
process.removeListener("unhandledRejection", listener);
|
process.removeListener("unhandledRejection", listener);
|
||||||
}
|
}
|
||||||
@@ -308,11 +439,11 @@ class Database {
|
|||||||
/**
|
/**
|
||||||
* One backup one time in this process.
|
* One backup one time in this process.
|
||||||
* Reset this.backupPath if you want to backup again
|
* Reset this.backupPath if you want to backup again
|
||||||
* @param version
|
* @param {string} version Version code of backup
|
||||||
*/
|
*/
|
||||||
static backup(version) {
|
static backup(version) {
|
||||||
if (! this.backupPath) {
|
if (! this.backupPath) {
|
||||||
console.info("Backup the db");
|
log.info("db", "Backing up the database");
|
||||||
this.backupPath = this.dataDir + "kuma.db.bak" + version;
|
this.backupPath = this.dataDir + "kuma.db.bak" + version;
|
||||||
fs.copyFileSync(Database.path, this.backupPath);
|
fs.copyFileSync(Database.path, this.backupPath);
|
||||||
|
|
||||||
@@ -327,15 +458,30 @@ class Database {
|
|||||||
this.backupWalPath = walPath + ".bak" + version;
|
this.backupWalPath = walPath + ".bak" + version;
|
||||||
fs.copyFileSync(walPath, this.backupWalPath);
|
fs.copyFileSync(walPath, this.backupWalPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Double confirm if all files actually backup
|
||||||
|
if (!fs.existsSync(this.backupPath)) {
|
||||||
|
throw new Error("Backup failed! " + this.backupPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (fs.existsSync(shmPath)) {
|
||||||
|
if (!fs.existsSync(this.backupShmPath)) {
|
||||||
|
throw new Error("Backup failed! " + this.backupShmPath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (fs.existsSync(walPath)) {
|
||||||
|
if (!fs.existsSync(this.backupWalPath)) {
|
||||||
|
throw new Error("Backup failed! " + this.backupWalPath);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/** Restore from most recent backup */
|
||||||
*
|
|
||||||
*/
|
|
||||||
static restore() {
|
static restore() {
|
||||||
if (this.backupPath) {
|
if (this.backupPath) {
|
||||||
console.error("Patch db failed!!! Restoring the backup");
|
log.error("db", "Patching the database failed!!! Restoring the backup");
|
||||||
|
|
||||||
const shmPath = Database.path + "-shm";
|
const shmPath = Database.path + "-shm";
|
||||||
const walPath = Database.path + "-wal";
|
const walPath = Database.path + "-wal";
|
||||||
@@ -354,7 +500,7 @@ class Database {
|
|||||||
fs.unlinkSync(walPath);
|
fs.unlinkSync(walPath);
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.log("Restore failed, you may need to restore the backup manually");
|
log.error("db", "Restore failed; you may need to restore the backup manually");
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -370,9 +516,25 @@ class Database {
|
|||||||
}
|
}
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
console.log("Nothing to restore");
|
log.info("db", "Nothing to restore");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Get the size of the database */
|
||||||
|
static getSize() {
|
||||||
|
log.debug("db", "Database.getSize()");
|
||||||
|
let stats = fs.statSync(Database.path);
|
||||||
|
log.debug("db", stats);
|
||||||
|
return stats.size;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Shrink the database
|
||||||
|
* @returns {Promise<void>}
|
||||||
|
*/
|
||||||
|
static async shrink() {
|
||||||
|
await R.exec("VACUUM");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = Database;
|
module.exports = Database;
|
||||||
|
118
server/docker.js
Normal file
118
server/docker.js
Normal file
@@ -0,0 +1,118 @@
|
|||||||
|
const axios = require("axios");
|
||||||
|
const { R } = require("redbean-node");
|
||||||
|
const version = require("../package.json").version;
|
||||||
|
const https = require("https");
|
||||||
|
|
||||||
|
class DockerHost {
|
||||||
|
/**
|
||||||
|
* Save a docker host
|
||||||
|
* @param {Object} dockerHost Docker host to save
|
||||||
|
* @param {?number} dockerHostID ID of the docker host to update
|
||||||
|
* @param {number} userID ID of the user who adds the docker host
|
||||||
|
* @returns {Promise<Bean>}
|
||||||
|
*/
|
||||||
|
static async save(dockerHost, dockerHostID, userID) {
|
||||||
|
let bean;
|
||||||
|
|
||||||
|
if (dockerHostID) {
|
||||||
|
bean = await R.findOne("docker_host", " id = ? AND user_id = ? ", [ dockerHostID, userID ]);
|
||||||
|
|
||||||
|
if (!bean) {
|
||||||
|
throw new Error("docker host not found");
|
||||||
|
}
|
||||||
|
|
||||||
|
} else {
|
||||||
|
bean = R.dispense("docker_host");
|
||||||
|
}
|
||||||
|
|
||||||
|
bean.user_id = userID;
|
||||||
|
bean.docker_daemon = dockerHost.dockerDaemon;
|
||||||
|
bean.docker_type = dockerHost.dockerType;
|
||||||
|
bean.name = dockerHost.name;
|
||||||
|
|
||||||
|
await R.store(bean);
|
||||||
|
|
||||||
|
return bean;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete a Docker host
|
||||||
|
* @param {number} dockerHostID ID of the Docker host to delete
|
||||||
|
* @param {number} userID ID of the user who created the Docker host
|
||||||
|
* @returns {Promise<void>}
|
||||||
|
*/
|
||||||
|
static async delete(dockerHostID, userID) {
|
||||||
|
let bean = await R.findOne("docker_host", " id = ? AND user_id = ? ", [ dockerHostID, userID ]);
|
||||||
|
|
||||||
|
if (!bean) {
|
||||||
|
throw new Error("docker host not found");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete removed proxy from monitors if exists
|
||||||
|
await R.exec("UPDATE monitor SET docker_host = null WHERE docker_host = ?", [ dockerHostID ]);
|
||||||
|
|
||||||
|
await R.trash(bean);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetches the amount of containers on the Docker host
|
||||||
|
* @param {Object} dockerHost Docker host to check for
|
||||||
|
* @returns {number} Total amount of containers on the host
|
||||||
|
*/
|
||||||
|
static async testDockerHost(dockerHost) {
|
||||||
|
const options = {
|
||||||
|
url: "/containers/json?all=true",
|
||||||
|
headers: {
|
||||||
|
"Accept": "*/*",
|
||||||
|
"User-Agent": "Uptime-Kuma/" + version
|
||||||
|
},
|
||||||
|
httpsAgent: new https.Agent({
|
||||||
|
maxCachedSessions: 0, // Use Custom agent to disable session reuse (https://github.com/nodejs/node/issues/3940)
|
||||||
|
rejectUnauthorized: false,
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
|
||||||
|
if (dockerHost.dockerType === "socket") {
|
||||||
|
options.socketPath = dockerHost.dockerDaemon;
|
||||||
|
} else if (dockerHost.dockerType === "tcp") {
|
||||||
|
options.baseURL = DockerHost.patchDockerURL(dockerHost.dockerDaemon);
|
||||||
|
}
|
||||||
|
|
||||||
|
let res = await axios.request(options);
|
||||||
|
|
||||||
|
if (Array.isArray(res.data)) {
|
||||||
|
|
||||||
|
if (res.data.length > 1) {
|
||||||
|
|
||||||
|
if ("ImageID" in res.data[0]) {
|
||||||
|
return res.data.length;
|
||||||
|
} else {
|
||||||
|
throw new Error("Invalid Docker response, is it Docker really a daemon?");
|
||||||
|
}
|
||||||
|
|
||||||
|
} else {
|
||||||
|
return res.data.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
} else {
|
||||||
|
throw new Error("Invalid Docker response, is it Docker really a daemon?");
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Since axios 0.27.X, it does not accept `tcp://` protocol.
|
||||||
|
* Change it to `http://` on the fly in order to fix it. (https://github.com/louislam/uptime-kuma/issues/2165)
|
||||||
|
*/
|
||||||
|
static patchDockerURL(url) {
|
||||||
|
if (typeof url === "string") {
|
||||||
|
// Replace the first occurrence only with g
|
||||||
|
return url.replace(/tcp:\/\//g, "http://");
|
||||||
|
}
|
||||||
|
return url;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
DockerHost,
|
||||||
|
};
|
@@ -3,12 +3,21 @@
|
|||||||
Modified with 0 dependencies
|
Modified with 0 dependencies
|
||||||
*/
|
*/
|
||||||
let fs = require("fs");
|
let fs = require("fs");
|
||||||
|
const { log } = require("../src/util");
|
||||||
|
|
||||||
let ImageDataURI = (() => {
|
let ImageDataURI = (() => {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Decode the data:image/ URI
|
||||||
|
* @param {string} dataURI data:image/ URI to decode
|
||||||
|
* @returns {?Object} An object with properties "imageType" and "dataBase64".
|
||||||
|
* The former is the image type, e.g., "png", and the latter is a base64
|
||||||
|
* encoded string of the image's binary data. If it fails to parse, returns
|
||||||
|
* null instead of an object.
|
||||||
|
*/
|
||||||
function decode(dataURI) {
|
function decode(dataURI) {
|
||||||
if (!/data:image\//.test(dataURI)) {
|
if (!/data:image\//.test(dataURI)) {
|
||||||
console.log("ImageDataURI :: Error :: It seems that it is not an Image Data URI. Couldn't match \"data:image/\"");
|
log.error("image-data-uri", "It seems that it is not an Image Data URI. Couldn't match \"data:image/\"");
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -20,9 +29,16 @@ let ImageDataURI = (() => {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Endcode an image into data:image/ URI
|
||||||
|
* @param {(Buffer|string)} data Data to encode
|
||||||
|
* @param {string} mediaType Media type of data
|
||||||
|
* @returns {(string|null)} A string representing the base64-encoded
|
||||||
|
* version of the given Buffer object or null if an error occurred.
|
||||||
|
*/
|
||||||
function encode(data, mediaType) {
|
function encode(data, mediaType) {
|
||||||
if (!data || !mediaType) {
|
if (!data || !mediaType) {
|
||||||
console.log("ImageDataURI :: Error :: Missing some of the required params: data, mediaType ");
|
log.error("image-data-uri", "Missing some of the required params: data, mediaType");
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -33,6 +49,12 @@ let ImageDataURI = (() => {
|
|||||||
return dataImgBase64;
|
return dataImgBase64;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Write data URI to file
|
||||||
|
* @param {string} dataURI data:image/ URI
|
||||||
|
* @param {string} [filePath] Path to write file to
|
||||||
|
* @returns {Promise<string>}
|
||||||
|
*/
|
||||||
function outputFile(dataURI, filePath) {
|
function outputFile(dataURI, filePath) {
|
||||||
filePath = filePath || "./";
|
filePath = filePath || "./";
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
|
@@ -1,16 +1,22 @@
|
|||||||
const path = require("path");
|
const path = require("path");
|
||||||
const Bree = require("bree");
|
const Bree = require("bree");
|
||||||
const { SHARE_ENV } = require("worker_threads");
|
const { SHARE_ENV } = require("worker_threads");
|
||||||
|
const { log } = require("../src/util");
|
||||||
|
let bree;
|
||||||
const jobs = [
|
const jobs = [
|
||||||
{
|
{
|
||||||
name: "clear-old-data",
|
name: "clear-old-data",
|
||||||
interval: "at 03:14",
|
interval: "at 03:14",
|
||||||
}
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize background jobs
|
||||||
|
* @param {Object} args Arguments to pass to workers
|
||||||
|
* @returns {Bree}
|
||||||
|
*/
|
||||||
const initBackgroundJobs = function (args) {
|
const initBackgroundJobs = function (args) {
|
||||||
const bree = new Bree({
|
bree = new Bree({
|
||||||
root: path.resolve("server", "jobs"),
|
root: path.resolve("server", "jobs"),
|
||||||
jobs,
|
jobs,
|
||||||
worker: {
|
worker: {
|
||||||
@@ -18,7 +24,7 @@ const initBackgroundJobs = function (args) {
|
|||||||
workerData: args,
|
workerData: args,
|
||||||
},
|
},
|
||||||
workerMessageHandler: (message) => {
|
workerMessageHandler: (message) => {
|
||||||
console.log("[Background Job]:", message);
|
log.info("jobs", message);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -26,6 +32,13 @@ const initBackgroundJobs = function (args) {
|
|||||||
return bree;
|
return bree;
|
||||||
};
|
};
|
||||||
|
|
||||||
module.exports = {
|
const stopBackgroundJobs = function () {
|
||||||
initBackgroundJobs
|
if (bree) {
|
||||||
|
bree.stop();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
initBackgroundJobs,
|
||||||
|
stopBackgroundJobs
|
||||||
};
|
};
|
||||||
|
@@ -30,7 +30,7 @@ const DEFAULT_KEEP_PERIOD = 180;
|
|||||||
try {
|
try {
|
||||||
await R.exec(
|
await R.exec(
|
||||||
"DELETE FROM heartbeat WHERE time < DATETIME('now', '-' || ? || ' days') ",
|
"DELETE FROM heartbeat WHERE time < DATETIME('now', '-' || ? || ' days') ",
|
||||||
[parsedPeriod]
|
[ parsedPeriod ]
|
||||||
);
|
);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
log(`Failed to clear old data: ${e.message}`);
|
log(`Failed to clear old data: ${e.message}`);
|
||||||
|
@@ -2,14 +2,24 @@ const { parentPort, workerData } = require("worker_threads");
|
|||||||
const Database = require("../database");
|
const Database = require("../database");
|
||||||
const path = require("path");
|
const path = require("path");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send message to parent process for logging
|
||||||
|
* since worker_thread does not have access to stdout, this is used
|
||||||
|
* instead of console.log()
|
||||||
|
* @param {any} any The message to log
|
||||||
|
*/
|
||||||
const log = function (any) {
|
const log = function (any) {
|
||||||
if (parentPort) {
|
if (parentPort) {
|
||||||
parentPort.postMessage(any);
|
parentPort.postMessage(any);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Exit the worker process
|
||||||
|
* @param {number} error The status code to exit
|
||||||
|
*/
|
||||||
const exit = function (error) {
|
const exit = function (error) {
|
||||||
if (error && error != 0) {
|
if (error && error !== 0) {
|
||||||
process.exit(error);
|
process.exit(error);
|
||||||
} else {
|
} else {
|
||||||
if (parentPort) {
|
if (parentPort) {
|
||||||
@@ -20,6 +30,7 @@ const exit = function (error) {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/** Connects to the database */
|
||||||
const connectDb = async function () {
|
const connectDb = async function () {
|
||||||
const dbPath = path.join(
|
const dbPath = path.join(
|
||||||
process.env.DATA_DIR || workerData["data-dir"] || "./data/"
|
process.env.DATA_DIR || workerData["data-dir"] || "./data/"
|
||||||
|
19
server/model/docker_host.js
Normal file
19
server/model/docker_host.js
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
const { BeanModel } = require("redbean-node/dist/bean-model");
|
||||||
|
|
||||||
|
class DockerHost extends BeanModel {
|
||||||
|
/**
|
||||||
|
* Returns an object that ready to parse to JSON
|
||||||
|
* @returns {Object}
|
||||||
|
*/
|
||||||
|
toJSON() {
|
||||||
|
return {
|
||||||
|
id: this.id,
|
||||||
|
userID: this.user_id,
|
||||||
|
dockerDaemon: this.docker_daemon,
|
||||||
|
dockerType: this.docker_type,
|
||||||
|
name: this.name,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = DockerHost;
|
@@ -3,12 +3,18 @@ const { R } = require("redbean-node");
|
|||||||
|
|
||||||
class Group extends BeanModel {
|
class Group extends BeanModel {
|
||||||
|
|
||||||
async toPublicJSON() {
|
/**
|
||||||
|
* Return an object that ready to parse to JSON for public
|
||||||
|
* Only show necessary data to public
|
||||||
|
* @param {boolean} [showTags=false] Should the JSON include monitor tags
|
||||||
|
* @returns {Object}
|
||||||
|
*/
|
||||||
|
async toPublicJSON(showTags = false) {
|
||||||
let monitorBeanList = await this.getMonitorList();
|
let monitorBeanList = await this.getMonitorList();
|
||||||
let monitorList = [];
|
let monitorList = [];
|
||||||
|
|
||||||
for (let bean of monitorBeanList) {
|
for (let bean of monitorBeanList) {
|
||||||
monitorList.push(await bean.toPublicJSON());
|
monitorList.push(await bean.toPublicJSON(showTags));
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -19,9 +25,13 @@ class Group extends BeanModel {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all monitors
|
||||||
|
* @returns {Bean[]}
|
||||||
|
*/
|
||||||
async getMonitorList() {
|
async getMonitorList() {
|
||||||
return R.convertToBeans("monitor", await R.getAll(`
|
return R.convertToBeans("monitor", await R.getAll(`
|
||||||
SELECT monitor.* FROM monitor, monitor_group
|
SELECT monitor.*, monitor_group.send_url FROM monitor, monitor_group
|
||||||
WHERE monitor.id = monitor_group.monitor_id
|
WHERE monitor.id = monitor_group.monitor_id
|
||||||
AND group_id = ?
|
AND group_id = ?
|
||||||
ORDER BY monitor_group.weight
|
ORDER BY monitor_group.weight
|
||||||
|
@@ -13,6 +13,11 @@ const { BeanModel } = require("redbean-node/dist/bean-model");
|
|||||||
*/
|
*/
|
||||||
class Heartbeat extends BeanModel {
|
class Heartbeat extends BeanModel {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return an object that ready to parse to JSON for public
|
||||||
|
* Only show necessary data to public
|
||||||
|
* @returns {Object}
|
||||||
|
*/
|
||||||
toPublicJSON() {
|
toPublicJSON() {
|
||||||
return {
|
return {
|
||||||
status: this.status,
|
status: this.status,
|
||||||
@@ -22,6 +27,10 @@ class Heartbeat extends BeanModel {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return an object that ready to parse to JSON
|
||||||
|
* @returns {Object}
|
||||||
|
*/
|
||||||
toJSON() {
|
toJSON() {
|
||||||
return {
|
return {
|
||||||
monitorID: this.monitor_id,
|
monitorID: this.monitor_id,
|
||||||
|
@@ -2,6 +2,11 @@ const { BeanModel } = require("redbean-node/dist/bean-model");
|
|||||||
|
|
||||||
class Incident extends BeanModel {
|
class Incident extends BeanModel {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return an object that ready to parse to JSON for public
|
||||||
|
* Only show necessary data to public
|
||||||
|
* @returns {Object}
|
||||||
|
*/
|
||||||
toPublicJSON() {
|
toPublicJSON() {
|
||||||
return {
|
return {
|
||||||
id: this.id,
|
id: this.id,
|
||||||
|
@@ -6,14 +6,18 @@ dayjs.extend(utc);
|
|||||||
dayjs.extend(timezone);
|
dayjs.extend(timezone);
|
||||||
const axios = require("axios");
|
const axios = require("axios");
|
||||||
const { Prometheus } = require("../prometheus");
|
const { Prometheus } = require("../prometheus");
|
||||||
const { debug, UP, DOWN, PENDING, flipStatus, TimeLogger } = require("../../src/util");
|
const { log, UP, DOWN, PENDING, flipStatus, TimeLogger } = require("../../src/util");
|
||||||
const { tcping, ping, dnsResolve, checkCertificate, checkStatusCode, getTotalClientInRoom, setting } = require("../util-server");
|
const { tcping, ping, dnsResolve, checkCertificate, checkStatusCode, getTotalClientInRoom, setting, mssqlQuery, postgresQuery, mqttAsync, setSetting, httpNtlm, radius } = require("../util-server");
|
||||||
const { R } = require("redbean-node");
|
const { R } = require("redbean-node");
|
||||||
const { BeanModel } = require("redbean-node/dist/bean-model");
|
const { BeanModel } = require("redbean-node/dist/bean-model");
|
||||||
const { Notification } = require("../notification");
|
const { Notification } = require("../notification");
|
||||||
|
const { Proxy } = require("../proxy");
|
||||||
const { demoMode } = require("../config");
|
const { demoMode } = require("../config");
|
||||||
const version = require("../../package.json").version;
|
const version = require("../../package.json").version;
|
||||||
const apicache = require("../modules/apicache");
|
const apicache = require("../modules/apicache");
|
||||||
|
const { UptimeKumaServer } = require("../uptime-kuma-server");
|
||||||
|
const { CacheableDnsHttpAgent } = require("../cacheable-dns-http-agent");
|
||||||
|
const { DockerHost } = require("../docker");
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* status:
|
* status:
|
||||||
@@ -24,20 +28,32 @@ const apicache = require("../modules/apicache");
|
|||||||
class Monitor extends BeanModel {
|
class Monitor extends BeanModel {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Return a object that ready to parse to JSON for public
|
* Return an object that ready to parse to JSON for public
|
||||||
* Only show necessary data to public
|
* Only show necessary data to public
|
||||||
|
* @returns {Object}
|
||||||
*/
|
*/
|
||||||
async toPublicJSON() {
|
async toPublicJSON(showTags = false) {
|
||||||
return {
|
let obj = {
|
||||||
id: this.id,
|
id: this.id,
|
||||||
name: this.name,
|
name: this.name,
|
||||||
|
sendUrl: this.sendUrl,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (this.sendUrl) {
|
||||||
|
obj.url = this.url;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (showTags) {
|
||||||
|
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
|
||||||
|
* @returns {Object}
|
||||||
*/
|
*/
|
||||||
async toJSON() {
|
async toJSON(includeSensitiveData = true) {
|
||||||
|
|
||||||
let notificationIDList = {};
|
let notificationIDList = {};
|
||||||
|
|
||||||
@@ -49,15 +65,13 @@ class Monitor extends BeanModel {
|
|||||||
notificationIDList[bean.notification_id] = true;
|
notificationIDList[bean.notification_id] = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
const tags = await R.getAll("SELECT mt.*, tag.name, tag.color FROM monitor_tag mt JOIN tag ON mt.tag_id = tag.id WHERE mt.monitor_id = ?", [this.id]);
|
const tags = await this.getTags();
|
||||||
|
|
||||||
return {
|
let data = {
|
||||||
id: this.id,
|
id: this.id,
|
||||||
name: this.name,
|
name: this.name,
|
||||||
url: this.url,
|
url: this.url,
|
||||||
method: this.method,
|
method: this.method,
|
||||||
body: this.body,
|
|
||||||
headers: this.headers,
|
|
||||||
hostname: this.hostname,
|
hostname: this.hostname,
|
||||||
port: this.port,
|
port: this.port,
|
||||||
maxretries: this.maxretries,
|
maxretries: this.maxretries,
|
||||||
@@ -66,7 +80,9 @@ class Monitor extends BeanModel {
|
|||||||
type: this.type,
|
type: this.type,
|
||||||
interval: this.interval,
|
interval: this.interval,
|
||||||
retryInterval: this.retryInterval,
|
retryInterval: this.retryInterval,
|
||||||
|
resendInterval: this.resendInterval,
|
||||||
keyword: this.keyword,
|
keyword: this.keyword,
|
||||||
|
expiryNotification: this.isEnabledExpiryNotification(),
|
||||||
ignoreTls: this.getIgnoreTls(),
|
ignoreTls: this.getIgnoreTls(),
|
||||||
upsideDown: this.isUpsideDown(),
|
upsideDown: this.isUpsideDown(),
|
||||||
maxredirects: this.maxredirects,
|
maxredirects: this.maxredirects,
|
||||||
@@ -75,9 +91,64 @@ class Monitor extends BeanModel {
|
|||||||
dns_resolve_server: this.dns_resolve_server,
|
dns_resolve_server: this.dns_resolve_server,
|
||||||
dns_last_result: this.dns_last_result,
|
dns_last_result: this.dns_last_result,
|
||||||
pushToken: this.pushToken,
|
pushToken: this.pushToken,
|
||||||
|
docker_container: this.docker_container,
|
||||||
|
docker_host: this.docker_host,
|
||||||
|
proxyId: this.proxy_id,
|
||||||
notificationIDList,
|
notificationIDList,
|
||||||
tags: tags,
|
tags: tags,
|
||||||
|
mqttUsername: this.mqttUsername,
|
||||||
|
mqttPassword: this.mqttPassword,
|
||||||
|
mqttTopic: this.mqttTopic,
|
||||||
|
mqttSuccessMessage: this.mqttSuccessMessage,
|
||||||
|
databaseConnectionString: this.databaseConnectionString,
|
||||||
|
databaseQuery: this.databaseQuery,
|
||||||
|
authMethod: this.authMethod,
|
||||||
|
authWorkstation: this.authWorkstation,
|
||||||
|
authDomain: this.authDomain,
|
||||||
|
radiusUsername: this.radiusUsername,
|
||||||
|
radiusPassword: this.radiusPassword,
|
||||||
|
radiusCalledStationId: this.radiusCalledStationId,
|
||||||
|
radiusCallingStationId: this.radiusCallingStationId,
|
||||||
|
radiusSecret: this.radiusSecret,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (includeSensitiveData) {
|
||||||
|
data = {
|
||||||
|
...data,
|
||||||
|
headers: this.headers,
|
||||||
|
body: this.body,
|
||||||
|
basic_auth_user: this.basic_auth_user,
|
||||||
|
basic_auth_pass: this.basic_auth_pass,
|
||||||
|
pushToken: this.pushToken,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all tags applied to this monitor
|
||||||
|
* @returns {Promise<LooseObject<any>[]>}
|
||||||
|
*/
|
||||||
|
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");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Is the TLS expiry notification enabled?
|
||||||
|
* @returns {boolean}
|
||||||
|
*/
|
||||||
|
isEnabledExpiryNotification() {
|
||||||
|
return Boolean(this.expiryNotification);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -96,10 +167,18 @@ class Monitor extends BeanModel {
|
|||||||
return Boolean(this.upsideDown);
|
return Boolean(this.upsideDown);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get accepted status codes
|
||||||
|
* @returns {Object}
|
||||||
|
*/
|
||||||
getAcceptedStatuscodes() {
|
getAcceptedStatuscodes() {
|
||||||
return JSON.parse(this.accepted_statuscodes_json);
|
return JSON.parse(this.accepted_statuscodes_json);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Start monitor
|
||||||
|
* @param {Server} io Socket server instance
|
||||||
|
*/
|
||||||
start(io) {
|
start(io) {
|
||||||
let previousBeat = null;
|
let previousBeat = null;
|
||||||
let retries = 0;
|
let retries = 0;
|
||||||
@@ -108,11 +187,24 @@ class Monitor extends BeanModel {
|
|||||||
|
|
||||||
const beat = async () => {
|
const beat = async () => {
|
||||||
|
|
||||||
|
let beatInterval = this.interval;
|
||||||
|
|
||||||
|
if (! beatInterval) {
|
||||||
|
beatInterval = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (demoMode) {
|
||||||
|
if (beatInterval < 20) {
|
||||||
|
console.log("beat interval too low, reset to 20s");
|
||||||
|
beatInterval = 20;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Expose here for prometheus update
|
// Expose here for prometheus update
|
||||||
// undefined if not https
|
// undefined if not https
|
||||||
let tlsInfo = undefined;
|
let tlsInfo = undefined;
|
||||||
|
|
||||||
if (! previousBeat) {
|
if (!previousBeat || this.type === "push") {
|
||||||
previousBeat = await R.findOne("heartbeat", " monitor_id = ? ORDER BY time DESC", [
|
previousBeat = await R.findOne("heartbeat", " monitor_id = ? ORDER BY time DESC", [
|
||||||
this.id,
|
this.id,
|
||||||
]);
|
]);
|
||||||
@@ -122,15 +214,16 @@ class Monitor extends BeanModel {
|
|||||||
|
|
||||||
let bean = R.dispense("heartbeat");
|
let bean = R.dispense("heartbeat");
|
||||||
bean.monitor_id = this.id;
|
bean.monitor_id = this.id;
|
||||||
bean.time = R.isoDateTime(dayjs.utc());
|
bean.time = R.isoDateTimeMillis(dayjs.utc());
|
||||||
bean.status = DOWN;
|
bean.status = DOWN;
|
||||||
|
bean.downCount = previousBeat?.downCount || 0;
|
||||||
|
|
||||||
if (this.isUpsideDown()) {
|
if (this.isUpsideDown()) {
|
||||||
bean.status = flipStatus(bean.status);
|
bean.status = flipStatus(bean.status);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Duration
|
// Duration
|
||||||
if (! isFirstBeat) {
|
if (!isFirstBeat) {
|
||||||
bean.duration = dayjs(bean.time).diff(dayjs(previousBeat.time), "second");
|
bean.duration = dayjs(bean.time).diff(dayjs(previousBeat.time), "second");
|
||||||
} else {
|
} else {
|
||||||
bean.duration = 0;
|
bean.duration = 0;
|
||||||
@@ -141,47 +234,104 @@ class Monitor extends BeanModel {
|
|||||||
// Do not do any queries/high loading things before the "bean.ping"
|
// Do not do any queries/high loading things before the "bean.ping"
|
||||||
let startTime = dayjs().valueOf();
|
let startTime = dayjs().valueOf();
|
||||||
|
|
||||||
|
// HTTP basic auth
|
||||||
|
let basicAuthHeader = {};
|
||||||
|
if (this.auth_method === "basic") {
|
||||||
|
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(),
|
||||||
|
};
|
||||||
|
|
||||||
|
log.debug("monitor", `[${this.name}] Prepare Options for axios`);
|
||||||
|
|
||||||
const options = {
|
const options = {
|
||||||
url: this.url,
|
url: this.url,
|
||||||
method: (this.method || "get").toLowerCase(),
|
method: (this.method || "get").toLowerCase(),
|
||||||
...(this.body ? { data: JSON.parse(this.body) } : {}),
|
...(this.body ? { data: JSON.parse(this.body) } : {}),
|
||||||
timeout: this.interval * 1000 * 0.8,
|
timeout: this.interval * 1000 * 0.8,
|
||||||
headers: {
|
headers: {
|
||||||
"Accept": "*/*",
|
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9",
|
||||||
"User-Agent": "Uptime-Kuma/" + version,
|
"User-Agent": "Uptime-Kuma/" + version,
|
||||||
...(this.headers ? JSON.parse(this.headers) : {}),
|
...(this.headers ? JSON.parse(this.headers) : {}),
|
||||||
|
...(basicAuthHeader),
|
||||||
},
|
},
|
||||||
httpsAgent: new https.Agent({
|
|
||||||
maxCachedSessions: 0, // Use Custom agent to disable session reuse (https://github.com/nodejs/node/issues/3940)
|
|
||||||
rejectUnauthorized: ! this.getIgnoreTls(),
|
|
||||||
}),
|
|
||||||
maxRedirects: this.maxredirects,
|
maxRedirects: this.maxredirects,
|
||||||
validateStatus: (status) => {
|
validateStatus: (status) => {
|
||||||
return checkStatusCode(status, this.getAcceptedStatuscodes());
|
return checkStatusCode(status, this.getAcceptedStatuscodes());
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
let res = await axios.request(options);
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
log.debug("monitor", `[${this.name}] Axios Options: ${JSON.stringify(options)}`);
|
||||||
|
log.debug("monitor", `[${this.name}] Axios Request`);
|
||||||
|
|
||||||
|
let res;
|
||||||
|
if (this.auth_method === "ntlm") {
|
||||||
|
options.httpsAgent.keepAlive = true;
|
||||||
|
|
||||||
|
res = await httpNtlm(options, {
|
||||||
|
username: this.basic_auth_user,
|
||||||
|
password: this.basic_auth_pass,
|
||||||
|
domain: this.authDomain,
|
||||||
|
workstation: this.authWorkstation ? this.authWorkstation : undefined
|
||||||
|
});
|
||||||
|
|
||||||
|
} else {
|
||||||
|
res = await axios.request(options);
|
||||||
|
}
|
||||||
|
|
||||||
bean.msg = `${res.status} - ${res.statusText}`;
|
bean.msg = `${res.status} - ${res.statusText}`;
|
||||||
bean.ping = dayjs().valueOf() - startTime;
|
bean.ping = dayjs().valueOf() - startTime;
|
||||||
|
|
||||||
// Check certificate if https is used
|
// Check certificate if https is used
|
||||||
let certInfoStartTime = dayjs().valueOf();
|
let certInfoStartTime = dayjs().valueOf();
|
||||||
if (this.getUrl()?.protocol === "https:") {
|
if (this.getUrl()?.protocol === "https:") {
|
||||||
|
log.debug("monitor", `[${this.name}] Check cert`);
|
||||||
try {
|
try {
|
||||||
tlsInfo = await this.updateTlsInfo(checkCertificate(res));
|
let tlsInfoObject = checkCertificate(res);
|
||||||
|
tlsInfo = await this.updateTlsInfo(tlsInfoObject);
|
||||||
|
|
||||||
|
if (!this.getIgnoreTls() && this.isEnabledExpiryNotification()) {
|
||||||
|
log.debug("monitor", `[${this.name}] call sendCertNotification`);
|
||||||
|
await this.sendCertNotification(tlsInfoObject);
|
||||||
|
}
|
||||||
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (e.message !== "No TLS certificate in response") {
|
if (e.message !== "No TLS certificate in response") {
|
||||||
console.error(e.message);
|
log.error("monitor", "Caught error");
|
||||||
|
log.error("monitor", e.message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (process.env.TIMELOGGER === "1") {
|
if (process.env.TIMELOGGER === "1") {
|
||||||
debug("Cert Info Query Time: " + (dayjs().valueOf() - certInfoStartTime) + "ms");
|
log.debug("monitor", "Cert Info Query Time: " + (dayjs().valueOf() - certInfoStartTime) + "ms");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (process.env.UPTIME_KUMA_LOG_RESPONSE_BODY_MONITOR_ID == this.id) {
|
if (process.env.UPTIME_KUMA_LOG_RESPONSE_BODY_MONITOR_ID === this.id) {
|
||||||
console.log(res.data);
|
log.info("monitor", res.data);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.type === "http") {
|
if (this.type === "http") {
|
||||||
@@ -199,7 +349,11 @@ class Monitor extends BeanModel {
|
|||||||
bean.msg += ", keyword is found";
|
bean.msg += ", keyword is found";
|
||||||
bean.status = UP;
|
bean.status = UP;
|
||||||
} else {
|
} else {
|
||||||
throw new Error(bean.msg + ", but keyword is not found");
|
data = data.replace(/<[^>]*>?|[\n\r]|\s+/gm, " ");
|
||||||
|
if (data.length > 50) {
|
||||||
|
data = data.substring(0, 47) + "...";
|
||||||
|
}
|
||||||
|
throw new Error(bean.msg + ", but keyword is not in [" + data + "]");
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -217,27 +371,27 @@ class Monitor extends BeanModel {
|
|||||||
let startTime = dayjs().valueOf();
|
let startTime = dayjs().valueOf();
|
||||||
let dnsMessage = "";
|
let dnsMessage = "";
|
||||||
|
|
||||||
let dnsRes = await dnsResolve(this.hostname, this.dns_resolve_server, this.dns_resolve_type);
|
let dnsRes = await dnsResolve(this.hostname, this.dns_resolve_server, this.port, this.dns_resolve_type);
|
||||||
bean.ping = dayjs().valueOf() - startTime;
|
bean.ping = dayjs().valueOf() - startTime;
|
||||||
|
|
||||||
if (this.dns_resolve_type == "A" || this.dns_resolve_type == "AAAA" || this.dns_resolve_type == "TXT") {
|
if (this.dns_resolve_type === "A" || this.dns_resolve_type === "AAAA" || this.dns_resolve_type === "TXT") {
|
||||||
dnsMessage += "Records: ";
|
dnsMessage += "Records: ";
|
||||||
dnsMessage += dnsRes.join(" | ");
|
dnsMessage += dnsRes.join(" | ");
|
||||||
} else if (this.dns_resolve_type == "CNAME" || this.dns_resolve_type == "PTR") {
|
} else if (this.dns_resolve_type === "CNAME" || this.dns_resolve_type === "PTR") {
|
||||||
dnsMessage = dnsRes[0];
|
dnsMessage = dnsRes[0];
|
||||||
} else if (this.dns_resolve_type == "CAA") {
|
} else if (this.dns_resolve_type === "CAA") {
|
||||||
dnsMessage = dnsRes[0].issue;
|
dnsMessage = dnsRes[0].issue;
|
||||||
} else if (this.dns_resolve_type == "MX") {
|
} else if (this.dns_resolve_type === "MX") {
|
||||||
dnsRes.forEach(record => {
|
dnsRes.forEach(record => {
|
||||||
dnsMessage += `Hostname: ${record.exchange} - Priority: ${record.priority} | `;
|
dnsMessage += `Hostname: ${record.exchange} - Priority: ${record.priority} | `;
|
||||||
});
|
});
|
||||||
dnsMessage = dnsMessage.slice(0, -2);
|
dnsMessage = dnsMessage.slice(0, -2);
|
||||||
} else if (this.dns_resolve_type == "NS") {
|
} else if (this.dns_resolve_type === "NS") {
|
||||||
dnsMessage += "Servers: ";
|
dnsMessage += "Servers: ";
|
||||||
dnsMessage += dnsRes.join(" | ");
|
dnsMessage += dnsRes.join(" | ");
|
||||||
} else if (this.dns_resolve_type == "SOA") {
|
} else if (this.dns_resolve_type === "SOA") {
|
||||||
dnsMessage += `NS-Name: ${dnsRes.nsname} | Hostmaster: ${dnsRes.hostmaster} | Serial: ${dnsRes.serial} | Refresh: ${dnsRes.refresh} | Retry: ${dnsRes.retry} | Expire: ${dnsRes.expire} | MinTTL: ${dnsRes.minttl}`;
|
dnsMessage += `NS-Name: ${dnsRes.nsname} | Hostmaster: ${dnsRes.hostmaster} | Serial: ${dnsRes.serial} | Refresh: ${dnsRes.refresh} | Retry: ${dnsRes.retry} | Expire: ${dnsRes.expire} | MinTTL: ${dnsRes.minttl}`;
|
||||||
} else if (this.dns_resolve_type == "SRV") {
|
} else if (this.dns_resolve_type === "SRV") {
|
||||||
dnsRes.forEach(record => {
|
dnsRes.forEach(record => {
|
||||||
dnsMessage += `Name: ${record.name} | Port: ${record.port} | Priority: ${record.priority} | Weight: ${record.weight} | `;
|
dnsMessage += `Name: ${record.name} | Port: ${record.port} | Priority: ${record.priority} | Weight: ${record.weight} | `;
|
||||||
});
|
});
|
||||||
@@ -254,22 +408,33 @@ class Monitor extends BeanModel {
|
|||||||
bean.msg = dnsMessage;
|
bean.msg = dnsMessage;
|
||||||
bean.status = UP;
|
bean.status = UP;
|
||||||
} else if (this.type === "push") { // Type: Push
|
} else if (this.type === "push") { // Type: Push
|
||||||
const time = R.isoDateTime(dayjs.utc().subtract(this.interval, "second"));
|
log.debug("monitor", `[${this.name}] Checking monitor at ${dayjs().format("YYYY-MM-DD HH:mm:ss.SSS")}`);
|
||||||
|
const bufferTime = 1000; // 1s buffer to accommodate clock differences
|
||||||
|
|
||||||
let heartbeatCount = await R.count("heartbeat", " monitor_id = ? AND time > ? ", [
|
if (previousBeat) {
|
||||||
this.id,
|
const msSinceLastBeat = dayjs.utc().valueOf() - dayjs.utc(previousBeat.time).valueOf();
|
||||||
time
|
|
||||||
]);
|
|
||||||
|
|
||||||
debug("heartbeatCount" + heartbeatCount + " " + time);
|
log.debug("monitor", `[${this.name}] msSinceLastBeat = ${msSinceLastBeat}`);
|
||||||
|
|
||||||
if (heartbeatCount <= 0) {
|
// If the previous beat was down or pending we use the regular
|
||||||
throw new Error("No heartbeat in the time window");
|
// beatInterval/retryInterval in the setTimeout further below
|
||||||
|
if (previousBeat.status !== (this.isUpsideDown() ? DOWN : UP) || msSinceLastBeat > beatInterval * 1000 + bufferTime) {
|
||||||
|
throw new Error("No heartbeat in the time window");
|
||||||
|
} else {
|
||||||
|
let timeout = beatInterval * 1000 - msSinceLastBeat;
|
||||||
|
if (timeout < 0) {
|
||||||
|
timeout = bufferTime;
|
||||||
|
} else {
|
||||||
|
timeout += bufferTime;
|
||||||
|
}
|
||||||
|
// No need to insert successful heartbeat for push type, so end here
|
||||||
|
retries = 0;
|
||||||
|
log.debug("monitor", `[${this.name}] timeout = ${timeout}`);
|
||||||
|
this.heartbeatInterval = setTimeout(beat, timeout);
|
||||||
|
return;
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
// No need to insert successful heartbeat for push type, so end here
|
throw new Error("No heartbeat in the time window");
|
||||||
retries = 0;
|
|
||||||
this.heartbeatInterval = setTimeout(beat, this.interval * 1000);
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
} else if (this.type === "steam") {
|
} else if (this.type === "steam") {
|
||||||
@@ -287,9 +452,12 @@ class Monitor extends BeanModel {
|
|||||||
"Accept": "*/*",
|
"Accept": "*/*",
|
||||||
"User-Agent": "Uptime-Kuma/" + version,
|
"User-Agent": "Uptime-Kuma/" + version,
|
||||||
},
|
},
|
||||||
httpsAgent: new https.Agent({
|
httpsAgent: CacheableDnsHttpAgent.getHttpsAgent({
|
||||||
maxCachedSessions: 0, // Use Custom agent to disable session reuse (https://github.com/nodejs/node/issues/3940)
|
maxCachedSessions: 0, // Use Custom agent to disable session reuse (https://github.com/nodejs/node/issues/3940)
|
||||||
rejectUnauthorized: ! this.getIgnoreTls(),
|
rejectUnauthorized: !this.getIgnoreTls(),
|
||||||
|
}),
|
||||||
|
httpAgent: CacheableDnsHttpAgent.getHttpAgent({
|
||||||
|
maxCachedSessions: 0,
|
||||||
}),
|
}),
|
||||||
maxRedirects: this.maxredirects,
|
maxRedirects: this.maxredirects,
|
||||||
validateStatus: (status) => {
|
validateStatus: (status) => {
|
||||||
@@ -311,7 +479,83 @@ class Monitor extends BeanModel {
|
|||||||
} else {
|
} else {
|
||||||
throw new Error("Server not found on Steam");
|
throw new Error("Server not found on Steam");
|
||||||
}
|
}
|
||||||
|
} else if (this.type === "docker") {
|
||||||
|
log.debug(`[${this.name}] Prepare Options for Axios`);
|
||||||
|
|
||||||
|
const dockerHost = await R.load("docker_host", this.docker_host);
|
||||||
|
|
||||||
|
const options = {
|
||||||
|
url: `/containers/${this.docker_container}/json`,
|
||||||
|
headers: {
|
||||||
|
"Accept": "*/*",
|
||||||
|
"User-Agent": "Uptime-Kuma/" + version,
|
||||||
|
},
|
||||||
|
httpsAgent: new https.Agent({
|
||||||
|
maxCachedSessions: 0, // Use Custom agent to disable session reuse (https://github.com/nodejs/node/issues/3940)
|
||||||
|
rejectUnauthorized: ! this.getIgnoreTls(),
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
|
||||||
|
if (dockerHost._dockerType === "socket") {
|
||||||
|
options.socketPath = dockerHost._dockerDaemon;
|
||||||
|
} else if (dockerHost._dockerType === "tcp") {
|
||||||
|
options.baseURL = DockerHost.patchDockerURL(dockerHost._dockerDaemon);
|
||||||
|
}
|
||||||
|
|
||||||
|
log.debug(`[${this.name}] Axios Request`);
|
||||||
|
let res = await axios.request(options);
|
||||||
|
if (res.data.State.Running) {
|
||||||
|
bean.status = UP;
|
||||||
|
bean.msg = "";
|
||||||
|
}
|
||||||
|
} else if (this.type === "mqtt") {
|
||||||
|
bean.msg = await mqttAsync(this.hostname, this.mqttTopic, this.mqttSuccessMessage, {
|
||||||
|
port: this.port,
|
||||||
|
username: this.mqttUsername,
|
||||||
|
password: this.mqttPassword,
|
||||||
|
interval: this.interval,
|
||||||
|
});
|
||||||
|
bean.status = UP;
|
||||||
|
} else if (this.type === "sqlserver") {
|
||||||
|
let startTime = dayjs().valueOf();
|
||||||
|
|
||||||
|
await mssqlQuery(this.databaseConnectionString, this.databaseQuery);
|
||||||
|
|
||||||
|
bean.msg = "";
|
||||||
|
bean.status = UP;
|
||||||
|
bean.ping = dayjs().valueOf() - startTime;
|
||||||
|
} else if (this.type === "postgres") {
|
||||||
|
let startTime = dayjs().valueOf();
|
||||||
|
|
||||||
|
await postgresQuery(this.databaseConnectionString, this.databaseQuery);
|
||||||
|
|
||||||
|
bean.msg = "";
|
||||||
|
bean.status = UP;
|
||||||
|
bean.ping = dayjs().valueOf() - startTime;
|
||||||
|
} else if (this.type === "radius") {
|
||||||
|
let startTime = dayjs().valueOf();
|
||||||
|
try {
|
||||||
|
const resp = await radius(
|
||||||
|
this.hostname,
|
||||||
|
this.radiusUsername,
|
||||||
|
this.radiusPassword,
|
||||||
|
this.radiusCalledStationId,
|
||||||
|
this.radiusCallingStationId,
|
||||||
|
this.radiusSecret
|
||||||
|
);
|
||||||
|
if (resp.code) {
|
||||||
|
bean.msg = resp.code;
|
||||||
|
}
|
||||||
|
bean.status = UP;
|
||||||
|
} catch (error) {
|
||||||
|
bean.status = DOWN;
|
||||||
|
if (error.response?.code) {
|
||||||
|
bean.msg = error.response.code;
|
||||||
|
} else {
|
||||||
|
bean.msg = error.message;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
bean.ping = dayjs().valueOf() - startTime;
|
||||||
} else {
|
} else {
|
||||||
bean.msg = "Unknown Monitor Type";
|
bean.msg = "Unknown Monitor Type";
|
||||||
bean.status = PENDING;
|
bean.status = PENDING;
|
||||||
@@ -342,72 +586,119 @@ class Monitor extends BeanModel {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let beatInterval = this.interval;
|
log.debug("monitor", `[${this.name}] Check isImportant`);
|
||||||
|
|
||||||
let isImportant = Monitor.isImportantBeat(isFirstBeat, previousBeat?.status, bean.status);
|
let isImportant = Monitor.isImportantBeat(isFirstBeat, previousBeat?.status, bean.status);
|
||||||
|
|
||||||
// Mark as important if status changed, ignore pending pings,
|
// Mark as important if status changed, ignore pending pings,
|
||||||
// Don't notify if disrupted changes to up
|
// Don't notify if disrupted changes to up
|
||||||
if (isImportant) {
|
if (isImportant) {
|
||||||
bean.important = true;
|
bean.important = true;
|
||||||
|
|
||||||
|
log.debug("monitor", `[${this.name}] sendNotification`);
|
||||||
await Monitor.sendNotification(isFirstBeat, this, bean);
|
await Monitor.sendNotification(isFirstBeat, this, bean);
|
||||||
|
|
||||||
|
// Reset down count
|
||||||
|
bean.downCount = 0;
|
||||||
|
|
||||||
|
// Clear Status Page Cache
|
||||||
|
log.debug("monitor", `[${this.name}] apicache clear`);
|
||||||
|
apicache.clear();
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
bean.important = false;
|
bean.important = false;
|
||||||
|
|
||||||
|
if (bean.status === DOWN && this.resendInterval > 0) {
|
||||||
|
++bean.downCount;
|
||||||
|
if (bean.downCount >= this.resendInterval) {
|
||||||
|
// Send notification again, because we are still DOWN
|
||||||
|
log.debug("monitor", `[${this.name}] sendNotification again: Down Count: ${bean.downCount} | Resend Interval: ${this.resendInterval}`);
|
||||||
|
await Monitor.sendNotification(isFirstBeat, this, bean);
|
||||||
|
|
||||||
|
// Reset down count
|
||||||
|
bean.downCount = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (bean.status === UP) {
|
if (bean.status === UP) {
|
||||||
console.info(`Monitor #${this.id} '${this.name}': Successful Response: ${bean.ping} ms | Interval: ${beatInterval} seconds | Type: ${this.type}`);
|
log.debug("monitor", `Monitor #${this.id} '${this.name}': Successful Response: ${bean.ping} ms | Interval: ${beatInterval} seconds | Type: ${this.type}`);
|
||||||
} else if (bean.status === PENDING) {
|
} else if (bean.status === PENDING) {
|
||||||
if (this.retryInterval > 0) {
|
if (this.retryInterval > 0) {
|
||||||
beatInterval = this.retryInterval;
|
beatInterval = this.retryInterval;
|
||||||
}
|
}
|
||||||
console.warn(`Monitor #${this.id} '${this.name}': Pending: ${bean.msg} | Max retries: ${this.maxretries} | Retry: ${retries} | Retry Interval: ${beatInterval} seconds | Type: ${this.type}`);
|
log.warn("monitor", `Monitor #${this.id} '${this.name}': Pending: ${bean.msg} | Max retries: ${this.maxretries} | Retry: ${retries} | Retry Interval: ${beatInterval} seconds | Type: ${this.type}`);
|
||||||
} else {
|
} else {
|
||||||
console.warn(`Monitor #${this.id} '${this.name}': Failing: ${bean.msg} | Interval: ${beatInterval} seconds | Type: ${this.type}`);
|
log.warn("monitor", `Monitor #${this.id} '${this.name}': Failing: ${bean.msg} | Interval: ${beatInterval} seconds | Type: ${this.type} | Down Count: ${bean.downCount} | Resend Interval: ${this.resendInterval}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
log.debug("monitor", `[${this.name}] Send to socket`);
|
||||||
io.to(this.user_id).emit("heartbeat", bean.toJSON());
|
io.to(this.user_id).emit("heartbeat", bean.toJSON());
|
||||||
Monitor.sendStats(io, this.id, this.user_id);
|
Monitor.sendStats(io, this.id, this.user_id);
|
||||||
|
|
||||||
|
log.debug("monitor", `[${this.name}] Store`);
|
||||||
await R.store(bean);
|
await R.store(bean);
|
||||||
|
|
||||||
|
log.debug("monitor", `[${this.name}] prometheus.update`);
|
||||||
prometheus.update(bean, tlsInfo);
|
prometheus.update(bean, tlsInfo);
|
||||||
|
|
||||||
previousBeat = bean;
|
previousBeat = bean;
|
||||||
|
|
||||||
if (! this.isStop) {
|
if (! this.isStop) {
|
||||||
|
log.debug("monitor", `[${this.name}] SetTimeout for next check.`);
|
||||||
if (demoMode) {
|
this.heartbeatInterval = setTimeout(safeBeat, beatInterval * 1000);
|
||||||
if (beatInterval < 20) {
|
} else {
|
||||||
console.log("beat interval too low, reset to 20s");
|
log.info("monitor", `[${this.name}] isStop = true, no next check.`);
|
||||||
beatInterval = 20;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
this.heartbeatInterval = setTimeout(beat, beatInterval * 1000);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/** Get a heartbeat and handle errors */
|
||||||
|
const safeBeat = async () => {
|
||||||
|
try {
|
||||||
|
await beat();
|
||||||
|
} catch (e) {
|
||||||
|
console.trace(e);
|
||||||
|
UptimeKumaServer.errorLog(e, false);
|
||||||
|
log.error("monitor", "Please report to https://github.com/louislam/uptime-kuma/issues");
|
||||||
|
|
||||||
|
if (! this.isStop) {
|
||||||
|
log.info("monitor", "Try to restart the monitor");
|
||||||
|
this.heartbeatInterval = setTimeout(safeBeat, this.interval * 1000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// Delay Push Type
|
// Delay Push Type
|
||||||
if (this.type === "push") {
|
if (this.type === "push") {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
beat();
|
safeBeat();
|
||||||
}, this.interval * 1000);
|
}, this.interval * 1000);
|
||||||
} else {
|
} else {
|
||||||
beat();
|
safeBeat();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Stop monitor */
|
||||||
stop() {
|
stop() {
|
||||||
clearTimeout(this.heartbeatInterval);
|
clearTimeout(this.heartbeatInterval);
|
||||||
this.isStop = true;
|
this.isStop = true;
|
||||||
|
|
||||||
|
this.prometheus().remove();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a new prometheus instance
|
||||||
|
* @returns {Prometheus}
|
||||||
|
*/
|
||||||
|
prometheus() {
|
||||||
|
return new Prometheus(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Helper Method:
|
* Helper Method:
|
||||||
* returns URL object for further usage
|
* returns URL object for further usage
|
||||||
* returns null if url is invalid
|
* returns null if url is invalid
|
||||||
* @returns {null|URL}
|
* @returns {(null|URL)}
|
||||||
*/
|
*/
|
||||||
getUrl() {
|
getUrl() {
|
||||||
try {
|
try {
|
||||||
@@ -420,22 +711,54 @@ class Monitor extends BeanModel {
|
|||||||
/**
|
/**
|
||||||
* Store TLS info to database
|
* Store TLS info to database
|
||||||
* @param checkCertificateResult
|
* @param checkCertificateResult
|
||||||
* @returns {Promise<object>}
|
* @returns {Promise<Object>}
|
||||||
*/
|
*/
|
||||||
async updateTlsInfo(checkCertificateResult) {
|
async updateTlsInfo(checkCertificateResult) {
|
||||||
let tls_info_bean = await R.findOne("monitor_tls_info", "monitor_id = ?", [
|
let tlsInfoBean = await R.findOne("monitor_tls_info", "monitor_id = ?", [
|
||||||
this.id,
|
this.id,
|
||||||
]);
|
]);
|
||||||
if (tls_info_bean == null) {
|
|
||||||
tls_info_bean = R.dispense("monitor_tls_info");
|
if (tlsInfoBean == null) {
|
||||||
tls_info_bean.monitor_id = this.id;
|
tlsInfoBean = R.dispense("monitor_tls_info");
|
||||||
|
tlsInfoBean.monitor_id = this.id;
|
||||||
|
} else {
|
||||||
|
|
||||||
|
// Clear sent history if the cert changed.
|
||||||
|
try {
|
||||||
|
let oldCertInfo = JSON.parse(tlsInfoBean.info_json);
|
||||||
|
|
||||||
|
let isValidObjects = oldCertInfo && oldCertInfo.certInfo && checkCertificateResult && checkCertificateResult.certInfo;
|
||||||
|
|
||||||
|
if (isValidObjects) {
|
||||||
|
if (oldCertInfo.certInfo.fingerprint256 !== checkCertificateResult.certInfo.fingerprint256) {
|
||||||
|
log.debug("monitor", "Resetting sent_history");
|
||||||
|
await R.exec("DELETE FROM notification_sent_history WHERE type = 'certificate' AND monitor_id = ?", [
|
||||||
|
this.id
|
||||||
|
]);
|
||||||
|
} else {
|
||||||
|
log.debug("monitor", "No need to reset sent_history");
|
||||||
|
log.debug("monitor", oldCertInfo.certInfo.fingerprint256);
|
||||||
|
log.debug("monitor", checkCertificateResult.certInfo.fingerprint256);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
log.debug("monitor", "Not valid object");
|
||||||
|
}
|
||||||
|
} catch (e) { }
|
||||||
|
|
||||||
}
|
}
|
||||||
tls_info_bean.info_json = JSON.stringify(checkCertificateResult);
|
|
||||||
await R.store(tls_info_bean);
|
tlsInfoBean.info_json = JSON.stringify(checkCertificateResult);
|
||||||
|
await R.store(tlsInfoBean);
|
||||||
|
|
||||||
return checkCertificateResult;
|
return checkCertificateResult;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send statistics to clients
|
||||||
|
* @param {Server} io Socket server instance
|
||||||
|
* @param {number} monitorID ID of monitor to send
|
||||||
|
* @param {number} userID ID of user to send to
|
||||||
|
*/
|
||||||
static async sendStats(io, monitorID, userID) {
|
static async sendStats(io, monitorID, userID) {
|
||||||
const hasClients = getTotalClientInRoom(io, userID) > 0;
|
const hasClients = getTotalClientInRoom(io, userID) > 0;
|
||||||
|
|
||||||
@@ -445,13 +768,13 @@ class Monitor extends BeanModel {
|
|||||||
await Monitor.sendUptime(24 * 30, io, monitorID, userID);
|
await Monitor.sendUptime(24 * 30, io, monitorID, userID);
|
||||||
await Monitor.sendCertInfo(io, monitorID, userID);
|
await Monitor.sendCertInfo(io, monitorID, userID);
|
||||||
} else {
|
} else {
|
||||||
debug("No clients in the room, no need to send stats");
|
log.debug("monitor", "No clients in the room, no need to send stats");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
* Send the average ping to user
|
||||||
* @param duration : int Hours
|
* @param {number} duration Hours
|
||||||
*/
|
*/
|
||||||
static async sendAvgPing(duration, io, monitorID, userID) {
|
static async sendAvgPing(duration, io, monitorID, userID) {
|
||||||
const timeLogger = new TimeLogger();
|
const timeLogger = new TimeLogger();
|
||||||
@@ -471,12 +794,18 @@ class Monitor extends BeanModel {
|
|||||||
io.to(userID).emit("avgPing", monitorID, avgPing);
|
io.to(userID).emit("avgPing", monitorID, avgPing);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send certificate information to client
|
||||||
|
* @param {Server} io Socket server instance
|
||||||
|
* @param {number} monitorID ID of monitor to send
|
||||||
|
* @param {number} userID ID of user to send to
|
||||||
|
*/
|
||||||
static async sendCertInfo(io, monitorID, userID) {
|
static async sendCertInfo(io, monitorID, userID) {
|
||||||
let tls_info = await R.findOne("monitor_tls_info", "monitor_id = ?", [
|
let tlsInfo = await R.findOne("monitor_tls_info", "monitor_id = ?", [
|
||||||
monitorID,
|
monitorID,
|
||||||
]);
|
]);
|
||||||
if (tls_info != null) {
|
if (tlsInfo != null) {
|
||||||
io.to(userID).emit("certInfo", monitorID, tls_info.info_json);
|
io.to(userID).emit("certInfo", monitorID, tlsInfo.info_json);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -484,7 +813,8 @@ class Monitor extends BeanModel {
|
|||||||
* Uptime with calculation
|
* Uptime with calculation
|
||||||
* Calculation based on:
|
* Calculation based on:
|
||||||
* https://www.uptrends.com/support/kb/reporting/calculation-of-uptime-and-downtime
|
* https://www.uptrends.com/support/kb/reporting/calculation-of-uptime-and-downtime
|
||||||
* @param duration : int Hours
|
* @param {number} duration Hours
|
||||||
|
* @param {number} monitorID ID of monitor to calculate
|
||||||
*/
|
*/
|
||||||
static async calcUptime(duration, monitorID) {
|
static async calcUptime(duration, monitorID) {
|
||||||
const timeLogger = new TimeLogger();
|
const timeLogger = new TimeLogger();
|
||||||
@@ -550,13 +880,23 @@ class Monitor extends BeanModel {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Send Uptime
|
* Send Uptime
|
||||||
* @param duration : int Hours
|
* @param {number} duration Hours
|
||||||
|
* @param {Server} io Socket server instance
|
||||||
|
* @param {number} monitorID ID of monitor to send
|
||||||
|
* @param {number} userID ID of user to send to
|
||||||
*/
|
*/
|
||||||
static async sendUptime(duration, io, monitorID, userID) {
|
static async sendUptime(duration, io, monitorID, userID) {
|
||||||
const uptime = await this.calcUptime(duration, monitorID);
|
const uptime = await this.calcUptime(duration, monitorID);
|
||||||
io.to(userID).emit("uptime", monitorID, duration, uptime);
|
io.to(userID).emit("uptime", monitorID, duration, uptime);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Has status of monitor changed since last beat?
|
||||||
|
* @param {boolean} isFirstBeat Is this the first beat of this monitor?
|
||||||
|
* @param {const} previousBeatStatus Status of the previous beat
|
||||||
|
* @param {const} currentBeatStatus Status of the current beat
|
||||||
|
* @returns {boolean} True if is an important beat else false
|
||||||
|
*/
|
||||||
static isImportantBeat(isFirstBeat, previousBeatStatus, currentBeatStatus) {
|
static isImportantBeat(isFirstBeat, previousBeatStatus, currentBeatStatus) {
|
||||||
// * ? -> ANY STATUS = important [isFirstBeat]
|
// * ? -> ANY STATUS = important [isFirstBeat]
|
||||||
// UP -> PENDING = not important
|
// UP -> PENDING = not important
|
||||||
@@ -575,11 +915,15 @@ class Monitor extends BeanModel {
|
|||||||
return isImportant;
|
return isImportant;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send a notification about a monitor
|
||||||
|
* @param {boolean} isFirstBeat Is this beat the first of this monitor?
|
||||||
|
* @param {Monitor} monitor The monitor to send a notificaton about
|
||||||
|
* @param {Bean} bean Status information about monitor
|
||||||
|
*/
|
||||||
static async sendNotification(isFirstBeat, monitor, bean) {
|
static async sendNotification(isFirstBeat, monitor, bean) {
|
||||||
if (!isFirstBeat || bean.status === DOWN) {
|
if (!isFirstBeat || bean.status === DOWN) {
|
||||||
let notificationList = await R.getAll("SELECT notification.* FROM notification, monitor_notification WHERE monitor_id = ? AND monitor_notification.notification_id = notification.id ", [
|
const notificationList = await Monitor.getNotificationList(monitor);
|
||||||
monitor.id,
|
|
||||||
]);
|
|
||||||
|
|
||||||
let text;
|
let text;
|
||||||
if (bean.status === UP) {
|
if (bean.status === UP) {
|
||||||
@@ -592,18 +936,119 @@ class Monitor extends BeanModel {
|
|||||||
|
|
||||||
for (let notification of notificationList) {
|
for (let notification of notificationList) {
|
||||||
try {
|
try {
|
||||||
await Notification.send(JSON.parse(notification.config), msg, await monitor.toJSON(), bean.toJSON());
|
await Notification.send(JSON.parse(notification.config), msg, await monitor.toJSON(false), bean.toJSON());
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error("Cannot send notification to " + notification.name);
|
log.error("monitor", "Cannot send notification to " + notification.name);
|
||||||
console.log(e);
|
log.error("monitor", e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Clear Status Page Cache
|
|
||||||
apicache.clear();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get list of notification providers for a given monitor
|
||||||
|
* @param {Monitor} monitor Monitor to get notification providers for
|
||||||
|
* @returns {Promise<LooseObject<any>[]>}
|
||||||
|
*/
|
||||||
|
static async getNotificationList(monitor) {
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send notification about a certificate
|
||||||
|
* @param {Object} tlsInfoObject Information about certificate
|
||||||
|
*/
|
||||||
|
async sendCertNotification(tlsInfoObject) {
|
||||||
|
if (tlsInfoObject && tlsInfoObject.certInfo && tlsInfoObject.certInfo.daysRemaining) {
|
||||||
|
const notificationList = await Monitor.getNotificationList(this);
|
||||||
|
|
||||||
|
let notifyDays = await setting("tlsExpiryNotifyDays");
|
||||||
|
if (notifyDays == null || !Array.isArray(notifyDays)) {
|
||||||
|
// Reset Default
|
||||||
|
setSetting("tlsExpiryNotifyDays", [ 7, 14, 21 ], "general");
|
||||||
|
notifyDays = [ 7, 14, 21 ];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (notifyDays != null && Array.isArray(notifyDays)) {
|
||||||
|
for (const day of notifyDays) {
|
||||||
|
log.debug("monitor", "call sendCertNotificationByTargetDays", day);
|
||||||
|
await this.sendCertNotificationByTargetDays(tlsInfoObject.certInfo.daysRemaining, day, notificationList);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send a certificate notification when certificate expires in less
|
||||||
|
* than target days
|
||||||
|
* @param {number} daysRemaining Number of days remaining on certifcate
|
||||||
|
* @param {number} targetDays Number of days to alert after
|
||||||
|
* @param {LooseObject<any>[]} notificationList List of notification providers
|
||||||
|
* @returns {Promise<void>}
|
||||||
|
*/
|
||||||
|
async sendCertNotificationByTargetDays(daysRemaining, targetDays, notificationList) {
|
||||||
|
|
||||||
|
if (daysRemaining > targetDays) {
|
||||||
|
log.debug("monitor", `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) {
|
||||||
|
log.debug("monitor", "Sent already, no need to send again");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let sent = false;
|
||||||
|
log.debug("monitor", "Send certificate notification");
|
||||||
|
|
||||||
|
for (let notification of notificationList) {
|
||||||
|
try {
|
||||||
|
log.debug("monitor", "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) {
|
||||||
|
log.error("monitor", "Cannot send cert notification to " + notification.name);
|
||||||
|
log.error("monitor", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sent) {
|
||||||
|
await R.exec("INSERT INTO notification_sent_history (type, monitor_id, days) VALUES(?, ?, ?)", [
|
||||||
|
"certificate",
|
||||||
|
this.id,
|
||||||
|
targetDays,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
log.debug("monitor", "No notification, no need to send cert notification");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the status of the previous heartbeat
|
||||||
|
* @param {number} monitorID ID of monitor to check
|
||||||
|
* @returns {Promise<LooseObject<any>>}
|
||||||
|
*/
|
||||||
|
static async getPreviousHeartbeat(monitorID) {
|
||||||
|
return await R.getRow(`
|
||||||
|
SELECT status, time FROM heartbeat
|
||||||
|
WHERE id = (select MAX(id) from heartbeat where monitor_id = ?)
|
||||||
|
`, [
|
||||||
|
monitorID
|
||||||
|
]);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = Monitor;
|
module.exports = Monitor;
|
||||||
|
25
server/model/proxy.js
Normal file
25
server/model/proxy.js
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
const { BeanModel } = require("redbean-node/dist/bean-model");
|
||||||
|
|
||||||
|
class Proxy extends BeanModel {
|
||||||
|
/**
|
||||||
|
* Return an object that ready to parse to JSON
|
||||||
|
* @returns {Object}
|
||||||
|
*/
|
||||||
|
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;
|
264
server/model/status_page.js
Normal file
264
server/model/status_page.js
Normal file
@@ -0,0 +1,264 @@
|
|||||||
|
const { BeanModel } = require("redbean-node/dist/bean-model");
|
||||||
|
const { R } = require("redbean-node");
|
||||||
|
const cheerio = require("cheerio");
|
||||||
|
const { UptimeKumaServer } = require("../uptime-kuma-server");
|
||||||
|
|
||||||
|
class StatusPage extends BeanModel {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Like this: { "test-uptime.kuma.pet": "default" }
|
||||||
|
* @type {{}}
|
||||||
|
*/
|
||||||
|
static domainMappingList = { };
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param {Response} response
|
||||||
|
* @param {string} indexHTML
|
||||||
|
* @param {string} slug
|
||||||
|
*/
|
||||||
|
static async handleStatusPageResponse(response, indexHTML, slug) {
|
||||||
|
let statusPage = await R.findOne("status_page", " slug = ? ", [
|
||||||
|
slug
|
||||||
|
]);
|
||||||
|
|
||||||
|
if (statusPage) {
|
||||||
|
response.send(await StatusPage.renderHTML(indexHTML, statusPage));
|
||||||
|
} else {
|
||||||
|
response.status(404).send(UptimeKumaServer.getInstance().indexHTML);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* SSR for status pages
|
||||||
|
* @param {string} indexHTML
|
||||||
|
* @param {StatusPage} statusPage
|
||||||
|
*/
|
||||||
|
static async renderHTML(indexHTML, statusPage) {
|
||||||
|
const $ = cheerio.load(indexHTML);
|
||||||
|
const description155 = statusPage.description?.substring(0, 155);
|
||||||
|
|
||||||
|
$("title").text(statusPage.title);
|
||||||
|
$("meta[name=description]").attr("content", description155);
|
||||||
|
|
||||||
|
if (statusPage.icon) {
|
||||||
|
$("link[rel=icon]")
|
||||||
|
.attr("href", statusPage.icon)
|
||||||
|
.removeAttr("type");
|
||||||
|
|
||||||
|
$("link[rel=apple-touch-icon]").remove();
|
||||||
|
}
|
||||||
|
|
||||||
|
const head = $("head");
|
||||||
|
|
||||||
|
// OG Meta Tags
|
||||||
|
head.append(`<meta property="og:title" content="${statusPage.title}" />`);
|
||||||
|
head.append(`<meta property="og:description" content="${description155}" />`);
|
||||||
|
|
||||||
|
// Preload data
|
||||||
|
const json = JSON.stringify(await StatusPage.getStatusPageData(statusPage));
|
||||||
|
head.append(`
|
||||||
|
<script>
|
||||||
|
window.preloadData = ${json}
|
||||||
|
</script>
|
||||||
|
`);
|
||||||
|
|
||||||
|
// manifest.json
|
||||||
|
$("link[rel=manifest]").attr("href", `/api/status-page/${statusPage.slug}/manifest.json`);
|
||||||
|
|
||||||
|
return $.root().html();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all status page data in one call
|
||||||
|
* @param {StatusPage} statusPage
|
||||||
|
*/
|
||||||
|
static async getStatusPageData(statusPage) {
|
||||||
|
// Incident
|
||||||
|
let incident = await R.findOne("incident", " pin = 1 AND active = 1 AND status_page_id = ? ", [
|
||||||
|
statusPage.id,
|
||||||
|
]);
|
||||||
|
|
||||||
|
if (incident) {
|
||||||
|
incident = incident.toPublicJSON();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Public Group List
|
||||||
|
const publicGroupList = [];
|
||||||
|
const showTags = !!statusPage.show_tags;
|
||||||
|
|
||||||
|
const list = await R.find("group", " public = 1 AND status_page_id = ? ORDER BY weight ", [
|
||||||
|
statusPage.id
|
||||||
|
]);
|
||||||
|
|
||||||
|
for (let groupBean of list) {
|
||||||
|
let monitorGroup = await groupBean.toPublicJSON(showTags);
|
||||||
|
publicGroupList.push(monitorGroup);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Response
|
||||||
|
return {
|
||||||
|
config: await statusPage.toPublicJSON(),
|
||||||
|
incident,
|
||||||
|
publicGroupList
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Loads domain mapping from DB
|
||||||
|
* Return object like this: { "test-uptime.kuma.pet": "default" }
|
||||||
|
* @returns {Promise<void>}
|
||||||
|
*/
|
||||||
|
static async loadDomainMappingList() {
|
||||||
|
StatusPage.domainMappingList = await R.getAssoc(`
|
||||||
|
SELECT domain, slug
|
||||||
|
FROM status_page, status_page_cname
|
||||||
|
WHERE status_page.id = status_page_cname.status_page_id
|
||||||
|
`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send status page list to client
|
||||||
|
* @param {Server} io io Socket server instance
|
||||||
|
* @param {Socket} socket Socket.io instance
|
||||||
|
* @returns {Promise<Bean[]>}
|
||||||
|
*/
|
||||||
|
static async sendStatusPageList(io, socket) {
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update list of domain names
|
||||||
|
* @param {string[]} domainNameList
|
||||||
|
* @returns {Promise<void>}
|
||||||
|
*/
|
||||||
|
async updateDomainNameList(domainNameList) {
|
||||||
|
|
||||||
|
if (!Array.isArray(domainNameList)) {
|
||||||
|
throw new Error("Invalid array");
|
||||||
|
}
|
||||||
|
|
||||||
|
let trx = await R.begin();
|
||||||
|
|
||||||
|
await trx.exec("DELETE FROM status_page_cname WHERE status_page_id = ?", [
|
||||||
|
this.id,
|
||||||
|
]);
|
||||||
|
|
||||||
|
try {
|
||||||
|
for (let domain of domainNameList) {
|
||||||
|
if (typeof domain !== "string") {
|
||||||
|
throw new Error("Invalid domain");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (domain.trim() === "") {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the domain name is used in another status page, delete it
|
||||||
|
await trx.exec("DELETE FROM status_page_cname WHERE domain = ?", [
|
||||||
|
domain,
|
||||||
|
]);
|
||||||
|
|
||||||
|
let mapping = trx.dispense("status_page_cname");
|
||||||
|
mapping.status_page_id = this.id;
|
||||||
|
mapping.domain = domain;
|
||||||
|
await trx.store(mapping);
|
||||||
|
}
|
||||||
|
await trx.commit();
|
||||||
|
} catch (error) {
|
||||||
|
await trx.rollback();
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get list of domain names
|
||||||
|
* @returns {Object[]}
|
||||||
|
*/
|
||||||
|
getDomainNameList() {
|
||||||
|
let domainList = [];
|
||||||
|
for (let domain in StatusPage.domainMappingList) {
|
||||||
|
let s = StatusPage.domainMappingList[domain];
|
||||||
|
|
||||||
|
if (this.slug === s) {
|
||||||
|
domainList.push(domain);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return domainList;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return an object that ready to parse to JSON
|
||||||
|
* @returns {Object}
|
||||||
|
*/
|
||||||
|
async toJSON() {
|
||||||
|
return {
|
||||||
|
id: this.id,
|
||||||
|
slug: this.slug,
|
||||||
|
title: this.title,
|
||||||
|
description: this.description,
|
||||||
|
icon: this.getIcon(),
|
||||||
|
theme: this.theme,
|
||||||
|
published: !!this.published,
|
||||||
|
showTags: !!this.show_tags,
|
||||||
|
domainNameList: this.getDomainNameList(),
|
||||||
|
customCSS: this.custom_css,
|
||||||
|
footerText: this.footer_text,
|
||||||
|
showPoweredBy: !!this.show_powered_by,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return an object that ready to parse to JSON for public
|
||||||
|
* Only show necessary data to public
|
||||||
|
* @returns {Object}
|
||||||
|
*/
|
||||||
|
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,
|
||||||
|
customCSS: this.custom_css,
|
||||||
|
footerText: this.footer_text,
|
||||||
|
showPoweredBy: !!this.show_powered_by,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert slug to status page ID
|
||||||
|
* @param {string} slug
|
||||||
|
*/
|
||||||
|
static async slugToID(slug) {
|
||||||
|
return await R.getCell("SELECT id FROM status_page WHERE slug = ? ", [
|
||||||
|
slug
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get path to the icon for the page
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
getIcon() {
|
||||||
|
if (!this.icon) {
|
||||||
|
return "/icon.svg";
|
||||||
|
} else {
|
||||||
|
return this.icon;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = StatusPage;
|
@@ -1,6 +1,11 @@
|
|||||||
const { BeanModel } = require("redbean-node/dist/bean-model");
|
const { BeanModel } = require("redbean-node/dist/bean-model");
|
||||||
|
|
||||||
class Tag extends BeanModel {
|
class Tag extends BeanModel {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return an object that ready to parse to JSON
|
||||||
|
* @returns {Object}
|
||||||
|
*/
|
||||||
toJSON() {
|
toJSON() {
|
||||||
return {
|
return {
|
||||||
id: this._id,
|
id: this._id,
|
||||||
|
@@ -3,19 +3,30 @@ const passwordHash = require("../password-hash");
|
|||||||
const { R } = require("redbean-node");
|
const { R } = require("redbean-node");
|
||||||
|
|
||||||
class User extends BeanModel {
|
class User extends BeanModel {
|
||||||
|
/**
|
||||||
|
* Reset user password
|
||||||
|
* Fix #1510, as in the context reset-password.js, there is no auto model mapping. Call this static function instead.
|
||||||
|
* @param {number} userID ID of user to update
|
||||||
|
* @param {string} newPassword
|
||||||
|
* @returns {Promise<void>}
|
||||||
|
*/
|
||||||
|
static async resetPassword(userID, newPassword) {
|
||||||
|
await R.exec("UPDATE `user` SET password = ? WHERE id = ? ", [
|
||||||
|
passwordHash.generate(newPassword),
|
||||||
|
userID
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Direct execute, no need R.store()
|
* Reset this users password
|
||||||
* @param newPassword
|
* @param {string} newPassword
|
||||||
* @returns {Promise<void>}
|
* @returns {Promise<void>}
|
||||||
*/
|
*/
|
||||||
async resetPassword(newPassword) {
|
async resetPassword(newPassword) {
|
||||||
await R.exec("UPDATE `user` SET password = ? WHERE id = ? ", [
|
await User.resetPassword(this.id, newPassword);
|
||||||
passwordHash.generate(newPassword),
|
|
||||||
this.id
|
|
||||||
]);
|
|
||||||
this.password = newPassword;
|
this.password = newPassword;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = User;
|
module.exports = User;
|
||||||
|
@@ -13,27 +13,49 @@ let t = {
|
|||||||
|
|
||||||
let instances = [];
|
let instances = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Does a === b
|
||||||
|
* @param {any} a
|
||||||
|
* @returns {function(any): boolean}
|
||||||
|
*/
|
||||||
let matches = function (a) {
|
let matches = function (a) {
|
||||||
return function (b) {
|
return function (b) {
|
||||||
return a === b;
|
return a === b;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Does a!==b
|
||||||
|
* @param {any} a
|
||||||
|
* @returns {function(any): boolean}
|
||||||
|
*/
|
||||||
let doesntMatch = function (a) {
|
let doesntMatch = function (a) {
|
||||||
return function (b) {
|
return function (b) {
|
||||||
return !matches(a)(b);
|
return !matches(a)(b);
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get log duration
|
||||||
|
* @param {number} d Time in ms
|
||||||
|
* @param {string} prefix Prefix for log
|
||||||
|
* @returns {string} Coloured log string
|
||||||
|
*/
|
||||||
let logDuration = function (d, prefix) {
|
let logDuration = function (d, prefix) {
|
||||||
let str = d > 1000 ? (d / 1000).toFixed(2) + "sec" : d + "ms";
|
let str = d > 1000 ? (d / 1000).toFixed(2) + "sec" : d + "ms";
|
||||||
return "\x1b[33m- " + (prefix ? prefix + " " : "") + str + "\x1b[0m";
|
return "\x1b[33m- " + (prefix ? prefix + " " : "") + str + "\x1b[0m";
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get safe headers
|
||||||
|
* @param {Object} res Express response object
|
||||||
|
* @returns {Object}
|
||||||
|
*/
|
||||||
function getSafeHeaders(res) {
|
function getSafeHeaders(res) {
|
||||||
return res.getHeaders ? res.getHeaders() : res._headers;
|
return res.getHeaders ? res.getHeaders() : res._headers;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Constructor for ApiCache instance */
|
||||||
function ApiCache() {
|
function ApiCache() {
|
||||||
let memCache = new MemoryCache();
|
let memCache = new MemoryCache();
|
||||||
|
|
||||||
@@ -68,6 +90,15 @@ function ApiCache() {
|
|||||||
instances.push(this);
|
instances.push(this);
|
||||||
this.id = instances.length;
|
this.id = instances.length;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Logs a message to the console if the `DEBUG` environment variable is set.
|
||||||
|
* @param {string} a The first argument to log.
|
||||||
|
* @param {string} b The second argument to log.
|
||||||
|
* @param {string} c The third argument to log.
|
||||||
|
* @param {string} d The fourth argument to log, and so on... (optional)
|
||||||
|
*
|
||||||
|
* Generated by Trelent
|
||||||
|
*/
|
||||||
function debug(a, b, c, d) {
|
function debug(a, b, c, d) {
|
||||||
let arr = ["\x1b[36m[apicache]\x1b[0m", a, b, c, d].filter(function (arg) {
|
let arr = ["\x1b[36m[apicache]\x1b[0m", a, b, c, d].filter(function (arg) {
|
||||||
return arg !== undefined;
|
return arg !== undefined;
|
||||||
@@ -77,6 +108,13 @@ function ApiCache() {
|
|||||||
return (globalOptions.debug || debugEnv) && console.log.apply(null, arr);
|
return (globalOptions.debug || debugEnv) && console.log.apply(null, arr);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns true if the given request and response should be logged.
|
||||||
|
* @param {Object} request The HTTP request object.
|
||||||
|
* @param {Object} response The HTTP response object.
|
||||||
|
* @param {function(Object, Object):boolean} toggle
|
||||||
|
* @returns {boolean}
|
||||||
|
*/
|
||||||
function shouldCacheResponse(request, response, toggle) {
|
function shouldCacheResponse(request, response, toggle) {
|
||||||
let opt = globalOptions;
|
let opt = globalOptions;
|
||||||
let codes = opt.statusCodes;
|
let codes = opt.statusCodes;
|
||||||
@@ -99,6 +137,11 @@ function ApiCache() {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add key to index array
|
||||||
|
* @param {string} key Key to add
|
||||||
|
* @param {Object} req Express request object
|
||||||
|
*/
|
||||||
function addIndexEntries(key, req) {
|
function addIndexEntries(key, req) {
|
||||||
let groupName = req.apicacheGroup;
|
let groupName = req.apicacheGroup;
|
||||||
|
|
||||||
@@ -111,6 +154,16 @@ function ApiCache() {
|
|||||||
index.all.unshift(key);
|
index.all.unshift(key);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a new object containing only the whitelisted headers.
|
||||||
|
* @param {Object} headers The original object of header names and
|
||||||
|
* values.
|
||||||
|
* @param {string[]} globalOptions.headerWhitelist An array of
|
||||||
|
* strings representing the whitelisted header names to keep in the
|
||||||
|
* output object.
|
||||||
|
*
|
||||||
|
* Generated by Trelent
|
||||||
|
*/
|
||||||
function filterBlacklistedHeaders(headers) {
|
function filterBlacklistedHeaders(headers) {
|
||||||
return Object.keys(headers)
|
return Object.keys(headers)
|
||||||
.filter(function (key) {
|
.filter(function (key) {
|
||||||
@@ -122,6 +175,14 @@ function ApiCache() {
|
|||||||
}, {});
|
}, {});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a cache object
|
||||||
|
* @param {Object} headers The response headers to filter.
|
||||||
|
* @returns {Object} A new object containing only the whitelisted
|
||||||
|
* response headers.
|
||||||
|
*
|
||||||
|
* Generated by Trelent
|
||||||
|
*/
|
||||||
function createCacheObject(status, headers, data, encoding) {
|
function createCacheObject(status, headers, data, encoding) {
|
||||||
return {
|
return {
|
||||||
status: status,
|
status: status,
|
||||||
@@ -132,6 +193,15 @@ function ApiCache() {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets a cache value for the given key.
|
||||||
|
* @param {string} key The cache key to set.
|
||||||
|
* @param {any} value The cache value to set.
|
||||||
|
* @param {number} duration How long in milliseconds the cached
|
||||||
|
* response should be valid for (defaults to 1 hour).
|
||||||
|
*
|
||||||
|
* Generated by Trelent
|
||||||
|
*/
|
||||||
function cacheResponse(key, value, duration) {
|
function cacheResponse(key, value, duration) {
|
||||||
let redis = globalOptions.redisClient;
|
let redis = globalOptions.redisClient;
|
||||||
let expireCallback = globalOptions.events.expire;
|
let expireCallback = globalOptions.events.expire;
|
||||||
@@ -154,6 +224,13 @@ function ApiCache() {
|
|||||||
}, Math.min(duration, 2147483647));
|
}, Math.min(duration, 2147483647));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Appends content to the response.
|
||||||
|
* @param {Object} res Express response object
|
||||||
|
* @param {(string|Buffer)} content The content to append.
|
||||||
|
*
|
||||||
|
* Generated by Trelent
|
||||||
|
*/
|
||||||
function accumulateContent(res, content) {
|
function accumulateContent(res, content) {
|
||||||
if (content) {
|
if (content) {
|
||||||
if (typeof content == "string") {
|
if (typeof content == "string") {
|
||||||
@@ -179,6 +256,17 @@ function ApiCache() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Monkeypatches the response object to add cache control headers
|
||||||
|
* and create a cache object.
|
||||||
|
* @param {Object} req Express request object
|
||||||
|
* @param {Object} res Express response object
|
||||||
|
* @param {function} next Function to call next
|
||||||
|
* @param {string} key Key to add response as
|
||||||
|
* @param {number} duration Time to cache response for
|
||||||
|
* @param {string} strDuration Duration in string form
|
||||||
|
* @param {function(Object, Object):boolean} toggle
|
||||||
|
*/
|
||||||
function makeResponseCacheable(req, res, next, key, duration, strDuration, toggle) {
|
function makeResponseCacheable(req, res, next, key, duration, strDuration, toggle) {
|
||||||
// monkeypatch res.end to create cache object
|
// monkeypatch res.end to create cache object
|
||||||
res._apicache = {
|
res._apicache = {
|
||||||
@@ -245,6 +333,17 @@ function ApiCache() {
|
|||||||
next();
|
next();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send a cached response to client
|
||||||
|
* @param {Request} request Express request object
|
||||||
|
* @param {Response} response Express response object
|
||||||
|
* @param {object} cacheObject Cache object to send
|
||||||
|
* @param {function(Object, Object):boolean} toggle
|
||||||
|
* @param {function} next Function to call next
|
||||||
|
* @param {number} duration Not used
|
||||||
|
* @returns {boolean|undefined} true if the request should be
|
||||||
|
* cached, false otherwise. If undefined, defaults to true.
|
||||||
|
*/
|
||||||
function sendCachedResponse(request, response, cacheObject, toggle, next, duration) {
|
function sendCachedResponse(request, response, cacheObject, toggle, next, duration) {
|
||||||
if (toggle && !toggle(request, response)) {
|
if (toggle && !toggle(request, response)) {
|
||||||
return next();
|
return next();
|
||||||
@@ -285,12 +384,19 @@ function ApiCache() {
|
|||||||
return response.end(data, cacheObject.encoding);
|
return response.end(data, cacheObject.encoding);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Sync caching options */
|
||||||
function syncOptions() {
|
function syncOptions() {
|
||||||
for (let i in middlewareOptions) {
|
for (let i in middlewareOptions) {
|
||||||
Object.assign(middlewareOptions[i].options, globalOptions, middlewareOptions[i].localOptions);
|
Object.assign(middlewareOptions[i].options, globalOptions, middlewareOptions[i].localOptions);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clear key from cache
|
||||||
|
* @param {string} target Key to clear
|
||||||
|
* @param {boolean} isAutomatic Is the key being cleared automatically
|
||||||
|
* @returns {number}
|
||||||
|
*/
|
||||||
this.clear = function (target, isAutomatic) {
|
this.clear = function (target, isAutomatic) {
|
||||||
let group = index.groups[target];
|
let group = index.groups[target];
|
||||||
let redis = globalOptions.redisClient;
|
let redis = globalOptions.redisClient;
|
||||||
@@ -365,6 +471,14 @@ function ApiCache() {
|
|||||||
return this.getIndex();
|
return this.getIndex();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts a duration string to an integer number of milliseconds.
|
||||||
|
* @param {(string|number)} duration The string to convert.
|
||||||
|
* @param {number} defaultDuration The default duration to return if
|
||||||
|
* can't parse duration
|
||||||
|
* @returns {number} The converted value in milliseconds, or the
|
||||||
|
* defaultDuration if it can't be parsed.
|
||||||
|
*/
|
||||||
function parseDuration(duration, defaultDuration) {
|
function parseDuration(duration, defaultDuration) {
|
||||||
if (typeof duration === "number") {
|
if (typeof duration === "number") {
|
||||||
return duration;
|
return duration;
|
||||||
@@ -387,17 +501,24 @@ function ApiCache() {
|
|||||||
return defaultDuration;
|
return defaultDuration;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse duration
|
||||||
|
* @param {(number|string)} duration
|
||||||
|
* @returns {number} Duration parsed to a number
|
||||||
|
*/
|
||||||
this.getDuration = function (duration) {
|
this.getDuration = function (duration) {
|
||||||
return parseDuration(duration, globalOptions.defaultDuration);
|
return parseDuration(duration, globalOptions.defaultDuration);
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Return cache performance statistics (hit rate). Suitable for putting into a route:
|
* Return cache performance statistics (hit rate). Suitable for
|
||||||
|
* putting into a route:
|
||||||
* <code>
|
* <code>
|
||||||
* app.get('/api/cache/performance', (req, res) => {
|
* app.get('/api/cache/performance', (req, res) => {
|
||||||
* res.json(apicache.getPerformance())
|
* res.json(apicache.getPerformance())
|
||||||
* })
|
* })
|
||||||
* </code>
|
* </code>
|
||||||
|
* @returns {any[]}
|
||||||
*/
|
*/
|
||||||
this.getPerformance = function () {
|
this.getPerformance = function () {
|
||||||
return performanceArray.map(function (p) {
|
return performanceArray.map(function (p) {
|
||||||
@@ -405,6 +526,11 @@ function ApiCache() {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get index of a group
|
||||||
|
* @param {string} group
|
||||||
|
* @returns {number}
|
||||||
|
*/
|
||||||
this.getIndex = function (group) {
|
this.getIndex = function (group) {
|
||||||
if (group) {
|
if (group) {
|
||||||
return index.groups[group];
|
return index.groups[group];
|
||||||
@@ -413,6 +539,14 @@ function ApiCache() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Express middleware
|
||||||
|
* @param {(string|number)} strDuration Duration to cache responses
|
||||||
|
* for.
|
||||||
|
* @param {function(Object, Object):boolean} middlewareToggle
|
||||||
|
* @param {Object} localOptions Options for APICache
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
this.middleware = function cache(strDuration, middlewareToggle, localOptions) {
|
this.middleware = function cache(strDuration, middlewareToggle, localOptions) {
|
||||||
let duration = instance.getDuration(strDuration);
|
let duration = instance.getDuration(strDuration);
|
||||||
let opt = {};
|
let opt = {};
|
||||||
@@ -436,63 +570,72 @@ function ApiCache() {
|
|||||||
options(localOptions);
|
options(localOptions);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A Function for non tracking performance
|
* A Function for non tracking performance
|
||||||
*/
|
*/
|
||||||
function NOOPCachePerformance() {
|
function NOOPCachePerformance() {
|
||||||
this.report = this.hit = this.miss = function () {}; // noop;
|
this.report = this.hit = this.miss = function () {}; // noop;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A function for tracking and reporting hit rate. These statistics are returned by the getPerformance() call above.
|
* A function for tracking and reporting hit rate. These
|
||||||
*/
|
* statistics are returned by the getPerformance() call above.
|
||||||
|
*/
|
||||||
function CachePerformance() {
|
function CachePerformance() {
|
||||||
/**
|
/**
|
||||||
* Tracks the hit rate for the last 100 requests.
|
* Tracks the hit rate for the last 100 requests. If there
|
||||||
* If there have been fewer than 100 requests, the hit rate just considers the requests that have happened.
|
* have been fewer than 100 requests, the hit rate just
|
||||||
*/
|
* considers the requests that have happened.
|
||||||
|
*/
|
||||||
this.hitsLast100 = new Uint8Array(100 / 4); // each hit is 2 bits
|
this.hitsLast100 = new Uint8Array(100 / 4); // each hit is 2 bits
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Tracks the hit rate for the last 1000 requests.
|
* Tracks the hit rate for the last 1000 requests. If there
|
||||||
* If there have been fewer than 1000 requests, the hit rate just considers the requests that have happened.
|
* have been fewer than 1000 requests, the hit rate just
|
||||||
*/
|
* considers the requests that have happened.
|
||||||
|
*/
|
||||||
this.hitsLast1000 = new Uint8Array(1000 / 4); // each hit is 2 bits
|
this.hitsLast1000 = new Uint8Array(1000 / 4); // each hit is 2 bits
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Tracks the hit rate for the last 10000 requests.
|
* Tracks the hit rate for the last 10000 requests. If there
|
||||||
* If there have been fewer than 10000 requests, the hit rate just considers the requests that have happened.
|
* have been fewer than 10000 requests, the hit rate just
|
||||||
*/
|
* considers the requests that have happened.
|
||||||
|
*/
|
||||||
this.hitsLast10000 = new Uint8Array(10000 / 4); // each hit is 2 bits
|
this.hitsLast10000 = new Uint8Array(10000 / 4); // each hit is 2 bits
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Tracks the hit rate for the last 100000 requests.
|
* Tracks the hit rate for the last 100000 requests. If
|
||||||
* If there have been fewer than 100000 requests, the hit rate just considers the requests that have happened.
|
* there have been fewer than 100000 requests, the hit rate
|
||||||
*/
|
* just considers the requests that have happened.
|
||||||
|
*/
|
||||||
this.hitsLast100000 = new Uint8Array(100000 / 4); // each hit is 2 bits
|
this.hitsLast100000 = new Uint8Array(100000 / 4); // each hit is 2 bits
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The number of calls that have passed through the middleware since the server started.
|
* The number of calls that have passed through the
|
||||||
*/
|
* middleware since the server started.
|
||||||
|
*/
|
||||||
this.callCount = 0;
|
this.callCount = 0;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The total number of hits since the server started
|
* The total number of hits since the server started
|
||||||
*/
|
*/
|
||||||
this.hitCount = 0;
|
this.hitCount = 0;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The key from the last cache hit. This is useful in identifying which route these statistics apply to.
|
* The key from the last cache hit. This is useful in
|
||||||
*/
|
* identifying which route these statistics apply to.
|
||||||
|
*/
|
||||||
this.lastCacheHit = null;
|
this.lastCacheHit = null;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The key from the last cache miss. This is useful in identifying which route these statistics apply to.
|
* The key from the last cache miss. This is useful in
|
||||||
*/
|
* identifying which route these statistics apply to.
|
||||||
|
*/
|
||||||
this.lastCacheMiss = null;
|
this.lastCacheMiss = null;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Return performance statistics
|
* Return performance statistics
|
||||||
*/
|
* @returns {Object}
|
||||||
|
*/
|
||||||
this.report = function () {
|
this.report = function () {
|
||||||
return {
|
return {
|
||||||
lastCacheHit: this.lastCacheHit,
|
lastCacheHit: this.lastCacheHit,
|
||||||
@@ -509,10 +652,13 @@ function ApiCache() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Computes a cache hit rate from an array of hits and misses.
|
* Computes a cache hit rate from an array of hits and
|
||||||
* @param {Uint8Array} array An array representing hits and misses.
|
* misses.
|
||||||
* @returns a number between 0 and 1, or null if the array has no hits or misses
|
* @param {Uint8Array} array An array representing hits and
|
||||||
*/
|
* misses.
|
||||||
|
* @returns {?number} a number between 0 and 1, or null if
|
||||||
|
* the array has no hits or misses
|
||||||
|
*/
|
||||||
this.hitRate = function (array) {
|
this.hitRate = function (array) {
|
||||||
let hits = 0;
|
let hits = 0;
|
||||||
let misses = 0;
|
let misses = 0;
|
||||||
@@ -538,16 +684,17 @@ function ApiCache() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Record a hit or miss in the given array. It will be recorded at a position determined
|
* Record a hit or miss in the given array. It will be
|
||||||
* by the current value of the callCount variable.
|
* recorded at a position determined by the current value of
|
||||||
* @param {Uint8Array} array An array representing hits and misses.
|
* the callCount variable.
|
||||||
* @param {boolean} hit true for a hit, false for a miss
|
* @param {Uint8Array} array An array representing hits and
|
||||||
* Each element in the array is 8 bits, and encodes 4 hit/miss records.
|
* misses.
|
||||||
* Each hit or miss is encoded as to bits as follows:
|
* @param {boolean} hit true for a hit, false for a miss
|
||||||
* 00 means no hit or miss has been recorded in these bits
|
* Each element in the array is 8 bits, and encodes 4
|
||||||
* 01 encodes a hit
|
* hit/miss records. Each hit or miss is encoded as to bits
|
||||||
* 10 encodes a miss
|
* as follows: 00 means no hit or miss has been recorded in
|
||||||
*/
|
* these bits 01 encodes a hit 10 encodes a miss
|
||||||
|
*/
|
||||||
this.recordHitInArray = function (array, hit) {
|
this.recordHitInArray = function (array, hit) {
|
||||||
let arrayIndex = ~~(this.callCount / 4) % array.length;
|
let arrayIndex = ~~(this.callCount / 4) % array.length;
|
||||||
let bitOffset = (this.callCount % 4) * 2; // 2 bits per record, 4 records per uint8 array element
|
let bitOffset = (this.callCount % 4) * 2; // 2 bits per record, 4 records per uint8 array element
|
||||||
@@ -557,9 +704,11 @@ function ApiCache() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Records the hit or miss in the tracking arrays and increments the call count.
|
* Records the hit or miss in the tracking arrays and
|
||||||
* @param {boolean} hit true records a hit, false records a miss
|
* increments the call count.
|
||||||
*/
|
* @param {boolean} hit true records a hit, false records a
|
||||||
|
* miss
|
||||||
|
*/
|
||||||
this.recordHit = function (hit) {
|
this.recordHit = function (hit) {
|
||||||
this.recordHitInArray(this.hitsLast100, hit);
|
this.recordHitInArray(this.hitsLast100, hit);
|
||||||
this.recordHitInArray(this.hitsLast1000, hit);
|
this.recordHitInArray(this.hitsLast1000, hit);
|
||||||
@@ -572,18 +721,18 @@ function ApiCache() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Records a hit event, setting lastCacheMiss to the given key
|
* Records a hit event, setting lastCacheMiss to the given key
|
||||||
* @param {string} key The key that had the cache hit
|
* @param {string} key The key that had the cache hit
|
||||||
*/
|
*/
|
||||||
this.hit = function (key) {
|
this.hit = function (key) {
|
||||||
this.recordHit(true);
|
this.recordHit(true);
|
||||||
this.lastCacheHit = key;
|
this.lastCacheHit = key;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Records a miss event, setting lastCacheMiss to the given key
|
* Records a miss event, setting lastCacheMiss to the given key
|
||||||
* @param {string} key The key that had the cache miss
|
* @param {string} key The key that had the cache miss
|
||||||
*/
|
*/
|
||||||
this.miss = function (key) {
|
this.miss = function (key) {
|
||||||
this.recordHit(false);
|
this.recordHit(false);
|
||||||
this.lastCacheMiss = key;
|
this.lastCacheMiss = key;
|
||||||
@@ -594,6 +743,13 @@ function ApiCache() {
|
|||||||
|
|
||||||
performanceArray.push(perf);
|
performanceArray.push(perf);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cache a request
|
||||||
|
* @param {Object} req Express request object
|
||||||
|
* @param {Object} res Express response object
|
||||||
|
* @param {function} next Function to call next
|
||||||
|
* @returns {any}
|
||||||
|
*/
|
||||||
let cache = function (req, res, next) {
|
let cache = function (req, res, next) {
|
||||||
function bypass() {
|
function bypass() {
|
||||||
debug("bypass detected, skipping cache.");
|
debug("bypass detected, skipping cache.");
|
||||||
@@ -701,6 +857,11 @@ function ApiCache() {
|
|||||||
return cache;
|
return cache;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Process options
|
||||||
|
* @param {Object} options
|
||||||
|
* @returns {Object}
|
||||||
|
*/
|
||||||
this.options = function (options) {
|
this.options = function (options) {
|
||||||
if (options) {
|
if (options) {
|
||||||
Object.assign(globalOptions, options);
|
Object.assign(globalOptions, options);
|
||||||
@@ -721,6 +882,7 @@ function ApiCache() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/** Reset the index */
|
||||||
this.resetIndex = function () {
|
this.resetIndex = function () {
|
||||||
index = {
|
index = {
|
||||||
all: [],
|
all: [],
|
||||||
@@ -728,6 +890,11 @@ function ApiCache() {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new instance of ApiCache
|
||||||
|
* @param {Object} config Config to pass
|
||||||
|
* @returns {ApiCache}
|
||||||
|
*/
|
||||||
this.newInstance = function (config) {
|
this.newInstance = function (config) {
|
||||||
let instance = new ApiCache();
|
let instance = new ApiCache();
|
||||||
|
|
||||||
@@ -738,6 +905,7 @@ function ApiCache() {
|
|||||||
return instance;
|
return instance;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/** Clone this instance */
|
||||||
this.clone = function () {
|
this.clone = function () {
|
||||||
return this.newInstance(this.options());
|
return this.newInstance(this.options());
|
||||||
};
|
};
|
||||||
|
@@ -3,6 +3,15 @@ function MemoryCache() {
|
|||||||
this.size = 0;
|
this.size = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param {string} key Key to store cache as
|
||||||
|
* @param {any} value Value to store
|
||||||
|
* @param {number} time Time to store for
|
||||||
|
* @param {function(any, string)} timeoutCallback Callback to call in
|
||||||
|
* case of timeout
|
||||||
|
* @returns {Object}
|
||||||
|
*/
|
||||||
MemoryCache.prototype.add = function (key, value, time, timeoutCallback) {
|
MemoryCache.prototype.add = function (key, value, time, timeoutCallback) {
|
||||||
let old = this.cache[key];
|
let old = this.cache[key];
|
||||||
let instance = this;
|
let instance = this;
|
||||||
@@ -22,6 +31,11 @@ MemoryCache.prototype.add = function (key, value, time, timeoutCallback) {
|
|||||||
return entry;
|
return entry;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete a cache entry
|
||||||
|
* @param {string} key Key to delete
|
||||||
|
* @returns {null}
|
||||||
|
*/
|
||||||
MemoryCache.prototype.delete = function (key) {
|
MemoryCache.prototype.delete = function (key) {
|
||||||
let entry = this.cache[key];
|
let entry = this.cache[key];
|
||||||
|
|
||||||
@@ -36,18 +50,32 @@ MemoryCache.prototype.delete = function (key) {
|
|||||||
return null;
|
return null;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get value of key
|
||||||
|
* @param {string} key
|
||||||
|
* @returns {Object}
|
||||||
|
*/
|
||||||
MemoryCache.prototype.get = function (key) {
|
MemoryCache.prototype.get = function (key) {
|
||||||
let entry = this.cache[key];
|
let entry = this.cache[key];
|
||||||
|
|
||||||
return entry;
|
return entry;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get value of cache entry
|
||||||
|
* @param {string} key
|
||||||
|
* @returns {any}
|
||||||
|
*/
|
||||||
MemoryCache.prototype.getValue = function (key) {
|
MemoryCache.prototype.getValue = function (key) {
|
||||||
let entry = this.get(key);
|
let entry = this.get(key);
|
||||||
|
|
||||||
return entry && entry.value;
|
return entry && entry.value;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clear cache
|
||||||
|
* @returns {boolean}
|
||||||
|
*/
|
||||||
MemoryCache.prototype.clear = function () {
|
MemoryCache.prototype.clear = function () {
|
||||||
Object.keys(this.cache).forEach(function (key) {
|
Object.keys(this.cache).forEach(function (key) {
|
||||||
this.delete(key);
|
this.delete(key);
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user