Compare commits
1423 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
6ccf741bc4 | ||
|
0a58069742 | ||
|
2b57b3e863 | ||
|
6cd6a2edf0 | ||
|
6961b1bdd2 | ||
|
54d4c4d3f7 | ||
|
c47b6c5995 | ||
|
7ef404ccc1 | ||
|
a5ff27da7a | ||
|
7bb12a7e00 | ||
|
27585d0812 | ||
|
e675316635 | ||
|
b073ec2287 | ||
|
31f45dcfc9 | ||
|
49ac71e25c | ||
|
1a9b013fc2 | ||
|
1326761a8a | ||
|
e48a987b9c | ||
|
712a3c29d4 | ||
|
e9497ac1ab | ||
|
6437ef198f | ||
|
973d5692d0 | ||
|
468cb004d6 | ||
|
f7d41a30fa | ||
|
0ef686ac2f | ||
|
3b5893ea60 | ||
|
21cd4d64c3 | ||
|
db757123ba | ||
|
4dcf31621e | ||
|
9c1ba97e7d | ||
|
e9564619f1 | ||
|
8433bceb32 | ||
|
98d001b38b | ||
|
d9f12a6376 | ||
|
56ba133a1f | ||
|
ec30147a7f | ||
|
2bc165379a | ||
|
2172112144 | ||
|
ecd661c801 | ||
|
8fab7112a1 | ||
|
cc4ed308b0 | ||
|
362280af14 | ||
|
636fc8fcfc | ||
|
1c05ba09dc | ||
|
1565da87cf | ||
|
890e3abf58 | ||
|
33355c51b7 | ||
|
5f5c2d7c46 | ||
|
71f4ab0aa6 | ||
|
24d1dd4c34 | ||
|
c00abac834 | ||
|
439f963749 | ||
|
f15c6470af | ||
|
32f7a0084a | ||
|
f8658d6160 | ||
|
d596f8f7eb | ||
|
23a525e36a | ||
|
221d1d40f5 | ||
|
60ec87941e | ||
|
e8e4361e09 | ||
|
675806829c | ||
|
21c1921867 | ||
|
e490ec6d29 | ||
|
7ad4392529 | ||
|
f3d3e064f8 | ||
|
80c91b8234 | ||
|
dc8289df12 | ||
|
caff9ca736 | ||
|
c7eb72e73b | ||
|
fc5ec5f492 | ||
|
40ebc2df79 | ||
|
abf5e435fe | ||
|
8a372201f1 | ||
|
8ec240fe19 | ||
|
5362aab0e5 | ||
|
bc7271b99c | ||
|
4ebf5a5f07 | ||
|
fbceefec36 | ||
|
0b959514f8 | ||
|
4c5e2ea237 | ||
|
7d92351568 | ||
|
494c53971c | ||
|
fc1914bccd | ||
|
4239cf4255 | ||
|
c196c34840 | ||
|
554402b484 | ||
|
b049e4e1b4 | ||
|
90a2668272 | ||
|
49b5de7d40 | ||
|
69e1880cd3 | ||
|
610b6248aa | ||
|
e591647b60 | ||
|
da99833d4c | ||
|
4bf23fdd1a | ||
|
f99a64da67 | ||
|
2e022789f6 | ||
|
73835f3328 | ||
|
4819112e25 | ||
|
2d3fd738e4 | ||
|
edd8fe2e22 | ||
|
204792dd2d | ||
|
b8e8c1b9db | ||
|
0cead83705 | ||
|
7b0093b8b3 | ||
|
cd7e362b81 | ||
|
a8af2a418e | ||
|
39ac9b887e | ||
|
28c3291020 | ||
|
50711391d1 | ||
|
e88e10cc8e | ||
|
27146ffeef | ||
|
41a9f2ff8a | ||
|
cd7a6e4019 | ||
|
8bb064c6fa | ||
|
1006fbd873 | ||
|
5554432b31 | ||
|
d111db0321 | ||
|
4147a4c404 | ||
|
8c684e9293 | ||
|
f025de6eaf | ||
|
7f394d0630 | ||
|
9fe9e235ca | ||
|
50b84f5f45 | ||
|
c60b741406 | ||
|
f6ea1fe9a5 | ||
|
b7e3ec2372 | ||
|
625fd7c2aa | ||
|
aec80b53d5 | ||
|
06852bbf0d | ||
|
056d957c1e | ||
|
e12225e595 | ||
|
1b6c587cc9 | ||
|
4a1db336df | ||
|
9e9c5cd1d2 | ||
|
1e689d99b4 | ||
|
14fffcf06b | ||
|
6962e056ce | ||
|
b7a898326e | ||
|
a89be0e6d4 | ||
|
b3ac7c3d43 | ||
|
c79b2913a2 | ||
|
b58b83541b | ||
|
df4f91c20d | ||
|
fc6b040a4e | ||
|
9838f36b50 | ||
|
a60072adbc | ||
|
e7aeb6f6bf | ||
|
06e570c52d | ||
|
890b8f8333 | ||
|
df21f7da76 | ||
|
3ea6711053 | ||
|
20c37a70f7 | ||
|
b75db27658 | ||
|
765d8e1297 | ||
|
9dc2cc1f0d | ||
|
2532becf61 | ||
|
6154776b34 | ||
|
7e6b92203d | ||
|
1da00d19fd | ||
|
da4bdab4f6 | ||
|
86ab97ef56 | ||
|
345b0c1829 | ||
|
2ac87fcea7 | ||
|
4862bec965 | ||
|
aa784fb3b2 | ||
|
466b403a96 | ||
|
f32441e2f6 | ||
|
39987ba9ac | ||
|
3b87209e26 | ||
|
e6dc0a0293 | ||
|
5c5a339a36 | ||
|
3040bd41d9 | ||
|
3b58fd3b3c | ||
|
bc86f8bb5f | ||
|
ab5f6dc82c | ||
|
5176fd02c1 | ||
|
02b5cae577 | ||
|
54aa7d5dca | ||
|
4cd5b5563f | ||
|
f1c30204b6 | ||
|
9da28fbbc7 | ||
|
851a04b082 | ||
|
68bc7ac421 | ||
|
73bfdb9ef9 | ||
|
e478084ff9 | ||
|
2dff7dd380 | ||
|
9bfa43100b | ||
|
ad5e1957b1 | ||
|
cc68ebca39 | ||
|
aa27d976c2 | ||
|
ecbc0f0477 | ||
|
f8c7da7995 | ||
|
f3660a0cec | ||
|
92caec95fe | ||
|
2c3abdc146 | ||
|
b1170211b7 | ||
|
eadf2c810a | ||
|
8aa97635ec | ||
|
ee1a56caae | ||
|
e886df4788 | ||
|
5196abfd36 | ||
|
3e68cf2a1c | ||
|
0ab82e6de3 | ||
|
8cdbe37f6f | ||
|
28d13e198c | ||
|
14a062804e | ||
|
cf3e03ab40 | ||
|
191f3ad53b | ||
|
370d522920 | ||
|
e0a1ad8a1c | ||
|
9720006934 | ||
|
cd270bd8b5 | ||
|
bc3229828e | ||
|
a5f23b9839 | ||
|
2052fa175f | ||
|
15b63c82c3 | ||
|
b053bc61ce | ||
|
0e30843a75 | ||
|
2103edb604 | ||
|
b059a36e66 | ||
|
258ff56962 | ||
|
cb4e512dc6 | ||
|
4042c26390 | ||
|
5da2315534 | ||
|
204015f1f5 | ||
|
cc6d17d2e0 | ||
|
68862c0b3f | ||
|
fd15e7c2dc | ||
|
5c4cf68937 | ||
|
214ddc264d | ||
|
2ea71839d1 | ||
|
54efde8185 | ||
|
705124d4ac | ||
|
1cb6940590 | ||
|
0f8ad288f3 | ||
|
434174d350 | ||
|
3d1237ed53 | ||
|
b459408b10 | ||
|
f04fe4d230 | ||
|
e579610426 | ||
|
b115d3f8b9 | ||
|
134b3b8ac1 | ||
|
e7e7751e7b | ||
|
5cd58e6fa3 | ||
|
08763b700a | ||
|
781f855921 | ||
|
9e81fe120f | ||
|
7313aa6563 | ||
|
c7871427c3 | ||
|
c0e67b6de9 | ||
|
a17084f75d | ||
|
e4fe7b802a | ||
|
5761bc9b90 | ||
|
92ea019fd4 | ||
|
a774b37369 | ||
|
06755f249d | ||
|
64f84eb118 | ||
|
afe12ccf24 | ||
|
6f4424de28 | ||
|
24cb212a37 | ||
|
d8a676abb6 | ||
|
0b8d4cdaac | ||
|
268cbdbf8d | ||
|
b60dde0b2d | ||
|
aecf95864e | ||
|
c662d259b0 | ||
|
6b47ad07ca | ||
|
f459ea845c | ||
|
b24c75eec5 | ||
|
cb5f90aa89 | ||
|
edacff123b | ||
|
2faf866e9e | ||
|
8cc3e4b7c1 | ||
|
7b9766091e | ||
|
d95e722658 | ||
|
39b6725163 | ||
|
dfb75c8afb | ||
|
e07aa982c3 | ||
|
1e8a16504b | ||
|
180d881ac1 | ||
|
2271ac4a5a | ||
|
f6bbd1ca67 | ||
|
2ee8378814 | ||
|
d5c02fc627 | ||
|
c84de4d259 | ||
|
c1ccaa7a9f | ||
|
539683f8e9 | ||
|
bd42450e55 | ||
|
71af23cf00 | ||
|
a577fba848 | ||
|
a36f24d827 | ||
|
b007681e67 | ||
|
07f9aafd7b | ||
|
1c8631af8d | ||
|
0d2e02f569 | ||
|
ad1a7c255f | ||
|
230e5110b1 | ||
|
f67d7cdf3f | ||
|
df2f536845 | ||
|
59e7aa74a3 | ||
|
43c1ec640c | ||
|
4f6dec41c6 | ||
|
e29527e22f | ||
|
91f9e10c94 | ||
|
7d3cc002ea | ||
|
6e07ed2081 | ||
|
60460442f8 | ||
|
959ecc65ff | ||
|
c24b64921d | ||
|
b879428a03 | ||
|
c28d8ddff9 | ||
|
3c5de1c889 | ||
|
528a615fb2 | ||
|
b993859926 | ||
|
a5c102e750 | ||
|
64ba2dce24 | ||
|
e5145a209a | ||
|
12696dd53e | ||
|
d565320f74 | ||
|
f1a9046193 | ||
|
afbc283423 | ||
|
16b2cf0e89 | ||
|
c538983b87 | ||
|
0686757160 | ||
|
3e699f8ac3 | ||
|
b0d6b5b13d | ||
|
25ea99a436 | ||
|
c2c3f981bc | ||
|
2c237e9c03 | ||
|
3e85893bdd | ||
|
543a74ecab | ||
|
62ad2f9bb4 | ||
|
7672057319 | ||
|
0f99d49a27 | ||
|
d93f7b33be | ||
|
894aeaea0a | ||
|
97dc8eba13 | ||
|
d39a4770e0 | ||
|
f6ac09b751 | ||
|
a42f7416b5 | ||
|
9c1ad4f8c6 | ||
|
8595824b5d | ||
|
da34685019 | ||
|
0cf28c2025 | ||
|
ed7bc0e6d1 | ||
|
6a3eccf6a6 | ||
|
f9be918246 | ||
|
314ae38f91 | ||
|
97de3959cd | ||
|
63e408f4f2 | ||
|
a3b1123e82 | ||
|
2e54dee817 | ||
|
ceeb47bf82 | ||
|
929d238106 | ||
|
c03d911657 | ||
|
e12642cf21 | ||
|
618d904001 | ||
|
6f86236b63 | ||
|
204339fbed | ||
|
b1465c0282 | ||
|
4002b9f577 | ||
|
bef9cb6a5f | ||
|
4157c7d546 | ||
|
3f63cb246b | ||
|
c3eef28443 | ||
|
f11dfc8f43 | ||
|
9d99c39f30 | ||
|
443235b20b | ||
|
35810e299d | ||
|
b03624b7e3 | ||
|
dcbd9c12cf | ||
|
83ca74eba7 | ||
|
c6cf600722 | ||
|
22ef8ff751 | ||
|
565e9233fe | ||
|
9a7c2d562a | ||
|
c4cb825fef | ||
|
3193533a60 | ||
|
1f1825dbff | ||
|
e4e47c3976 | ||
|
617ba49e6c | ||
|
7853c2cc38 | ||
|
f61c1c47aa | ||
|
9fe07742ea | ||
|
a29eae3213 | ||
|
80698a58b8 | ||
|
bb883e6fa0 | ||
|
120e578398 | ||
|
7017c2e625 | ||
|
2f67d26702 | ||
|
90761cf831 | ||
|
1c4e97439c | ||
|
d23085cddc | ||
|
f96bad1629 | ||
|
38c45a3fe3 | ||
|
e815e51608 | ||
|
bec3b0d2dc | ||
|
2d5096317f | ||
|
1c3da995e3 | ||
|
db6fdf5e26 | ||
|
fce175cad6 | ||
|
b673cfbe94 | ||
|
0ae8010156 | ||
|
527e479f2d | ||
|
68875c3091 | ||
|
f35d7c0a1a | ||
|
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 | ||
|
3a90d246a4 | ||
|
5c25354682 | ||
|
2aad2510b7 | ||
|
fac2f1cbc6 | ||
|
8bc3651a7d | ||
|
684d0a7eb8 | ||
|
b3712ee1cc | ||
|
6bb79597e8 | ||
|
af94424283 | ||
|
728e811969 | ||
|
a6007adce3 | ||
|
30b72d81cf | ||
|
de6e1e7ddd | ||
|
2af754b5e8 | ||
|
3b3763351b | ||
|
34ab6142db | ||
|
9a488d6968 | ||
|
aca395cea1 | ||
|
a49faf09b9 | ||
|
d0d1e0de28 | ||
|
2232236a7a | ||
|
dcecd10c88 | ||
|
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 | ||
|
af07c7f050 | ||
|
95dba6dcaf | ||
|
90c2bf7c94 | ||
|
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 | ||
|
fa777c5bc0 | ||
|
6d0683b055 | ||
|
25262cfb91 | ||
|
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 | ||
|
7a46b44d25 | ||
|
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 | ||
|
42f931f6cf | ||
|
5bf58cc6c4 | ||
|
d344914ca0 | ||
|
2fe5c090aa | ||
|
ed218e73bb | ||
|
201a25c659 | ||
|
b680371746 | ||
|
e488e2dc0a | ||
|
4e3258579d | ||
|
aa8ea6d398 | ||
|
cd3fbc80b4 | ||
|
bb7d67f717 | ||
|
9a35386841 | ||
|
8b0813ceff | ||
|
91178ce6a5 | ||
|
429ad384d0 | ||
|
24e52726b2 | ||
|
e0a0a5db4c | ||
|
93050208bb | ||
|
98ee9caf2c | ||
|
8e99cbf426 | ||
|
cbfecab850 | ||
|
25cc54bf72 | ||
|
3700b16c5b | ||
|
4b9dc2890d | ||
|
f9004bcbed | ||
|
bc174c3325 | ||
|
4c2753af46 | ||
|
c6ba5b621c | ||
|
96536ae391 | ||
|
ba46544772 | ||
|
5c852db1cf | ||
|
069d3765f0 | ||
|
15820c6937 | ||
|
2b14bdae62 | ||
|
000cbeb0ce | ||
|
e118d59ac8 | ||
|
39aa0a7f07 | ||
|
a12dffd1bc | ||
|
410805052e | ||
|
02a8147f22 | ||
|
d962ab7a1c | ||
|
63c8d24d6f | ||
|
254a6bfd36 | ||
|
29f3cbe8c6 | ||
|
53b98ad3e4 | ||
|
dbd7c087e0 | ||
|
d0546afe71 | ||
|
272956025c | ||
|
31b90d12a4 | ||
|
b4ffcc5555 | ||
|
db50ba91cc | ||
|
42ea3fb412 | ||
|
9f8b3151d8 | ||
|
57368c8c6c | ||
|
11ef22edec | ||
|
73e38a13d2 | ||
|
f78d01d770 | ||
|
7532acc95d | ||
|
f4515ad8c5 | ||
|
ed84e56a85 | ||
|
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 | ||
|
a997f8e4f9 | ||
|
09dbb143ea | ||
|
f19e983818 | ||
|
454c1687cf | ||
|
c75c6c5640 | ||
|
5aed36b470 | ||
|
76b9fb967f | ||
|
b58120d258 | ||
|
244a7b3671 | ||
|
7d8b72c6c0 | ||
|
40cc885eb8 | ||
|
f1007ad42f | ||
|
dd28ecaa2d | ||
|
ffa585376d | ||
|
11c2e86bfe | ||
|
1bbf17f3da | ||
|
39f8b30b36 | ||
|
ffb2c2996b | ||
|
b13b20bd95 | ||
|
8febff9282 | ||
|
90f2497548 | ||
|
28be32fc68 | ||
|
cefe43800f | ||
|
97a5b400db | ||
|
ca89f84b9a | ||
|
eaf370637e | ||
|
b49e5d5c39 | ||
|
ee90d2713f | ||
|
e7b2832967 | ||
|
d446a57d42 | ||
|
855b12f435 | ||
|
f390a8caf1 | ||
|
30ce53f57c | ||
|
8c4ab9d652 | ||
|
f931e709e6 | ||
|
5fda1f0f59 | ||
|
11e9eee09d | ||
|
65fc71e485 | ||
|
b69a8b8493 | ||
|
1ac904d6d6 | ||
|
0d3414c6d6 | ||
|
dd3992063e | ||
|
29df70949d | ||
|
0313acd4c5 | ||
|
cd19b9fc49 | ||
|
c57b2c4d28 | ||
|
3dda5938f2 | ||
|
23796723dd | ||
|
51b7a2badb | ||
|
74c584f544 | ||
|
0345719e53 | ||
|
22256dfcd2 | ||
|
3e87eb596f | ||
|
c679613f7e | ||
|
4818bb67d6 | ||
|
9619d31a05 | ||
|
c5cc42272f | ||
|
b0259b5592 | ||
|
227bbdea2f | ||
|
6272514820 | ||
|
1c8407a433 | ||
|
32ec4beda0 | ||
|
9462646ad3 | ||
|
482b3f9233 | ||
|
6014ed1156 | ||
|
bfee63452d | ||
|
076d6bdbb6 | ||
|
ea43422ccf | ||
|
0bbe157099 | ||
|
0053a29d10 | ||
|
2c8d5d28e9 | ||
|
1bbd744d02 | ||
|
2e0e35a1ee | ||
|
1e92487f30 | ||
|
edd2534a1b | ||
|
f6ef390c76 | ||
|
f00ec4dfef | ||
|
43f8fc701c | ||
|
499042504f | ||
|
faf6719e7c | ||
|
a9d264ccfc | ||
|
df8f93f0c2 | ||
|
28c0e16a0c | ||
|
6acc9546a0 | ||
|
f455e3a454 | ||
|
7abbf421d0 | ||
|
3625915a85 | ||
|
d74404e106 | ||
|
1c5bce8afa | ||
|
8b5997691e | ||
|
35360e2069 | ||
|
72a59ce7a4 | ||
|
3d002b3ce9 | ||
|
3d6c52fbea | ||
|
9ee591417d | ||
|
4118de6d53 | ||
|
3a12e209da | ||
|
2c2a824f97 | ||
|
931ca6a3ef | ||
|
d3c90df8a8 | ||
|
38f8a8ac2f | ||
|
807519d07d | ||
|
e684712a77 | ||
|
5afc6a41e3 | ||
|
dcc7856b5d | ||
|
c9b0a81cdc | ||
|
2f97f44086 | ||
|
a13bdaac84 | ||
|
e3745da986 | ||
|
35da8c78f4 | ||
|
7179c6cc4c | ||
|
ed96757b24 | ||
|
3306f4a8e0 | ||
|
a2de9e4e36 | ||
|
3f5133d1ba | ||
|
6f2dcc6dd7 | ||
|
57bed4d672 | ||
|
df36a4bb3c | ||
|
e5913c5abc | ||
|
d21f7971b5 | ||
|
bdcdf47e52 | ||
|
f55350bebc | ||
|
3721d11259 | ||
|
149015556b | ||
|
2bcbeba384 | ||
|
d5d07da4ee | ||
|
2d802585ff | ||
|
6828e8ef6d | ||
|
670754b697 | ||
|
6d1baa329a | ||
|
036218f711 |
@ -1,6 +1,7 @@
|
|||||||
/.idea
|
/.idea
|
||||||
/node_modules
|
/node_modules
|
||||||
/data
|
/data
|
||||||
|
/cypress
|
||||||
/out
|
/out
|
||||||
/test
|
/test
|
||||||
/kubernetes
|
/kubernetes
|
||||||
@ -28,6 +29,11 @@ SECURITY.md
|
|||||||
tsconfig.json
|
tsconfig.json
|
||||||
.env
|
.env
|
||||||
/tmp
|
/tmp
|
||||||
|
/babel.config.js
|
||||||
|
/ecosystem.config.js
|
||||||
|
/extra/healthcheck.exe
|
||||||
|
/extra/healthcheck
|
||||||
|
|
||||||
|
|
||||||
### .gitignore content (commented rules are duplicated)
|
### .gitignore content (commented rules are duplicated)
|
||||||
|
|
||||||
@ -42,4 +48,6 @@ dist-ssr
|
|||||||
#!/data/.gitkeep
|
#!/data/.gitkeep
|
||||||
#.vscode
|
#.vscode
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
### End of .gitignore content
|
### End of .gitignore content
|
||||||
|
@ -19,3 +19,6 @@ indent_size = 2
|
|||||||
|
|
||||||
[*.vue]
|
[*.vue]
|
||||||
trim_trailing_whitespace = false
|
trim_trailing_whitespace = false
|
||||||
|
|
||||||
|
[*.go]
|
||||||
|
indent_style = tab
|
||||||
|
28
.eslintrc.js
28
.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,14 +22,16 @@ module.exports = {
|
|||||||
requireConfigFile: false,
|
requireConfigFile: false,
|
||||||
},
|
},
|
||||||
rules: {
|
rules: {
|
||||||
|
"yoda": "error",
|
||||||
|
eqeqeq: [ "warn", "smart" ],
|
||||||
"linebreak-style": [ "error", "unix" ],
|
"linebreak-style": [ "error", "unix" ],
|
||||||
"camelcase": [ "warn", {
|
"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,
|
||||||
@ -33,16 +40,23 @@ module.exports = {
|
|||||||
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
|
||||||
|
"vue/multi-word-component-names": "off",
|
||||||
"no-multi-spaces": [ "error", {
|
"no-multi-spaces": [ "error", {
|
||||||
ignoreEOLComments: true,
|
ignoreEOLComments: true,
|
||||||
}],
|
}],
|
||||||
|
"array-bracket-spacing": [ "warn", "always", {
|
||||||
|
"singleValue": true,
|
||||||
|
"objectsInArrays": false,
|
||||||
|
"arraysInArrays": false
|
||||||
|
}],
|
||||||
"space-before-function-paren": [ "error", {
|
"space-before-function-paren": [ "error", {
|
||||||
"anonymous": "always",
|
"anonymous": "always",
|
||||||
"named": "never",
|
"named": "never",
|
||||||
@ -59,7 +73,7 @@ module.exports = {
|
|||||||
"keyword-spacing": "warn",
|
"keyword-spacing": "warn",
|
||||||
"space-infix-ops": "warn",
|
"space-infix-ops": "warn",
|
||||||
"arrow-spacing": "warn",
|
"arrow-spacing": "warn",
|
||||||
"no-trailing-spaces": "warn",
|
"no-trailing-spaces": "error",
|
||||||
"no-constant-condition": [ "error", {
|
"no-constant-condition": [ "error", {
|
||||||
"checkLoops": false,
|
"checkLoops": false,
|
||||||
}],
|
}],
|
||||||
|
7
.github/PULL_REQUEST_TEMPLATE.md
vendored
7
.github/PULL_REQUEST_TEMPLATE.md
vendored
@ -1,3 +1,9 @@
|
|||||||
|
⚠️⚠️⚠️ Since we do not accept all types of pull requests and do not want to waste your time. Please be sure that you have read pull request rules:
|
||||||
|
https://github.com/louislam/uptime-kuma/blob/master/CONTRIBUTING.md#can-i-create-a-pull-request-for-uptime-kuma
|
||||||
|
|
||||||
|
Tick the checkbox if you understand [x]:
|
||||||
|
- [ ] I have read and understand the pull request rules.
|
||||||
|
|
||||||
# Description
|
# Description
|
||||||
|
|
||||||
Fixes #(issue)
|
Fixes #(issue)
|
||||||
@ -20,6 +26,7 @@ Please delete any options that are not relevant.
|
|||||||
- [ ] I ran ESLint and other linters for modified files
|
- [ ] I ran ESLint and other linters for modified files
|
||||||
- [ ] I have performed a self-review of my own code and tested it
|
- [ ] I have performed a self-review of my own code and tested it
|
||||||
- [ ] I have commented my code, particularly in hard-to-understand areas
|
- [ ] I have commented my code, particularly in hard-to-understand areas
|
||||||
|
(including JSDoc for methods)
|
||||||
- [ ] My changes generate no new warnings
|
- [ ] My changes generate no new warnings
|
||||||
- [ ] My code needed automated testing. I have added them (this is optional task)
|
- [ ] My code needed automated testing. I have added them (this is optional task)
|
||||||
|
|
||||||
|
61
.github/workflows/auto-test.yml
vendored
61
.github/workflows/auto-test.yml
vendored
@ -11,25 +11,74 @@ on:
|
|||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
auto-test:
|
auto-test:
|
||||||
|
needs: [ check-linters ]
|
||||||
runs-on: ${{ matrix.os }}
|
runs-on: ${{ matrix.os }}
|
||||||
|
timeout-minutes: 15
|
||||||
|
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
os: [macos-latest, ubuntu-latest, windows-latest]
|
os: [macos-latest, ubuntu-latest, windows-latest]
|
||||||
node-version: [14.x, 16.x, 17.x]
|
node: [ 14, 16, 18, 19 ]
|
||||||
# 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
|
||||||
|
|
||||||
|
frontend-unit-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:run:unit
|
||||||
|
24
.github/workflows/stale-bot.yml
vendored
24
.github/workflows/stale-bot.yml
vendored
@ -1,22 +1,22 @@
|
|||||||
name: 'Automatically close stale issues and PRs'
|
name: 'Automatically close stale issues and PRs'
|
||||||
on:
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
schedule:
|
schedule:
|
||||||
- cron: '0 0 * * *'
|
- cron: '0 */6 * * *'
|
||||||
#Run once a day at midnight
|
#Run every 6 hours
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
stale:
|
stale:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/stale@v4
|
- uses: actions/stale@v5
|
||||||
with:
|
with:
|
||||||
stale-issue-message: 'We are clearing up our old issues and your ticket has been open for 6 months with no activity. Remove stale label or comment or this will be closed in 7 days.'
|
stale-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 6 months with no activity. Remove stale label or comment or this will be closed in 7 days.'
|
close-issue-message: 'This issue was closed because it has been stalled for 2 days with no activity.'
|
||||||
close-issue-message: 'This issue was closed because it has been stalled for 7 days with no activity.'
|
days-before-stale: 90
|
||||||
close-pr-message: 'This PR was closed because it has been stalled for 7 days with no activity.'
|
days-before-close: 2
|
||||||
days-before-stale: 180
|
days-before-pr-stale: 999999999
|
||||||
days-before-close: 0
|
days-before-pr-close: 1
|
||||||
exempt-issue-labels: 'News,Medium,High,discussion,bug,doc,'
|
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-issue-assignees: 'louislam'
|
||||||
exempt-pr-assignees: 'louislam'
|
operations-per-run: 200
|
||||||
|
7
.gitignore
vendored
7
.gitignore
vendored
@ -13,3 +13,10 @@ dist-ssr
|
|||||||
/out
|
/out
|
||||||
/tmp
|
/tmp
|
||||||
.env
|
.env
|
||||||
|
|
||||||
|
cypress/videos
|
||||||
|
cypress/screenshots
|
||||||
|
|
||||||
|
/extra/healthcheck.exe
|
||||||
|
/extra/healthcheck
|
||||||
|
/extra/healthcheck-armv7
|
||||||
|
@ -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,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
170
CONTRIBUTING.md
170
CONTRIBUTING.md
@ -1,6 +1,6 @@
|
|||||||
# 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 structured and commented so well, lol. Sorry about that.
|
First of all, I want to thank everyone who made pull requests for Uptime Kuma. I never thought the GitHub Community would be so nice! Because of this, I also never thought that other people would actually read and edit my code. It is not very well structured or commented, sorry about that.
|
||||||
|
|
||||||
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 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.
|
||||||
|
|
||||||
@ -27,12 +27,43 @@ 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 does not affect any existing logic, workflow and performance, 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 have a discussion first**. Especially for a large pull request or you don't know it will be merged or not.
|
||||||
|
|
||||||
|
Here are some references:
|
||||||
|
|
||||||
|
✅ Usually Accept:
|
||||||
|
- Bug/Security fix
|
||||||
|
- Translations
|
||||||
|
- Adding notification providers
|
||||||
|
|
||||||
|
⚠️ Discussion First
|
||||||
|
- Large pull requests
|
||||||
|
- New features
|
||||||
|
|
||||||
|
❌ Won't Merge
|
||||||
|
- Do not pass auto test
|
||||||
|
- Any breaking changes
|
||||||
|
- Duplicated pull request
|
||||||
|
- Buggy
|
||||||
|
- UI/UX is not close to Uptime Kuma
|
||||||
|
- Existing logic is completely modified or deleted for no reason
|
||||||
|
- A function that is completely out of scope
|
||||||
|
- Convert existing code into other programming languages
|
||||||
|
- Unnecessary large code changes (Hard to review, causes code conflicts to other pull requests)
|
||||||
|
|
||||||
|
The above cases cannot cover all situations.
|
||||||
|
|
||||||
|
I (@louislam) have the final say. If your pull request does not meet my expectations, I will reject it, no matter how much time you spend on it. Therefore, it is essential to have a discussion beforehand.
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
If you are not sure whether I will accept your pull request, feel free to create an empty pull request draft first.
|
|
||||||
|
|
||||||
### Recommended Pull Request Guideline
|
### Recommended Pull Request Guideline
|
||||||
|
|
||||||
|
Before deep into coding, discussion first is preferred. Creating an empty pull request for discussion would be recommended.
|
||||||
|
|
||||||
1. Fork the project
|
1. Fork the project
|
||||||
1. Clone your fork repo to local
|
1. Clone your fork repo to local
|
||||||
1. Create a new branch
|
1. Create a new branch
|
||||||
@ -42,78 +73,38 @@ If you are not sure whether I will accept your pull request, feel free to create
|
|||||||
1. Create a pull request: https://github.com/louislam/uptime-kuma/compare
|
1. Create a pull request: https://github.com/louislam/uptime-kuma/compare
|
||||||
1. Write a proper description
|
1. Write a proper description
|
||||||
1. Click "Change to draft"
|
1. Click "Change to draft"
|
||||||
|
1. Discussion
|
||||||
### Pull Request Examples
|
|
||||||
|
|
||||||
Here are some example situations in the past.
|
|
||||||
|
|
||||||
#### ✅ High - Medium Priority
|
|
||||||
|
|
||||||
Easy to review, no breaking change and not touching the existing code
|
|
||||||
|
|
||||||
- Add a new notification
|
|
||||||
- Add a chart
|
|
||||||
- Fix a bug
|
|
||||||
- Translations
|
|
||||||
- Add a independent new feature
|
|
||||||
|
|
||||||
#### *️⃣ Requires one more reviewer
|
|
||||||
|
|
||||||
I do not have such knowledge to test it.
|
|
||||||
|
|
||||||
- Add k8s supports
|
|
||||||
|
|
||||||
#### ⚠ Low Priority - Harsh Mode
|
|
||||||
|
|
||||||
Some pull requests are required to modify the core. To be honest, I do not want anyone to try to do that, because it would spend a lot of your time. I will review your pull request harshly. Also, you may need to write a lot of unit tests to ensure that there is no breaking change.
|
|
||||||
|
|
||||||
- Touch large parts of code of any very important features
|
|
||||||
- Touch monitoring logic
|
|
||||||
- Drop a table or drop a column for any reason
|
|
||||||
- Touch the entry point of Docker or Node.js
|
|
||||||
- Modify auth
|
|
||||||
|
|
||||||
#### *️⃣ Low Priority
|
|
||||||
|
|
||||||
It changed my current workflow and require further studies.
|
|
||||||
|
|
||||||
- Change my release approach
|
|
||||||
|
|
||||||
#### ❌ Won't Merge
|
|
||||||
|
|
||||||
- Any breaking changes
|
|
||||||
- Duplicated pull request
|
|
||||||
- Buggy
|
|
||||||
- Existing logic is completely modified or deleted
|
|
||||||
- A function that is completely out of scope
|
|
||||||
|
|
||||||
## Project Styles
|
## Project Styles
|
||||||
|
|
||||||
I personally do not like something need to learn so much and need to config so much before you can finally start the app.
|
I personally do not like it when something requires so much learning and configuration before you can finally start the app.
|
||||||
|
|
||||||
- Easy to install for non-Docker users, no native build dependency is needed (at least for x86_64), no extra config, no extra effort to get it run
|
- Easy to install for non-Docker users, no native build dependency is needed (at least for x86_64), no extra config, no extra effort required to get it running
|
||||||
- Single container for Docker users, no very complex docker-compose file. Just map the volume and expose the port, then good to go
|
- Single container for Docker users, no very complex docker-compose file. Just map the volume and expose the port, then good to go
|
||||||
- Settings should be configurable in the frontend. Env var is not encouraged.
|
- Settings should be configurable in the frontend. Environment variable is not encouraged, unless it is related to startup such as `DATA_DIR`.
|
||||||
- Easy to use
|
- Easy to use
|
||||||
|
- The web UI styling should be consistent and nice.
|
||||||
|
|
||||||
## Coding Styles
|
## Coding Styles
|
||||||
|
|
||||||
- 4 spaces indentation
|
- 4 spaces indentation
|
||||||
- Follow `.editorconfig`
|
- Follow `.editorconfig`
|
||||||
- Follow ESLint
|
- Follow ESLint
|
||||||
|
- Methods and functions should be documented with JSDoc
|
||||||
|
|
||||||
## Name convention
|
## Name convention
|
||||||
|
|
||||||
- Javascript/Typescript: camelCaseType
|
- Javascript/Typescript: camelCaseType
|
||||||
- SQLite: underscore_type
|
- SQLite: snake_case (Underscore)
|
||||||
- CSS/SCSS: dash-type
|
- CSS/SCSS: kebab-case (Dash)
|
||||||
|
|
||||||
## Tools
|
## Tools
|
||||||
|
|
||||||
- Node.js >= 14
|
- Node.js >= 14
|
||||||
|
- NPM >= 8.5
|
||||||
- Git
|
- Git
|
||||||
- IDE that supports ESLint and EditorConfig (I am using IntelliJ IDEA)
|
- IDE that supports ESLint and EditorConfig (I am using IntelliJ IDEA)
|
||||||
- A SQLite tool (SQLite Expert Personal is suggested)
|
- A SQLite GUI tool (SQLite Expert Personal is suggested)
|
||||||
|
|
||||||
## Install dependencies
|
## Install dependencies
|
||||||
|
|
||||||
@ -121,39 +112,45 @@ I personally do not like something need to learn so much and need to config so m
|
|||||||
npm ci
|
npm ci
|
||||||
```
|
```
|
||||||
|
|
||||||
## How to start the Backend Dev Server
|
## Dev Server
|
||||||
|
|
||||||
(2021-09-23 Update)
|
(2022-04-26 Update)
|
||||||
|
|
||||||
|
We can start the frontend dev server and the backend dev server in one command.
|
||||||
|
|
||||||
|
Port `3000` and port `3001` will be used.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
npm run start-server-dev
|
npm run dev
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Backend Server
|
||||||
|
|
||||||
It binds to `0.0.0.0:3001` by default.
|
It binds to `0.0.0.0:3001` by default.
|
||||||
|
|
||||||
### Backend Details
|
|
||||||
|
|
||||||
It is mainly a socket.io app + express.js.
|
It is mainly a socket.io app + express.js.
|
||||||
|
|
||||||
express.js is just used for serving the frontend built files (index.html, .js and .css etc.)
|
express.js is used for:
|
||||||
|
- entry point such as redirecting to a status page or the dashboard
|
||||||
|
- serving the frontend built files (index.html, .js and .css etc.)
|
||||||
|
- serving internal APIs of status page
|
||||||
|
|
||||||
|
|
||||||
|
### Structure in /server/
|
||||||
|
|
||||||
- model/ (Object model, auto mapping to the database table name)
|
- model/ (Object model, auto mapping to the database table name)
|
||||||
- modules/ (Modified 3rd-party modules)
|
- modules/ (Modified 3rd-party modules)
|
||||||
- notification-providers/ (individual notification logic)
|
- notification-providers/ (individual notification logic)
|
||||||
- routers/ (Express Routers)
|
- routers/ (Express Routers)
|
||||||
- socket-handler (Socket.io Handlers)
|
- socket-handler (Socket.io Handlers)
|
||||||
- server.js (Server main logic)
|
- server.js (Server entry point and main logic)
|
||||||
|
|
||||||
## How to start the Frontend Dev Server
|
## Frontend Dev Server
|
||||||
|
|
||||||
1. Set the env var `NODE_ENV` to "development".
|
It binds to `0.0.0.0:3000` by default. Frontend dev server is used for development only.
|
||||||
2. Start the frontend dev server by the following command.
|
|
||||||
|
|
||||||
```bash
|
For production, it is not used. It will be compiled to `dist` directory instead.
|
||||||
npm run dev
|
|
||||||
```
|
|
||||||
|
|
||||||
It binds to `0.0.0.0:3000` by default.
|
|
||||||
|
|
||||||
You can use Vue.js devtools Chrome extension for debugging.
|
You can use Vue.js devtools Chrome extension for debugging.
|
||||||
|
|
||||||
@ -180,16 +177,23 @@ The data and socket logic are in `src/mixins/socket.js`.
|
|||||||
|
|
||||||
## Unit Test
|
## Unit Test
|
||||||
|
|
||||||
It is an end-to-end testing. It is using Jest and Puppeteer.
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
npm run build
|
npm run build
|
||||||
npm test
|
npm test
|
||||||
```
|
```
|
||||||
|
|
||||||
By default, the Chromium window will be shown up during the test. Specifying `HEADLESS_TEST=1` for terminal environments.
|
## Dependencies
|
||||||
|
|
||||||
## Update 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
|
||||||
@ -221,14 +225,13 @@ https://github.com/louislam/uptime-kuma/issues?q=sort%3Aupdated-desc
|
|||||||
### Release Procedures
|
### Release Procedures
|
||||||
|
|
||||||
1. Draft a release note
|
1. Draft a release note
|
||||||
1. Make sure the repo is cleared
|
2. Make sure the repo is cleared
|
||||||
1. `npm run update-version 1.X.X`
|
3. `npm run release-final with env vars: `VERSION` and `GITHUB_TOKEN`
|
||||||
1. `npm run build`
|
4. Wait until the `Press any key to continue`
|
||||||
1. `npm run build-docker`
|
5. `git push`
|
||||||
1. `git push`
|
6. Publish the release note as 1.X.X
|
||||||
1. Publish the release note as 1.X.X
|
7. Press any key to continue
|
||||||
1. `npm run upload-artifacts`
|
8. SSH to demo site server and update to 1.X.X
|
||||||
1. SSH to demo site server and update to 1.X.X
|
|
||||||
|
|
||||||
Checking:
|
Checking:
|
||||||
|
|
||||||
@ -236,6 +239,15 @@ Checking:
|
|||||||
- Try the Docker image with tag 1.X.X (Clean install / amd64 / arm64 / armv7)
|
- Try the Docker image with tag 1.X.X (Clean install / amd64 / arm64 / armv7)
|
||||||
- Try clean installation with Node.js
|
- Try clean installation with Node.js
|
||||||
|
|
||||||
|
### Release Beta Procedures
|
||||||
|
|
||||||
|
1. Draft a release note, check "This is a pre-release"
|
||||||
|
2. Make sure the repo is cleared
|
||||||
|
3. `npm run release-beta` with env vars: `VERSION` and `GITHUB_TOKEN`
|
||||||
|
4. Wait until the `Press any key to continue`
|
||||||
|
5. Publish the release note as 1.X.X-beta.X
|
||||||
|
6. Press any key to continue
|
||||||
|
|
||||||
### Release Wiki
|
### Release Wiki
|
||||||
|
|
||||||
#### Setup Repo
|
#### Setup Repo
|
||||||
|
88
README.md
88
README.md
@ -7,47 +7,51 @@
|
|||||||
<img src="./public/icon.svg" width="128" alt="" />
|
<img src="./public/icon.svg" width="128" alt="" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
It is a self-hosted monitoring tool like "Uptime Robot".
|
Uptime Kuma is an easy-to-use self-hosted monitoring tool.
|
||||||
|
|
||||||
<img src="https://uptime.kuma.pet/img/dark.jpg" width="700" alt="" />
|
<img src="https://user-images.githubusercontent.com/1336778/212262296-e6205815-ad62-488c-83ec-a5b0d0689f7c.jpg" width="700" alt="" />
|
||||||
|
|
||||||
## 🥔 Live Demo
|
## 🥔 Live Demo
|
||||||
|
|
||||||
Try it!
|
Try it!
|
||||||
|
|
||||||
https://demo.uptime.kuma.pet
|
- Tokyo Demo Server: https://demo.uptime.kuma.pet (Sponsored by [Uptime Kuma Sponsors](https://github.com/louislam/uptime-kuma#%EF%B8%8F-sponsors))
|
||||||
|
- Europe Demo Server: https://demo.uptime-kuma.karimi.dev:27000 (Provided by [@mhkarimi1383](https://github.com/mhkarimi1383))
|
||||||
|
|
||||||
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.
|
It is a temporary live demo, all data will be deleted after 10 minutes. Use the one that is closer to you, but I suggest that you should install and try it out for the best demo experience.
|
||||||
|
|
||||||
VPS is sponsored by Uptime Kuma sponsors on [Open Collective](https://opencollective.com/uptime-kuma)! Thank you so much!
|
|
||||||
|
|
||||||
## ⭐ Features
|
## ⭐ Features
|
||||||
|
|
||||||
* Monitoring uptime for HTTP(s) / TCP / HTTP(s) Keyword / Ping / DNS Record / Push / Steam Game Server.
|
* Monitoring uptime for HTTP(s) / TCP / HTTP(s) Keyword / Ping / DNS Record / Push / Steam Game Server / Docker Containers
|
||||||
* Fancy, Reactive, Fast UI/UX.
|
* Fancy, Reactive, Fast UI/UX
|
||||||
* Notifications via Telegram, Discord, Gotify, Slack, Pushover, Email (SMTP), and [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
|
||||||
* Ping Chart
|
* Map status pages to specific domains
|
||||||
* Certificate Info
|
* Ping chart
|
||||||
|
* Certificate info
|
||||||
|
* Proxy support
|
||||||
|
* 2FA support
|
||||||
|
|
||||||
## 🔧 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.
|
⚠️ Please use a **local volume** only. Other types such as NFS are not supported.
|
||||||
|
|
||||||
Browse to http://localhost:3001 after starting.
|
Uptime Kuma is now running on http://localhost:3001
|
||||||
|
|
||||||
### 💪🏻 Non-Docker
|
### 💪🏻 Non-Docker
|
||||||
|
|
||||||
Required Tools: Node.js >= 14, git and pm2.
|
Required Tools:
|
||||||
|
- [Node.js](https://nodejs.org/en/download/) >= 14
|
||||||
|
- [Git](https://git-scm.com/downloads)
|
||||||
|
- [pm2](https://pm2.keymetrics.io/) - For running Uptime Kuma in the background
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Update your npm to the latest version
|
# Update your npm to the latest version
|
||||||
@ -61,11 +65,25 @@ 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
|
||||||
```
|
|
||||||
|
|
||||||
Browse to http://localhost:3001 after starting.
|
# Start Server
|
||||||
|
pm2 start server/server.js --name uptime-kuma
|
||||||
|
|
||||||
|
|
||||||
|
```
|
||||||
|
Uptime Kuma is now running on http://localhost:3001
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
@ -87,13 +105,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
|
## ❤️ 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)
|
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=3" alt />
|
<img src="https://uptime.kuma.pet/sponsors?v=6" alt />
|
||||||
|
|
||||||
## 🖼 More Screenshots
|
## 🖼 More Screenshots
|
||||||
|
|
||||||
@ -115,7 +133,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.
|
||||||
@ -132,16 +150,30 @@ You can discuss or ask for help in [issues](https://github.com/louislam/uptime-k
|
|||||||
|
|
||||||
### Subreddit
|
### Subreddit
|
||||||
|
|
||||||
My Reddit account: louislamlam
|
My Reddit account: [u/louislamlam](https://reddit.com/u/louislamlam).
|
||||||
You can mention me if you ask a question on Reddit.
|
You can mention me if you ask a question on Reddit.
|
||||||
https://www.reddit.com/r/UptimeKuma/
|
[r/Uptime kuma](https://www.reddit.com/r/UptimeKuma/)
|
||||||
|
|
||||||
## Contribute
|
## Contribute
|
||||||
|
|
||||||
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
|
||||||
|
|
||||||
|
There are a lot of pull requests right now, but I don't have time to test them all.
|
||||||
|
|
||||||
|
If you want to help, you can check this:
|
||||||
|
https://github.com/louislam/uptime-kuma/wiki/Test-Pull-Requests
|
||||||
|
|
||||||
|
### Test Beta Version
|
||||||
|
|
||||||
|
Check out the latest beta release here: https://github.com/louislam/uptime-kuma/releases
|
||||||
|
|
||||||
|
### 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
|
If you want to translate Uptime Kuma into your language, please read: https://github.com/louislam/uptime-kuma/tree/master/src/languages
|
||||||
|
|
||||||
If you want to modify Uptime Kuma, this guideline may be useful for you: https://github.com/louislam/uptime-kuma/blob/master/CONTRIBUTING.md
|
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.
|
||||||
|
|
||||||
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.
|
### 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
|
||||||
|
16
SECURITY.md
16
SECURITY.md
@ -2,21 +2,15 @@
|
|||||||
|
|
||||||
## Reporting a Vulnerability
|
## Reporting a Vulnerability
|
||||||
|
|
||||||
Please report security issues to uptime@kuma.pet.
|
Please report security issues to https://github.com/louislam/uptime-kuma/security/advisories/new.
|
||||||
|
|
||||||
Do not use the issue tracker or discuss it in the public as it will cause more damage.
|
Do not use the public 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.9.X | :white_check_mark: |
|
|
||||||
| <= 1.8.X | ❌ |
|
|
||||||
|
|
||||||
### Upgradable Docker Tags
|
### Upgradable Docker Tags
|
||||||
|
|
||||||
@ -24,8 +18,8 @@ currently being supported with security updates.
|
|||||||
| ------- | ------------------ |
|
| ------- | ------------------ |
|
||||||
| 1 | :white_check_mark: |
|
| 1 | :white_check_mark: |
|
||||||
| 1-debian | :white_check_mark: |
|
| 1-debian | :white_check_mark: |
|
||||||
| 1-alpine | :white_check_mark: |
|
|
||||||
| latest | :white_check_mark: |
|
| latest | :white_check_mark: |
|
||||||
| debian | :white_check_mark: |
|
| debian | :white_check_mark: |
|
||||||
| alpine | :white_check_mark: |
|
| 1-alpine | ⚠️ Deprecated |
|
||||||
|
| alpine | ⚠️ Deprecated |
|
||||||
| All other tags | ❌ |
|
| All other tags | ❌ |
|
||||||
|
28
config/cypress.config.js
Normal file
28
config/cypress.config.js
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
const { defineConfig } = require("cypress");
|
||||||
|
|
||||||
|
module.exports = defineConfig({
|
||||||
|
projectId: "vyjuem",
|
||||||
|
e2e: {
|
||||||
|
experimentalStudio: true,
|
||||||
|
setupNodeEvents(on, config) {
|
||||||
|
|
||||||
|
},
|
||||||
|
fixturesFolder: "test/cypress/fixtures",
|
||||||
|
screenshotsFolder: "test/cypress/screenshots",
|
||||||
|
videosFolder: "test/cypress/videos",
|
||||||
|
downloadsFolder: "test/cypress/downloads",
|
||||||
|
supportFile: "test/cypress/support/e2e.js",
|
||||||
|
baseUrl: "http://localhost:3002",
|
||||||
|
defaultCommandTimeout: 10000,
|
||||||
|
pageLoadTimeout: 60000,
|
||||||
|
viewportWidth: 1920,
|
||||||
|
viewportHeight: 1080,
|
||||||
|
specPattern: [
|
||||||
|
"test/cypress/e2e/setup.cy.js",
|
||||||
|
"test/cypress/e2e/**/*.js"
|
||||||
|
],
|
||||||
|
},
|
||||||
|
env: {
|
||||||
|
baseUrl: "http://localhost:3002",
|
||||||
|
},
|
||||||
|
});
|
10
config/cypress.frontend.config.js
Normal file
10
config/cypress.frontend.config.js
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
const { defineConfig } = require("cypress");
|
||||||
|
|
||||||
|
module.exports = defineConfig({
|
||||||
|
e2e: {
|
||||||
|
supportFile: false,
|
||||||
|
specPattern: [
|
||||||
|
"test/cypress/unit/**/*.js"
|
||||||
|
],
|
||||||
|
}
|
||||||
|
});
|
@ -1,33 +0,0 @@
|
|||||||
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,5 +0,0 @@
|
|||||||
module.exports = {
|
|
||||||
"rootDir": "..",
|
|
||||||
"testRegex": "./test/frontend.spec.js",
|
|
||||||
};
|
|
||||||
|
|
@ -1,20 +0,0 @@
|
|||||||
module.exports = {
|
|
||||||
"launch": {
|
|
||||||
"dumpio": true,
|
|
||||||
"slowMo": 500,
|
|
||||||
"headless": process.env.HEADLESS_TEST || false,
|
|
||||||
"userDataDir": "./data/test-chrome-profile",
|
|
||||||
args: [
|
|
||||||
"--disable-setuid-sandbox",
|
|
||||||
"--disable-gpu",
|
|
||||||
"--disable-dev-shm-usage",
|
|
||||||
"--no-default-browser-check",
|
|
||||||
"--no-experiments",
|
|
||||||
"--no-first-run",
|
|
||||||
"--no-pings",
|
|
||||||
"--no-sandbox",
|
|
||||||
"--no-zygote",
|
|
||||||
"--single-process",
|
|
||||||
],
|
|
||||||
}
|
|
||||||
};
|
|
@ -1,12 +0,0 @@
|
|||||||
module.exports = {
|
|
||||||
"verbose": true,
|
|
||||||
"preset": "jest-puppeteer",
|
|
||||||
"globals": {
|
|
||||||
"__DEV__": true
|
|
||||||
},
|
|
||||||
"testRegex": "./test/e2e.spec.js",
|
|
||||||
"testEnvironment": "./config/jest-debug-env.js",
|
|
||||||
"rootDir": "..",
|
|
||||||
"testTimeout": 30000,
|
|
||||||
};
|
|
||||||
|
|
@ -1,18 +1,38 @@
|
|||||||
import legacy from "@vitejs/plugin-legacy";
|
import legacy from "@vitejs/plugin-legacy";
|
||||||
import vue from "@vitejs/plugin-vue";
|
import vue from "@vitejs/plugin-vue";
|
||||||
import { defineConfig } from "vite";
|
import { defineConfig } from "vite";
|
||||||
|
import visualizer from "rollup-plugin-visualizer";
|
||||||
|
import viteCompression from "vite-plugin-compression";
|
||||||
|
|
||||||
const postCssScss = require("postcss-scss");
|
const postCssScss = require("postcss-scss");
|
||||||
const postcssRTLCSS = require("postcss-rtlcss");
|
const postcssRTLCSS = require("postcss-rtlcss");
|
||||||
|
|
||||||
|
const viteCompressionFilter = /\.(js|mjs|json|css|html|svg)$/i;
|
||||||
|
|
||||||
// https://vitejs.dev/config/
|
// https://vitejs.dev/config/
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
|
server: {
|
||||||
|
port: 3000,
|
||||||
|
},
|
||||||
|
define: {
|
||||||
|
"FRONTEND_VERSION": JSON.stringify(process.env.npm_package_version),
|
||||||
|
},
|
||||||
plugins: [
|
plugins: [
|
||||||
vue(),
|
vue(),
|
||||||
legacy({
|
legacy({
|
||||||
targets: ["ie > 11"],
|
targets: [ "since 2015" ],
|
||||||
additionalLegacyPolyfills: ["regenerator-runtime/runtime"]
|
}),
|
||||||
})
|
visualizer({
|
||||||
|
filename: "tmp/dist-stats.html"
|
||||||
|
}),
|
||||||
|
viteCompression({
|
||||||
|
algorithm: "gzip",
|
||||||
|
filter: viteCompressionFilter,
|
||||||
|
}),
|
||||||
|
viteCompression({
|
||||||
|
algorithm: "brotliCompress",
|
||||||
|
filter: viteCompressionFilter,
|
||||||
|
}),
|
||||||
],
|
],
|
||||||
css: {
|
css: {
|
||||||
postcss: {
|
postcss: {
|
||||||
@ -21,4 +41,13 @@ export default defineConfig({
|
|||||||
"plugins": [ postcssRTLCSS ]
|
"plugins": [ postcssRTLCSS ]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
build: {
|
||||||
|
rollupOptions: {
|
||||||
|
output: {
|
||||||
|
manualChunks(id, { getModuleInfo, getModuleIds }) {
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
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;
|
25
db/patch-grpc-monitor.sql
Normal file
25
db/patch-grpc-monitor.sql
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
-- You should not modify if this have pushed to Github, unless it does serious wrong with the db.
|
||||||
|
BEGIN TRANSACTION;
|
||||||
|
|
||||||
|
ALTER TABLE monitor
|
||||||
|
ADD grpc_url VARCHAR(255) default null;
|
||||||
|
|
||||||
|
ALTER TABLE monitor
|
||||||
|
ADD grpc_protobuf TEXT default null;
|
||||||
|
|
||||||
|
ALTER TABLE monitor
|
||||||
|
ADD grpc_body TEXT default null;
|
||||||
|
|
||||||
|
ALTER TABLE monitor
|
||||||
|
ADD grpc_metadata TEXT default null;
|
||||||
|
|
||||||
|
ALTER TABLE monitor
|
||||||
|
ADD grpc_method VARCHAR(255) default null;
|
||||||
|
|
||||||
|
ALTER TABLE monitor
|
||||||
|
ADD grpc_service_name VARCHAR(255) default null;
|
||||||
|
|
||||||
|
ALTER TABLE monitor
|
||||||
|
ADD grpc_enable_tls BOOLEAN default 0 not null;
|
||||||
|
|
||||||
|
COMMIT;
|
83
db/patch-maintenance-table2.sql
Normal file
83
db/patch-maintenance-table2.sql
Normal file
@ -0,0 +1,83 @@
|
|||||||
|
-- You should not modify if this have pushed to Github, unless it does serious wrong with the db.
|
||||||
|
BEGIN TRANSACTION;
|
||||||
|
|
||||||
|
-- Just for someone who tested maintenance before (patch-maintenance-table.sql)
|
||||||
|
DROP TABLE IF EXISTS maintenance_status_page;
|
||||||
|
DROP TABLE IF EXISTS monitor_maintenance;
|
||||||
|
DROP TABLE IF EXISTS maintenance;
|
||||||
|
DROP TABLE IF EXISTS maintenance_timeslot;
|
||||||
|
|
||||||
|
-- maintenance
|
||||||
|
CREATE TABLE [maintenance] (
|
||||||
|
[id] INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
|
||||||
|
[title] VARCHAR(150) NOT NULL,
|
||||||
|
[description] TEXT NOT NULL,
|
||||||
|
[user_id] INTEGER REFERENCES [user]([id]) ON DELETE SET NULL ON UPDATE CASCADE,
|
||||||
|
[active] BOOLEAN NOT NULL DEFAULT 1,
|
||||||
|
[strategy] VARCHAR(50) NOT NULL DEFAULT 'single',
|
||||||
|
[start_date] DATETIME,
|
||||||
|
[end_date] DATETIME,
|
||||||
|
[start_time] TIME,
|
||||||
|
[end_time] TIME,
|
||||||
|
[weekdays] VARCHAR2(250) DEFAULT '[]',
|
||||||
|
[days_of_month] TEXT DEFAULT '[]',
|
||||||
|
[interval_day] INTEGER
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX [manual_active] ON [maintenance] (
|
||||||
|
[strategy],
|
||||||
|
[active]
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX [active] ON [maintenance] ([active]);
|
||||||
|
|
||||||
|
CREATE INDEX [maintenance_user_id] ON [maintenance] ([user_id]);
|
||||||
|
|
||||||
|
-- maintenance_status_page
|
||||||
|
CREATE TABLE maintenance_status_page (
|
||||||
|
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
|
||||||
|
status_page_id INTEGER NOT NULL,
|
||||||
|
maintenance_id INTEGER NOT NULL,
|
||||||
|
CONSTRAINT FK_maintenance FOREIGN KEY (maintenance_id) REFERENCES maintenance (id) ON DELETE CASCADE ON UPDATE CASCADE,
|
||||||
|
CONSTRAINT FK_status_page FOREIGN KEY (status_page_id) REFERENCES status_page (id) ON DELETE CASCADE ON UPDATE CASCADE
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX [status_page_id_index]
|
||||||
|
ON [maintenance_status_page]([status_page_id]);
|
||||||
|
|
||||||
|
CREATE INDEX [maintenance_id_index]
|
||||||
|
ON [maintenance_status_page]([maintenance_id]);
|
||||||
|
|
||||||
|
-- maintenance_timeslot
|
||||||
|
CREATE TABLE [maintenance_timeslot] (
|
||||||
|
[id] INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
|
||||||
|
[maintenance_id] INTEGER NOT NULL CONSTRAINT [FK_maintenance] REFERENCES [maintenance]([id]) ON DELETE CASCADE ON UPDATE CASCADE,
|
||||||
|
[start_date] DATETIME NOT NULL,
|
||||||
|
[end_date] DATETIME,
|
||||||
|
[generated_next] BOOLEAN DEFAULT 0
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX [maintenance_id] ON [maintenance_timeslot] ([maintenance_id] DESC);
|
||||||
|
|
||||||
|
CREATE INDEX [active_timeslot_index] ON [maintenance_timeslot] (
|
||||||
|
[maintenance_id] DESC,
|
||||||
|
[start_date] DESC,
|
||||||
|
[end_date] DESC
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX [generated_next_index] ON [maintenance_timeslot] ([generated_next]);
|
||||||
|
|
||||||
|
-- monitor_maintenance
|
||||||
|
CREATE TABLE monitor_maintenance (
|
||||||
|
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
|
||||||
|
monitor_id INTEGER NOT NULL,
|
||||||
|
maintenance_id INTEGER NOT NULL,
|
||||||
|
CONSTRAINT FK_maintenance FOREIGN KEY (maintenance_id) REFERENCES maintenance (id) ON DELETE CASCADE ON UPDATE CASCADE,
|
||||||
|
CONSTRAINT FK_monitor FOREIGN KEY (monitor_id) REFERENCES monitor (id) ON DELETE CASCADE ON UPDATE CASCADE
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX [maintenance_id_index2] ON [monitor_maintenance]([maintenance_id]);
|
||||||
|
|
||||||
|
CREATE INDEX [monitor_id_index] ON [monitor_maintenance]([monitor_id]);
|
||||||
|
|
||||||
|
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;
|
7
db/patch-monitor-expiry-notification.sql
Normal file
7
db/patch-monitor-expiry-notification.sql
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
-- You should not modify if this have pushed to Github, unless it does serious wrong with the db.
|
||||||
|
BEGIN TRANSACTION;
|
||||||
|
|
||||||
|
ALTER TABLE monitor
|
||||||
|
ADD expiry_notification BOOLEAN default 1;
|
||||||
|
|
||||||
|
COMMIT;
|
23
db/patch-proxy.sql
Normal file
23
db/patch-proxy.sql
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
-- You should not modify if this have pushed to Github, unless it does serious wrong with the db.
|
||||||
|
BEGIN TRANSACTION;
|
||||||
|
|
||||||
|
CREATE TABLE proxy (
|
||||||
|
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
|
||||||
|
user_id INT NOT NULL,
|
||||||
|
protocol VARCHAR(10) NOT NULL,
|
||||||
|
host VARCHAR(255) NOT NULL,
|
||||||
|
port SMALLINT NOT NULL,
|
||||||
|
auth BOOLEAN NOT NULL,
|
||||||
|
username VARCHAR(255) NULL,
|
||||||
|
password VARCHAR(255) NULL,
|
||||||
|
active BOOLEAN NOT NULL DEFAULT 1,
|
||||||
|
'default' BOOLEAN NOT NULL DEFAULT 0,
|
||||||
|
created_date DATETIME DEFAULT (DATETIME('now')) NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
ALTER TABLE monitor ADD COLUMN proxy_id INTEGER REFERENCES proxy(id);
|
||||||
|
|
||||||
|
CREATE INDEX proxy_id ON monitor (proxy_id);
|
||||||
|
CREATE INDEX proxy_user_id ON proxy (user_id);
|
||||||
|
|
||||||
|
COMMIT;
|
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==0.9.6 && \
|
pip3 --no-cache-dir install apprise==1.2.1 && \
|
||||||
rm -rf /root/.cache
|
rm -rf /root/.cache
|
||||||
|
16
docker/builder-go.dockerfile
Normal file
16
docker/builder-go.dockerfile
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
############################################
|
||||||
|
# Build in Golang
|
||||||
|
# Run npm run build-healthcheck-armv7 in the host first, another it will be super slow where it is building the armv7 healthcheck
|
||||||
|
############################################
|
||||||
|
FROM golang:1.19.4-buster
|
||||||
|
WORKDIR /app
|
||||||
|
ARG TARGETPLATFORM
|
||||||
|
COPY ./extra/ ./extra/
|
||||||
|
|
||||||
|
# Compile healthcheck.go
|
||||||
|
RUN apt update && \
|
||||||
|
apt --yes --no-install-recommends install curl && \
|
||||||
|
curl -sL https://deb.nodesource.com/setup_18.x | bash && \
|
||||||
|
apt --yes --no-install-recommends install nodejs && \
|
||||||
|
node ./extra/build-healthcheck.js $TARGETPLATFORM && \
|
||||||
|
apt --yes remove nodejs
|
@ -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, specify --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==0.9.6 && \
|
pip3 --no-cache-dir install apprise==1.2.1 && \
|
||||||
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
|
||||||
|
@ -1,31 +1,82 @@
|
|||||||
|
############################################
|
||||||
|
# Build in Golang
|
||||||
|
# Run npm run build-healthcheck-armv7 in the host first, another it will be super slow where it is building the armv7 healthcheck
|
||||||
|
# Check file: builder-go.dockerfile
|
||||||
|
############################################
|
||||||
|
FROM louislam/uptime-kuma:builder-go AS build_healthcheck
|
||||||
|
|
||||||
|
############################################
|
||||||
|
# Build in Node.js
|
||||||
|
############################################
|
||||||
FROM louislam/uptime-kuma:base-debian AS build
|
FROM louislam/uptime-kuma:base-debian AS build
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
ENV PUPPETEER_SKIP_CHROMIUM_DOWNLOAD=1
|
ENV PUPPETEER_SKIP_CHROMIUM_DOWNLOAD=1
|
||||||
|
COPY .npmrc .npmrc
|
||||||
|
COPY package.json package.json
|
||||||
|
COPY package-lock.json package-lock.json
|
||||||
|
RUN npm ci --omit=dev
|
||||||
COPY . .
|
COPY . .
|
||||||
RUN npm ci --production && \
|
COPY --from=build_healthcheck /app/extra/healthcheck /app/extra/healthcheck
|
||||||
chmod +x /app/extra/entrypoint.sh
|
RUN chmod +x /app/extra/entrypoint.sh
|
||||||
|
|
||||||
|
|
||||||
|
############################################
|
||||||
|
# ⭐ Main Image
|
||||||
|
############################################
|
||||||
FROM louislam/uptime-kuma:base-debian AS release
|
FROM louislam/uptime-kuma:base-debian AS release
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
# Copy app files from build layer
|
# Copy app files from build layer
|
||||||
COPY --from=build /app /app
|
COPY --from=build /app /app
|
||||||
|
|
||||||
|
|
||||||
EXPOSE 3001
|
EXPOSE 3001
|
||||||
VOLUME ["/app/data"]
|
VOLUME ["/app/data"]
|
||||||
HEALTHCHECK --interval=60s --timeout=30s --start-period=180s --retries=5 CMD node extra/healthcheck.js
|
HEALTHCHECK --interval=60s --timeout=30s --start-period=180s --retries=5 CMD extra/healthcheck
|
||||||
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"]
|
||||||
|
|
||||||
|
############################################
|
||||||
|
# Mark as Nightly
|
||||||
|
############################################
|
||||||
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 && \
|
||||||
|
@ -3,10 +3,12 @@ WORKDIR /app
|
|||||||
|
|
||||||
ENV PUPPETEER_SKIP_CHROMIUM_DOWNLOAD=1
|
ENV PUPPETEER_SKIP_CHROMIUM_DOWNLOAD=1
|
||||||
|
|
||||||
|
COPY .npmrc .npmrc
|
||||||
|
COPY package.json package.json
|
||||||
|
COPY package-lock.json package-lock.json
|
||||||
|
RUN npm ci --omit=dev
|
||||||
COPY . .
|
COPY . .
|
||||||
RUN npm ci --production && \
|
RUN chmod +x /app/extra/entrypoint.sh
|
||||||
chmod +x /app/extra/entrypoint.sh
|
|
||||||
|
|
||||||
|
|
||||||
FROM louislam/uptime-kuma:base-alpine AS release
|
FROM louislam/uptime-kuma:base-alpine AS release
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
@ -3,4 +3,4 @@ module.exports = {
|
|||||||
name: "uptime-kuma",
|
name: "uptime-kuma",
|
||||||
script: "./server/server.js",
|
script: "./server/server.js",
|
||||||
}]
|
}]
|
||||||
}
|
};
|
||||||
|
79
extra/beta/update-version.js
Normal file
79
extra/beta/update-version.js
Normal file
@ -0,0 +1,79 @@
|
|||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Commit updated files
|
||||||
|
* @param {string} version Version to update to
|
||||||
|
*/
|
||||||
|
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());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a tag with the specified version
|
||||||
|
* @param {string} version Tag to create
|
||||||
|
*/
|
||||||
|
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());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if a tag exists for the specified version
|
||||||
|
* @param {string} version Version to check
|
||||||
|
* @returns {boolean} Does the tag already exist
|
||||||
|
*/
|
||||||
|
function tagExists(version) {
|
||||||
|
if (! version) {
|
||||||
|
throw new Error("invalid version");
|
||||||
|
}
|
||||||
|
|
||||||
|
let res = childProcess.spawnSync("git", [ "tag", "-l", version ]);
|
||||||
|
|
||||||
|
return res.stdout.toString().trim() === version;
|
||||||
|
}
|
27
extra/build-healthcheck.js
Normal file
27
extra/build-healthcheck.js
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
const childProcess = require("child_process");
|
||||||
|
const fs = require("fs");
|
||||||
|
const platform = process.argv[2];
|
||||||
|
|
||||||
|
if (!platform) {
|
||||||
|
console.error("No platform??");
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (platform === "linux/arm/v7") {
|
||||||
|
console.log("Arch: armv7");
|
||||||
|
if (fs.existsSync("./extra/healthcheck-armv7")) {
|
||||||
|
fs.renameSync("./extra/healthcheck-armv7", "./extra/healthcheck");
|
||||||
|
console.log("Already built in the host, skip.");
|
||||||
|
process.exit(0);
|
||||||
|
} else {
|
||||||
|
console.log("prebuilt not found, it will be slow! You should execute `npm run build-healthcheck-armv7` before build.");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (fs.existsSync("./extra/healthcheck-armv7")) {
|
||||||
|
fs.rmSync("./extra/healthcheck-armv7");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const output = childProcess.execSync("go build -x -o ./extra/healthcheck ./extra/healthcheck.go").toString("utf8");
|
||||||
|
console.log(output);
|
||||||
|
|
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());
|
48
extra/download-cloudflared.js
Normal file
48
extra/download-cloudflared.js
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
//
|
||||||
|
|
||||||
|
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");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Download specified file
|
||||||
|
* @param {string} url URL to request
|
||||||
|
*/
|
||||||
|
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
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -35,7 +42,7 @@ function download(url) {
|
|||||||
|
|
||||||
tarStream.on("close", () => {
|
tarStream.on("close", () => {
|
||||||
if (fs.existsSync("./dist-backup")) {
|
if (fs.existsSync("./dist-backup")) {
|
||||||
fs.rmdirSync("./dist-backup", {
|
rmSync("./dist-backup", {
|
||||||
recursive: true
|
recursive: true
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
19
extra/env2arg.js
Normal file
19
extra/env2arg.js
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
|
||||||
|
const childProcess = require("child_process");
|
||||||
|
let env = process.env;
|
||||||
|
|
||||||
|
let cmd = process.argv[2];
|
||||||
|
let args = process.argv.slice(3);
|
||||||
|
let replacedArgs = [];
|
||||||
|
|
||||||
|
for (let arg of args) {
|
||||||
|
for (let key in env) {
|
||||||
|
arg = arg.replaceAll(`$${key}`, env[key]);
|
||||||
|
}
|
||||||
|
replacedArgs.push(arg);
|
||||||
|
}
|
||||||
|
|
||||||
|
let child = childProcess.spawn(cmd, replacedArgs);
|
||||||
|
child.stdout.pipe(process.stdout);
|
||||||
|
child.stderr.pipe(process.stderr);
|
23
extra/fs-rmSync.js
Normal file
23
extra/fs-rmSync.js
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
const fs = require("fs");
|
||||||
|
/**
|
||||||
|
* Detect if `fs.rmSync` is available
|
||||||
|
* to avoid the runtime deprecation warning triggered for using `fs.rmdirSync` with `{ recursive: true }` in Node.js v16,
|
||||||
|
* or the `recursive` property removing completely in the future Node.js version.
|
||||||
|
* See the link below.
|
||||||
|
*
|
||||||
|
* @todo Once we drop the support for Node.js v14 (or at least versions before v14.14.0), we can safely replace this function with `fs.rmSync`, since `fs.rmSync` was add in Node.js v14.14.0 and currently we supports all the Node.js v14 versions that include the versions before the v14.14.0, and this function have almost the same signature with `fs.rmSync`.
|
||||||
|
* @link https://nodejs.org/docs/latest-v16.x/api/deprecations.html#dep0147-fsrmdirpath--recursive-true- the deprecation infomation of `fs.rmdirSync`
|
||||||
|
* @link https://nodejs.org/docs/latest-v16.x/api/fs.html#fsrmsyncpath-options the document of `fs.rmSync`
|
||||||
|
* @param {fs.PathLike} path Valid types for path values in "fs".
|
||||||
|
* @param {fs.RmDirOptions} [options] options for `fs.rmdirSync`, if `fs.rmSync` is available and property `recursive` is true, it will automatically have property `force` with value `true`.
|
||||||
|
*/
|
||||||
|
const rmSync = (path, options) => {
|
||||||
|
if (typeof fs.rmSync === "function") {
|
||||||
|
if (options.recursive) {
|
||||||
|
options.force = true;
|
||||||
|
}
|
||||||
|
return fs.rmSync(path, options);
|
||||||
|
}
|
||||||
|
return fs.rmdirSync(path, options);
|
||||||
|
};
|
||||||
|
module.exports = rmSync;
|
81
extra/healthcheck.go
Normal file
81
extra/healthcheck.go
Normal file
@ -0,0 +1,81 @@
|
|||||||
|
/*
|
||||||
|
* If changed, have to run `npm run build-docker-builder-go`.
|
||||||
|
* This script should be run after a period of time (180s), because the server may need some time to prepare.
|
||||||
|
*/
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/tls"
|
||||||
|
"io/ioutil"
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"runtime"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
isFreeBSD := runtime.GOOS == "freebsd"
|
||||||
|
|
||||||
|
// process.env.NODE_TLS_REJECT_UNAUTHORIZED = "0";
|
||||||
|
http.DefaultTransport.(*http.Transport).TLSClientConfig = &tls.Config{
|
||||||
|
InsecureSkipVerify: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
client := http.Client{
|
||||||
|
Timeout: 28 * time.Second,
|
||||||
|
}
|
||||||
|
|
||||||
|
sslKey := os.Getenv("UPTIME_KUMA_SSL_KEY")
|
||||||
|
if len(sslKey) == 0 {
|
||||||
|
sslKey = os.Getenv("SSL_KEY")
|
||||||
|
}
|
||||||
|
|
||||||
|
sslCert := os.Getenv("UPTIME_KUMA_SSL_CERT")
|
||||||
|
if len(sslCert) == 0 {
|
||||||
|
sslCert = os.Getenv("SSL_CERT")
|
||||||
|
}
|
||||||
|
|
||||||
|
hostname := os.Getenv("UPTIME_KUMA_HOST")
|
||||||
|
if len(hostname) == 0 && !isFreeBSD {
|
||||||
|
hostname = os.Getenv("HOST")
|
||||||
|
}
|
||||||
|
if len(hostname) == 0 {
|
||||||
|
hostname = "127.0.0.1"
|
||||||
|
}
|
||||||
|
|
||||||
|
port := os.Getenv("UPTIME_KUMA_PORT")
|
||||||
|
if len(port) == 0 {
|
||||||
|
port = os.Getenv("PORT")
|
||||||
|
}
|
||||||
|
if len(port) == 0 {
|
||||||
|
port = "3001"
|
||||||
|
}
|
||||||
|
|
||||||
|
protocol := ""
|
||||||
|
if len(sslKey) != 0 && len(sslCert) != 0 {
|
||||||
|
protocol = "https"
|
||||||
|
} else {
|
||||||
|
protocol = "http"
|
||||||
|
}
|
||||||
|
|
||||||
|
url := protocol + "://" + hostname + ":" + port
|
||||||
|
|
||||||
|
log.Println("Checking " + url)
|
||||||
|
resp, err := client.Get(url)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalln(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
_, err = ioutil.ReadAll(resp.Body)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalln(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf("Health Check OK [Res Code: %d]\n", resp.StatusCode)
|
||||||
|
|
||||||
|
}
|
@ -1,4 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
|
* ⚠️ Deprecated: Changed to healthcheck.go, it will be deleted in the future.
|
||||||
* 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");
|
const { FBSD } = require("../server/util-server");
|
||||||
|
@ -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-" + util.genSecret(8);
|
||||||
|
|
||||||
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));
|
||||||
|
|
@ -43,6 +43,11 @@ const main = async () => {
|
|||||||
console.log("Finished.");
|
console.log("Finished.");
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ask question of user
|
||||||
|
* @param {string} question Question to ask
|
||||||
|
* @returns {Promise<string>} Users response
|
||||||
|
*/
|
||||||
function question(question) {
|
function question(question) {
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
rl.question(question, (answer) => {
|
rl.question(question, (answer) => {
|
||||||
|
@ -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();
|
||||||
@ -53,6 +53,11 @@ const main = async () => {
|
|||||||
console.log("Finished.");
|
console.log("Finished.");
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ask question of user
|
||||||
|
* @param {string} question Question to ask
|
||||||
|
* @returns {Promise<string>} Users response
|
||||||
|
*/
|
||||||
function question(question) {
|
function question(question) {
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
rl.question(question, (answer) => {
|
rl.question(question, (answer) => {
|
||||||
|
@ -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,
|
||||||
@ -135,6 +135,11 @@ server.listen({
|
|||||||
udp: 5300
|
udp: 5300
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get human readable request type from request code
|
||||||
|
* @param {number} code Request code to translate
|
||||||
|
* @returns {string} Human readable request type
|
||||||
|
*/
|
||||||
function type(code) {
|
function type(code) {
|
||||||
for (let name in Packet.TYPE) {
|
for (let name in Packet.TYPE) {
|
||||||
if (Packet.TYPE[name] === code) {
|
if (Packet.TYPE[name] === code) {
|
||||||
|
51
extra/simple-mqtt-server.js
Normal file
51
extra/simple-mqtt-server.js
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
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 the MQTT server */
|
||||||
|
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();
|
@ -1,50 +1,45 @@
|
|||||||
// Need to use ES6 to read language files
|
// Need to use ES6 to read language files
|
||||||
|
|
||||||
import fs from "fs";
|
import fs from "fs";
|
||||||
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
|
|
||||||
/**
|
/**
|
||||||
* Look ma, it's cp -R.
|
* Copy across the required language files
|
||||||
* @param {string} src The path to the thing to copy.
|
* Creates a local directory (./languages) and copies the required files
|
||||||
* @param {string} dest The path to the new copy.
|
* into it.
|
||||||
|
* @param {string} langCode Code of language to update. A file will be
|
||||||
|
* created with this code if one does not already exist
|
||||||
|
* @param {string} baseLang The second base language file to copy. This
|
||||||
|
* will be ignored if set to "en" as en.js is copied by default
|
||||||
*/
|
*/
|
||||||
const copyRecursiveSync = function (src, dest) {
|
function copyFiles(langCode, baseLang) {
|
||||||
let exists = fs.existsSync(src);
|
|
||||||
let stats = exists && fs.statSync(src);
|
|
||||||
let isDirectory = exists && stats.isDirectory();
|
|
||||||
|
|
||||||
if (isDirectory) {
|
|
||||||
fs.mkdirSync(dest);
|
|
||||||
fs.readdirSync(src).forEach(function (childItemName) {
|
|
||||||
copyRecursiveSync(path.join(src, childItemName),
|
|
||||||
path.join(dest, childItemName));
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
fs.copyFileSync(src, dest);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
console.log("Arguments:", process.argv);
|
|
||||||
const baseLangCode = process.argv[2] || "en";
|
|
||||||
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");
|
fs.mkdirSync("./languages");
|
||||||
|
|
||||||
|
if (!fs.existsSync(`../../src/languages/${langCode}.js`)) {
|
||||||
|
fs.closeSync(fs.openSync(`./languages/${langCode}.js`, "a"));
|
||||||
|
} else {
|
||||||
|
fs.copyFileSync(`../../src/languages/${langCode}.js`, `./languages/${langCode}.js`);
|
||||||
|
}
|
||||||
|
fs.copyFileSync("../../src/languages/en.js", "./languages/en.js");
|
||||||
|
if (baseLang !== "en") {
|
||||||
|
fs.copyFileSync(`../../src/languages/${baseLang}.js`, `./languages/${baseLang}.js`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update the specified language file
|
||||||
|
* @param {string} langCode Language code to update
|
||||||
|
* @param {string} baseLang Second language to copy keys from
|
||||||
|
*/
|
||||||
|
async function updateLanguage(langCode, baseLangCode) {
|
||||||
const en = (await import("./languages/en.js")).default;
|
const en = (await import("./languages/en.js")).default;
|
||||||
const baseLang = (await import(`./languages/${baseLangCode}.js`)).default;
|
const baseLang = (await import(`./languages/${baseLangCode}.js`)).default;
|
||||||
const files = fs.readdirSync("./languages");
|
|
||||||
console.log("Files:", files);
|
|
||||||
|
|
||||||
for (const file of files) {
|
|
||||||
if (!file.endsWith(".js")) {
|
|
||||||
console.log("Skipping " + file);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
let file = langCode + ".js";
|
||||||
console.log("Processing " + file);
|
console.log("Processing " + file);
|
||||||
const lang = await import("./languages/" + file);
|
const lang = await import("./languages/" + file);
|
||||||
|
|
||||||
@ -82,5 +77,20 @@ for (const file of files) {
|
|||||||
fs.writeFileSync(`../../src/languages/${file}`, code);
|
fs.writeFileSync(`../../src/languages/${file}`, code);
|
||||||
}
|
}
|
||||||
|
|
||||||
fs.rmdirSync("./languages", { recursive: true });
|
// Get command line arguments
|
||||||
|
const baseLangCode = process.env.npm_config_baselang || "en";
|
||||||
|
const langCode = process.env.npm_config_language;
|
||||||
|
|
||||||
|
// We need the file to edit
|
||||||
|
if (langCode == null) {
|
||||||
|
throw new Error("Argument --language=<code> must be provided");
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("Base Lang: " + baseLangCode);
|
||||||
|
console.log("Updating: " + langCode);
|
||||||
|
|
||||||
|
copyFiles(langCode, baseLangCode);
|
||||||
|
await updateLanguage(langCode, baseLangCode);
|
||||||
|
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,29 @@ 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");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Commit updated files
|
||||||
|
* @param {string} version Version to update to
|
||||||
|
*/
|
||||||
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);
|
||||||
|
|
||||||
@ -49,52 +51,26 @@ function commit(version) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a tag with the specified version
|
||||||
|
* @param {string} version Tag to create
|
||||||
|
*/
|
||||||
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());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if a tag exists for the specified version
|
||||||
|
* @param {string} version Version to check
|
||||||
|
* @returns {boolean} Does the tag already exist
|
||||||
|
*/
|
||||||
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,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
56
extra/update-wiki-version.js
Normal file
56
extra/update-wiki-version.js
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
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);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update the wiki with new version number
|
||||||
|
* @param {string} newVersion Version to update to
|
||||||
|
*/
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if a directory exists and then delete it
|
||||||
|
* @param {string} dir Directory to delete
|
||||||
|
*/
|
||||||
|
function safeDelete(dir) {
|
||||||
|
if (fs.existsSync(dir)) {
|
||||||
|
fs.rm(dir, {
|
||||||
|
recursive: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
@ -159,7 +159,7 @@ fi
|
|||||||
check=$(pm2 --version)
|
check=$(pm2 --version)
|
||||||
if [ "$check" == "" ]; then
|
if [ "$check" == "" ]; then
|
||||||
"echo" "-e" "Installing PM2"
|
"echo" "-e" "Installing PM2"
|
||||||
npm install pm2 -g
|
npm install pm2 -g && pm2 install pm2-logrotate
|
||||||
pm2 startup
|
pm2 startup
|
||||||
fi
|
fi
|
||||||
mkdir -p $installPath
|
mkdir -p $installPath
|
||||||
|
22400
package-lock.json
generated
22400
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
192
package.json
192
package.json
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "uptime-kuma",
|
"name": "uptime-kuma",
|
||||||
"version": "1.12.0",
|
"version": "1.19.6",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
@ -10,35 +10,37 @@
|
|||||||
"node": "14.* || >=16.*"
|
"node": "14.* || >=16.*"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"install-legacy": "npm install --legacy-peer-deps",
|
"install-legacy": "npm install",
|
||||||
"update-legacy": "npm update --legacy-peer-deps",
|
"update-legacy": "npm update",
|
||||||
"lint:js": "eslint --ext \".js,.vue\" --ignore-path .gitignore .",
|
"lint:js": "eslint --ext \".js,.vue\" --ignore-path .gitignore .",
|
||||||
|
"lint-fix:js": "eslint --ext \".js,.vue\" --fix --ignore-path .gitignore .",
|
||||||
"lint: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 && npm run jest-backend",
|
||||||
"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-backend": "cross-env TEST_BACKEND=1 jest --runInBand --detectOpenHandles --forceExit --config=./config/jest-backend.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",
|
|
||||||
"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 && 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.12.0-alpine --target release . --push",
|
"build-docker-builder-go": "docker buildx build -f docker/builder-go.dockerfile --platform linux/amd64,linux/arm64,linux/arm/v7 -t louislam/uptime-kuma:builder-go . --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.12.0 -t louislam/uptime-kuma:debian -t louislam/uptime-kuma:1-debian -t louislam/uptime-kuma:1.12.0-debian --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": "node ./extra/env2arg.js docker buildx build -f docker/dockerfile --platform linux/amd64,linux/arm64,linux/arm/v7 -t louislam/uptime-kuma -t louislam/uptime-kuma:1 -t louislam/uptime-kuma:$VERSION -t louislam/uptime-kuma:debian -t louislam/uptime-kuma:1-debian -t louislam/uptime-kuma:$VERSION-debian --target release . --push",
|
||||||
"build-docker-nightly": "npm run build && docker buildx build -f docker/dockerfile --platform linux/amd64,linux/arm64,linux/arm/v7 -t louislam/uptime-kuma:nightly --target nightly . --push",
|
"build-docker-nightly": "npm run build && docker buildx build -f docker/dockerfile --platform linux/amd64,linux/arm64,linux/arm/v7 -t louislam/uptime-kuma:nightly --target nightly . --push",
|
||||||
"build-docker-nightly-alpine": "docker buildx build -f docker/dockerfile-alpine --platform linux/amd64,linux/arm64,linux/arm/v7 -t louislam/uptime-kuma:nightly-alpine --target nightly . --push",
|
"build-docker-nightly-alpine": "docker buildx build -f docker/dockerfile-alpine --platform linux/amd64,linux/arm64,linux/arm/v7 -t louislam/uptime-kuma:nightly-alpine --target nightly . --push",
|
||||||
"build-docker-nightly-amd64": "docker buildx build -f docker/dockerfile --platform linux/amd64 -t louislam/uptime-kuma:nightly-amd64 --target nightly . --push --progress plain",
|
"build-docker-nightly-amd64": "docker buildx build -f docker/dockerfile --platform linux/amd64 -t louislam/uptime-kuma:nightly-amd64 --target nightly . --push --progress plain",
|
||||||
|
"build-docker-pr-test": "docker buildx build -f docker/dockerfile --platform linux/amd64,linux/arm64 -t louislam/uptime-kuma:pr-test --target pr-test . --push",
|
||||||
"upload-artifacts": "docker buildx build -f docker/dockerfile --platform linux/amd64 -t louislam/uptime-kuma:upload-artifact --build-arg VERSION --build-arg GITHUB_TOKEN --target upload-artifact . --progress plain",
|
"upload-artifacts": "docker buildx build -f docker/dockerfile --platform linux/amd64 -t louislam/uptime-kuma:upload-artifact --build-arg VERSION --build-arg GITHUB_TOKEN --target upload-artifact . --progress plain",
|
||||||
"setup": "git checkout 1.12.0 && npm ci --production && npm run download-dist",
|
"setup": "git checkout 1.19.6 && 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",
|
"remove-2fa": "node extra/remove-2fa.js",
|
||||||
@ -49,86 +51,130 @@
|
|||||||
"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",
|
||||||
"update-language-files-with-base-lang": "cd extra/update-language-files && node index.js %npm_config_base_lang% && eslint ../../src/languages/**.js --fix",
|
"simple-mqtt-server": "node extra/simple-mqtt-server.js",
|
||||||
"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 && cross-env-shell eslint ../../src/languages/$npm_config_language.js --fix",
|
||||||
"ncu-patch": "ncu -u -t patch"
|
"ncu-patch": "npm-check-updates -u -t patch",
|
||||||
|
"release-final": "node extra/update-version.js && npm run build-docker && node ./extra/press-any-key.js && npm run upload-artifacts && node ./extra/update-wiki-version.js",
|
||||||
|
"release-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 --config-file ./config/cypress.config.js",
|
||||||
|
"cy:run:unit": "npx cypress run --browser chrome --headless --config-file ./config/cypress.frontend.config.js",
|
||||||
|
"cypress-open": "concurrently -k -r \"node test/prepare-test-server.js && node server/server.js --port=3002 --data-dir=./data/test/\" \"cypress open --config-file ./config/cypress.config.js\"",
|
||||||
|
"build-healthcheck-armv7": "cross-env GOOS=linux GOARCH=arm GOARM=7 go build -x -o ./extra/healthcheck-armv7 ./extra/healthcheck.go"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@grpc/grpc-js": "~1.7.3",
|
||||||
|
"@louislam/ping": "~0.4.2-mod.1",
|
||||||
|
"@louislam/sqlite3": "15.1.2",
|
||||||
|
"args-parser": "~1.3.0",
|
||||||
|
"axios": "~0.27.0",
|
||||||
|
"axios-ntlm": "1.3.0",
|
||||||
|
"badge-maker": "~3.3.1",
|
||||||
|
"bcryptjs": "~2.4.3",
|
||||||
|
"bree": "~7.1.5",
|
||||||
|
"cacheable-lookup": "~6.0.4",
|
||||||
|
"chardet": "~1.4.0",
|
||||||
|
"check-password-strength": "^2.0.5",
|
||||||
|
"cheerio": "~1.0.0-rc.12",
|
||||||
|
"chroma-js": "~2.4.2",
|
||||||
|
"command-exists": "~1.2.9",
|
||||||
|
"compare-versions": "~3.6.0",
|
||||||
|
"compression": "~1.7.4",
|
||||||
|
"dayjs": "~1.11.5",
|
||||||
|
"express": "~4.17.3",
|
||||||
|
"express-basic-auth": "~1.2.1",
|
||||||
|
"express-static-gzip": "~2.1.7",
|
||||||
|
"form-data": "~4.0.0",
|
||||||
|
"http-graceful-shutdown": "~3.1.7",
|
||||||
|
"http-proxy-agent": "~5.0.0",
|
||||||
|
"https-proxy-agent": "~5.0.1",
|
||||||
|
"iconv-lite": "~0.6.3",
|
||||||
|
"jsesc": "~3.0.2",
|
||||||
|
"jsonwebtoken": "~9.0.0",
|
||||||
|
"jwt-decode": "~3.1.2",
|
||||||
|
"limiter": "~2.1.0",
|
||||||
|
"mongodb": "~4.13.0",
|
||||||
|
"mqtt": "~4.3.7",
|
||||||
|
"mssql": "~8.1.4",
|
||||||
|
"mysql2": "~2.3.3",
|
||||||
|
"node-cloudflared-tunnel": "~1.0.9",
|
||||||
|
"node-radius-client": "~1.0.0",
|
||||||
|
"nodemailer": "~6.6.5",
|
||||||
|
"notp": "~2.0.3",
|
||||||
|
"password-hash": "~1.2.2",
|
||||||
|
"pg": "~8.8.0",
|
||||||
|
"pg-connection-string": "~2.5.0",
|
||||||
|
"prom-client": "~13.2.0",
|
||||||
|
"prometheus-api-metrics": "~3.2.1",
|
||||||
|
"protobufjs": "~7.1.1",
|
||||||
|
"redbean-node": "~0.2.0",
|
||||||
|
"redis": "~4.5.1",
|
||||||
|
"socket.io": "~4.5.3",
|
||||||
|
"socket.io-client": "~4.5.3",
|
||||||
|
"socks-proxy-agent": "6.1.1",
|
||||||
|
"tar": "~6.1.11",
|
||||||
|
"tcp-ping": "~0.1.1",
|
||||||
|
"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/fontawesome-svg-core": "~1.2.36",
|
||||||
"@fortawesome/free-regular-svg-icons": "~5.15.4",
|
"@fortawesome/free-regular-svg-icons": "~5.15.4",
|
||||||
"@fortawesome/free-solid-svg-icons": "~5.15.4",
|
"@fortawesome/free-solid-svg-icons": "~5.15.4",
|
||||||
"@fortawesome/vue-fontawesome": "~3.0.0-5",
|
"@fortawesome/vue-fontawesome": "~3.0.0-5",
|
||||||
"@louislam/sqlite3": "~6.0.1",
|
|
||||||
"@popperjs/core": "~2.10.2",
|
"@popperjs/core": "~2.10.2",
|
||||||
"args-parser": "~1.3.0",
|
"@types/bootstrap": "~5.1.9",
|
||||||
"axios": "~0.26.0",
|
"@vitejs/plugin-legacy": "~2.1.0",
|
||||||
"bcryptjs": "~2.4.3",
|
"@vitejs/plugin-vue": "~3.1.0",
|
||||||
|
"@vue/compiler-sfc": "~3.2.36",
|
||||||
|
"@vuepic/vue-datepicker": "~3.4.8",
|
||||||
|
"aedes": "^0.46.3",
|
||||||
|
"babel-plugin-rewire": "~1.2.0",
|
||||||
"bootstrap": "5.1.3",
|
"bootstrap": "5.1.3",
|
||||||
"bree": "~7.1.0",
|
"chart.js": "~3.6.2",
|
||||||
"chardet": "^1.3.0",
|
|
||||||
"chart.js": "~3.6.0",
|
|
||||||
"chartjs-adapter-dayjs": "~1.0.0",
|
"chartjs-adapter-dayjs": "~1.0.0",
|
||||||
"check-password-strength": "^2.0.3",
|
"concurrently": "^7.1.0",
|
||||||
"command-exists": "~1.2.9",
|
"core-js": "~3.26.1",
|
||||||
"compare-versions": "~3.6.0",
|
"cross-env": "~7.0.3",
|
||||||
"dayjs": "~1.10.7",
|
"cypress": "^10.1.0",
|
||||||
"express": "~4.17.1",
|
"delay": "^5.0.0",
|
||||||
"express-basic-auth": "~1.2.0",
|
"dns2": "~2.0.1",
|
||||||
"form-data": "~4.0.0",
|
"eslint": "~8.14.0",
|
||||||
"http-graceful-shutdown": "~3.1.5",
|
"eslint-plugin-vue": "~8.7.1",
|
||||||
"iconv-lite": "^0.6.3",
|
"favico.js": "~0.3.10",
|
||||||
"jsonwebtoken": "~8.5.1",
|
"jest": "~27.2.5",
|
||||||
"jwt-decode": "^3.1.2",
|
"postcss-html": "~1.5.0",
|
||||||
"limiter": "^2.1.0",
|
"postcss-rtlcss": "~3.7.2",
|
||||||
"nodemailer": "~6.6.5",
|
"postcss-scss": "~4.0.4",
|
||||||
"notp": "~2.0.3",
|
"prismjs": "~1.29.0",
|
||||||
"password-hash": "~1.2.2",
|
|
||||||
"postcss-rtlcss": "~3.4.1",
|
|
||||||
"postcss-scss": "~4.0.2",
|
|
||||||
"prom-client": "~13.2.0",
|
|
||||||
"prometheus-api-metrics": "~3.2.0",
|
|
||||||
"qrcode": "~1.5.0",
|
"qrcode": "~1.5.0",
|
||||||
"redbean-node": "0.1.3",
|
"rollup-plugin-visualizer": "^5.6.0",
|
||||||
"socket.io": "~4.4.1",
|
"sass": "~1.42.1",
|
||||||
"socket.io-client": "~4.4.1",
|
"stylelint": "~14.7.1",
|
||||||
"tar": "^6.1.11",
|
"stylelint-config-standard": "~25.0.0",
|
||||||
"tcp-ping": "~0.1.1",
|
"terser": "~5.15.0",
|
||||||
"thirty-two": "~1.0.2",
|
|
||||||
"timezones-list": "~3.0.1",
|
"timezones-list": "~3.0.1",
|
||||||
|
"typescript": "~4.4.4",
|
||||||
"v-pagination-3": "~0.1.7",
|
"v-pagination-3": "~0.1.7",
|
||||||
|
"vite": "~3.1.0",
|
||||||
|
"vite-plugin-compression": "^0.5.1",
|
||||||
"vue": "next",
|
"vue": "next",
|
||||||
"vue-chart-3": "3.0.9",
|
"vue-chart-3": "3.0.9",
|
||||||
"vue-confirm-dialog": "~1.0.2",
|
"vue-confirm-dialog": "~1.0.2",
|
||||||
"vue-contenteditable": "~3.0.4",
|
"vue-contenteditable": "~3.0.4",
|
||||||
"vue-i18n": "~9.1.9",
|
"vue-i18n": "~9.2.2",
|
||||||
"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.12",
|
"vue-router": "~4.0.14",
|
||||||
"vue-toastification": "~2.0.0-rc.5",
|
"vue-toastification": "~2.0.0-rc.5",
|
||||||
"vuedraggable": "~4.1.0"
|
"vuedraggable": "~4.1.0",
|
||||||
},
|
"wait-on": "^6.0.1"
|
||||||
"devDependencies": {
|
|
||||||
"@actions/github": "~5.0.0",
|
|
||||||
"@babel/eslint-parser": "~7.15.8",
|
|
||||||
"@babel/preset-env": "^7.15.8",
|
|
||||||
"@types/bootstrap": "~5.1.6",
|
|
||||||
"@vitejs/plugin-legacy": "~1.6.3",
|
|
||||||
"@vitejs/plugin-vue": "~1.9.4",
|
|
||||||
"@vue/compiler-sfc": "~3.2.22",
|
|
||||||
"babel-plugin-rewire": "~1.2.0",
|
|
||||||
"core-js": "~3.18.3",
|
|
||||||
"cross-env": "~7.0.3",
|
|
||||||
"dns2": "~2.0.1",
|
|
||||||
"eslint": "~7.32.0",
|
|
||||||
"eslint-plugin-vue": "~7.18.0",
|
|
||||||
"jest": "~27.2.5",
|
|
||||||
"jest-puppeteer": "~6.0.0",
|
|
||||||
"puppeteer": "~13.1.3",
|
|
||||||
"sass": "~1.42.1",
|
|
||||||
"stylelint": "~14.2.0",
|
|
||||||
"stylelint-config-standard": "~24.0.0",
|
|
||||||
"typescript": "~4.4.4",
|
|
||||||
"vite": "~2.6.14"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
File diff suppressed because one or more lines are too long
Before Width: | Height: | Size: 6.4 KiB After Width: | Height: | Size: 893 B |
@ -1,8 +1,12 @@
|
|||||||
const { checkLogin } = require("./util-server");
|
|
||||||
const { R } = require("redbean-node");
|
const { R } = require("redbean-node");
|
||||||
|
|
||||||
class TwoFA {
|
class TwoFA {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Disable 2FA for specified user
|
||||||
|
* @param {number} userID ID of user to disable
|
||||||
|
* @returns {Promise<void>}
|
||||||
|
*/
|
||||||
static async disable2FA(userID) {
|
static async disable2FA(userID) {
|
||||||
return await R.exec("UPDATE `user` SET twofa_status = 0 WHERE id = ? ", [
|
return await R.exec("UPDATE `user` SET twofa_status = 0 WHERE id = ? ", [
|
||||||
userID,
|
userID,
|
||||||
|
@ -2,16 +2,19 @@ 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");
|
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,
|
||||||
]);
|
]);
|
||||||
@ -30,11 +33,20 @@ 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) {
|
||||||
setting("disableAuth").then((result) => {
|
|
||||||
if (result) {
|
|
||||||
callback(null, true);
|
|
||||||
} else {
|
|
||||||
// Login Rate Limit
|
// Login Rate Limit
|
||||||
loginRateLimiter.pass(null, 0).then((pass) => {
|
loginRateLimiter.pass(null, 0).then((pass) => {
|
||||||
if (pass) {
|
if (pass) {
|
||||||
@ -49,13 +61,26 @@ function myAuthorizer(username, password, callback) {
|
|||||||
callback(null, false);
|
callback(null, false);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
exports.basicAuth = basicAuth({
|
/**
|
||||||
|
* Use basic auth if auth is not disabled
|
||||||
|
* @param {express.Request} req Express request object
|
||||||
|
* @param {express.Response} res Express response object
|
||||||
|
* @param {express.NextFunction} next
|
||||||
|
*/
|
||||||
|
exports.basicAuth = async function (req, res, next) {
|
||||||
|
const middleware = basicAuth({
|
||||||
authorizer: myAuthorizer,
|
authorizer: myAuthorizer,
|
||||||
authorizeAsync: true,
|
authorizeAsync: true,
|
||||||
challenge: true,
|
challenge: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const disabledAuth = await setting("disableAuth");
|
||||||
|
|
||||||
|
if (!disabledAuth) {
|
||||||
|
middleware(req, res, next);
|
||||||
|
} else {
|
||||||
|
next();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
86
server/cacheable-dns-http-agent.js
Normal file
86
server/cacheable-dns-http-agent.js
Normal file
@ -0,0 +1,86 @@
|
|||||||
|
const https = require("https");
|
||||||
|
const http = require("http");
|
||||||
|
const CacheableLookup = require("cacheable-lookup");
|
||||||
|
const { Settings } = require("./settings");
|
||||||
|
const { log } = require("../src/util");
|
||||||
|
|
||||||
|
class CacheableDnsHttpAgent {
|
||||||
|
|
||||||
|
static cacheable = new CacheableLookup();
|
||||||
|
|
||||||
|
static httpAgentList = {};
|
||||||
|
static httpsAgentList = {};
|
||||||
|
|
||||||
|
static enable = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Register/Disable cacheable to global agents
|
||||||
|
*/
|
||||||
|
static async update() {
|
||||||
|
log.debug("CacheableDnsHttpAgent", "update");
|
||||||
|
let isEnable = await Settings.get("dnsCache");
|
||||||
|
|
||||||
|
if (isEnable !== this.enable) {
|
||||||
|
log.debug("CacheableDnsHttpAgent", "value changed");
|
||||||
|
|
||||||
|
if (isEnable) {
|
||||||
|
log.debug("CacheableDnsHttpAgent", "enable");
|
||||||
|
this.cacheable.install(http.globalAgent);
|
||||||
|
this.cacheable.install(https.globalAgent);
|
||||||
|
} else {
|
||||||
|
log.debug("CacheableDnsHttpAgent", "disable");
|
||||||
|
this.cacheable.uninstall(http.globalAgent);
|
||||||
|
this.cacheable.uninstall(https.globalAgent);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.enable = isEnable;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Attach cacheable to HTTP agent
|
||||||
|
* @param {http.Agent} agent Agent to install
|
||||||
|
*/
|
||||||
|
static install(agent) {
|
||||||
|
this.cacheable.install(agent);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var {https.AgentOptions} agentOptions
|
||||||
|
* @return {https.Agent}
|
||||||
|
*/
|
||||||
|
static getHttpsAgent(agentOptions) {
|
||||||
|
if (!this.enable) {
|
||||||
|
return new https.Agent(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) {
|
||||||
|
if (!this.enable) {
|
||||||
|
return new http.Agent(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,11 +1,13 @@
|
|||||||
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 {
|
||||||
@ -16,6 +18,19 @@ exports.startInterval = () => {
|
|||||||
res.data.slow = "1000.0.0";
|
res.data.slow = "1000.0.0";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (await setting("checkUpdate") === false) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let checkBeta = await setting("checkBeta");
|
||||||
|
|
||||||
|
if (checkBeta && res.data.beta) {
|
||||||
|
if (compareVersions.compare(res.data.beta, res.data.slow, ">")) {
|
||||||
|
exports.latestVersion = res.data.beta;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (res.data.slow) {
|
if (res.data.slow) {
|
||||||
exports.latestVersion = res.data.slow;
|
exports.latestVersion = res.data.slow;
|
||||||
}
|
}
|
||||||
@ -28,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,17 @@
|
|||||||
*/
|
*/
|
||||||
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 server = UptimeKumaServer.getInstance();
|
||||||
|
const io = server.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 +23,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 +38,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();
|
||||||
@ -56,10 +69,11 @@ 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,18 +97,66 @@ 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,
|
||||||
latestVersion: checkVersion.latestVersion,
|
latestVersion: checkVersion.latestVersion,
|
||||||
primaryBaseURL: await setting("primaryBaseURL")
|
primaryBaseURL: await setting("primaryBaseURL"),
|
||||||
|
serverTimezone: await server.getTimezone(),
|
||||||
|
serverTimezoneOffset: server.getTimezoneOffset(),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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");
|
||||||
|
|
||||||
@ -53,7 +53,20 @@ class Database {
|
|||||||
"patch-2fa-invalidate-used-token.sql": true,
|
"patch-2fa-invalidate-used-token.sql": true,
|
||||||
"patch-notification_sent_history.sql": true,
|
"patch-notification_sent_history.sql": true,
|
||||||
"patch-monitor-basic-auth.sql": true,
|
"patch-monitor-basic-auth.sql": true,
|
||||||
}
|
"patch-add-docker-columns.sql": true,
|
||||||
|
"patch-status-page.sql": true,
|
||||||
|
"patch-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-grpc-monitor.sql": true,
|
||||||
|
"patch-add-radius-monitor.sql": true,
|
||||||
|
"patch-monitor-add-resend-interval.sql": true,
|
||||||
|
"patch-maintenance-table2.sql": true,
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The final version should be 10 after merged tag feature
|
* The final version should be 10 after merged tag feature
|
||||||
@ -63,6 +76,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/";
|
||||||
@ -77,10 +94,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(testMode = false) {
|
/**
|
||||||
|
* 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");
|
||||||
@ -110,7 +136,10 @@ class Database {
|
|||||||
|
|
||||||
// Auto map the model to a bean object
|
// Auto map the model to a bean object
|
||||||
R.freeze(true);
|
R.freeze(true);
|
||||||
|
|
||||||
|
if (autoloadModels) {
|
||||||
await R.autoloadModels("./server/model");
|
await R.autoloadModels("./server/model");
|
||||||
|
}
|
||||||
|
|
||||||
await R.exec("PRAGMA foreign_keys = ON");
|
await R.exec("PRAGMA foreign_keys = ON");
|
||||||
if (testMode) {
|
if (testMode) {
|
||||||
@ -123,12 +152,20 @@ class Database {
|
|||||||
await R.exec("PRAGMA cache_size = -12000");
|
await R.exec("PRAGMA cache_size = -12000");
|
||||||
await R.exec("PRAGMA auto_vacuum = FULL");
|
await R.exec("PRAGMA auto_vacuum = FULL");
|
||||||
|
|
||||||
console.log("SQLite config:");
|
// This ensures that an operating system crash or power failure will not corrupt the database.
|
||||||
console.log(await R.getAll("PRAGMA journal_mode"));
|
// FULL synchronous is very safe, but it is also slower.
|
||||||
console.log(await R.getAll("PRAGMA cache_size"));
|
// Read more: https://sqlite.org/pragma.html#pragma_synchronous
|
||||||
console.log("SQLite Version: " + await R.getCell("SELECT sqlite_version()"));
|
await R.exec("PRAGMA synchronous = FULL");
|
||||||
|
|
||||||
|
if (!noLog) {
|
||||||
|
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"));
|
||||||
|
|
||||||
@ -136,33 +173,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 patch not needed");
|
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");
|
||||||
|
|
||||||
|
try {
|
||||||
this.backup(version);
|
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 issue patching the database");
|
log.error("db", "Start Uptime-Kuma failed due to issue patching the database");
|
||||||
console.error("Please submit a bug report if you still encounter the problem after restart: https://github.com/louislam/uptime-kuma/issues");
|
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);
|
||||||
@ -170,22 +213,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) {
|
||||||
@ -193,15 +239,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 issue patching the database");
|
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();
|
||||||
|
|
||||||
@ -212,24 +258,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);
|
||||||
}
|
}
|
||||||
@ -237,24 +354,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 + " was 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();
|
||||||
@ -282,6 +399,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();
|
||||||
}
|
}
|
||||||
@ -296,7 +417,7 @@ class Database {
|
|||||||
};
|
};
|
||||||
process.addListener("unhandledRejection", listener);
|
process.addListener("unhandledRejection", listener);
|
||||||
|
|
||||||
console.log("Closing the database");
|
log.info("db", "Closing the database");
|
||||||
|
|
||||||
while (true) {
|
while (true) {
|
||||||
Database.noReject = true;
|
Database.noReject = true;
|
||||||
@ -306,10 +427,10 @@ class Database {
|
|||||||
if (Database.noReject) {
|
if (Database.noReject) {
|
||||||
break;
|
break;
|
||||||
} else {
|
} else {
|
||||||
console.log("Waiting to close the database");
|
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);
|
||||||
}
|
}
|
||||||
@ -317,11 +438,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("Backing up the database");
|
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);
|
||||||
|
|
||||||
@ -336,15 +457,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("Patching the database 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";
|
||||||
@ -363,7 +499,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);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -379,17 +515,22 @@ class Database {
|
|||||||
}
|
}
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
console.log("Nothing to restore");
|
log.info("db", "Nothing to restore");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Get the size of the database */
|
||||||
static getSize() {
|
static getSize() {
|
||||||
debug("Database.getSize()");
|
log.debug("db", "Database.getSize()");
|
||||||
let stats = fs.statSync(Database.path);
|
let stats = fs.statSync(Database.path);
|
||||||
debug(stats);
|
log.debug("db", stats);
|
||||||
return stats.size;
|
return stats.size;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Shrink the database
|
||||||
|
* @returns {Promise<void>}
|
||||||
|
*/
|
||||||
static async shrink() {
|
static async shrink() {
|
||||||
await R.exec("VACUUM");
|
await R.exec("VACUUM");
|
||||||
}
|
}
|
||||||
|
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,7 +1,8 @@
|
|||||||
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",
|
||||||
@ -9,8 +10,13 @@ const jobs = [
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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,14 @@ const initBackgroundJobs = function (args) {
|
|||||||
return bree;
|
return bree;
|
||||||
};
|
};
|
||||||
|
|
||||||
module.exports = {
|
/** Stop all background jobs if running */
|
||||||
initBackgroundJobs
|
const stopBackgroundJobs = function () {
|
||||||
|
if (bree) {
|
||||||
|
bree.stop();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
initBackgroundJobs,
|
||||||
|
stopBackgroundJobs
|
||||||
};
|
};
|
||||||
|
@ -25,6 +25,10 @@ const DEFAULT_KEEP_PERIOD = 180;
|
|||||||
parsedPeriod = DEFAULT_KEEP_PERIOD;
|
parsedPeriod = DEFAULT_KEEP_PERIOD;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (parsedPeriod < 1) {
|
||||||
|
log(`Data deletion has been disabled as period is less than 1. Period is ${parsedPeriod} days.`);
|
||||||
|
} else {
|
||||||
|
|
||||||
log(`Clearing Data older than ${parsedPeriod} days...`);
|
log(`Clearing Data older than ${parsedPeriod} days...`);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@ -35,6 +39,7 @@ const DEFAULT_KEEP_PERIOD = 180;
|
|||||||
} catch (e) {
|
} catch (e) {
|
||||||
log(`Failed to clear old data: ${e.message}`);
|
log(`Failed to clear old data: ${e.message}`);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
exit();
|
exit();
|
||||||
})();
|
})();
|
||||||
|
@ -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
|
||||||
|
@ -1,8 +1,3 @@
|
|||||||
const dayjs = require("dayjs");
|
|
||||||
const utc = require("dayjs/plugin/utc");
|
|
||||||
let timezone = require("dayjs/plugin/timezone");
|
|
||||||
dayjs.extend(utc);
|
|
||||||
dayjs.extend(timezone);
|
|
||||||
const { BeanModel } = require("redbean-node/dist/bean-model");
|
const { BeanModel } = require("redbean-node/dist/bean-model");
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -10,9 +5,15 @@ const { BeanModel } = require("redbean-node/dist/bean-model");
|
|||||||
* 0 = DOWN
|
* 0 = DOWN
|
||||||
* 1 = UP
|
* 1 = UP
|
||||||
* 2 = PENDING
|
* 2 = PENDING
|
||||||
|
* 3 = MAINTENANCE
|
||||||
*/
|
*/
|
||||||
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 +23,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,
|
||||||
|
240
server/model/maintenance.js
Normal file
240
server/model/maintenance.js
Normal file
@ -0,0 +1,240 @@
|
|||||||
|
const { BeanModel } = require("redbean-node/dist/bean-model");
|
||||||
|
const { parseTimeObject, parseTimeFromTimeObject, utcToLocal, localToUTC, log } = require("../../src/util");
|
||||||
|
const { timeObjectToUTC, timeObjectToLocal } = require("../util-server");
|
||||||
|
const { R } = require("redbean-node");
|
||||||
|
const dayjs = require("dayjs");
|
||||||
|
|
||||||
|
class Maintenance extends BeanModel {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return an object that ready to parse to JSON for public
|
||||||
|
* Only show necessary data to public
|
||||||
|
* @returns {Object}
|
||||||
|
*/
|
||||||
|
async toPublicJSON() {
|
||||||
|
|
||||||
|
let dateRange = [];
|
||||||
|
if (this.start_date) {
|
||||||
|
dateRange.push(utcToLocal(this.start_date));
|
||||||
|
if (this.end_date) {
|
||||||
|
dateRange.push(utcToLocal(this.end_date));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let timeRange = [];
|
||||||
|
let startTime = timeObjectToLocal(parseTimeObject(this.start_time));
|
||||||
|
timeRange.push(startTime);
|
||||||
|
let endTime = timeObjectToLocal(parseTimeObject(this.end_time));
|
||||||
|
timeRange.push(endTime);
|
||||||
|
|
||||||
|
let obj = {
|
||||||
|
id: this.id,
|
||||||
|
title: this.title,
|
||||||
|
description: this.description,
|
||||||
|
strategy: this.strategy,
|
||||||
|
intervalDay: this.interval_day,
|
||||||
|
active: !!this.active,
|
||||||
|
dateRange: dateRange,
|
||||||
|
timeRange: timeRange,
|
||||||
|
weekdays: (this.weekdays) ? JSON.parse(this.weekdays) : [],
|
||||||
|
daysOfMonth: (this.days_of_month) ? JSON.parse(this.days_of_month) : [],
|
||||||
|
timeslotList: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
const timeslotList = await this.getTimeslotList();
|
||||||
|
|
||||||
|
for (let timeslot of timeslotList) {
|
||||||
|
obj.timeslotList.push(await timeslot.toPublicJSON());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!Array.isArray(obj.weekdays)) {
|
||||||
|
obj.weekdays = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!Array.isArray(obj.daysOfMonth)) {
|
||||||
|
obj.daysOfMonth = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Maintenance Status
|
||||||
|
if (!obj.active) {
|
||||||
|
obj.status = "inactive";
|
||||||
|
} else if (obj.strategy === "manual") {
|
||||||
|
obj.status = "under-maintenance";
|
||||||
|
} else if (obj.timeslotList.length > 0) {
|
||||||
|
let currentTimestamp = dayjs().unix();
|
||||||
|
|
||||||
|
for (let timeslot of obj.timeslotList) {
|
||||||
|
if (dayjs.utc(timeslot.startDate).unix() <= currentTimestamp && dayjs.utc(timeslot.endDate).unix() >= currentTimestamp) {
|
||||||
|
log.debug("timeslot", "Timeslot ID: " + timeslot.id);
|
||||||
|
log.debug("timeslot", "currentTimestamp:" + currentTimestamp);
|
||||||
|
log.debug("timeslot", "timeslot.start_date:" + dayjs.utc(timeslot.startDate).unix());
|
||||||
|
log.debug("timeslot", "timeslot.end_date:" + dayjs.utc(timeslot.endDate).unix());
|
||||||
|
|
||||||
|
obj.status = "under-maintenance";
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!obj.status) {
|
||||||
|
obj.status = "scheduled";
|
||||||
|
}
|
||||||
|
} else if (obj.timeslotList.length === 0) {
|
||||||
|
obj.status = "ended";
|
||||||
|
} else {
|
||||||
|
obj.status = "unknown";
|
||||||
|
}
|
||||||
|
|
||||||
|
return obj;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Only get future or current timeslots only
|
||||||
|
* @returns {Promise<[]>}
|
||||||
|
*/
|
||||||
|
async getTimeslotList() {
|
||||||
|
return R.convertToBeans("maintenance_timeslot", await R.getAll(`
|
||||||
|
SELECT maintenance_timeslot.*
|
||||||
|
FROM maintenance_timeslot, maintenance
|
||||||
|
WHERE maintenance_timeslot.maintenance_id = maintenance.id
|
||||||
|
AND maintenance.id = ?
|
||||||
|
AND ${Maintenance.getActiveAndFutureMaintenanceSQLCondition()}
|
||||||
|
`, [
|
||||||
|
this.id
|
||||||
|
]));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return an object that ready to parse to JSON
|
||||||
|
* @param {string} timezone If not specified, the timeRange will be in UTC
|
||||||
|
* @returns {Object}
|
||||||
|
*/
|
||||||
|
async toJSON(timezone = null) {
|
||||||
|
return this.toPublicJSON(timezone);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a list of weekdays that the maintenance is active for
|
||||||
|
* Monday=1, Tuesday=2 etc.
|
||||||
|
* @returns {number[]} Array of active weekdays
|
||||||
|
*/
|
||||||
|
getDayOfWeekList() {
|
||||||
|
log.debug("timeslot", "List: " + this.weekdays);
|
||||||
|
return JSON.parse(this.weekdays).sort(function (a, b) {
|
||||||
|
return a - b;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a list of days in month that maintenance is active for
|
||||||
|
* @returns {number[]} Array of active days in month
|
||||||
|
*/
|
||||||
|
getDayOfMonthList() {
|
||||||
|
return JSON.parse(this.days_of_month).sort(function (a, b) {
|
||||||
|
return a - b;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the start date and time for maintenance
|
||||||
|
* @returns {dayjs.Dayjs} Start date and time
|
||||||
|
*/
|
||||||
|
getStartDateTime() {
|
||||||
|
let startOfTheDay = dayjs.utc(this.start_date).format("HH:mm");
|
||||||
|
log.debug("timeslot", "startOfTheDay: " + startOfTheDay);
|
||||||
|
|
||||||
|
// Start Time
|
||||||
|
let startTimeSecond = dayjs.utc(this.start_time, "HH:mm").diff(dayjs.utc(startOfTheDay, "HH:mm"), "second");
|
||||||
|
log.debug("timeslot", "startTime: " + startTimeSecond);
|
||||||
|
|
||||||
|
// Bake StartDate + StartTime = Start DateTime
|
||||||
|
return dayjs.utc(this.start_date).add(startTimeSecond, "second");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the duraction of maintenance in seconds
|
||||||
|
* @returns {number} Duration of maintenance
|
||||||
|
*/
|
||||||
|
getDuration() {
|
||||||
|
let duration = dayjs.utc(this.end_time, "HH:mm").diff(dayjs.utc(this.start_time, "HH:mm"), "second");
|
||||||
|
// Add 24hours if it is across day
|
||||||
|
if (duration < 0) {
|
||||||
|
duration += 24 * 3600;
|
||||||
|
}
|
||||||
|
return duration;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert data from socket to bean
|
||||||
|
* @param {Bean} bean Bean to fill in
|
||||||
|
* @param {Object} obj Data to fill bean with
|
||||||
|
* @returns {Bean} Filled bean
|
||||||
|
*/
|
||||||
|
static jsonToBean(bean, obj) {
|
||||||
|
if (obj.id) {
|
||||||
|
bean.id = obj.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply timezone offset to timeRange, as it cannot apply automatically.
|
||||||
|
if (obj.timeRange[0]) {
|
||||||
|
timeObjectToUTC(obj.timeRange[0]);
|
||||||
|
if (obj.timeRange[1]) {
|
||||||
|
timeObjectToUTC(obj.timeRange[1]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bean.title = obj.title;
|
||||||
|
bean.description = obj.description;
|
||||||
|
bean.strategy = obj.strategy;
|
||||||
|
bean.interval_day = obj.intervalDay;
|
||||||
|
bean.active = obj.active;
|
||||||
|
|
||||||
|
if (obj.dateRange[0]) {
|
||||||
|
bean.start_date = localToUTC(obj.dateRange[0]);
|
||||||
|
|
||||||
|
if (obj.dateRange[1]) {
|
||||||
|
bean.end_date = localToUTC(obj.dateRange[1]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bean.start_time = parseTimeFromTimeObject(obj.timeRange[0]);
|
||||||
|
bean.end_time = parseTimeFromTimeObject(obj.timeRange[1]);
|
||||||
|
|
||||||
|
bean.weekdays = JSON.stringify(obj.weekdays);
|
||||||
|
bean.days_of_month = JSON.stringify(obj.daysOfMonth);
|
||||||
|
|
||||||
|
return bean;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* SQL conditions for active maintenance
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
static getActiveMaintenanceSQLCondition() {
|
||||||
|
return `
|
||||||
|
(
|
||||||
|
(maintenance_timeslot.start_date <= DATETIME('now')
|
||||||
|
AND maintenance_timeslot.end_date >= DATETIME('now')
|
||||||
|
AND maintenance.active = 1)
|
||||||
|
OR
|
||||||
|
(maintenance.strategy = 'manual' AND active = 1)
|
||||||
|
)
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* SQL conditions for active and future maintenance
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
static getActiveAndFutureMaintenanceSQLCondition() {
|
||||||
|
return `
|
||||||
|
(
|
||||||
|
((maintenance_timeslot.end_date >= DATETIME('now')
|
||||||
|
AND maintenance.active = 1)
|
||||||
|
OR
|
||||||
|
(maintenance.strategy = 'manual' AND active = 1))
|
||||||
|
)
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = Maintenance;
|
198
server/model/maintenance_timeslot.js
Normal file
198
server/model/maintenance_timeslot.js
Normal file
@ -0,0 +1,198 @@
|
|||||||
|
const { BeanModel } = require("redbean-node/dist/bean-model");
|
||||||
|
const { R } = require("redbean-node");
|
||||||
|
const dayjs = require("dayjs");
|
||||||
|
const { log, utcToLocal, SQL_DATETIME_FORMAT_WITHOUT_SECOND, localToUTC } = require("../../src/util");
|
||||||
|
const { UptimeKumaServer } = require("../uptime-kuma-server");
|
||||||
|
|
||||||
|
class MaintenanceTimeslot extends BeanModel {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return an object that ready to parse to JSON for public
|
||||||
|
* Only show necessary data to public
|
||||||
|
* @returns {Object}
|
||||||
|
*/
|
||||||
|
async toPublicJSON() {
|
||||||
|
const serverTimezoneOffset = UptimeKumaServer.getInstance().getTimezoneOffset();
|
||||||
|
|
||||||
|
const obj = {
|
||||||
|
id: this.id,
|
||||||
|
startDate: this.start_date,
|
||||||
|
endDate: this.end_date,
|
||||||
|
startDateServerTimezone: utcToLocal(this.start_date, SQL_DATETIME_FORMAT_WITHOUT_SECOND),
|
||||||
|
endDateServerTimezone: utcToLocal(this.end_date, SQL_DATETIME_FORMAT_WITHOUT_SECOND),
|
||||||
|
serverTimezoneOffset,
|
||||||
|
};
|
||||||
|
|
||||||
|
return obj;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return an object that ready to parse to JSON
|
||||||
|
* @returns {Object}
|
||||||
|
*/
|
||||||
|
async toJSON() {
|
||||||
|
return await this.toPublicJSON();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {Maintenance} maintenance
|
||||||
|
* @param {dayjs} minDate (For recurring type only) Generate a next timeslot from this date.
|
||||||
|
* @param {boolean} removeExist Remove existing timeslot before create
|
||||||
|
* @returns {Promise<MaintenanceTimeslot>}
|
||||||
|
*/
|
||||||
|
static async generateTimeslot(maintenance, minDate = null, removeExist = false) {
|
||||||
|
if (removeExist) {
|
||||||
|
await R.exec("DELETE FROM maintenance_timeslot WHERE maintenance_id = ? ", [
|
||||||
|
maintenance.id
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (maintenance.strategy === "manual") {
|
||||||
|
log.debug("maintenance", "No need to generate timeslot for manual type");
|
||||||
|
|
||||||
|
} else if (maintenance.strategy === "single") {
|
||||||
|
let bean = R.dispense("maintenance_timeslot");
|
||||||
|
bean.maintenance_id = maintenance.id;
|
||||||
|
bean.start_date = maintenance.start_date;
|
||||||
|
bean.end_date = maintenance.end_date;
|
||||||
|
bean.generated_next = true;
|
||||||
|
return await R.store(bean);
|
||||||
|
|
||||||
|
} else if (maintenance.strategy === "recurring-interval") {
|
||||||
|
// Prevent dead loop, in case interval_day is not set
|
||||||
|
if (!maintenance.interval_day || maintenance.interval_day <= 0) {
|
||||||
|
maintenance.interval_day = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
return await this.handleRecurringType(maintenance, minDate, (startDateTime) => {
|
||||||
|
return startDateTime.add(maintenance.interval_day, "day");
|
||||||
|
}, () => {
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
|
||||||
|
} else if (maintenance.strategy === "recurring-weekday") {
|
||||||
|
let dayOfWeekList = maintenance.getDayOfWeekList();
|
||||||
|
log.debug("timeslot", dayOfWeekList);
|
||||||
|
|
||||||
|
if (dayOfWeekList.length <= 0) {
|
||||||
|
log.debug("timeslot", "No weekdays selected?");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const isValid = (startDateTime) => {
|
||||||
|
log.debug("timeslot", "nextDateTime: " + startDateTime);
|
||||||
|
|
||||||
|
let day = startDateTime.local().day();
|
||||||
|
log.debug("timeslot", "nextDateTime.day(): " + day);
|
||||||
|
|
||||||
|
return dayOfWeekList.includes(day);
|
||||||
|
};
|
||||||
|
|
||||||
|
return await this.handleRecurringType(maintenance, minDate, (startDateTime) => {
|
||||||
|
while (true) {
|
||||||
|
startDateTime = startDateTime.add(1, "day");
|
||||||
|
|
||||||
|
if (isValid(startDateTime)) {
|
||||||
|
return startDateTime;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, isValid);
|
||||||
|
|
||||||
|
} else if (maintenance.strategy === "recurring-day-of-month") {
|
||||||
|
let dayOfMonthList = maintenance.getDayOfMonthList();
|
||||||
|
if (dayOfMonthList.length <= 0) {
|
||||||
|
log.debug("timeslot", "No day selected?");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const isValid = (startDateTime) => {
|
||||||
|
let day = parseInt(startDateTime.local().format("D"));
|
||||||
|
|
||||||
|
log.debug("timeslot", "day: " + day);
|
||||||
|
|
||||||
|
// Check 1-31
|
||||||
|
if (dayOfMonthList.includes(day)) {
|
||||||
|
return startDateTime;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check "lastDay1","lastDay2"...
|
||||||
|
let daysInMonth = startDateTime.daysInMonth();
|
||||||
|
let lastDayList = [];
|
||||||
|
|
||||||
|
// Small first, e.g. 28 > 29 > 30 > 31
|
||||||
|
for (let i = 4; i >= 1; i--) {
|
||||||
|
if (dayOfMonthList.includes("lastDay" + i)) {
|
||||||
|
lastDayList.push(daysInMonth - i + 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
log.debug("timeslot", lastDayList);
|
||||||
|
return lastDayList.includes(day);
|
||||||
|
};
|
||||||
|
|
||||||
|
return await this.handleRecurringType(maintenance, minDate, (startDateTime) => {
|
||||||
|
while (true) {
|
||||||
|
startDateTime = startDateTime.add(1, "day");
|
||||||
|
if (isValid(startDateTime)) {
|
||||||
|
return startDateTime;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, isValid);
|
||||||
|
} else {
|
||||||
|
throw new Error("Unknown maintenance strategy");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate a next timeslot for all recurring types
|
||||||
|
* @param maintenance
|
||||||
|
* @param minDate
|
||||||
|
* @param {function} nextDayCallback The logic how to get the next possible day
|
||||||
|
* @param {function} isValidCallback Check the day whether is matched the current strategy
|
||||||
|
* @returns {Promise<null|MaintenanceTimeslot>}
|
||||||
|
*/
|
||||||
|
static async handleRecurringType(maintenance, minDate, nextDayCallback, isValidCallback) {
|
||||||
|
let bean = R.dispense("maintenance_timeslot");
|
||||||
|
|
||||||
|
let duration = maintenance.getDuration();
|
||||||
|
let startDateTime = maintenance.getStartDateTime();
|
||||||
|
let endDateTime;
|
||||||
|
|
||||||
|
// Keep generating from the first possible date, until it is ok
|
||||||
|
while (true) {
|
||||||
|
log.debug("timeslot", "startDateTime: " + startDateTime.format());
|
||||||
|
|
||||||
|
// Handling out of effective date range
|
||||||
|
if (startDateTime.diff(dayjs.utc(maintenance.end_date)) > 0) {
|
||||||
|
log.debug("timeslot", "Out of effective date range");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
endDateTime = startDateTime.add(duration, "second");
|
||||||
|
|
||||||
|
// If endDateTime is out of effective date range, use the end datetime from effective date range
|
||||||
|
if (endDateTime.diff(dayjs.utc(maintenance.end_date)) > 0) {
|
||||||
|
endDateTime = dayjs.utc(maintenance.end_date);
|
||||||
|
}
|
||||||
|
|
||||||
|
// If minDate is set, the endDateTime must be bigger than it.
|
||||||
|
// And the endDateTime must be bigger current time
|
||||||
|
// Is valid under current recurring strategy
|
||||||
|
if (
|
||||||
|
(!minDate || endDateTime.diff(minDate) > 0) &&
|
||||||
|
endDateTime.diff(dayjs()) > 0 &&
|
||||||
|
isValidCallback(startDateTime)
|
||||||
|
) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
startDateTime = nextDayCallback(startDateTime);
|
||||||
|
}
|
||||||
|
|
||||||
|
bean.maintenance_id = maintenance.id;
|
||||||
|
bean.start_date = localToUTC(startDateTime);
|
||||||
|
bean.end_date = localToUTC(endDateTime);
|
||||||
|
bean.generated_next = false;
|
||||||
|
return await R.store(bean);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = MaintenanceTimeslot;
|
File diff suppressed because it is too large
Load Diff
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;
|
307
server/model/status_page.js
Normal file
307
server/model/status_page.js
Normal file
@ -0,0 +1,307 @@
|
|||||||
|
const { BeanModel } = require("redbean-node/dist/bean-model");
|
||||||
|
const { R } = require("redbean-node");
|
||||||
|
const cheerio = require("cheerio");
|
||||||
|
const { UptimeKumaServer } = require("../uptime-kuma-server");
|
||||||
|
const jsesc = require("jsesc");
|
||||||
|
const Maintenance = require("./maintenance");
|
||||||
|
|
||||||
|
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
|
||||||
|
// Add jsesc, fix https://github.com/louislam/uptime-kuma/issues/2186
|
||||||
|
const escapedJSONObject = jsesc(await StatusPage.getStatusPageData(statusPage), {
|
||||||
|
"isScriptContext": true
|
||||||
|
});
|
||||||
|
|
||||||
|
const script = $(`
|
||||||
|
<script id="preload-data" data-json="{}">
|
||||||
|
window.preloadData = ${escapedJSONObject};
|
||||||
|
</script>
|
||||||
|
`);
|
||||||
|
|
||||||
|
head.append(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();
|
||||||
|
}
|
||||||
|
|
||||||
|
let maintenanceList = await StatusPage.getMaintenanceList(statusPage.id);
|
||||||
|
|
||||||
|
// 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,
|
||||||
|
maintenanceList,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get list of maintenances
|
||||||
|
* @param {number} statusPageId ID of status page to get maintenance for
|
||||||
|
* @returns {Object} Object representing maintenances sanitized for public
|
||||||
|
*/
|
||||||
|
static async getMaintenanceList(statusPageId) {
|
||||||
|
try {
|
||||||
|
const publicMaintenanceList = [];
|
||||||
|
|
||||||
|
let activeCondition = Maintenance.getActiveMaintenanceSQLCondition();
|
||||||
|
let maintenanceBeanList = R.convertToBeans("maintenance", await R.getAll(`
|
||||||
|
SELECT DISTINCT maintenance.*
|
||||||
|
FROM maintenance
|
||||||
|
JOIN maintenance_status_page
|
||||||
|
ON maintenance_status_page.maintenance_id = maintenance.id
|
||||||
|
AND maintenance_status_page.status_page_id = ?
|
||||||
|
LEFT JOIN maintenance_timeslot
|
||||||
|
ON maintenance_timeslot.maintenance_id = maintenance.id
|
||||||
|
WHERE ${activeCondition}
|
||||||
|
ORDER BY maintenance.end_date
|
||||||
|
`, [ statusPageId ]));
|
||||||
|
|
||||||
|
for (const bean of maintenanceBeanList) {
|
||||||
|
publicMaintenanceList.push(await bean.toPublicJSON());
|
||||||
|
}
|
||||||
|
|
||||||
|
return publicMaintenanceList;
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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 = {};
|
||||||
@ -443,35 +577,41 @@ function ApiCache() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 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;
|
||||||
|
|
||||||
@ -481,17 +621,20 @@ function ApiCache() {
|
|||||||
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 {
|
||||||
@ -509,9 +652,12 @@ 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;
|
||||||
@ -538,15 +684,16 @@ 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 {Uint8Array} array An array representing hits and
|
||||||
|
* misses.
|
||||||
* @param {boolean} hit true for a hit, false for a miss
|
* @param {boolean} hit true for a hit, false for a miss
|
||||||
* Each element in the array is 8 bits, and encodes 4 hit/miss records.
|
* Each element in the array is 8 bits, and encodes 4
|
||||||
* Each hit or miss is encoded as to bits as follows:
|
* hit/miss records. Each hit or miss is encoded as to bits
|
||||||
* 00 means no hit or miss has been recorded in these bits
|
* as follows: 00 means no hit or miss has been recorded in
|
||||||
* 01 encodes a hit
|
* these bits 01 encodes a hit 10 encodes a miss
|
||||||
* 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;
|
||||||
@ -557,8 +704,10 @@ 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);
|
||||||
@ -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);
|
||||||
|
20
server/modules/dayjs/plugin/timezone.d.ts
vendored
Normal file
20
server/modules/dayjs/plugin/timezone.d.ts
vendored
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
import { PluginFunc, ConfigType } from 'dayjs'
|
||||||
|
|
||||||
|
declare const plugin: PluginFunc
|
||||||
|
export = plugin
|
||||||
|
|
||||||
|
declare module 'dayjs' {
|
||||||
|
interface Dayjs {
|
||||||
|
tz(timezone?: string, keepLocalTime?: boolean): Dayjs
|
||||||
|
offsetName(type?: 'short' | 'long'): string | undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
interface DayjsTimezone {
|
||||||
|
(date: ConfigType, timezone?: string): Dayjs
|
||||||
|
(date: ConfigType, format: string, timezone?: string): Dayjs
|
||||||
|
guess(): string
|
||||||
|
setDefault(timezone?: string): void
|
||||||
|
}
|
||||||
|
|
||||||
|
const tz: DayjsTimezone
|
||||||
|
}
|
115
server/modules/dayjs/plugin/timezone.js
Normal file
115
server/modules/dayjs/plugin/timezone.js
Normal file
@ -0,0 +1,115 @@
|
|||||||
|
/**
|
||||||
|
* Copy from node_modules/dayjs/plugin/timezone.js
|
||||||
|
* Try to fix https://github.com/louislam/uptime-kuma/issues/2318
|
||||||
|
* Source: https://github.com/iamkun/dayjs/tree/dev/src/plugin/utc
|
||||||
|
* License: MIT
|
||||||
|
*/
|
||||||
|
!function (t, e) {
|
||||||
|
// eslint-disable-next-line no-undef
|
||||||
|
typeof exports == "object" && typeof module != "undefined" ? module.exports = e() : typeof define == "function" && define.amd ? define(e) : (t = typeof globalThis != "undefined" ? globalThis : t || self).dayjs_plugin_timezone = e();
|
||||||
|
}(this, (function () {
|
||||||
|
"use strict";
|
||||||
|
let t = {
|
||||||
|
year: 0,
|
||||||
|
month: 1,
|
||||||
|
day: 2,
|
||||||
|
hour: 3,
|
||||||
|
minute: 4,
|
||||||
|
second: 5
|
||||||
|
};
|
||||||
|
let e = {};
|
||||||
|
return function (n, i, o) {
|
||||||
|
let r;
|
||||||
|
let a = function (t, n, i) {
|
||||||
|
void 0 === i && (i = {});
|
||||||
|
let o = new Date(t);
|
||||||
|
let r = function (t, n) {
|
||||||
|
void 0 === n && (n = {});
|
||||||
|
let i = n.timeZoneName || "short";
|
||||||
|
let o = t + "|" + i;
|
||||||
|
let r = e[o];
|
||||||
|
return r || (r = new Intl.DateTimeFormat("en-US", {
|
||||||
|
hour12: !1,
|
||||||
|
timeZone: t,
|
||||||
|
year: "numeric",
|
||||||
|
month: "2-digit",
|
||||||
|
day: "2-digit",
|
||||||
|
hour: "2-digit",
|
||||||
|
minute: "2-digit",
|
||||||
|
second: "2-digit",
|
||||||
|
timeZoneName: i
|
||||||
|
}), e[o] = r), r;
|
||||||
|
}(n, i);
|
||||||
|
return r.formatToParts(o);
|
||||||
|
};
|
||||||
|
let u = function (e, n) {
|
||||||
|
let i = a(e, n);
|
||||||
|
let r = [];
|
||||||
|
let u = 0;
|
||||||
|
for (; u < i.length; u += 1) {
|
||||||
|
let f = i[u];
|
||||||
|
let s = f.type;
|
||||||
|
let m = f.value;
|
||||||
|
let c = t[s];
|
||||||
|
c >= 0 && (r[c] = parseInt(m, 10));
|
||||||
|
}
|
||||||
|
let d = r[3];
|
||||||
|
let l = d === 24 ? 0 : d;
|
||||||
|
let v = r[0] + "-" + r[1] + "-" + r[2] + " " + l + ":" + r[4] + ":" + r[5] + ":000";
|
||||||
|
let h = +e;
|
||||||
|
return (o.utc(v).valueOf() - (h -= h % 1e3)) / 6e4;
|
||||||
|
};
|
||||||
|
let f = i.prototype;
|
||||||
|
f.tz = function (t, e) {
|
||||||
|
void 0 === t && (t = r);
|
||||||
|
let n = this.utcOffset();
|
||||||
|
let i = this.toDate();
|
||||||
|
let a = i.toLocaleString("en-US", { timeZone: t }).replace("\u202f", " ");
|
||||||
|
let u = Math.round((i - new Date(a)) / 1e3 / 60);
|
||||||
|
let f = o(a).$set("millisecond", this.$ms).utcOffset(15 * -Math.round(i.getTimezoneOffset() / 15) - u, !0);
|
||||||
|
if (e) {
|
||||||
|
let s = f.utcOffset();
|
||||||
|
f = f.add(n - s, "minute");
|
||||||
|
}
|
||||||
|
return f.$x.$timezone = t, f;
|
||||||
|
}, f.offsetName = function (t) {
|
||||||
|
let e = this.$x.$timezone || o.tz.guess();
|
||||||
|
let n = a(this.valueOf(), e, { timeZoneName: t }).find((function (t) {
|
||||||
|
return t.type.toLowerCase() === "timezonename";
|
||||||
|
}));
|
||||||
|
return n && n.value;
|
||||||
|
};
|
||||||
|
let s = f.startOf;
|
||||||
|
f.startOf = function (t, e) {
|
||||||
|
if (!this.$x || !this.$x.$timezone) {
|
||||||
|
return s.call(this, t, e);
|
||||||
|
}
|
||||||
|
let n = o(this.format("YYYY-MM-DD HH:mm:ss:SSS"));
|
||||||
|
return s.call(n, t, e).tz(this.$x.$timezone, !0);
|
||||||
|
}, o.tz = function (t, e, n) {
|
||||||
|
let i = n && e;
|
||||||
|
let a = n || e || r;
|
||||||
|
let f = u(+o(), a);
|
||||||
|
if (typeof t != "string") {
|
||||||
|
return o(t).tz(a);
|
||||||
|
}
|
||||||
|
let s = function (t, e, n) {
|
||||||
|
let i = t - 60 * e * 1e3;
|
||||||
|
let o = u(i, n);
|
||||||
|
if (e === o) {
|
||||||
|
return [ i, e ];
|
||||||
|
}
|
||||||
|
let r = u(i -= 60 * (o - e) * 1e3, n);
|
||||||
|
return o === r ? [ i, o ] : [ t - 60 * Math.min(o, r) * 1e3, Math.max(o, r) ];
|
||||||
|
}(o.utc(t, i).valueOf(), f, a);
|
||||||
|
let m = s[0];
|
||||||
|
let c = s[1];
|
||||||
|
let d = o(m).utcOffset(c);
|
||||||
|
return d.$x.$timezone = a, d;
|
||||||
|
}, o.tz.guess = function () {
|
||||||
|
return Intl.DateTimeFormat().resolvedOptions().timeZone;
|
||||||
|
}, o.tz.setDefault = function (t) {
|
||||||
|
r = t;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}));
|
67
server/notification-providers/alerta.js
Normal file
67
server/notification-providers/alerta.js
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
const NotificationProvider = require("./notification-provider");
|
||||||
|
const { DOWN, UP } = require("../../src/util");
|
||||||
|
const axios = require("axios");
|
||||||
|
|
||||||
|
class Alerta extends NotificationProvider {
|
||||||
|
|
||||||
|
name = "alerta";
|
||||||
|
|
||||||
|
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
|
||||||
|
let okMsg = "Sent Successfully.";
|
||||||
|
|
||||||
|
try {
|
||||||
|
let alertaUrl = `${notification.alertaApiEndpoint}`;
|
||||||
|
let config = {
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json;charset=UTF-8",
|
||||||
|
"Authorization": "Key " + notification.alertaApiKey,
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let data = {
|
||||||
|
environment: notification.alertaEnvironment,
|
||||||
|
severity: "critical",
|
||||||
|
correlate: [],
|
||||||
|
service: [ "UptimeKuma" ],
|
||||||
|
value: "Timeout",
|
||||||
|
tags: [ "uptimekuma" ],
|
||||||
|
attributes: {},
|
||||||
|
origin: "uptimekuma",
|
||||||
|
type: "exceptionAlert",
|
||||||
|
};
|
||||||
|
|
||||||
|
if (heartbeatJSON == null) {
|
||||||
|
let postData = Object.assign({
|
||||||
|
event: "msg",
|
||||||
|
text: msg,
|
||||||
|
group: "uptimekuma-msg",
|
||||||
|
resource: "Message",
|
||||||
|
}, data);
|
||||||
|
|
||||||
|
await axios.post(alertaUrl, postData, config);
|
||||||
|
} else {
|
||||||
|
let datadup = Object.assign( {
|
||||||
|
correlate: [ "service_up", "service_down" ],
|
||||||
|
event: monitorJSON["type"],
|
||||||
|
group: "uptimekuma-" + monitorJSON["type"],
|
||||||
|
resource: monitorJSON["name"],
|
||||||
|
}, data );
|
||||||
|
|
||||||
|
if (heartbeatJSON["status"] === DOWN) {
|
||||||
|
datadup.severity = notification.alertaAlertState; // critical
|
||||||
|
datadup.text = "Service " + monitorJSON["type"] + " is down.";
|
||||||
|
await axios.post(alertaUrl, datadup, config);
|
||||||
|
} else if (heartbeatJSON["status"] === UP) {
|
||||||
|
datadup.severity = notification.alertaRecoverState; // cleaned
|
||||||
|
datadup.text = "Service " + monitorJSON["type"] + " is up.";
|
||||||
|
await axios.post(alertaUrl, datadup, config);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return okMsg;
|
||||||
|
} catch (error) {
|
||||||
|
this.throwGeneralAxiosError(error);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = Alerta;
|
50
server/notification-providers/alertnow.js
Normal file
50
server/notification-providers/alertnow.js
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
const NotificationProvider = require("./notification-provider");
|
||||||
|
const axios = require("axios");
|
||||||
|
const { setting } = require("../util-server");
|
||||||
|
const { getMonitorRelativeURL, UP, DOWN } = require("../../src/util");
|
||||||
|
|
||||||
|
class AlertNow extends NotificationProvider {
|
||||||
|
|
||||||
|
name = "AlertNow";
|
||||||
|
|
||||||
|
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
|
||||||
|
let okMsg = "Sent Successfully.";
|
||||||
|
try {
|
||||||
|
let textMsg = "";
|
||||||
|
let status = "open";
|
||||||
|
let eventType = "ERROR";
|
||||||
|
let eventId = new Date().toISOString().slice(0, 10).replace(/-/g, "");
|
||||||
|
|
||||||
|
if (heartbeatJSON && heartbeatJSON.status === UP) {
|
||||||
|
textMsg = `[${heartbeatJSON.name}] ✅ Application is back online`;
|
||||||
|
status = "close";
|
||||||
|
eventType = "INFO";
|
||||||
|
eventId += `_${heartbeatJSON.name.replace(/\s/g, "")}`;
|
||||||
|
} else if (heartbeatJSON && heartbeatJSON.status === DOWN) {
|
||||||
|
textMsg = `[${heartbeatJSON.name}] 🔴 Application went down`;
|
||||||
|
}
|
||||||
|
|
||||||
|
textMsg += ` - ${msg}`;
|
||||||
|
|
||||||
|
const baseURL = await setting("primaryBaseURL");
|
||||||
|
if (baseURL && monitorJSON) {
|
||||||
|
textMsg += ` >> ${baseURL + getMonitorRelativeURL(monitorJSON.id)}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = {
|
||||||
|
"summary": textMsg,
|
||||||
|
"status": status,
|
||||||
|
"event_type": eventType,
|
||||||
|
"event_id": eventId,
|
||||||
|
};
|
||||||
|
|
||||||
|
await axios.post(notification.alertNowWebhookURL, data);
|
||||||
|
return okMsg;
|
||||||
|
} catch (error) {
|
||||||
|
this.throwGeneralAxiosError(error);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = AlertNow;
|
@ -37,6 +37,12 @@ class AliyunSMS extends NotificationProvider {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send the SMS notification
|
||||||
|
* @param {BeanModel} notification Notification details
|
||||||
|
* @param {string} msgbody Message template
|
||||||
|
* @returns {boolean} True if successful else false
|
||||||
|
*/
|
||||||
async sendSms(notification, msgbody) {
|
async sendSms(notification, msgbody) {
|
||||||
let params = {
|
let params = {
|
||||||
PhoneNumbers: notification.phonenumber,
|
PhoneNumbers: notification.phonenumber,
|
||||||
@ -64,13 +70,18 @@ class AliyunSMS extends NotificationProvider {
|
|||||||
};
|
};
|
||||||
|
|
||||||
let result = await axios(config);
|
let result = await axios(config);
|
||||||
if (result.data.Message == "OK") {
|
if (result.data.Message === "OK") {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Aliyun request sign */
|
/**
|
||||||
|
* Aliyun request sign
|
||||||
|
* @param {Object} param Parameters object to sign
|
||||||
|
* @param {string} AccessKeySecret Secret key to sign parameters with
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
sign(param, AccessKeySecret) {
|
sign(param, AccessKeySecret) {
|
||||||
let param2 = {};
|
let param2 = {};
|
||||||
let data = [];
|
let data = [];
|
||||||
@ -82,8 +93,23 @@ class AliyunSMS extends NotificationProvider {
|
|||||||
param2[key] = param[key];
|
param2[key] = param[key];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Escape more characters than encodeURIComponent does.
|
||||||
|
// For generating Aliyun signature, all characters except A-Za-z0-9~-._ are encoded.
|
||||||
|
// See https://help.aliyun.com/document_detail/315526.html
|
||||||
|
// This encoding methods as known as RFC 3986 (https://tools.ietf.org/html/rfc3986)
|
||||||
|
let moreEscapesTable = function (m) {
|
||||||
|
return {
|
||||||
|
"!": "%21",
|
||||||
|
"*": "%2A",
|
||||||
|
"'": "%27",
|
||||||
|
"(": "%28",
|
||||||
|
")": "%29"
|
||||||
|
}[m];
|
||||||
|
};
|
||||||
|
|
||||||
for (let key in param2) {
|
for (let key in param2) {
|
||||||
data.push(`${encodeURIComponent(key)}=${encodeURIComponent(param2[key])}`);
|
let value = encodeURIComponent(param2[key]).replace(/[!*'()]/g, moreEscapesTable);
|
||||||
|
data.push(`${encodeURIComponent(key)}=${value}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
let StringToSign = `POST&${encodeURIComponent("/")}&${encodeURIComponent(data.join("&"))}`;
|
let StringToSign = `POST&${encodeURIComponent("/")}&${encodeURIComponent(data.join("&"))}`;
|
||||||
@ -93,6 +119,11 @@ class AliyunSMS extends NotificationProvider {
|
|||||||
.digest("base64");
|
.digest("base64");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert status constant to string
|
||||||
|
* @param {const} status The status constant
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
statusToString(status) {
|
statusToString(status) {
|
||||||
switch (status) {
|
switch (status) {
|
||||||
case DOWN:
|
case DOWN:
|
||||||
|
@ -1,14 +1,19 @@
|
|||||||
const NotificationProvider = require("./notification-provider");
|
const NotificationProvider = require("./notification-provider");
|
||||||
const child_process = require("child_process");
|
const childProcess = require("child_process");
|
||||||
|
|
||||||
class Apprise extends NotificationProvider {
|
class Apprise extends NotificationProvider {
|
||||||
|
|
||||||
name = "apprise";
|
name = "apprise";
|
||||||
|
|
||||||
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
|
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
|
||||||
let s = child_process.spawnSync("apprise", [ "-vv", "-b", msg, notification.appriseURL])
|
const args = [ "-vv", "-b", msg, notification.appriseURL ];
|
||||||
|
if (notification.title) {
|
||||||
|
args.push("-t");
|
||||||
|
args.push(notification.title);
|
||||||
|
}
|
||||||
|
const s = childProcess.spawnSync("apprise", args);
|
||||||
|
|
||||||
let output = (s.stdout) ? s.stdout.toString() : "ERROR: maybe apprise not found";
|
const output = (s.stdout) ? s.stdout.toString() : "ERROR: maybe apprise not found";
|
||||||
|
|
||||||
if (output) {
|
if (output) {
|
||||||
|
|
||||||
@ -16,7 +21,7 @@ class Apprise extends NotificationProvider {
|
|||||||
return "Sent Successfully";
|
return "Sent Successfully";
|
||||||
}
|
}
|
||||||
|
|
||||||
throw new Error(output)
|
throw new Error(output);
|
||||||
} else {
|
} else {
|
||||||
return "No output from apprise";
|
return "No output from apprise";
|
||||||
}
|
}
|
||||||
|
@ -12,55 +12,67 @@ const { default: axios } = require("axios");
|
|||||||
|
|
||||||
// bark is an APN bridge that sends notifications to Apple devices.
|
// bark is an APN bridge that sends notifications to Apple devices.
|
||||||
|
|
||||||
const barkNotificationGroup = "UptimeKuma";
|
|
||||||
const barkNotificationAvatar = "https://github.com/louislam/uptime-kuma/raw/master/public/icon.png";
|
const barkNotificationAvatar = "https://github.com/louislam/uptime-kuma/raw/master/public/icon.png";
|
||||||
const barkNotificationSound = "telegraph";
|
|
||||||
const successMessage = "Successes!";
|
const successMessage = "Successes!";
|
||||||
|
|
||||||
class Bark extends NotificationProvider {
|
class Bark extends NotificationProvider {
|
||||||
name = "Bark";
|
name = "Bark";
|
||||||
|
|
||||||
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
|
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
|
||||||
try {
|
let barkEndpoint = notification.barkEndpoint;
|
||||||
var barkEndpoint = notification.barkEndpoint;
|
|
||||||
|
|
||||||
// check if the endpoint has a "/" suffix, if so, delete it first
|
// check if the endpoint has a "/" suffix, if so, delete it first
|
||||||
if (barkEndpoint.endsWith("/")) {
|
if (barkEndpoint.endsWith("/")) {
|
||||||
barkEndpoint = barkEndpoint.substring(0, barkEndpoint.length - 1);
|
barkEndpoint = barkEndpoint.substring(0, barkEndpoint.length - 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (msg != null && heartbeatJSON != null && heartbeatJSON["status"] == UP) {
|
if (msg != null && heartbeatJSON != null && heartbeatJSON["status"] === UP) {
|
||||||
let title = "UptimeKuma Monitor Up";
|
let title = "UptimeKuma Monitor Up";
|
||||||
return await this.postNotification(title, msg, barkEndpoint);
|
return await this.postNotification(notification, title, msg, barkEndpoint);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (msg != null && heartbeatJSON != null && heartbeatJSON["status"] == DOWN) {
|
if (msg != null && heartbeatJSON != null && heartbeatJSON["status"] === DOWN) {
|
||||||
let title = "UptimeKuma Monitor Down";
|
let title = "UptimeKuma Monitor Down";
|
||||||
return await this.postNotification(title, msg, barkEndpoint);
|
return await this.postNotification(notification, title, msg, barkEndpoint);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (msg != null) {
|
if (msg != null) {
|
||||||
let title = "UptimeKuma Message";
|
let title = "UptimeKuma Message";
|
||||||
return await this.postNotification(title, msg, barkEndpoint);
|
return await this.postNotification(notification, title, msg, barkEndpoint);
|
||||||
}
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
throw error;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// add additional parameter for better on device styles (iOS 15 optimized)
|
/**
|
||||||
appendAdditionalParameters(postUrl) {
|
* Add additional parameter for better on device styles (iOS 15
|
||||||
// grouping all our notifications
|
* optimized)
|
||||||
postUrl += "?group=" + barkNotificationGroup;
|
* @param {string} postUrl URL to append parameters to
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
appendAdditionalParameters(notification, postUrl) {
|
||||||
// set icon to uptime kuma icon, 11kb should be fine
|
// set icon to uptime kuma icon, 11kb should be fine
|
||||||
postUrl += "&icon=" + barkNotificationAvatar;
|
postUrl += "?icon=" + barkNotificationAvatar;
|
||||||
|
// grouping all our notifications
|
||||||
|
if (notification.barkGroup != null) {
|
||||||
|
postUrl += "&group=" + notification.barkGroup;
|
||||||
|
} else {
|
||||||
|
// default name
|
||||||
|
postUrl += "&group=" + "UptimeKuma";
|
||||||
|
}
|
||||||
// picked a sound, this should follow system's mute status when arrival
|
// picked a sound, this should follow system's mute status when arrival
|
||||||
postUrl += "&sound=" + barkNotificationSound;
|
if (notification.barkSound != null) {
|
||||||
|
postUrl += "&sound=" + notification.barkSound;
|
||||||
|
} else {
|
||||||
|
// default sound
|
||||||
|
postUrl += "&sound=" + "telegraph";
|
||||||
|
}
|
||||||
return postUrl;
|
return postUrl;
|
||||||
}
|
}
|
||||||
|
|
||||||
// thrown if failed to check result, result code should be in range 2xx
|
/**
|
||||||
|
* Check if result is successful
|
||||||
|
* @param {Object} result Axios response object
|
||||||
|
* @throws {Error} The status code is not in range 2xx
|
||||||
|
*/
|
||||||
checkResult(result) {
|
checkResult(result) {
|
||||||
if (result.status == null) {
|
if (result.status == null) {
|
||||||
throw new Error("Bark notification failed with invalid response!");
|
throw new Error("Bark notification failed with invalid response!");
|
||||||
@ -70,12 +82,19 @@ class Bark extends NotificationProvider {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async postNotification(title, subtitle, endpoint) {
|
/**
|
||||||
|
* Send the message
|
||||||
|
* @param {string} title Message title
|
||||||
|
* @param {string} subtitle Message
|
||||||
|
* @param {string} endpoint Endpoint to send request to
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
async postNotification(notification, title, subtitle, endpoint) {
|
||||||
// url encode title and subtitle
|
// url encode title and subtitle
|
||||||
title = encodeURIComponent(title);
|
title = encodeURIComponent(title);
|
||||||
subtitle = encodeURIComponent(subtitle);
|
subtitle = encodeURIComponent(subtitle);
|
||||||
let postUrl = endpoint + "/" + title + "/" + subtitle;
|
let postUrl = endpoint + "/" + title + "/" + subtitle;
|
||||||
postUrl = this.appendAdditionalParameters(postUrl);
|
postUrl = this.appendAdditionalParameters(notification, postUrl);
|
||||||
let result = await axios.get(postUrl);
|
let result = await axios.get(postUrl);
|
||||||
this.checkResult(result);
|
this.checkResult(result);
|
||||||
if (result.statusText != null) {
|
if (result.statusText != null) {
|
||||||
|
@ -12,7 +12,7 @@ class ClickSendSMS extends NotificationProvider {
|
|||||||
let config = {
|
let config = {
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
"Authorization": "Basic " + Buffer.from(notification.clicksendsmsLogin + ":" + notification.clicksendsmsPassword).toString('base64'),
|
"Authorization": "Basic " + Buffer.from(notification.clicksendsmsLogin + ":" + notification.clicksendsmsPassword).toString("base64"),
|
||||||
"Accept": "text/json",
|
"Accept": "text/json",
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -37,6 +37,12 @@ class DingDing extends NotificationProvider {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send message to DingDing
|
||||||
|
* @param {BeanModel} notification
|
||||||
|
* @param {Object} params Parameters of message
|
||||||
|
* @returns {boolean} True if successful else false
|
||||||
|
*/
|
||||||
async sendToDingDing(notification, params) {
|
async sendToDingDing(notification, params) {
|
||||||
let timestamp = Date.now();
|
let timestamp = Date.now();
|
||||||
|
|
||||||
@ -50,13 +56,18 @@ class DingDing extends NotificationProvider {
|
|||||||
};
|
};
|
||||||
|
|
||||||
let result = await axios(config);
|
let result = await axios(config);
|
||||||
if (result.data.errmsg == "ok") {
|
if (result.data.errmsg === "ok") {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** DingDing sign */
|
/**
|
||||||
|
* DingDing sign
|
||||||
|
* @param {Date} timestamp Timestamp of message
|
||||||
|
* @param {string} secretKey Secret key to sign data with
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
sign(timestamp, secretKey) {
|
sign(timestamp, secretKey) {
|
||||||
return Crypto
|
return Crypto
|
||||||
.createHmac("sha256", Buffer.from(secretKey, "utf8"))
|
.createHmac("sha256", Buffer.from(secretKey, "utf8"))
|
||||||
@ -64,7 +75,13 @@ class DingDing extends NotificationProvider {
|
|||||||
.digest("base64");
|
.digest("base64");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert status constant to string
|
||||||
|
* @param {const} status The status constant
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
statusToString(status) {
|
statusToString(status) {
|
||||||
|
// TODO: Move to notification-provider.js to avoid repetition in classes
|
||||||
switch (status) {
|
switch (status) {
|
||||||
case DOWN:
|
case DOWN:
|
||||||
return "DOWN";
|
return "DOWN";
|
||||||
|
@ -17,25 +17,32 @@ class Discord extends NotificationProvider {
|
|||||||
let discordtestdata = {
|
let discordtestdata = {
|
||||||
username: discordDisplayName,
|
username: discordDisplayName,
|
||||||
content: msg,
|
content: msg,
|
||||||
}
|
};
|
||||||
await axios.post(notification.discordWebhookUrl, discordtestdata)
|
await axios.post(notification.discordWebhookUrl, discordtestdata);
|
||||||
return okMsg;
|
return okMsg;
|
||||||
}
|
}
|
||||||
|
|
||||||
let url;
|
let address;
|
||||||
|
|
||||||
if (monitorJSON["type"] === "port") {
|
switch (monitorJSON["type"]) {
|
||||||
url = monitorJSON["hostname"];
|
case "ping":
|
||||||
|
address = monitorJSON["hostname"];
|
||||||
|
break;
|
||||||
|
case "port":
|
||||||
|
case "dns":
|
||||||
|
case "steam":
|
||||||
|
address = monitorJSON["hostname"];
|
||||||
if (monitorJSON["port"]) {
|
if (monitorJSON["port"]) {
|
||||||
url += ":" + monitorJSON["port"];
|
address += ":" + monitorJSON["port"];
|
||||||
}
|
}
|
||||||
|
break;
|
||||||
} else {
|
default:
|
||||||
url = monitorJSON["url"];
|
address = monitorJSON["url"];
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
// If heartbeatJSON is not null, we go into the normal alerting loop.
|
// If heartbeatJSON is not null, we go into the normal alerting loop.
|
||||||
if (heartbeatJSON["status"] == DOWN) {
|
if (heartbeatJSON["status"] === DOWN) {
|
||||||
let discorddowndata = {
|
let discorddowndata = {
|
||||||
username: discordDisplayName,
|
username: discordDisplayName,
|
||||||
embeds: [{
|
embeds: [{
|
||||||
@ -48,8 +55,8 @@ class Discord extends NotificationProvider {
|
|||||||
value: monitorJSON["name"],
|
value: monitorJSON["name"],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Service URL",
|
name: monitorJSON["type"] === "push" ? "Service Type" : "Service URL",
|
||||||
value: url,
|
value: monitorJSON["type"] === "push" ? "Heartbeat" : address,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Time (UTC)",
|
name: "Time (UTC)",
|
||||||
@ -57,20 +64,20 @@ class Discord extends NotificationProvider {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Error",
|
name: "Error",
|
||||||
value: heartbeatJSON["msg"],
|
value: heartbeatJSON["msg"] == null ? "N/A" : heartbeatJSON["msg"],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
}],
|
}],
|
||||||
}
|
};
|
||||||
|
|
||||||
if (notification.discordPrefixMessage) {
|
if (notification.discordPrefixMessage) {
|
||||||
discorddowndata.content = notification.discordPrefixMessage;
|
discorddowndata.content = notification.discordPrefixMessage;
|
||||||
}
|
}
|
||||||
|
|
||||||
await axios.post(notification.discordWebhookUrl, discorddowndata)
|
await axios.post(notification.discordWebhookUrl, discorddowndata);
|
||||||
return okMsg;
|
return okMsg;
|
||||||
|
|
||||||
} else if (heartbeatJSON["status"] == UP) {
|
} else if (heartbeatJSON["status"] === UP) {
|
||||||
let discordupdata = {
|
let discordupdata = {
|
||||||
username: discordDisplayName,
|
username: discordDisplayName,
|
||||||
embeds: [{
|
embeds: [{
|
||||||
@ -83,8 +90,8 @@ class Discord extends NotificationProvider {
|
|||||||
value: monitorJSON["name"],
|
value: monitorJSON["name"],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Service URL",
|
name: monitorJSON["type"] === "push" ? "Service Type" : "Service URL",
|
||||||
value: url.startsWith("http") ? "[Visit Service](" + url + ")" : url,
|
value: monitorJSON["type"] === "push" ? "Heartbeat" : address,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Time (UTC)",
|
name: "Time (UTC)",
|
||||||
@ -92,21 +99,21 @@ class Discord extends NotificationProvider {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Ping",
|
name: "Ping",
|
||||||
value: heartbeatJSON["ping"] + "ms",
|
value: heartbeatJSON["ping"] == null ? "N/A" : heartbeatJSON["ping"] + " ms",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
}],
|
}],
|
||||||
}
|
};
|
||||||
|
|
||||||
if (notification.discordPrefixMessage) {
|
if (notification.discordPrefixMessage) {
|
||||||
discordupdata.content = notification.discordPrefixMessage;
|
discordupdata.content = notification.discordPrefixMessage;
|
||||||
}
|
}
|
||||||
|
|
||||||
await axios.post(notification.discordWebhookUrl, discordupdata)
|
await axios.post(notification.discordWebhookUrl, discordupdata);
|
||||||
return okMsg;
|
return okMsg;
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.throwGeneralAxiosError(error)
|
this.throwGeneralAxiosError(error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -21,7 +21,7 @@ class Feishu extends NotificationProvider {
|
|||||||
return okMsg;
|
return okMsg;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (heartbeatJSON["status"] == DOWN) {
|
if (heartbeatJSON["status"] === DOWN) {
|
||||||
let downdata = {
|
let downdata = {
|
||||||
msg_type: "post",
|
msg_type: "post",
|
||||||
content: {
|
content: {
|
||||||
@ -48,7 +48,7 @@ class Feishu extends NotificationProvider {
|
|||||||
return okMsg;
|
return okMsg;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (heartbeatJSON["status"] == UP) {
|
if (heartbeatJSON["status"] === UP) {
|
||||||
let updata = {
|
let updata = {
|
||||||
msg_type: "post",
|
msg_type: "post",
|
||||||
content: {
|
content: {
|
||||||
|
24
server/notification-providers/freemobile.js
Normal file
24
server/notification-providers/freemobile.js
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
const NotificationProvider = require("./notification-provider");
|
||||||
|
const axios = require("axios");
|
||||||
|
|
||||||
|
class FreeMobile extends NotificationProvider {
|
||||||
|
|
||||||
|
name = "FreeMobile";
|
||||||
|
|
||||||
|
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
|
||||||
|
let okMsg = "Sent Successfully.";
|
||||||
|
try {
|
||||||
|
await axios.post(`https://smsapi.free-mobile.fr/sendmsg?msg=${encodeURIComponent(msg.replace("🔴", "⛔️"))}`, {
|
||||||
|
"user": notification.freemobileUser,
|
||||||
|
"pass": notification.freemobilePass,
|
||||||
|
});
|
||||||
|
|
||||||
|
return okMsg;
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
this.throwGeneralAxiosError(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = FreeMobile;
|
35
server/notification-providers/goalert.js
Normal file
35
server/notification-providers/goalert.js
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
const NotificationProvider = require("./notification-provider");
|
||||||
|
const axios = require("axios");
|
||||||
|
const { UP } = require("../../src/util");
|
||||||
|
|
||||||
|
class GoAlert extends NotificationProvider {
|
||||||
|
|
||||||
|
name = "GoAlert";
|
||||||
|
|
||||||
|
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
|
||||||
|
let okMsg = "Sent Successfully.";
|
||||||
|
try {
|
||||||
|
let closeAction = "close";
|
||||||
|
let data = {
|
||||||
|
summary: msg,
|
||||||
|
};
|
||||||
|
if (heartbeatJSON != null && heartbeatJSON["status"] === UP) {
|
||||||
|
data["action"] = closeAction;
|
||||||
|
}
|
||||||
|
let headers = {
|
||||||
|
"Content-Type": "multipart/form-data",
|
||||||
|
};
|
||||||
|
let config = {
|
||||||
|
headers: headers
|
||||||
|
};
|
||||||
|
await axios.post(`${notification.goAlertBaseURL}/api/v2/generic/incoming?token=${notification.goAlertToken}`, data, config);
|
||||||
|
return okMsg;
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
let msg = (error.response.data) ? error.response.data : "Error without response";
|
||||||
|
throw new Error(msg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = GoAlert;
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user