Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e052adfd91 | ||
|
|
34e2ecd194 | ||
|
|
7a8d909d50 | ||
|
|
6f3cf2759a | ||
|
|
5d859e5d75 | ||
|
|
d20cc35f18 | ||
|
|
221a34906b | ||
|
|
d757aabd57 | ||
|
|
606c799166 | ||
|
|
387975ae19 | ||
|
|
2a15c1589e | ||
|
|
a430ccc051 | ||
|
|
b24208f73e | ||
|
|
5d9b1dd6ef | ||
|
|
a46f5ae3ab | ||
|
|
485e520dcb | ||
|
|
8578304ca4 | ||
|
|
7833315fa4 | ||
|
|
9b2480d8b3 | ||
|
|
00aa848dcf | ||
|
|
bc001c6a0e | ||
|
|
53445befea | ||
|
|
69ec9f95d8 | ||
|
|
7984972191 | ||
|
|
389b5bdf56 | ||
|
|
fe22e778ff | ||
|
|
0d12c78b1f | ||
|
|
2c0d02c84e | ||
|
|
2f2d354576 | ||
|
|
82de7ed47c | ||
|
|
5001c5cb47 | ||
|
|
c9ba0be2c5 | ||
|
|
a798f98ff4 | ||
|
|
6e95d28e27 | ||
|
|
ad9d735127 | ||
|
|
df304ced93 | ||
|
|
86bed1b3b2 | ||
|
|
bde789dc59 | ||
|
|
e3c0944062 | ||
|
|
114cfd6cb7 | ||
|
|
7c5c821fdd | ||
|
|
335098e2a8 | ||
|
|
f48b769baa | ||
|
|
0a89c595de | ||
|
|
4a3636789e | ||
|
|
ef4bfec3f2 | ||
|
|
f0c363e9bf | ||
|
|
1255e2c13d | ||
|
|
f4a8c226b2 | ||
|
|
308e884b36 | ||
|
|
bdeb33b8d3 | ||
|
|
a9a34f8725 | ||
|
|
f7a3172251 | ||
|
|
9dc3fe3a27 | ||
|
|
8f348507dc | ||
|
|
7b65debed6 | ||
|
|
b0182cff6d | ||
|
|
68296eccc3 | ||
|
|
211a1a2711 | ||
|
|
686e1f09d9 | ||
|
|
dbda203ba3 | ||
|
|
f3e7e5eca6 | ||
|
|
35ae579509 | ||
|
|
22a0caf919 | ||
|
|
acc673c244 | ||
|
|
f5547a1825 | ||
|
|
8e87bdccd0 | ||
|
|
47e051c2bb | ||
|
|
c8418705b1 | ||
|
|
5f96b17595 | ||
|
|
6ac19000f7 | ||
|
|
fa106d99ed | ||
|
|
361991141a | ||
|
|
d305c63a27 | ||
|
|
c5522a710a | ||
|
|
79d105e80e | ||
|
|
4c545f3d5a | ||
|
|
1cdb20e678 | ||
|
|
133bf736d7 | ||
|
|
fbe34d3488 | ||
|
|
8fcafc0694 | ||
|
|
d11fc90a1d | ||
|
|
1dbe405fc1 | ||
|
|
26df485644 | ||
|
|
2499925423 | ||
|
|
3a31387d00 | ||
|
|
45a52d9e7c | ||
|
|
5b3b78bd3f | ||
|
|
fc84d8e986 | ||
|
|
d507f5e7fe | ||
|
|
3b6a610c96 | ||
|
|
1f2a19fb43 | ||
|
|
c1d1e0ad1d | ||
|
|
e0e80dc9c7 | ||
|
|
6c598b976f | ||
|
|
960022d770 | ||
|
|
f724997b02 | ||
|
|
53c468f0f3 | ||
|
|
c27ed0f700 | ||
|
|
34389d4878 | ||
|
|
31509247de | ||
|
|
9ef259fb26 | ||
|
|
b99cdaca4e | ||
|
|
ca30dcc33c | ||
|
|
4acb80076a | ||
|
|
32eff933ca | ||
|
|
e2cd6254ee | ||
|
|
c4adb445d6 | ||
|
|
298202c9c2 | ||
|
|
2e63c4a189 | ||
|
|
bc85062dd1 | ||
|
|
9a33dffa50 | ||
|
|
ac904aca8b | ||
|
|
9b3085c84d | ||
|
|
6268b2573c | ||
|
|
6418efde2a | ||
|
|
73ac91c201 | ||
|
|
b6a7cfa7cc | ||
|
|
806f530016 | ||
|
|
114f4ffb05 | ||
|
|
c40408ae5e | ||
|
|
453c8f6b12 | ||
|
|
dbb0a6ffd9 | ||
|
|
b8984c0160 | ||
|
|
43a56e1de1 | ||
|
|
7fc5f2d025 | ||
|
|
f4bbbc5fa9 | ||
|
|
6037d13106 | ||
|
|
c3edbb779a | ||
|
|
d418e2f1f8 | ||
|
|
5c4d094da7 | ||
|
|
488ba717ac | ||
|
|
1cae407f65 | ||
|
|
179e95eefe | ||
|
|
97419b436c | ||
|
|
0832e8438a | ||
|
|
11b97ab3b4 | ||
|
|
992bf099fa | ||
|
|
b2bc441c7f | ||
|
|
edd9b5a6aa | ||
|
|
f29dc3f038 | ||
|
|
5ec032466b | ||
|
|
fe340e8661 | ||
|
|
38ee162f61 | ||
|
|
09fb5595ab | ||
|
|
888f644c7c | ||
|
|
bfc01de28c | ||
|
|
7454e8812e | ||
|
|
ffd193437e | ||
|
|
5dbd8378e2 | ||
|
|
4e8675df45 | ||
|
|
e2df0e7c6c | ||
|
|
3b9ffa7804 | ||
|
|
7527a7eb4f | ||
|
|
7996210852 | ||
|
|
8f10f5fa3e | ||
|
|
43cbb07b45 | ||
|
|
5c4174d3ac | ||
|
|
47e0c9d54f | ||
|
|
d642b93455 | ||
|
|
06057671b4 | ||
|
|
711e866702 | ||
|
|
20c27ee3e7 | ||
|
|
825777c2a8 | ||
|
|
d55119c28d | ||
|
|
35d2d8301e | ||
|
|
f2407ac5fa | ||
|
|
4ed7d426e8 | ||
|
|
d849374225 | ||
|
|
115831ca34 | ||
|
|
36014fada2 | ||
|
|
c44152de93 | ||
|
|
885a89e36b | ||
|
|
05f1b2b6b4 | ||
|
|
7a78a6d6b0 | ||
|
|
24863a2fb7 | ||
|
|
17f2039fc2 | ||
|
|
fe97390e53 | ||
|
|
4ea66541fd | ||
|
|
7ee05e8af2 | ||
|
|
e314def79a | ||
|
|
db4dbc6a5f | ||
|
|
1697e853e6 | ||
|
|
fd483b1857 | ||
|
|
bd03964ab6 | ||
|
|
8cccc58d39 | ||
|
|
53f2361a10 | ||
|
|
c9ab3d9fde | ||
|
|
0a49a070db | ||
|
|
7582dfdcc3 | ||
|
|
d2ab33492a | ||
|
|
9fba9e939e | ||
|
|
577bc9447d | ||
|
|
6c09606e40 | ||
|
|
ee30f44062 | ||
|
|
91a2a72bf2 |
No files matched your search
+2
-1
@@ -1,4 +1,5 @@
|
||||
.vite
|
||||
.vscode
|
||||
*.sh
|
||||
!uptime.sh
|
||||
!uptime.sh
|
||||
*.idea
|
||||
Generated
+87
-83
@@ -1759,9 +1759,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@rollup/rollup-android-arm-eabi": {
|
||||
"version": "4.18.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.18.1.tgz",
|
||||
"integrity": "sha512-lncuC4aHicncmbORnx+dUaAgzee9cm/PbIqgWz1PpXuwc+sa1Ct83tnqUDy/GFKleLiN7ZIeytM6KJ4cAn1SxA==",
|
||||
"version": "4.22.4",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.22.4.tgz",
|
||||
"integrity": "sha512-Fxamp4aEZnfPOcGA8KSNEohV8hX7zVHOemC8jVBoBUHu5zpJK/Eu3uJwt6BMgy9fkvzxDaurgj96F/NiLukF2w==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
@@ -1772,9 +1772,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-android-arm64": {
|
||||
"version": "4.18.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.18.1.tgz",
|
||||
"integrity": "sha512-F/tkdw0WSs4ojqz5Ovrw5r9odqzFjb5LIgHdHZG65dFI1lWTWRVy32KDJLKRISHgJvqUeUhdIvy43fX41znyDg==",
|
||||
"version": "4.22.4",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.22.4.tgz",
|
||||
"integrity": "sha512-VXoK5UMrgECLYaMuGuVTOx5kcuap1Jm8g/M83RnCHBKOqvPPmROFJGQaZhGccnsFtfXQ3XYa4/jMCJvZnbJBdA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -1785,9 +1785,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-darwin-arm64": {
|
||||
"version": "4.18.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.18.1.tgz",
|
||||
"integrity": "sha512-vk+ma8iC1ebje/ahpxpnrfVQJibTMyHdWpOGZ3JpQ7Mgn/3QNHmPq7YwjZbIE7km73dH5M1e6MRRsnEBW7v5CQ==",
|
||||
"version": "4.22.4",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.22.4.tgz",
|
||||
"integrity": "sha512-xMM9ORBqu81jyMKCDP+SZDhnX2QEVQzTcC6G18KlTQEzWK8r/oNZtKuZaCcHhnsa6fEeOBionoyl5JsAbE/36Q==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -1798,9 +1798,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-darwin-x64": {
|
||||
"version": "4.18.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.18.1.tgz",
|
||||
"integrity": "sha512-IgpzXKauRe1Tafcej9STjSSuG0Ghu/xGYH+qG6JwsAUxXrnkvNHcq/NL6nz1+jzvWAnQkuAJ4uIwGB48K9OCGA==",
|
||||
"version": "4.22.4",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.22.4.tgz",
|
||||
"integrity": "sha512-aJJyYKQwbHuhTUrjWjxEvGnNNBCnmpHDvrb8JFDbeSH3m2XdHcxDd3jthAzvmoI8w/kSjd2y0udT+4okADsZIw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -1811,9 +1811,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-arm-gnueabihf": {
|
||||
"version": "4.18.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.18.1.tgz",
|
||||
"integrity": "sha512-P9bSiAUnSSM7EmyRK+e5wgpqai86QOSv8BwvkGjLwYuOpaeomiZWifEos517CwbG+aZl1T4clSE1YqqH2JRs+g==",
|
||||
"version": "4.22.4",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.22.4.tgz",
|
||||
"integrity": "sha512-j63YtCIRAzbO+gC2L9dWXRh5BFetsv0j0va0Wi9epXDgU/XUi5dJKo4USTttVyK7fGw2nPWK0PbAvyliz50SCQ==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
@@ -1824,9 +1824,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-arm-musleabihf": {
|
||||
"version": "4.18.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.18.1.tgz",
|
||||
"integrity": "sha512-5RnjpACoxtS+aWOI1dURKno11d7krfpGDEn19jI8BuWmSBbUC4ytIADfROM1FZrFhQPSoP+KEa3NlEScznBTyQ==",
|
||||
"version": "4.22.4",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.22.4.tgz",
|
||||
"integrity": "sha512-dJnWUgwWBX1YBRsuKKMOlXCzh2Wu1mlHzv20TpqEsfdZLb3WoJW2kIEsGwLkroYf24IrPAvOT/ZQ2OYMV6vlrg==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
@@ -1837,9 +1837,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-arm64-gnu": {
|
||||
"version": "4.18.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.18.1.tgz",
|
||||
"integrity": "sha512-8mwmGD668m8WaGbthrEYZ9CBmPug2QPGWxhJxh/vCgBjro5o96gL04WLlg5BA233OCWLqERy4YUzX3bJGXaJgQ==",
|
||||
"version": "4.22.4",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.22.4.tgz",
|
||||
"integrity": "sha512-AdPRoNi3NKVLolCN/Sp4F4N1d98c4SBnHMKoLuiG6RXgoZ4sllseuGioszumnPGmPM2O7qaAX/IJdeDU8f26Aw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -1850,9 +1850,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-arm64-musl": {
|
||||
"version": "4.18.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.18.1.tgz",
|
||||
"integrity": "sha512-dJX9u4r4bqInMGOAQoGYdwDP8lQiisWb9et+T84l2WXk41yEej8v2iGKodmdKimT8cTAYt0jFb+UEBxnPkbXEQ==",
|
||||
"version": "4.22.4",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.22.4.tgz",
|
||||
"integrity": "sha512-Gl0AxBtDg8uoAn5CCqQDMqAx22Wx22pjDOjBdmG0VIWX3qUBHzYmOKh8KXHL4UpogfJ14G4wk16EQogF+v8hmA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -1863,9 +1863,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-powerpc64le-gnu": {
|
||||
"version": "4.18.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.18.1.tgz",
|
||||
"integrity": "sha512-V72cXdTl4EI0x6FNmho4D502sy7ed+LuVW6Ym8aI6DRQ9hQZdp5sj0a2usYOlqvFBNKQnLQGwmYnujo2HvjCxQ==",
|
||||
"version": "4.22.4",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.22.4.tgz",
|
||||
"integrity": "sha512-3aVCK9xfWW1oGQpTsYJJPF6bfpWfhbRnhdlyhak2ZiyFLDaayz0EP5j9V1RVLAAxlmWKTDfS9wyRyY3hvhPoOg==",
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
@@ -1876,9 +1876,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-riscv64-gnu": {
|
||||
"version": "4.18.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.18.1.tgz",
|
||||
"integrity": "sha512-f+pJih7sxoKmbjghrM2RkWo2WHUW8UbfxIQiWo5yeCaCM0TveMEuAzKJte4QskBp1TIinpnRcxkquY+4WuY/tg==",
|
||||
"version": "4.22.4",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.22.4.tgz",
|
||||
"integrity": "sha512-ePYIir6VYnhgv2C5Xe9u+ico4t8sZWXschR6fMgoPUK31yQu7hTEJb7bCqivHECwIClJfKgE7zYsh1qTP3WHUA==",
|
||||
"cpu": [
|
||||
"riscv64"
|
||||
],
|
||||
@@ -1889,9 +1889,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-s390x-gnu": {
|
||||
"version": "4.18.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.18.1.tgz",
|
||||
"integrity": "sha512-qb1hMMT3Fr/Qz1OKovCuUM11MUNLUuHeBC2DPPAWUYYUAOFWaxInaTwTQmc7Fl5La7DShTEpmYwgdt2hG+4TEg==",
|
||||
"version": "4.22.4",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.22.4.tgz",
|
||||
"integrity": "sha512-GqFJ9wLlbB9daxhVlrTe61vJtEY99/xB3C8e4ULVsVfflcpmR6c8UZXjtkMA6FhNONhj2eA5Tk9uAVw5orEs4Q==",
|
||||
"cpu": [
|
||||
"s390x"
|
||||
],
|
||||
@@ -1902,9 +1902,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-x64-gnu": {
|
||||
"version": "4.18.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.18.1.tgz",
|
||||
"integrity": "sha512-7O5u/p6oKUFYjRbZkL2FLbwsyoJAjyeXHCU3O4ndvzg2OFO2GinFPSJFGbiwFDaCFc+k7gs9CF243PwdPQFh5g==",
|
||||
"version": "4.22.4",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.22.4.tgz",
|
||||
"integrity": "sha512-87v0ol2sH9GE3cLQLNEy0K/R0pz1nvg76o8M5nhMR0+Q+BBGLnb35P0fVz4CQxHYXaAOhE8HhlkaZfsdUOlHwg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -1915,9 +1915,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-x64-musl": {
|
||||
"version": "4.18.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.18.1.tgz",
|
||||
"integrity": "sha512-pDLkYITdYrH/9Cv/Vlj8HppDuLMDUBmgsM0+N+xLtFd18aXgM9Nyqupb/Uw+HeidhfYg2lD6CXvz6CjoVOaKjQ==",
|
||||
"version": "4.22.4",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.22.4.tgz",
|
||||
"integrity": "sha512-UV6FZMUgePDZrFjrNGIWzDo/vABebuXBhJEqrHxrGiU6HikPy0Z3LfdtciIttEUQfuDdCn8fqh7wiFJjCNwO+g==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -1928,9 +1928,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-win32-arm64-msvc": {
|
||||
"version": "4.18.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.18.1.tgz",
|
||||
"integrity": "sha512-W2ZNI323O/8pJdBGil1oCauuCzmVd9lDmWBBqxYZcOqWD6aWqJtVBQ1dFrF4dYpZPks6F+xCZHfzG5hYlSHZ6g==",
|
||||
"version": "4.22.4",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.22.4.tgz",
|
||||
"integrity": "sha512-BjI+NVVEGAXjGWYHz/vv0pBqfGoUH0IGZ0cICTn7kB9PyjrATSkX+8WkguNjWoj2qSr1im/+tTGRaY+4/PdcQw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -1941,9 +1941,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-win32-ia32-msvc": {
|
||||
"version": "4.18.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.18.1.tgz",
|
||||
"integrity": "sha512-ELfEX1/+eGZYMaCIbK4jqLxO1gyTSOIlZr6pbC4SRYFaSIDVKOnZNMdoZ+ON0mrFDp4+H5MhwNC1H/AhE3zQLg==",
|
||||
"version": "4.22.4",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.22.4.tgz",
|
||||
"integrity": "sha512-SiWG/1TuUdPvYmzmYnmd3IEifzR61Tragkbx9D3+R8mzQqDBz8v+BvZNDlkiTtI9T15KYZhP0ehn3Dld4n9J5g==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
@@ -1954,9 +1954,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-win32-x64-msvc": {
|
||||
"version": "4.18.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.18.1.tgz",
|
||||
"integrity": "sha512-yjk2MAkQmoaPYCSu35RLJ62+dz358nE83VfTePJRp8CG7aMg25mEJYpXFiD+NcevhX8LxD5OP5tktPXnXN7GDw==",
|
||||
"version": "4.22.4",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.22.4.tgz",
|
||||
"integrity": "sha512-j8pPKp53/lq9lMXN57S8cFz0MynJk8OWNuUnXct/9KCpKU7DgU3bYMJhwWmcqC0UU29p8Lr0/7KEVcaM6bf47Q==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -5249,9 +5249,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/picocolors": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.1.tgz",
|
||||
"integrity": "sha512-anP1Z8qwhkbmu7MFP5iTt+wQKXgwzf7zTyGlcdzabySa9vd0Xt392U0rVmz9poOaBj0uHJKyyo9/upk0HrEQew==",
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.0.tgz",
|
||||
"integrity": "sha512-TQ92mBOW0l3LeMeyLV6mzy/kWr8lkd/hp3mTg7wYK7zJhuBStmGMBG0BdeDZS/dZx1IukaX6Bk11zcln25o1Aw==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/picomatch": {
|
||||
@@ -5277,9 +5277,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/postcss": {
|
||||
"version": "8.4.39",
|
||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.39.tgz",
|
||||
"integrity": "sha512-0vzE+lAiG7hZl1/9I8yzKLx3aR9Xbof3fBHKunvMfOCYAtMhrsnccJY2iTURb9EZd5+pLuiNV9/c/GZJOHsgIw==",
|
||||
"version": "8.4.47",
|
||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.47.tgz",
|
||||
"integrity": "sha512-56rxCq7G/XfB4EkXq9Egn5GCqugWvDFjafDOThIdMBsI15iqPqR5r15TfSr1YPYeEI19YeaXMCbY6u88Y76GLQ==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "opencollective",
|
||||
@@ -5297,8 +5297,8 @@
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"nanoid": "^3.3.7",
|
||||
"picocolors": "^1.0.1",
|
||||
"source-map-js": "^1.2.0"
|
||||
"picocolors": "^1.1.0",
|
||||
"source-map-js": "^1.2.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^10 || ^12 || >=14"
|
||||
@@ -5678,9 +5678,9 @@
|
||||
"license": "Unlicense"
|
||||
},
|
||||
"node_modules/rollup": {
|
||||
"version": "4.18.1",
|
||||
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.18.1.tgz",
|
||||
"integrity": "sha512-Elx2UT8lzxxOXMpy5HWQGZqkrQOtrVDDa/bm9l10+U4rQnVzbL/LgZ4NOM1MPIDyHk69W4InuYDF5dzRh4Kw1A==",
|
||||
"version": "4.22.4",
|
||||
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.22.4.tgz",
|
||||
"integrity": "sha512-vD8HJ5raRcWOyymsR6Z3o6+RzfEPCnVLMFJ6vRslO1jt4LO6dUo5Qnpg7y4RkZFM2DMe3WUirkI5c16onjrc6A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/estree": "1.0.5"
|
||||
@@ -5693,22 +5693,22 @@
|
||||
"npm": ">=8.0.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@rollup/rollup-android-arm-eabi": "4.18.1",
|
||||
"@rollup/rollup-android-arm64": "4.18.1",
|
||||
"@rollup/rollup-darwin-arm64": "4.18.1",
|
||||
"@rollup/rollup-darwin-x64": "4.18.1",
|
||||
"@rollup/rollup-linux-arm-gnueabihf": "4.18.1",
|
||||
"@rollup/rollup-linux-arm-musleabihf": "4.18.1",
|
||||
"@rollup/rollup-linux-arm64-gnu": "4.18.1",
|
||||
"@rollup/rollup-linux-arm64-musl": "4.18.1",
|
||||
"@rollup/rollup-linux-powerpc64le-gnu": "4.18.1",
|
||||
"@rollup/rollup-linux-riscv64-gnu": "4.18.1",
|
||||
"@rollup/rollup-linux-s390x-gnu": "4.18.1",
|
||||
"@rollup/rollup-linux-x64-gnu": "4.18.1",
|
||||
"@rollup/rollup-linux-x64-musl": "4.18.1",
|
||||
"@rollup/rollup-win32-arm64-msvc": "4.18.1",
|
||||
"@rollup/rollup-win32-ia32-msvc": "4.18.1",
|
||||
"@rollup/rollup-win32-x64-msvc": "4.18.1",
|
||||
"@rollup/rollup-android-arm-eabi": "4.22.4",
|
||||
"@rollup/rollup-android-arm64": "4.22.4",
|
||||
"@rollup/rollup-darwin-arm64": "4.22.4",
|
||||
"@rollup/rollup-darwin-x64": "4.22.4",
|
||||
"@rollup/rollup-linux-arm-gnueabihf": "4.22.4",
|
||||
"@rollup/rollup-linux-arm-musleabihf": "4.22.4",
|
||||
"@rollup/rollup-linux-arm64-gnu": "4.22.4",
|
||||
"@rollup/rollup-linux-arm64-musl": "4.22.4",
|
||||
"@rollup/rollup-linux-powerpc64le-gnu": "4.22.4",
|
||||
"@rollup/rollup-linux-riscv64-gnu": "4.22.4",
|
||||
"@rollup/rollup-linux-s390x-gnu": "4.22.4",
|
||||
"@rollup/rollup-linux-x64-gnu": "4.22.4",
|
||||
"@rollup/rollup-linux-x64-musl": "4.22.4",
|
||||
"@rollup/rollup-win32-arm64-msvc": "4.22.4",
|
||||
"@rollup/rollup-win32-ia32-msvc": "4.22.4",
|
||||
"@rollup/rollup-win32-x64-msvc": "4.22.4",
|
||||
"fsevents": "~2.3.2"
|
||||
}
|
||||
},
|
||||
@@ -5887,9 +5887,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/source-map-js": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.0.tgz",
|
||||
"integrity": "sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg==",
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
|
||||
"integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
|
||||
"license": "BSD-3-Clause",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
@@ -6266,14 +6266,14 @@
|
||||
}
|
||||
},
|
||||
"node_modules/vite": {
|
||||
"version": "5.3.4",
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-5.3.4.tgz",
|
||||
"integrity": "sha512-Cw+7zL3ZG9/NZBB8C+8QbQZmR54GwqIz+WMI4b3JgdYJvX+ny9AjJXqkGQlDXSXRP9rP0B4tbciRMOVEKulVOA==",
|
||||
"version": "5.4.8",
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-5.4.8.tgz",
|
||||
"integrity": "sha512-FqrItQ4DT1NC4zCUqMB4c4AZORMKIa0m8/URVCZ77OZ/QSNeJ54bU1vrFADbDsuwfIPcgknRkmqakQcgnL4GiQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"esbuild": "^0.21.3",
|
||||
"postcss": "^8.4.39",
|
||||
"rollup": "^4.13.0"
|
||||
"postcss": "^8.4.43",
|
||||
"rollup": "^4.20.0"
|
||||
},
|
||||
"bin": {
|
||||
"vite": "bin/vite.js"
|
||||
@@ -6292,6 +6292,7 @@
|
||||
"less": "*",
|
||||
"lightningcss": "^1.21.0",
|
||||
"sass": "*",
|
||||
"sass-embedded": "*",
|
||||
"stylus": "*",
|
||||
"sugarss": "*",
|
||||
"terser": "^5.4.0"
|
||||
@@ -6309,6 +6310,9 @@
|
||||
"sass": {
|
||||
"optional": true
|
||||
},
|
||||
"sass-embedded": {
|
||||
"optional": true
|
||||
},
|
||||
"stylus": {
|
||||
"optional": true
|
||||
},
|
||||
|
||||
+41
-11
@@ -19,13 +19,14 @@ import SetNewPassword from "./Pages/Auth/SetNewPassword";
|
||||
import NewPasswordConfirmed from "./Pages/Auth/NewPasswordConfirmed";
|
||||
import ProtectedRoute from "./Components/ProtectedRoute";
|
||||
import Details from "./Pages/Monitors/Details";
|
||||
import AdvancedSettings from "./Pages/AdvancedSettings";
|
||||
import Maintenance from "./Pages/Maintenance";
|
||||
import withAdminCheck from "./HOC/withAdminCheck";
|
||||
import withAdminProp from "./HOC/withAdminProp";
|
||||
import Configure from "./Pages/Monitors/Configure";
|
||||
import PageSpeed from "./Pages/PageSpeed";
|
||||
import CreatePageSpeed from "./Pages/PageSpeed/CreatePageSpeed";
|
||||
import CreateNewMaintenanceWindow from "./Pages/Maintenance/CreateMaintenanceWindow";
|
||||
import CreateNewMaintenanceWindow from "./Pages/Maintenance/CreateMaintenance";
|
||||
import PageSpeedDetails from "./Pages/PageSpeed/Details";
|
||||
import PageSpeedConfigure from "./Pages/PageSpeed/Configure";
|
||||
import { ThemeProvider } from "@emotion/react";
|
||||
@@ -33,16 +34,37 @@ import lightTheme from "./Utils/Theme/lightTheme";
|
||||
import darkTheme from "./Utils/Theme/darkTheme";
|
||||
import { useSelector } from "react-redux";
|
||||
import { CssBaseline } from "@mui/material";
|
||||
|
||||
import { useEffect } from "react";
|
||||
import { useDispatch } from "react-redux";
|
||||
import { getAppSettings } from "./Features/Settings/settingsSlice";
|
||||
import { logger } from "./Utils/Logger"; // Import the logger
|
||||
import { networkService } from "./main";
|
||||
function App() {
|
||||
const AdminCheckedRegister = withAdminCheck(Register);
|
||||
const MonitorsWithAdminProp = withAdminProp(Monitors);
|
||||
const DetailsWithAdminProp = withAdminProp(Details);
|
||||
const MonitorDetailsWithAdminProp = withAdminProp(Details);
|
||||
const PageSpeedWithAdminProp = withAdminProp(PageSpeed);
|
||||
const PageSpeedDetailsWithAdminProp = withAdminProp(PageSpeedDetails);
|
||||
const MaintenanceWithAdminProp = withAdminProp(Maintenance);
|
||||
const SettingsWithAdminProp = withAdminProp(Settings);
|
||||
|
||||
const AdvancedSettingsWithAdminProp = withAdminProp(AdvancedSettings);
|
||||
const mode = useSelector((state) => state.ui.mode);
|
||||
const { authToken } = useSelector((state) => state.auth);
|
||||
const dispatch = useDispatch();
|
||||
|
||||
useEffect(() => {
|
||||
if (authToken) {
|
||||
dispatch(getAppSettings({ authToken }));
|
||||
}
|
||||
}, [dispatch, authToken]);
|
||||
|
||||
// Cleanup
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
logger.cleanup();
|
||||
networkService.cleanup();
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<ThemeProvider theme={mode === "light" ? lightTheme : darkTheme}>
|
||||
@@ -64,7 +86,7 @@ function App() {
|
||||
/>
|
||||
<Route
|
||||
path="/monitors/:monitorId/"
|
||||
element={<ProtectedRoute Component={DetailsWithAdminProp} />}
|
||||
element={<ProtectedRoute Component={MonitorDetailsWithAdminProp} />}
|
||||
/>
|
||||
<Route
|
||||
path="/monitors/configure/:monitorId/"
|
||||
@@ -83,18 +105,24 @@ function App() {
|
||||
path="integrations"
|
||||
element={<ProtectedRoute Component={Integrations} />}
|
||||
/>
|
||||
{/* <Route
|
||||
<Route
|
||||
path="maintenance"
|
||||
element={<ProtectedRoute Component={MaintenanceWithAdminProp} />}
|
||||
/> */}
|
||||
{/* <Route
|
||||
path="/maintenance/create"
|
||||
/>
|
||||
<Route
|
||||
path="/maintenance/create/:maintenanceWindowId?"
|
||||
element={<CreateNewMaintenanceWindow />}
|
||||
/> */}
|
||||
/>
|
||||
<Route
|
||||
path="settings"
|
||||
element={<ProtectedRoute Component={SettingsWithAdminProp} />}
|
||||
/>
|
||||
<Route
|
||||
path="advanced-settings"
|
||||
element={
|
||||
<ProtectedRoute Component={AdvancedSettingsWithAdminProp} />
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="account/profile"
|
||||
element={<ProtectedRoute Component={Account} open="profile" />}
|
||||
@@ -117,7 +145,9 @@ function App() {
|
||||
/>
|
||||
<Route
|
||||
path="pagespeed/:monitorId"
|
||||
element={<ProtectedRoute Component={PageSpeedDetails} />}
|
||||
element={
|
||||
<ProtectedRoute Component={PageSpeedDetailsWithAdminProp} />
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="pagespeed/configure/:monitorId"
|
||||
|
||||
@@ -16,6 +16,7 @@ import "./index.css";
|
||||
* @param {Object} props
|
||||
* @param {string} [props.type] - Type of input field (e.g., 'text', 'password').
|
||||
* @param {string} props.id - ID of the input field.
|
||||
* @param {string} props.name - Name of the input field.
|
||||
* @param {string} [props.label] - Label for the input field.
|
||||
* @param {boolean} [props.https] - Indicates if it should display http or https.
|
||||
* @param {boolean} [props.isRequired] - Indicates if the field is required, will display a red asterisk.
|
||||
@@ -27,6 +28,7 @@ import "./index.css";
|
||||
* @param {function} props.onChange - Function called on input change.
|
||||
* @param {string} [props.error] - Error message to display for the input field.
|
||||
* @param {boolean} [props.disabled] - Indicates if the input field is disabled.
|
||||
* @param {boolean} [props.hidden] - Indicates if the input field is hidden.
|
||||
* @param {React.Ref} [ref] - Ref forwarded to the underlying `TextField` component. Allows for direct interactions such as focusing.
|
||||
*/
|
||||
|
||||
@@ -35,6 +37,7 @@ const Field = forwardRef(
|
||||
{
|
||||
type = "text",
|
||||
id,
|
||||
name,
|
||||
label,
|
||||
https,
|
||||
isRequired,
|
||||
@@ -47,6 +50,7 @@ const Field = forwardRef(
|
||||
onInput,
|
||||
error,
|
||||
disabled,
|
||||
hidden,
|
||||
},
|
||||
ref
|
||||
) => {
|
||||
@@ -107,6 +111,7 @@ const Field = forwardRef(
|
||||
)}
|
||||
<TextField
|
||||
type={type === "password" ? (isVisible ? "text" : type) : type}
|
||||
name={name}
|
||||
id={id}
|
||||
autoComplete={autoComplete}
|
||||
placeholder={placeholder}
|
||||
@@ -136,7 +141,9 @@ const Field = forwardRef(
|
||||
borderBottomLeftRadius: theme.shape.borderRadius,
|
||||
},
|
||||
}
|
||||
: {}
|
||||
: {
|
||||
display: hidden ? "none" : "",
|
||||
}
|
||||
}
|
||||
InputProps={{
|
||||
startAdornment: type === "url" && (
|
||||
@@ -213,6 +220,7 @@ Field.propTypes = {
|
||||
"number",
|
||||
]),
|
||||
id: PropTypes.string.isRequired,
|
||||
name: PropTypes.string,
|
||||
label: PropTypes.string,
|
||||
https: PropTypes.bool,
|
||||
isRequired: PropTypes.bool,
|
||||
@@ -225,6 +233,7 @@ Field.propTypes = {
|
||||
onInput: PropTypes.func,
|
||||
error: PropTypes.string,
|
||||
disabled: PropTypes.bool,
|
||||
hidden: PropTypes.bool,
|
||||
};
|
||||
|
||||
export default Field;
|
||||
@@ -1,5 +1,12 @@
|
||||
import PropTypes from "prop-types";
|
||||
import { Box, ListItem, Autocomplete, TextField } from "@mui/material";
|
||||
import {
|
||||
Box,
|
||||
ListItem,
|
||||
Autocomplete,
|
||||
TextField,
|
||||
Stack,
|
||||
Typography,
|
||||
} from "@mui/material";
|
||||
import { useTheme } from "@emotion/react";
|
||||
import SearchIcon from "../../../assets/icons/search.svg?react";
|
||||
|
||||
@@ -15,21 +22,52 @@ import SearchIcon from "../../../assets/icons/search.svg?react";
|
||||
* @param {Object} props.sx - Additional styles to apply to the component
|
||||
* @returns {JSX.Element} The rendered Search component
|
||||
*/
|
||||
|
||||
const SearchAdornment = () => {
|
||||
const theme = useTheme();
|
||||
return (
|
||||
<Box
|
||||
mr={theme.spacing(4)}
|
||||
height={16}
|
||||
sx={{
|
||||
"& svg": {
|
||||
width: 16,
|
||||
height: 16,
|
||||
"& path": {
|
||||
stroke: theme.palette.text.tertiary,
|
||||
strokeWidth: 1.2,
|
||||
},
|
||||
},
|
||||
}}
|
||||
>
|
||||
<SearchIcon />
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
const Search = ({
|
||||
id,
|
||||
options,
|
||||
filteredBy,
|
||||
secondaryLabel,
|
||||
value,
|
||||
inputValue,
|
||||
handleInputChange,
|
||||
handleChange,
|
||||
sx,
|
||||
multiple = false,
|
||||
isAdorned = true,
|
||||
error,
|
||||
disabled,
|
||||
}) => {
|
||||
const theme = useTheme();
|
||||
|
||||
return (
|
||||
<Autocomplete
|
||||
multiple={multiple}
|
||||
id={id}
|
||||
inputValue={value}
|
||||
value={value}
|
||||
inputValue={inputValue}
|
||||
onInputChange={(_, newValue) => {
|
||||
handleInputChange(newValue);
|
||||
}}
|
||||
@@ -38,45 +76,45 @@ const Search = ({
|
||||
}}
|
||||
fullWidth
|
||||
freeSolo
|
||||
disabled={disabled}
|
||||
disableClearable
|
||||
options={options}
|
||||
getOptionLabel={(option) => option[filteredBy]}
|
||||
renderInput={(params) => (
|
||||
<TextField
|
||||
{...params}
|
||||
placeholder="Type to search"
|
||||
InputProps={{
|
||||
...params.InputProps,
|
||||
startAdornment: (
|
||||
<Box
|
||||
mr={theme.spacing(4)}
|
||||
height={16}
|
||||
sx={{
|
||||
"& svg": {
|
||||
width: 16,
|
||||
height: 16,
|
||||
"& path": {
|
||||
stroke: theme.palette.text.tertiary,
|
||||
strokeWidth: 1.2,
|
||||
},
|
||||
},
|
||||
}}
|
||||
>
|
||||
<SearchIcon />
|
||||
</Box>
|
||||
),
|
||||
}}
|
||||
sx={{
|
||||
"& fieldset": {
|
||||
borderColor: theme.palette.border.light,
|
||||
borderRadius: theme.shape.borderRadius,
|
||||
},
|
||||
"& .MuiOutlinedInput-root:hover:not(:has(input:focus)):not(:has(textarea:focus)) fieldset":
|
||||
{
|
||||
<Stack>
|
||||
<TextField
|
||||
{...params}
|
||||
error={Boolean(error)}
|
||||
placeholder="Type to search"
|
||||
InputProps={{
|
||||
...params.InputProps,
|
||||
...(isAdorned && { startAdornment: <SearchAdornment /> }),
|
||||
}}
|
||||
sx={{
|
||||
"& fieldset": {
|
||||
borderColor: theme.palette.border.light,
|
||||
borderRadius: theme.shape.borderRadius,
|
||||
},
|
||||
}}
|
||||
/>
|
||||
"& .MuiOutlinedInput-root:hover:not(:has(input:focus)):not(:has(textarea:focus)) fieldset":
|
||||
{
|
||||
borderColor: theme.palette.border.light,
|
||||
},
|
||||
}}
|
||||
/>
|
||||
{error && (
|
||||
<Typography
|
||||
component="span"
|
||||
className="input-error"
|
||||
color={theme.palette.error.text}
|
||||
mt={theme.spacing(2)}
|
||||
sx={{
|
||||
opacity: 0.8,
|
||||
}}
|
||||
>
|
||||
{error}
|
||||
</Typography>
|
||||
)}
|
||||
</Stack>
|
||||
)}
|
||||
filterOptions={(options, { inputValue }) => {
|
||||
const filtered = options.filter((option) =>
|
||||
@@ -103,7 +141,8 @@ const Search = ({
|
||||
: {}
|
||||
}
|
||||
>
|
||||
{option[filteredBy]}
|
||||
{option[filteredBy] +
|
||||
(secondaryLabel ? ` (${option[secondaryLabel]})` : "")}
|
||||
</ListItem>
|
||||
);
|
||||
}}
|
||||
@@ -139,12 +178,18 @@ const Search = ({
|
||||
|
||||
Search.propTypes = {
|
||||
id: PropTypes.string,
|
||||
multiple: PropTypes.bool,
|
||||
options: PropTypes.array.isRequired,
|
||||
filteredBy: PropTypes.string.isRequired,
|
||||
value: PropTypes.string.isRequired,
|
||||
secondaryLabel: PropTypes.string,
|
||||
value: PropTypes.array,
|
||||
inputValue: PropTypes.string.isRequired,
|
||||
handleInputChange: PropTypes.func.isRequired,
|
||||
handleChange: PropTypes.func,
|
||||
isAdorned: PropTypes.bool,
|
||||
sx: PropTypes.object,
|
||||
error: PropTypes.string,
|
||||
disabled: PropTypes.bool,
|
||||
};
|
||||
|
||||
export default Search;
|
||||
@@ -34,6 +34,7 @@ import "./index.css";
|
||||
*
|
||||
* <Select
|
||||
* id="frequency-id"
|
||||
* name="my-name"
|
||||
* label="Check frequency"
|
||||
* placeholder="Select frequency"
|
||||
* value={value}
|
||||
@@ -51,6 +52,7 @@ const Select = ({
|
||||
items,
|
||||
onChange,
|
||||
sx,
|
||||
name = "",
|
||||
}) => {
|
||||
const theme = useTheme();
|
||||
const itemStyles = {
|
||||
@@ -77,6 +79,7 @@ const Select = ({
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
displayEmpty
|
||||
name={name}
|
||||
inputProps={{ id: id }}
|
||||
IconComponent={KeyboardArrowDownIcon}
|
||||
MenuProps={{ disableScrollLock: true }}
|
||||
@@ -127,6 +130,7 @@ const Select = ({
|
||||
|
||||
Select.propTypes = {
|
||||
id: PropTypes.string.isRequired,
|
||||
name: PropTypes.string,
|
||||
label: PropTypes.string,
|
||||
placeholder: PropTypes.string,
|
||||
isHidden: PropTypes.bool,
|
||||
|
||||
@@ -55,7 +55,7 @@ const menu = [
|
||||
},
|
||||
{ name: "Incidents", path: "incidents", icon: <Incidents /> },
|
||||
// { name: "Status pages", path: "status", icon: <StatusPages /> },
|
||||
// { name: "Maintenance", path: "maintenance", icon: <Maintenance /> },
|
||||
{ name: "Maintenance", path: "maintenance", icon: <Maintenance /> },
|
||||
// { name: "Integrations", path: "integrations", icon: <Integrations /> },
|
||||
{
|
||||
name: "Account",
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import TabPanel from "@mui/lab/TabPanel";
|
||||
import React, { useState } from "react";
|
||||
import { useState } from "react";
|
||||
import { useTheme } from "@emotion/react";
|
||||
import { Box, Stack, Typography } from "@mui/material";
|
||||
import LoadingButton from "@mui/lab/LoadingButton";
|
||||
@@ -116,6 +116,14 @@ const PasswordPanel = () => {
|
||||
<Box flex={0.9}>
|
||||
<Typography component="h1">Current password</Typography>
|
||||
</Box>
|
||||
<Field
|
||||
type="text"
|
||||
id="hidden-username"
|
||||
name="username"
|
||||
autoComplete="username"
|
||||
hidden={true}
|
||||
value=""
|
||||
/>
|
||||
<Field
|
||||
type="password"
|
||||
id="edit-current-password"
|
||||
|
||||
@@ -0,0 +1,117 @@
|
||||
import { networkService } from "../../main";
|
||||
import { createAsyncThunk, createSlice } from "@reduxjs/toolkit";
|
||||
|
||||
const initialState = {
|
||||
isLoading: false,
|
||||
apiBaseUrl: "",
|
||||
logLevel: "debug",
|
||||
};
|
||||
|
||||
export const getAppSettings = createAsyncThunk(
|
||||
"settings/getSettings",
|
||||
async (data, thunkApi) => {
|
||||
try {
|
||||
const res = await networkService.getAppSettings({
|
||||
authToken: data.authToken,
|
||||
});
|
||||
return res.data;
|
||||
} catch (error) {
|
||||
if (error.response.data) {
|
||||
return thunkApi.rejectWithValue(error.response.data);
|
||||
}
|
||||
const payload = {
|
||||
status: false,
|
||||
msg: error.message ? error.message : "Unknown error",
|
||||
};
|
||||
return thunkApi.rejectWithValue(payload);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
export const updateAppSettings = createAsyncThunk(
|
||||
"settings/updateSettings",
|
||||
async ({ settings, authToken }, thunkApi) => {
|
||||
networkService.setBaseUrl(settings.apiBaseUrl);
|
||||
try {
|
||||
const parsedSettings = {
|
||||
apiBaseUrl: settings.apiBaseUrl,
|
||||
logLevel: settings.logLevel,
|
||||
clientHost: settings.clientHost,
|
||||
jwtSecret: settings.jwtSecret,
|
||||
dbType: settings.dbType,
|
||||
dbConnectionString: settings.dbConnectionString,
|
||||
redisHost: settings.redisHost,
|
||||
redisPort: settings.redisPort,
|
||||
jwtTTL: settings.jwtTTL,
|
||||
pagespeedApiKey: settings.pagespeedApiKey,
|
||||
systemEmailHost: settings.systemEmailHost,
|
||||
systemEmailPort: settings.systemEmailPort,
|
||||
systemEmailAddress: settings.systemEmailAddress,
|
||||
systemEmailPassword: settings.systemEmailPassword,
|
||||
};
|
||||
const res = await networkService.updateAppSettings({
|
||||
settings: parsedSettings,
|
||||
authToken,
|
||||
});
|
||||
return res.data;
|
||||
} catch (error) {
|
||||
if (error.response && error.response.data) {
|
||||
return thunkApi.rejectWithValue(error.response.data);
|
||||
}
|
||||
const payload = {
|
||||
status: false,
|
||||
msg: error.message ? error.message : "Unknown error",
|
||||
};
|
||||
return thunkApi.rejectWithValue(payload);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
const handleGetSettingsFulfilled = (state, action) => {
|
||||
state.isLoading = false;
|
||||
state.success = action.payload.success;
|
||||
state.msg = action.payload.msg;
|
||||
state.apiBaseUrl = action.payload.data.apiBaseUrl;
|
||||
state.logLevel = action.payload.data.logLevel;
|
||||
};
|
||||
const handleGetSettingsRejected = (state, action) => {
|
||||
state.isLoading = false;
|
||||
state.success = false;
|
||||
state.msg = action.payload ? action.payload.msg : "Failed to get settings.";
|
||||
};
|
||||
const handleUpdateSettingsFulfilled = (state, action) => {
|
||||
state.isLoading = false;
|
||||
state.success = action.payload.success;
|
||||
state.msg = action.payload.msg;
|
||||
state.apiBaseUrl = action.payload.data.apiBaseUrl;
|
||||
state.logLevel = action.payload.data.logLevel;
|
||||
};
|
||||
const handleUpdateSettingsRejected = (state, action) => {
|
||||
state.isLoading = false;
|
||||
state.success = false;
|
||||
state.msg = action.payload
|
||||
? action.payload.msg
|
||||
: "Failed to update settings.";
|
||||
};
|
||||
|
||||
const settingsSlice = createSlice({
|
||||
name: "settings",
|
||||
initialState,
|
||||
extraReducers: (builder) => {
|
||||
builder
|
||||
.addCase(getAppSettings.pending, (state) => {
|
||||
state.isLoading = true;
|
||||
})
|
||||
.addCase(getAppSettings.fulfilled, handleGetSettingsFulfilled)
|
||||
.addCase(getAppSettings.rejected, handleGetSettingsRejected);
|
||||
|
||||
builder
|
||||
.addCase(updateAppSettings.pending, (state) => {
|
||||
state.isLoading = true;
|
||||
})
|
||||
.addCase(updateAppSettings.fulfilled, handleUpdateSettingsFulfilled)
|
||||
.addCase(updateAppSettings.rejected, handleUpdateSettingsRejected);
|
||||
},
|
||||
});
|
||||
|
||||
export default settingsSlice.reducer;
|
||||
@@ -9,6 +9,9 @@ const initialState = {
|
||||
team: {
|
||||
rowsPerPage: 5,
|
||||
},
|
||||
maintenance: {
|
||||
rowsPerPage: 5,
|
||||
},
|
||||
sidebar: {
|
||||
collapsed: false,
|
||||
},
|
||||
|
||||
Whitespace-only changes.
@@ -0,0 +1,265 @@
|
||||
import { useTheme } from "@emotion/react";
|
||||
import { Box, Stack, Typography } from "@mui/material";
|
||||
import Field from "../../Components/Inputs/Field";
|
||||
import Link from "../../Components/Link";
|
||||
import "./index.css";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import { createToast } from "../../Utils/toastUtils";
|
||||
import PropTypes from "prop-types";
|
||||
import LoadingButton from "@mui/lab/LoadingButton";
|
||||
import { ConfigBox } from "../Settings/styled";
|
||||
import { useNavigate } from "react-router";
|
||||
import {
|
||||
getAppSettings,
|
||||
updateAppSettings,
|
||||
} from "../../Features/Settings/settingsSlice";
|
||||
import { useState, useEffect } from "react";
|
||||
import Select from "../../Components/Inputs/Select";
|
||||
|
||||
const AdvancedSettings = ({ isAdmin }) => {
|
||||
const navigate = useNavigate();
|
||||
|
||||
useEffect(() => {
|
||||
if (!isAdmin) {
|
||||
navigate("/");
|
||||
}
|
||||
}, [navigate, isAdmin]);
|
||||
|
||||
const theme = useTheme();
|
||||
const { authToken } = useSelector((state) => state.auth);
|
||||
const dispatch = useDispatch();
|
||||
const settings = useSelector((state) => state.settings);
|
||||
const [localSettings, setLocalSettings] = useState({
|
||||
apiBaseUrl: "",
|
||||
logLevel: "debug",
|
||||
systemEmailHost: "",
|
||||
systemEmailPort: "",
|
||||
systemEmailAddress: "",
|
||||
systemEmailPassword: "",
|
||||
jwtTTL: "",
|
||||
dbType: "",
|
||||
redisHost: "",
|
||||
redisPort: "",
|
||||
pagespeedApiKey: "",
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
const getSettings = async () => {
|
||||
const action = await dispatch(getAppSettings({ authToken }));
|
||||
if (action.payload.success) {
|
||||
setLocalSettings(action.payload.data);
|
||||
} else {
|
||||
createToast({ body: "Failed to get settings" });
|
||||
}
|
||||
};
|
||||
getSettings();
|
||||
}, [authToken, dispatch]);
|
||||
|
||||
const logItems = [
|
||||
{ _id: 1, name: "none" },
|
||||
{ _id: 2, name: "debug" },
|
||||
{ _id: 3, name: "error" },
|
||||
{ _id: 4, name: "warn" },
|
||||
];
|
||||
|
||||
const logItemLookup = {
|
||||
none: 1,
|
||||
debug: 2,
|
||||
error: 3,
|
||||
warn: 4,
|
||||
};
|
||||
|
||||
const handleLogLevel = (e) => {
|
||||
const id = e.target.value;
|
||||
const newLogLevel = logItems.find((item) => item._id === id).name;
|
||||
setLocalSettings({ ...localSettings, logLevel: newLogLevel });
|
||||
};
|
||||
|
||||
const handleChange = (event) => {
|
||||
const { value, id } = event.target;
|
||||
setLocalSettings({ ...localSettings, [id]: value });
|
||||
};
|
||||
|
||||
const handleSave = async () => {
|
||||
const action = await dispatch(
|
||||
updateAppSettings({ settings: localSettings, authToken })
|
||||
);
|
||||
let body = "";
|
||||
if (action.payload.success) {
|
||||
console.log(action.payload.data);
|
||||
setLocalSettings(action.payload.data);
|
||||
body = "Settings saved successfully";
|
||||
} else {
|
||||
body = "Failed to save settings";
|
||||
}
|
||||
createToast({ body });
|
||||
};
|
||||
|
||||
return (
|
||||
<Box
|
||||
className="settings"
|
||||
style={{
|
||||
paddingBottom: 0,
|
||||
}}
|
||||
>
|
||||
<Stack
|
||||
component="form"
|
||||
gap={theme.spacing(12)}
|
||||
noValidate
|
||||
spellCheck="false"
|
||||
>
|
||||
<ConfigBox>
|
||||
<Box>
|
||||
<Typography component="h1">Client settings</Typography>
|
||||
<Typography sx={{ mt: theme.spacing(2) }}>
|
||||
Modify client settings here.
|
||||
</Typography>
|
||||
</Box>
|
||||
<Stack gap={theme.spacing(20)}>
|
||||
<Field
|
||||
id="apiBaseUrl"
|
||||
label="API URL Host"
|
||||
value={localSettings.apiBaseUrl}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
<Select
|
||||
id="logLevel"
|
||||
label="Log level"
|
||||
name="logLevel"
|
||||
items={logItems}
|
||||
value={logItemLookup[localSettings.logLevel]}
|
||||
onChange={handleLogLevel}
|
||||
/>
|
||||
</Stack>
|
||||
</ConfigBox>
|
||||
<ConfigBox>
|
||||
<Box>
|
||||
<Typography component="h1">Email settings</Typography>
|
||||
<Typography sx={{ mt: theme.spacing(2) }}>
|
||||
Set your host email settings here. These settings are used for
|
||||
sending system emails.
|
||||
</Typography>
|
||||
</Box>
|
||||
<Stack gap={theme.spacing(20)}>
|
||||
<Field
|
||||
type="text"
|
||||
id="systemEmailHost"
|
||||
label="Email host"
|
||||
name="systemEmailHost"
|
||||
value={localSettings.systemEmailHost}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
<Field
|
||||
type="number"
|
||||
id="systemEmailPort"
|
||||
label="System email address"
|
||||
name="systemEmailPort"
|
||||
value={localSettings.systemEmailPort.toString()}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
<Field
|
||||
type="email"
|
||||
id="systemEmailAddress"
|
||||
label="System email address"
|
||||
name="systemEmailAddress"
|
||||
value={localSettings.systemEmailAddress}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
<Field
|
||||
type="text"
|
||||
id="systemEmailPassword"
|
||||
label="System email password"
|
||||
name="systemEmailPassword"
|
||||
value={localSettings.systemEmailPassword}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
</Stack>
|
||||
</ConfigBox>
|
||||
<ConfigBox>
|
||||
<Box>
|
||||
<Typography component="h1">Server settings</Typography>
|
||||
<Typography sx={{ mt: theme.spacing(2) }}>
|
||||
Modify server settings here.
|
||||
</Typography>
|
||||
</Box>
|
||||
<Stack gap={theme.spacing(20)}>
|
||||
<Field
|
||||
type="text"
|
||||
id="jwtTTL"
|
||||
label="JWT time to live"
|
||||
name="jwtTTL"
|
||||
value={localSettings.jwtTTL}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
<Field
|
||||
type="text"
|
||||
id="dbType"
|
||||
label="Database type"
|
||||
name="dbType"
|
||||
value={localSettings.dbType}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
<Field
|
||||
type="text"
|
||||
id="redisHost"
|
||||
label="Redis host"
|
||||
name="redisHost"
|
||||
value={localSettings.redisHost}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
<Field
|
||||
type="number"
|
||||
id="redisPort"
|
||||
label="Redis port"
|
||||
name="redisPort"
|
||||
value={localSettings.redisPort.toString()}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
<Field
|
||||
type="text"
|
||||
id="pagespeedApiKey"
|
||||
label="PageSpeed API key"
|
||||
name="pagespeedApiKey"
|
||||
value={localSettings.pagespeedApiKey}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
</Stack>
|
||||
</ConfigBox>
|
||||
<ConfigBox>
|
||||
<Box>
|
||||
<Typography component="h1">About</Typography>
|
||||
</Box>
|
||||
<Box>
|
||||
<Typography component="h2">BlueWave Uptime v1.0.0</Typography>
|
||||
<Typography
|
||||
sx={{ mt: theme.spacing(2), mb: theme.spacing(6), opacity: 0.6 }}
|
||||
>
|
||||
Developed by Bluewave Labs.
|
||||
</Typography>
|
||||
<Link
|
||||
level="secondary"
|
||||
url="https://github.com/bluewave-labs"
|
||||
label="https://github.com/bluewave-labs"
|
||||
/>
|
||||
</Box>
|
||||
</ConfigBox>
|
||||
<Stack direction="row" justifyContent="flex-end">
|
||||
<LoadingButton
|
||||
loading={settings.isLoading || settings.authIsLoading}
|
||||
variant="contained"
|
||||
color="primary"
|
||||
sx={{ px: theme.spacing(12), mt: theme.spacing(20) }}
|
||||
onClick={handleSave}
|
||||
>
|
||||
Save
|
||||
</LoadingButton>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
AdvancedSettings.propTypes = {
|
||||
isAdmin: PropTypes.bool,
|
||||
};
|
||||
export default AdvancedSettings;
|
||||
@@ -596,6 +596,7 @@ const Register = ({ isSuperAdmin }) => {
|
||||
let registerForm = {
|
||||
...form,
|
||||
role: isSuperAdmin ? ["superadmin"] : form.role,
|
||||
inviteToken: token ? token : "", // Add the token to the request for verification
|
||||
};
|
||||
let error = validateForm(registerForm, {
|
||||
context: { password: form.password },
|
||||
@@ -608,8 +609,8 @@ const Register = ({ isSuperAdmin }) => {
|
||||
delete registerForm.confirm;
|
||||
const action = await dispatch(register(registerForm));
|
||||
if (action.payload.success) {
|
||||
const token = action.payload.data;
|
||||
localStorage.setItem("token", token);
|
||||
const authToken = action.payload.data;
|
||||
localStorage.setItem("token", authToken);
|
||||
navigate("/");
|
||||
createToast({
|
||||
body: "Welcome! Your account was created successfully.",
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
.create-maintenance button {
|
||||
height: 35px;
|
||||
}
|
||||
@@ -0,0 +1,570 @@
|
||||
import { Box, Button, duration, Stack, Typography } from "@mui/material";
|
||||
import { useSelector } from "react-redux";
|
||||
import { useTheme } from "@emotion/react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { ConfigBox } from "./styled";
|
||||
import { AdapterDayjs } from "@mui/x-date-pickers/AdapterDayjs";
|
||||
import { LocalizationProvider } from "@mui/x-date-pickers";
|
||||
import { DatePicker } from "@mui/x-date-pickers/DatePicker";
|
||||
import { MobileTimePicker } from "@mui/x-date-pickers/MobileTimePicker";
|
||||
import { maintenanceWindowValidation } from "../../../Validation/validation";
|
||||
import { createToast } from "../../../Utils/toastUtils";
|
||||
import LoadingButton from "@mui/lab/LoadingButton";
|
||||
|
||||
import dayjs from "dayjs";
|
||||
import Select from "../../../Components/Inputs/Select";
|
||||
import Field from "../../../Components/Inputs/Field";
|
||||
import Breadcrumbs from "../../../Components/Breadcrumbs";
|
||||
import CalendarIcon from "../../../assets/icons/calendar.svg?react";
|
||||
import "./index.css";
|
||||
import Search from "../../../Components/Inputs/Search";
|
||||
import { networkService } from "../../../main";
|
||||
import { logger } from "../../../Utils/Logger";
|
||||
import {
|
||||
MS_PER_SECOND,
|
||||
MS_PER_MINUTE,
|
||||
MS_PER_HOUR,
|
||||
MS_PER_DAY,
|
||||
MS_PER_WEEK,
|
||||
} from "../../../Utils/timeUtils";
|
||||
import { useNavigate, useParams } from "react-router-dom";
|
||||
|
||||
const getDurationAndUnit = (durationInMs) => {
|
||||
if (durationInMs % MS_PER_DAY === 0) {
|
||||
return {
|
||||
duration: (durationInMs / MS_PER_DAY).toString(),
|
||||
durationUnit: "days",
|
||||
};
|
||||
} else if (durationInMs % MS_PER_HOUR === 0) {
|
||||
return {
|
||||
duration: (durationInMs / MS_PER_HOUR).toString(),
|
||||
durationUnit: "hours",
|
||||
};
|
||||
} else if (durationInMs % MS_PER_MINUTE === 0) {
|
||||
return {
|
||||
duration: (durationInMs / MS_PER_MINUTE).toString(),
|
||||
durationUnit: "minutes",
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
duration: (durationInMs / MS_PER_SECOND).toString(),
|
||||
durationUnit: "seconds",
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
const MS_LOOKUP = {
|
||||
seconds: MS_PER_SECOND,
|
||||
minutes: MS_PER_MINUTE,
|
||||
hours: MS_PER_HOUR,
|
||||
days: MS_PER_DAY,
|
||||
weeks: MS_PER_WEEK,
|
||||
};
|
||||
|
||||
const REPEAT_LOOKUP = {
|
||||
none: 0,
|
||||
daily: MS_PER_DAY,
|
||||
weekly: MS_PER_DAY * 7,
|
||||
};
|
||||
|
||||
const REVERSE_REPEAT_LOOKUP = {
|
||||
0: "none",
|
||||
[MS_PER_DAY]: "daily",
|
||||
[MS_PER_WEEK]: "weekly",
|
||||
};
|
||||
|
||||
const repeatConfig = [
|
||||
{ _id: 0, name: "Don't repeat", value: "none" },
|
||||
{
|
||||
_id: 1,
|
||||
name: "Repeat daily",
|
||||
value: "daily",
|
||||
},
|
||||
{ _id: 2, name: "Repeat weekly", value: "weekly" },
|
||||
];
|
||||
|
||||
const durationConfig = [
|
||||
{ _id: 0, name: "seconds" },
|
||||
{ _id: 1, name: "minutes" },
|
||||
{ _id: 2, name: "hours" },
|
||||
{
|
||||
_id: 3,
|
||||
name: "days",
|
||||
},
|
||||
];
|
||||
|
||||
const getValueById = (config, id) => {
|
||||
const item = config.find((config) => config._id === id);
|
||||
return item ? (item.value ? item.value : item.name) : null;
|
||||
};
|
||||
|
||||
const getIdByValue = (config, name) => {
|
||||
const item = config.find((config) => {
|
||||
if (config.value) {
|
||||
return config.value === name;
|
||||
} else {
|
||||
return config.name === name;
|
||||
}
|
||||
});
|
||||
return item ? item._id : null;
|
||||
};
|
||||
|
||||
const CreateMaintenance = () => {
|
||||
const { maintenanceWindowId } = useParams();
|
||||
const navigate = useNavigate();
|
||||
const theme = useTheme();
|
||||
const { user, authToken } = useSelector((state) => state.auth);
|
||||
const [monitors, setMonitors] = useState([]);
|
||||
const [search, setSearch] = useState("");
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [form, setForm] = useState({
|
||||
repeat: "none",
|
||||
startDate: dayjs(),
|
||||
startTime: dayjs(),
|
||||
duration: "",
|
||||
durationUnit: "seconds",
|
||||
name: "",
|
||||
monitors: [],
|
||||
});
|
||||
const [errors, setErrors] = useState({});
|
||||
|
||||
useEffect(() => {
|
||||
const fetchMonitors = async () => {
|
||||
setIsLoading(true);
|
||||
try {
|
||||
const response = await networkService.getMonitorsByTeamId({
|
||||
authToken: authToken,
|
||||
teamId: user.teamId,
|
||||
limit: -1,
|
||||
types: ["http", "ping", "pagespeed"],
|
||||
});
|
||||
const monitors = response.data.data.monitors;
|
||||
setMonitors(monitors);
|
||||
|
||||
if (maintenanceWindowId === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
const res = await networkService.getMaintenanceWindowById({
|
||||
authToken: authToken,
|
||||
maintenanceWindowId: maintenanceWindowId,
|
||||
});
|
||||
const maintenanceWindow = res.data.data;
|
||||
const { name, start, end, repeat, monitorId } = maintenanceWindow;
|
||||
const startTime = dayjs(start);
|
||||
const endTime = dayjs(end);
|
||||
const durationInMs = endTime.diff(startTime, "milliseconds").toString();
|
||||
const { duration, durationUnit } = getDurationAndUnit(durationInMs);
|
||||
const monitor = monitors.find((monitor) => monitor._id === monitorId);
|
||||
setForm({
|
||||
...form,
|
||||
name,
|
||||
repeat: REVERSE_REPEAT_LOOKUP[repeat],
|
||||
startDate: startTime,
|
||||
startTime,
|
||||
duration,
|
||||
durationUnit,
|
||||
monitors: monitor ? [monitor] : [],
|
||||
});
|
||||
} catch (error) {
|
||||
createToast({ body: "Failed to fetch data" });
|
||||
logger.error("Failed to fetch monitors", error);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
fetchMonitors();
|
||||
}, [authToken, user]);
|
||||
|
||||
const buildErrors = (prev, id, error) => {
|
||||
const updatedErrors = { ...prev };
|
||||
if (error) {
|
||||
updatedErrors[id] = error.details[0].message;
|
||||
} else {
|
||||
delete updatedErrors[id];
|
||||
}
|
||||
return updatedErrors;
|
||||
};
|
||||
|
||||
const handleSearch = (value) => {
|
||||
setSearch(value);
|
||||
};
|
||||
|
||||
const handleSelectMonitors = (monitors) => {
|
||||
setForm({ ...form, monitors });
|
||||
const { error } = maintenanceWindowValidation.validate(
|
||||
{ monitors },
|
||||
{ abortEarly: false }
|
||||
);
|
||||
setErrors((prev) => {
|
||||
return buildErrors(prev, "monitors", error);
|
||||
});
|
||||
};
|
||||
|
||||
const handleFormChange = (key, value) => {
|
||||
setForm({ ...form, [key]: value });
|
||||
const { error } = maintenanceWindowValidation.validate(
|
||||
{ [key]: value },
|
||||
{ abortEarly: false }
|
||||
);
|
||||
setErrors((prev) => {
|
||||
return buildErrors(prev, key, error);
|
||||
});
|
||||
};
|
||||
|
||||
const handleTimeChange = (key, newTime) => {
|
||||
setForm({ ...form, [key]: newTime });
|
||||
const { error } = maintenanceWindowValidation.validate(
|
||||
{ [key]: newTime },
|
||||
{ abortEarly: false }
|
||||
);
|
||||
setErrors((prev) => {
|
||||
return buildErrors(prev, key, error);
|
||||
});
|
||||
};
|
||||
|
||||
const handleSubmit = async () => {
|
||||
const { error } = maintenanceWindowValidation.validate(form, {
|
||||
abortEarly: false,
|
||||
});
|
||||
|
||||
// If errors, return early
|
||||
if (error) {
|
||||
const newErrors = {};
|
||||
error.details.forEach((err) => {
|
||||
newErrors[err.path[0]] = err.message;
|
||||
});
|
||||
setErrors(newErrors);
|
||||
logger.error(error);
|
||||
return;
|
||||
}
|
||||
// Build timestamp for maintenance window from startDate and startTime
|
||||
const start = dayjs(form.startDate)
|
||||
.set("hour", form.startTime.hour())
|
||||
.set("minute", form.startTime.minute());
|
||||
// Build end timestamp for maintenance window
|
||||
const MS_MULTIPLIER = MS_LOOKUP[form.durationUnit];
|
||||
const durationInMs = form.duration * MS_MULTIPLIER;
|
||||
const end = start.add(durationInMs);
|
||||
|
||||
// Get repeat value in milliseconds
|
||||
const repeat = REPEAT_LOOKUP[form.repeat];
|
||||
|
||||
const submit = {
|
||||
monitors: form.monitors.map((monitor) => monitor._id),
|
||||
name: form.name,
|
||||
start: start.toISOString(),
|
||||
end: end.toISOString(),
|
||||
repeat,
|
||||
};
|
||||
|
||||
if (repeat === 0) {
|
||||
submit.expiry = end;
|
||||
}
|
||||
|
||||
const requestConfig = { authToken: authToken, maintenanceWindow: submit };
|
||||
|
||||
if (maintenanceWindowId !== undefined) {
|
||||
requestConfig.maintenanceWindowId = maintenanceWindowId;
|
||||
}
|
||||
const request =
|
||||
maintenanceWindowId === undefined
|
||||
? networkService.createMaintenanceWindow(requestConfig)
|
||||
: networkService.editMaintenanceWindow(requestConfig);
|
||||
|
||||
try {
|
||||
setIsLoading(true);
|
||||
await request;
|
||||
createToast({
|
||||
body: "Successfully created maintenance window",
|
||||
});
|
||||
navigate("/maintenance");
|
||||
} catch (error) {
|
||||
createToast({
|
||||
body: `Failed to ${
|
||||
maintenanceWindowId === undefined ? "create" : "edit"
|
||||
} maintenance window`,
|
||||
});
|
||||
logger.error(error);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Box className="create-maintenance">
|
||||
<Breadcrumbs
|
||||
list={[
|
||||
{ name: "maintenance", path: "/maintenance" },
|
||||
{
|
||||
name: maintenanceWindowId === undefined ? "create" : "edit",
|
||||
path: `/maintenance/create`,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
<Stack
|
||||
component="form"
|
||||
noValidate
|
||||
spellCheck="false"
|
||||
gap={theme.spacing(12)}
|
||||
mt={theme.spacing(6)}
|
||||
>
|
||||
<Box>
|
||||
<Typography component="h1" variant="h1">
|
||||
<Typography component="span" fontSize="inherit">
|
||||
{`${maintenanceWindowId === undefined ? "Create a" : "Edit"}`}{" "}
|
||||
</Typography>
|
||||
<Typography
|
||||
component="span"
|
||||
variant="h2"
|
||||
fontSize="inherit"
|
||||
fontWeight="inherit"
|
||||
>
|
||||
maintenance{" "}
|
||||
</Typography>
|
||||
<Typography component="span" fontSize="inherit">
|
||||
window
|
||||
</Typography>
|
||||
</Typography>
|
||||
<Typography component="h2" variant="body2" fontSize={14}>
|
||||
Your pings won't be sent during this time frame
|
||||
</Typography>
|
||||
</Box>
|
||||
<ConfigBox direction="row">
|
||||
<Typography component="h2" variant="h2">
|
||||
General Settings
|
||||
</Typography>
|
||||
<Box
|
||||
px={theme.spacing(14)}
|
||||
borderLeft={1}
|
||||
borderLeftColor={theme.palette.border.light}
|
||||
>
|
||||
<Select
|
||||
id="repeat"
|
||||
name="maintenance-repeat"
|
||||
label="Maintenance Repeat"
|
||||
value={getIdByValue(repeatConfig, form.repeat)}
|
||||
onChange={(event) => {
|
||||
handleFormChange(
|
||||
"repeat",
|
||||
getValueById(repeatConfig, event.target.value)
|
||||
);
|
||||
}}
|
||||
items={repeatConfig}
|
||||
/>
|
||||
<Stack gap={theme.spacing(2)} mt={theme.spacing(16)}>
|
||||
<Typography component="h3">Date</Typography>
|
||||
<LocalizationProvider dateAdapter={AdapterDayjs}>
|
||||
<DatePicker
|
||||
id="startDate"
|
||||
disablePast
|
||||
disableHighlightToday
|
||||
value={form.startDate}
|
||||
slots={{ openPickerIcon: CalendarIcon }}
|
||||
slotProps={{
|
||||
switchViewButton: { sx: { display: "none" } },
|
||||
nextIconButton: { sx: { ml: theme.spacing(2) } },
|
||||
field: {
|
||||
sx: {
|
||||
width: "fit-content",
|
||||
"& > .MuiOutlinedInput-root": {
|
||||
flexDirection: "row-reverse",
|
||||
},
|
||||
"& input": {
|
||||
height: 34,
|
||||
p: 0,
|
||||
pr: theme.spacing(5),
|
||||
},
|
||||
"& fieldset": {
|
||||
borderColor: theme.palette.border.dark,
|
||||
borderRadius: theme.shape.borderRadius,
|
||||
},
|
||||
"&:not(:has(.Mui-disabled)):not(:has(.Mui-error)) .MuiOutlinedInput-root:not(:has(input:focus)):hover fieldset":
|
||||
{
|
||||
borderColor: theme.palette.border.dark,
|
||||
},
|
||||
},
|
||||
},
|
||||
inputAdornment: { sx: { ml: 0, px: 3 } },
|
||||
openPickerButton: {
|
||||
sx: {
|
||||
py: 0,
|
||||
mr: 0,
|
||||
"& path": {
|
||||
stroke: theme.palette.other.icon,
|
||||
strokeWidth: 1.1,
|
||||
},
|
||||
"&:hover": { backgroundColor: "transparent" },
|
||||
},
|
||||
},
|
||||
}}
|
||||
sx={{}}
|
||||
onChange={(newDate) => {
|
||||
handleTimeChange("startDate", newDate);
|
||||
}}
|
||||
error={errors["startDate"]}
|
||||
/>
|
||||
</LocalizationProvider>
|
||||
</Stack>
|
||||
</Box>
|
||||
</ConfigBox>
|
||||
<ConfigBox>
|
||||
<Stack direction="row">
|
||||
<Box>
|
||||
<Typography component="h2" variant="h2">
|
||||
Start time
|
||||
</Typography>
|
||||
<Typography>
|
||||
All dates and times are in GMT+0 time zone.
|
||||
</Typography>
|
||||
</Box>
|
||||
<Stack direction="row">
|
||||
<LocalizationProvider dateAdapter={AdapterDayjs}>
|
||||
<MobileTimePicker
|
||||
id="startTime"
|
||||
value={form.startTime}
|
||||
onChange={(newTime) => {
|
||||
handleTimeChange("startTime", newTime);
|
||||
}}
|
||||
slotProps={{
|
||||
nextIconButton: { sx: { ml: theme.spacing(2) } },
|
||||
field: {
|
||||
sx: {
|
||||
width: "fit-content",
|
||||
"& > .MuiOutlinedInput-root": {
|
||||
flexDirection: "row-reverse",
|
||||
},
|
||||
"& input": {
|
||||
height: 34,
|
||||
p: 0,
|
||||
pl: theme.spacing(5),
|
||||
},
|
||||
"& fieldset": {
|
||||
borderColor: theme.palette.border.dark,
|
||||
borderRadius: theme.shape.borderRadius,
|
||||
},
|
||||
"&:not(:has(.Mui-disabled)):not(:has(.Mui-error)) .MuiOutlinedInput-root:not(:has(input:focus)):hover fieldset":
|
||||
{
|
||||
borderColor: theme.palette.border.dark,
|
||||
},
|
||||
},
|
||||
},
|
||||
}}
|
||||
error={errors["startTime"]}
|
||||
/>
|
||||
</LocalizationProvider>
|
||||
</Stack>
|
||||
</Stack>
|
||||
<Stack direction="row">
|
||||
<Box>
|
||||
<Typography component="h2" variant="h2">
|
||||
Duration
|
||||
</Typography>
|
||||
</Box>
|
||||
<Stack direction="row" spacing={theme.spacing(8)}>
|
||||
<Field
|
||||
type="number"
|
||||
id="duration"
|
||||
value={form.duration}
|
||||
onChange={(event) => {
|
||||
handleFormChange("duration", event.target.value);
|
||||
}}
|
||||
error={errors["duration"]}
|
||||
/>
|
||||
<Select
|
||||
id="durationUnit"
|
||||
value={getIdByValue(durationConfig, form.durationUnit)}
|
||||
items={durationConfig}
|
||||
onChange={(event) => {
|
||||
handleFormChange(
|
||||
"durationUnit",
|
||||
getValueById(durationConfig, event.target.value)
|
||||
);
|
||||
}}
|
||||
error={errors["durationUnit"]}
|
||||
/>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</ConfigBox>
|
||||
<Box>
|
||||
<Typography
|
||||
component="h2"
|
||||
variant="h2"
|
||||
fontSize={16}
|
||||
my={theme.spacing(6)}
|
||||
>
|
||||
Monitor related settings
|
||||
</Typography>
|
||||
<ConfigBox>
|
||||
<Stack direction="row">
|
||||
<Box>
|
||||
<Typography component="h2" variant="h2">
|
||||
Friendly name
|
||||
</Typography>
|
||||
</Box>
|
||||
<Box>
|
||||
<Field
|
||||
id="name"
|
||||
placeholder="Maintenance at __ : __ for ___ minutes"
|
||||
value={form.name}
|
||||
onChange={(event) => {
|
||||
handleFormChange("name", event.target.value);
|
||||
}}
|
||||
error={errors["name"]}
|
||||
/>
|
||||
</Box>
|
||||
</Stack>
|
||||
<Stack direction="row">
|
||||
<Box>
|
||||
<Typography component="h2" variant="h2">
|
||||
Add monitors
|
||||
</Typography>
|
||||
</Box>
|
||||
<Box>
|
||||
<Search
|
||||
id={"monitors"}
|
||||
multiple={true}
|
||||
isAdorned={false}
|
||||
options={monitors ? monitors : []}
|
||||
filteredBy="name"
|
||||
secondaryLabel={"type"}
|
||||
inputValue={search}
|
||||
value={form.monitors}
|
||||
handleInputChange={handleSearch}
|
||||
handleChange={handleSelectMonitors}
|
||||
error={errors["monitors"]}
|
||||
disabled={maintenanceWindowId !== undefined}
|
||||
/>
|
||||
</Box>
|
||||
</Stack>
|
||||
</ConfigBox>
|
||||
</Box>
|
||||
<Box ml="auto" display="inline-block">
|
||||
<Button
|
||||
variant="contained"
|
||||
color="secondary"
|
||||
onClick={() => navigate("/maintenance")}
|
||||
sx={{ mr: theme.spacing(6) }}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<LoadingButton
|
||||
loading={isLoading}
|
||||
variant="contained"
|
||||
color="primary"
|
||||
onClick={handleSubmit}
|
||||
disabled={false}
|
||||
>
|
||||
{`${
|
||||
maintenanceWindowId === undefined
|
||||
? "Create maintenance"
|
||||
: "Edit maintenance"
|
||||
}`}
|
||||
</LoadingButton>
|
||||
</Box>
|
||||
</Stack>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default CreateMaintenance;
|
||||
@@ -0,0 +1,25 @@
|
||||
import { Stack, styled } from "@mui/material";
|
||||
|
||||
export const ConfigBox = styled(Stack)(({ theme }) => ({
|
||||
border: 1,
|
||||
borderStyle: "solid",
|
||||
borderColor: theme.palette.border.light,
|
||||
borderRadius: theme.spacing(2),
|
||||
backgroundColor: theme.palette.background.main,
|
||||
"& > *": { padding: theme.spacing(14) },
|
||||
"& > :first-of-type, & > .MuiStack-root > div:first-of-type": {
|
||||
flex: 0.6,
|
||||
},
|
||||
"& > div:last-of-type, & > .MuiStack-root > div:last-of-type": {
|
||||
flex: 1,
|
||||
},
|
||||
"& > .MuiStack-root > div:first-of-type": { paddingRight: theme.spacing(14) },
|
||||
"& > .MuiStack-root > div:last-of-type": {
|
||||
paddingLeft: theme.spacing(14),
|
||||
},
|
||||
"& h2": { fontSize: 13.5, fontWeight: 500 },
|
||||
"& h3, & p": {
|
||||
color: theme.palette.text.tertiary,
|
||||
},
|
||||
"& h3": { fontWeight: 500 },
|
||||
}));
|
||||
@@ -1,71 +0,0 @@
|
||||
.maintenance-options .MuiStack-root .MuiStack-root.select-wrapper,
|
||||
.maintenance-options .MuiStack-root .field {
|
||||
width: 60%;
|
||||
max-width: 380px;
|
||||
}
|
||||
|
||||
.date-picker .MuiOutlinedInput-root,
|
||||
.time-picker .MuiOutlinedInput-root {
|
||||
height: 34px;
|
||||
font-size: var(--env-var-font-size-medium);
|
||||
}
|
||||
|
||||
.date-picker .MuiOutlinedInput-root {
|
||||
width: 140px;
|
||||
}
|
||||
|
||||
.time-picker .MuiOutlinedInput-root {
|
||||
width: 90px;
|
||||
}
|
||||
|
||||
.maintenance-options .MuiInputAdornment-outlined .MuiIconButton-root svg {
|
||||
width: 20px;
|
||||
}
|
||||
|
||||
.duration-config .field-text .MuiTextField-root,
|
||||
.maintenance-options .duration-config .field {
|
||||
width: 70px;
|
||||
min-width: 70px;
|
||||
max-width: 70px;
|
||||
}
|
||||
|
||||
.duration-config > .MuiStack-root.select-wrapper {
|
||||
width: 100px;
|
||||
}
|
||||
|
||||
.duration-config
|
||||
> .MuiStack-root.select-wrapper
|
||||
> .MuiInputBase-root:has(> .MuiSelect-select) {
|
||||
width: 100px;
|
||||
min-width: 100px;
|
||||
}
|
||||
|
||||
.duration-config > .select-wrapper .select-component > .MuiSelect-select {
|
||||
text-transform: none;
|
||||
}
|
||||
|
||||
.maintenance-options .add-monitors-fields .field.field-text {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.MuiPopover-paper.MuiMenu-paper.select-dropdown
|
||||
.MuiMenu-list
|
||||
.MuiButtonBase-root {
|
||||
text-transform: none;
|
||||
}
|
||||
|
||||
.date-picker
|
||||
.MuiOutlinedInput-root
|
||||
.MuiInputAdornment-outlined
|
||||
.MuiIconButton-root {
|
||||
outline: none;
|
||||
|
||||
&:hover {
|
||||
background-color: transparent;
|
||||
}
|
||||
}
|
||||
|
||||
.create-maintenance-window .MuiStack-root .MuiStack-root > button {
|
||||
width: 110px;
|
||||
height: 34px;
|
||||
}
|
||||
@@ -1,298 +0,0 @@
|
||||
import { Box, Button, Stack, Typography } from "@mui/material";
|
||||
import "./index.css";
|
||||
import { useState } from "react";
|
||||
import Back from "../../../assets/icons/left-arrow-long.svg?react";
|
||||
import Select from "../../../Components/Inputs/Select";
|
||||
import { AdapterDayjs } from "@mui/x-date-pickers/AdapterDayjs";
|
||||
import { LocalizationProvider } from "@mui/x-date-pickers/LocalizationProvider";
|
||||
import dayjs from "dayjs";
|
||||
import { DatePicker } from "@mui/x-date-pickers/DatePicker";
|
||||
import { MobileTimePicker } from "@mui/x-date-pickers/MobileTimePicker";
|
||||
import Field from "../../../Components/Inputs/Field";
|
||||
import { maintenanceWindowValidation } from "../../../Validation/validation";
|
||||
import { logger } from "../../../Utils/Logger";
|
||||
import { createToast } from "../../../Utils/toastUtils";
|
||||
import { useTheme } from "@emotion/react";
|
||||
|
||||
const directory = {
|
||||
title: "Create a maintenance window",
|
||||
description: "Your pings won’t be sent in this time frame.",
|
||||
};
|
||||
|
||||
const repeatOptions = [
|
||||
{ _id: 1, name: "Don't repeat" },
|
||||
{ _id: 2, name: "Repeat daily" },
|
||||
{ _id: 3, name: "Repeat weekly" },
|
||||
];
|
||||
|
||||
const configOptionTitle = (title, description) => {
|
||||
return (
|
||||
<Stack width="40%" gap={1}>
|
||||
<Typography
|
||||
style={{
|
||||
fontWeight: 600,
|
||||
fontSize: "var(--env-var-font-size-medium)",
|
||||
}}
|
||||
>
|
||||
{title}
|
||||
</Typography>
|
||||
{description && (
|
||||
<Typography
|
||||
style={{
|
||||
fontSize: "var(--env-var-font-size-small)",
|
||||
}}
|
||||
>
|
||||
{description}
|
||||
</Typography>
|
||||
)}
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
const durationOptions = [
|
||||
{ _id: "minutes", name: "minutes" },
|
||||
{ _id: "hours", name: "hours" },
|
||||
{ _id: "days", name: "days" },
|
||||
];
|
||||
|
||||
const CreateNewMaintenanceWindow = () => {
|
||||
const theme = useTheme();
|
||||
|
||||
const [values, setValues] = useState({
|
||||
repeat: 1,
|
||||
date: dayjs(),
|
||||
startTime: dayjs(),
|
||||
duration: "60",
|
||||
unit: "minutes",
|
||||
displayName: "",
|
||||
AddMonitors: "",
|
||||
});
|
||||
const [errors, setErrors] = useState({});
|
||||
|
||||
const handleChange = (event, name) => {
|
||||
const { value } = event.target;
|
||||
setValues((prev) => ({
|
||||
...prev,
|
||||
[name]: value,
|
||||
}));
|
||||
};
|
||||
|
||||
const handleSubmit = async () => {
|
||||
const data = {
|
||||
repeat: values.repeat,
|
||||
date: values.date.format("YYYY-MM-DD"),
|
||||
startTime: values.startTime.format("HH:mm"),
|
||||
duration: values.duration,
|
||||
unit: values.unit,
|
||||
displayName: values.displayName,
|
||||
addMonitors: values.AddMonitors,
|
||||
};
|
||||
|
||||
const { error } = maintenanceWindowValidation.validate(data, {
|
||||
abortEarly: false,
|
||||
});
|
||||
logger.log("error: ", error);
|
||||
if (!error || error.details.length === 0) {
|
||||
setErrors({});
|
||||
} else {
|
||||
const newErrors = {};
|
||||
error.details.forEach((err) => {
|
||||
newErrors[err.path[0]] = err.message;
|
||||
});
|
||||
setErrors(newErrors);
|
||||
createToast({
|
||||
body:
|
||||
error.details && error.details.length > 0
|
||||
? error.details[0].message
|
||||
: "Error validating data",
|
||||
});
|
||||
logger.error("Validation errors:", error.details);
|
||||
}
|
||||
|
||||
logger.log("Submitting data: ", data);
|
||||
};
|
||||
|
||||
const configOptions = [
|
||||
{
|
||||
title: "Repeat",
|
||||
component: (
|
||||
<Select
|
||||
onChange={(e) => handleChange(e, "repeat")}
|
||||
id="repeat-mode"
|
||||
items={repeatOptions}
|
||||
value={values.repeat}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: "Date",
|
||||
component: (
|
||||
<LocalizationProvider
|
||||
className="date-localization-provider"
|
||||
dateAdapter={AdapterDayjs}
|
||||
>
|
||||
<DatePicker
|
||||
className="date-picker"
|
||||
defaultValue={values.date}
|
||||
onChange={(date) =>
|
||||
handleChange({ target: { value: date } }, "date")
|
||||
}
|
||||
/>
|
||||
</LocalizationProvider>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: "Start time",
|
||||
component: (
|
||||
<LocalizationProvider
|
||||
className="time-localization-provider"
|
||||
dateAdapter={AdapterDayjs}
|
||||
>
|
||||
<MobileTimePicker
|
||||
className="time-picker"
|
||||
defaultValue={values.startTime}
|
||||
onChange={(time) =>
|
||||
handleChange({ target: { value: time } }, "startTime")
|
||||
}
|
||||
/>
|
||||
</LocalizationProvider>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: "Duration",
|
||||
component: (
|
||||
<Stack
|
||||
className="duration-config"
|
||||
gap={theme.spacing(5)}
|
||||
direction="row"
|
||||
>
|
||||
<Field
|
||||
id="duration-value"
|
||||
placeholder="60"
|
||||
onChange={(e) => handleChange(e, "duration")}
|
||||
value={values.duration}
|
||||
error={errors.duration}
|
||||
type="number"
|
||||
/>
|
||||
<Select
|
||||
onChange={(e) => handleChange(e, "unit")}
|
||||
id="duration-unit"
|
||||
items={durationOptions}
|
||||
value={values.unit}
|
||||
/>
|
||||
</Stack>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: "Display name",
|
||||
component: (
|
||||
<Field
|
||||
id="display-name"
|
||||
placeholder="Maintanence at __ : __ for ___ minutes"
|
||||
value={values.displayName}
|
||||
onChange={(e) => handleChange(e, "displayName")}
|
||||
error={errors.displayName}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: "Add monitors",
|
||||
component: (
|
||||
<Stack
|
||||
className="add-monitors-fields"
|
||||
sx={{ width: "60%", maxWidth: "380px" }}
|
||||
gap={theme.spacing(5)}
|
||||
>
|
||||
<Field
|
||||
id="add-monitors"
|
||||
placeholder="Start typing to search for current monitors"
|
||||
value={values.AddMonitors}
|
||||
onChange={(e) => handleChange(e, "AddMonitors")}
|
||||
error={errors.addMonitors}
|
||||
/>
|
||||
<Typography
|
||||
sx={{
|
||||
width: "fit-content",
|
||||
fontSize: "var(--env-var-font-size-small)",
|
||||
borderBottom: `1px dashed ${theme.palette.primary.main}`,
|
||||
paddingBottom: "4px",
|
||||
}}
|
||||
>
|
||||
Add all monitors to this maintenance window
|
||||
</Typography>
|
||||
</Stack>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="create-maintenance-window">
|
||||
<Stack gap={theme.spacing(10)}>
|
||||
<Button
|
||||
variant="contained"
|
||||
color="secondary"
|
||||
sx={{
|
||||
width: "100px",
|
||||
height: "30px",
|
||||
gap: "10px",
|
||||
}}
|
||||
>
|
||||
<Back />
|
||||
Back
|
||||
</Button>
|
||||
<Box>
|
||||
<Typography
|
||||
sx={{
|
||||
fontSize: "var(--env-var-font-size-large)",
|
||||
fontWeight: 600,
|
||||
color: theme.palette.text.secondary,
|
||||
}}
|
||||
>
|
||||
{directory.title}
|
||||
</Typography>
|
||||
|
||||
<Typography sx={{ fontSize: "var(--env-var-font-size-medium)" }}>
|
||||
{directory.description}
|
||||
</Typography>
|
||||
</Box>
|
||||
<Stack
|
||||
className="maintenance-options"
|
||||
gap={theme.spacing(20)}
|
||||
paddingY={theme.spacing(15)}
|
||||
paddingX={theme.spacing(20)}
|
||||
sx={{
|
||||
border: 1,
|
||||
borderColor: theme.palette.border.light,
|
||||
borderRadius: theme.shape.borderRadius,
|
||||
backgroundColor: theme.palette.background.main,
|
||||
}}
|
||||
>
|
||||
{configOptions.map((item, index) => (
|
||||
<Stack key={index} display="-webkit-inline-box">
|
||||
{item.title && configOptionTitle(item.title, item.description)}
|
||||
{item.component && item.component}
|
||||
</Stack>
|
||||
))}
|
||||
</Stack>
|
||||
<Stack justifyContent="end" direction="row" marginTop={3}>
|
||||
<Button
|
||||
variant="text"
|
||||
color="info"
|
||||
sx={{
|
||||
"&:hover": {
|
||||
backgroundColor: "transparent",
|
||||
},
|
||||
}}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button variant="contained" color="primary" onClick={handleSubmit}>
|
||||
Create
|
||||
</Button>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default CreateNewMaintenanceWindow;
|
||||
@@ -0,0 +1,229 @@
|
||||
import { useState } from "react";
|
||||
import { useTheme } from "@emotion/react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { useSelector } from "react-redux";
|
||||
import {
|
||||
Button,
|
||||
IconButton,
|
||||
Menu,
|
||||
MenuItem,
|
||||
Modal,
|
||||
Stack,
|
||||
Typography,
|
||||
} from "@mui/material";
|
||||
import LoadingButton from "@mui/lab/LoadingButton";
|
||||
import { logger } from "../../../../Utils/Logger";
|
||||
import Settings from "../../../../assets/icons/settings-bold.svg?react";
|
||||
import PropTypes from "prop-types";
|
||||
import { networkService } from "../../../../main";
|
||||
import { createToast } from "../../../../Utils/toastUtils";
|
||||
|
||||
const ActionsMenu = ({ isAdmin, maintenanceWindow, updateCallback }) => {
|
||||
maintenanceWindow;
|
||||
const { authToken } = useSelector((state) => state.auth);
|
||||
const [anchorEl, setAnchorEl] = useState(null);
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
const theme = useTheme();
|
||||
|
||||
const handleRemove = async (event) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
try {
|
||||
setIsLoading(true);
|
||||
await networkService.deleteMaintenanceWindow({
|
||||
authToken,
|
||||
maintenanceWindowId: maintenanceWindow._id,
|
||||
});
|
||||
updateCallback();
|
||||
createToast({ body: "Maintenance window deleted successfully." });
|
||||
} catch (error) {
|
||||
createToast({ body: "Failed to delete maintenance window." });
|
||||
logger.error("Failed to delete maintenance window", error);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
setIsOpen(false);
|
||||
};
|
||||
|
||||
const handlePause = async () => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
const data = {
|
||||
active: !maintenanceWindow.active,
|
||||
};
|
||||
await networkService.editMaintenanceWindow({
|
||||
authToken,
|
||||
maintenanceWindowId: maintenanceWindow._id,
|
||||
maintenanceWindow: data,
|
||||
});
|
||||
updateCallback();
|
||||
} catch (error) {
|
||||
logger.error(error);
|
||||
createToast({ body: "Failed to pause maintenance window." });
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleEdit = () => {
|
||||
navigate(`/maintenance/create/${maintenanceWindow._id}`);
|
||||
};
|
||||
|
||||
const openMenu = (event) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
setAnchorEl(event.currentTarget);
|
||||
};
|
||||
|
||||
const openRemove = (e) => {
|
||||
closeMenu(e);
|
||||
setIsOpen(true);
|
||||
};
|
||||
|
||||
const closeMenu = (e) => {
|
||||
e.stopPropagation();
|
||||
setAnchorEl(null);
|
||||
};
|
||||
|
||||
const navigate = useNavigate();
|
||||
|
||||
return (
|
||||
<>
|
||||
<IconButton
|
||||
aria-label="monitor actions"
|
||||
onClick={(event) => {
|
||||
event.stopPropagation();
|
||||
openMenu(event);
|
||||
}}
|
||||
sx={{
|
||||
"&:focus": {
|
||||
outline: "none",
|
||||
},
|
||||
"& svg path": {
|
||||
stroke: theme.palette.other.icon,
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Settings />
|
||||
</IconButton>
|
||||
|
||||
<Menu
|
||||
className="actions-menu"
|
||||
anchorEl={anchorEl}
|
||||
open={Boolean(anchorEl)}
|
||||
onClose={(e) => closeMenu(e)}
|
||||
disableScrollLock
|
||||
slotProps={{
|
||||
paper: {
|
||||
sx: {
|
||||
"& ul": { p: theme.spacing(2.5) },
|
||||
"& li": { m: 0 },
|
||||
"& li:last-of-type": {
|
||||
color: theme.palette.error.text,
|
||||
},
|
||||
},
|
||||
},
|
||||
}}
|
||||
>
|
||||
<MenuItem
|
||||
onClick={(e) => {
|
||||
closeMenu(e);
|
||||
e.stopPropagation();
|
||||
handleEdit();
|
||||
}}
|
||||
>
|
||||
Edit
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
onClick={(e) => {
|
||||
handlePause();
|
||||
closeMenu(e);
|
||||
e.stopPropagation();
|
||||
}}
|
||||
>
|
||||
{`${maintenanceWindow.active === true ? "Pause" : "Resume"}`}
|
||||
</MenuItem>
|
||||
|
||||
<MenuItem
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
openRemove(e);
|
||||
}}
|
||||
>
|
||||
Remove
|
||||
</MenuItem>
|
||||
</Menu>
|
||||
<Modal
|
||||
aria-labelledby="modal-delete-monitor"
|
||||
aria-describedby="delete-monitor-confirmation"
|
||||
open={isOpen}
|
||||
onClose={(e) => {
|
||||
e.stopPropagation();
|
||||
setIsOpen(false);
|
||||
}}
|
||||
>
|
||||
<Stack
|
||||
gap={theme.spacing(2)}
|
||||
sx={{
|
||||
position: "absolute",
|
||||
top: "50%",
|
||||
left: "50%",
|
||||
transform: "translate(-50%, -50%)",
|
||||
width: 400,
|
||||
bgcolor: theme.palette.background.main,
|
||||
border: 1,
|
||||
borderColor: theme.palette.border.light,
|
||||
borderRadius: theme.shape.borderRadius,
|
||||
boxShadow: 24,
|
||||
p: theme.spacing(15),
|
||||
"&:focus": {
|
||||
outline: "none",
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Typography id="modal-delete-maintenance" component="h2" variant="h2">
|
||||
Do you really want to remove this maintenance window?
|
||||
</Typography>
|
||||
<Stack
|
||||
direction="row"
|
||||
gap={theme.spacing(4)}
|
||||
mt={theme.spacing(12)}
|
||||
justifyContent="flex-end"
|
||||
>
|
||||
<Button
|
||||
variant="text"
|
||||
color="info"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
setIsOpen(false);
|
||||
}}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<LoadingButton
|
||||
loading={isLoading}
|
||||
variant="contained"
|
||||
color="error"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation(e);
|
||||
handleRemove(e);
|
||||
}}
|
||||
>
|
||||
Delete
|
||||
</LoadingButton>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Modal>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
ActionsMenu.propTypes = {
|
||||
maintenanceWindow: PropTypes.object,
|
||||
isAdmin: PropTypes.bool,
|
||||
updateCallback: PropTypes.func,
|
||||
};
|
||||
|
||||
export default ActionsMenu;
|
||||
@@ -0,0 +1,363 @@
|
||||
import PropTypes from "prop-types";
|
||||
import {
|
||||
TableContainer,
|
||||
Table,
|
||||
TableHead,
|
||||
TableRow,
|
||||
TableCell,
|
||||
TableBody,
|
||||
Paper,
|
||||
Box,
|
||||
TablePagination,
|
||||
Stack,
|
||||
Typography,
|
||||
Button,
|
||||
} from "@mui/material";
|
||||
import ArrowDownwardRoundedIcon from "@mui/icons-material/ArrowDownwardRounded";
|
||||
import ArrowUpwardRoundedIcon from "@mui/icons-material/ArrowUpwardRounded";
|
||||
import ActionsMenu from "./ActionsMenu";
|
||||
import { useState, useEffect, memo, useCallback, useRef } from "react";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import { useTheme } from "@emotion/react";
|
||||
import LeftArrowDouble from "../../../assets/icons/left-arrow-double.svg?react";
|
||||
import RightArrowDouble from "../../../assets/icons/right-arrow-double.svg?react";
|
||||
import LeftArrow from "../../../assets/icons/left-arrow.svg?react";
|
||||
import RightArrow from "../../../assets/icons/right-arrow.svg?react";
|
||||
import SelectorVertical from "../../../assets/icons/selector-vertical.svg?react";
|
||||
import { formatDurationRounded } from "../../../Utils/timeUtils";
|
||||
import { StatusLabel } from "../../../Components/Label";
|
||||
import { setRowsPerPage } from "../../../Features/UI/uiSlice";
|
||||
import dayjs from "dayjs";
|
||||
|
||||
/**
|
||||
* Component for pagination actions (first, previous, next, last).
|
||||
*
|
||||
* @component
|
||||
* @param {Object} props
|
||||
* @param {number} props.count - Total number of items.
|
||||
* @param {number} props.page - Current page number.
|
||||
* @param {number} props.rowsPerPage - Number of rows per page.
|
||||
* @param {function} props.onPageChange - Callback function to handle page change.
|
||||
*
|
||||
* @returns {JSX.Element} Pagination actions component.
|
||||
*/
|
||||
const TablePaginationActions = (props) => {
|
||||
const { count, page, rowsPerPage, onPageChange } = props;
|
||||
const handleFirstPageButtonClick = (event) => {
|
||||
onPageChange(event, 0);
|
||||
};
|
||||
const handleBackButtonClick = (event) => {
|
||||
onPageChange(event, page - 1);
|
||||
};
|
||||
const handleNextButtonClick = (event) => {
|
||||
onPageChange(event, page + 1);
|
||||
};
|
||||
const handleLastPageButtonClick = (event) => {
|
||||
onPageChange(event, Math.max(0, Math.ceil(count / rowsPerPage) - 1));
|
||||
};
|
||||
|
||||
return (
|
||||
<Box sx={{ flexShrink: 0, ml: "24px" }}>
|
||||
<Button
|
||||
variant="group"
|
||||
onClick={handleFirstPageButtonClick}
|
||||
disabled={page === 0}
|
||||
aria-label="first page"
|
||||
>
|
||||
<LeftArrowDouble />
|
||||
</Button>
|
||||
<Button
|
||||
variant="group"
|
||||
onClick={handleBackButtonClick}
|
||||
disabled={page === 0}
|
||||
aria-label="previous page"
|
||||
>
|
||||
<LeftArrow />
|
||||
</Button>
|
||||
<Button
|
||||
variant="group"
|
||||
onClick={handleNextButtonClick}
|
||||
disabled={page >= Math.ceil(count / rowsPerPage) - 1}
|
||||
aria-label="next page"
|
||||
>
|
||||
<RightArrow />
|
||||
</Button>
|
||||
<Button
|
||||
variant="group"
|
||||
onClick={handleLastPageButtonClick}
|
||||
disabled={page >= Math.ceil(count / rowsPerPage) - 1}
|
||||
aria-label="last page"
|
||||
>
|
||||
<RightArrowDouble />
|
||||
</Button>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
TablePaginationActions.propTypes = {
|
||||
count: PropTypes.number.isRequired,
|
||||
page: PropTypes.number.isRequired,
|
||||
rowsPerPage: PropTypes.number.isRequired,
|
||||
onPageChange: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
const MaintenanceTable = ({
|
||||
page,
|
||||
setPage,
|
||||
sort,
|
||||
setSort,
|
||||
maintenanceWindows,
|
||||
maintenanceWindowCount,
|
||||
updateCallback,
|
||||
}) => {
|
||||
const { rowsPerPage } = useSelector((state) => state.ui.maintenance);
|
||||
const theme = useTheme();
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const handleChangePage = (event, newPage) => {
|
||||
setPage(newPage);
|
||||
};
|
||||
|
||||
const getTimeToNextWindow = (startTime, endTime, repeat) => {
|
||||
//1. Advance time closest to next window as possible
|
||||
const now = dayjs();
|
||||
let start = dayjs(startTime);
|
||||
let end = dayjs(endTime);
|
||||
if (repeat > 0) {
|
||||
// Advance time closest to next window as possible
|
||||
while (start.isBefore(now) && end.isBefore(now)) {
|
||||
start = start.add(repeat, "milliseconds");
|
||||
end = end.add(repeat, "milliseconds");
|
||||
}
|
||||
}
|
||||
|
||||
//Check if we are in a window
|
||||
if (now.isAfter(start) && now.isBefore(end)) {
|
||||
return "In maintenance window";
|
||||
}
|
||||
|
||||
if (start.isAfter(now)) {
|
||||
const diffInMinutes = start.diff(now, "minutes");
|
||||
const diffInHours = start.diff(now, "hours");
|
||||
const diffInDays = start.diff(now, "days");
|
||||
|
||||
if (diffInMinutes < 60) {
|
||||
return diffInMinutes + " minutes";
|
||||
} else if (diffInHours < 24) {
|
||||
return diffInHours + " hours";
|
||||
} else if (diffInDays < 7) {
|
||||
return diffInDays + " days";
|
||||
} else {
|
||||
return diffInDays + " days";
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleChangeRowsPerPage = (event) => {
|
||||
dispatch(
|
||||
setRowsPerPage({
|
||||
value: parseInt(event.target.value, 10),
|
||||
table: "maintenance",
|
||||
})
|
||||
);
|
||||
setPage(0);
|
||||
};
|
||||
|
||||
/**
|
||||
* Helper function to calculate the range of displayed rows.
|
||||
* @returns {string}
|
||||
*/
|
||||
const getRange = () => {
|
||||
let start = page * rowsPerPage + 1;
|
||||
let end = Math.min(
|
||||
page * rowsPerPage + rowsPerPage,
|
||||
maintenanceWindowCount
|
||||
);
|
||||
return `${start} - ${end}`;
|
||||
};
|
||||
|
||||
const handleSort = async (field) => {
|
||||
let order = "";
|
||||
if (sort.field !== field) {
|
||||
order = "desc";
|
||||
} else {
|
||||
order = sort.order === "asc" ? "desc" : "asc";
|
||||
}
|
||||
setSort({ field, order });
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<TableContainer component={Paper}>
|
||||
<Table>
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableCell
|
||||
sx={{ cursor: "pointer" }}
|
||||
onClick={() => handleSort("name")}
|
||||
>
|
||||
<Box>
|
||||
Maintenance Window Name
|
||||
<span
|
||||
style={{
|
||||
visibility: sort.field === "name" ? "visible" : "hidden",
|
||||
}}
|
||||
>
|
||||
{sort.order === "asc" ? (
|
||||
<ArrowUpwardRoundedIcon />
|
||||
) : (
|
||||
<ArrowDownwardRoundedIcon />
|
||||
)}
|
||||
</span>
|
||||
</Box>
|
||||
</TableCell>
|
||||
<TableCell
|
||||
sx={{ cursor: "pointer" }}
|
||||
onClick={() => handleSort("status")}
|
||||
>
|
||||
{" "}
|
||||
<Box width="max-content">
|
||||
{" "}
|
||||
Status
|
||||
<span
|
||||
style={{
|
||||
visibility:
|
||||
sort.field === "active" ? "visible" : "hidden",
|
||||
}}
|
||||
>
|
||||
{sort.order === "asc" ? (
|
||||
<ArrowUpwardRoundedIcon />
|
||||
) : (
|
||||
<ArrowDownwardRoundedIcon />
|
||||
)}
|
||||
</span>
|
||||
</Box>
|
||||
</TableCell>
|
||||
<TableCell>Next Window</TableCell>
|
||||
<TableCell>Repeat</TableCell>
|
||||
<TableCell>Actions</TableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{maintenanceWindows.map((maintenanceWindow) => {
|
||||
const text = maintenanceWindow.active ? "active" : "paused";
|
||||
const status = maintenanceWindow.active ? "up" : "paused";
|
||||
return (
|
||||
<TableRow key={maintenanceWindow._id}>
|
||||
<TableCell>{maintenanceWindow.name}</TableCell>
|
||||
<TableCell>
|
||||
<StatusLabel
|
||||
status={status}
|
||||
text={text}
|
||||
customStyles={{ textTransform: "capitalize" }}
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
{getTimeToNextWindow(
|
||||
maintenanceWindow.start,
|
||||
maintenanceWindow.end,
|
||||
maintenanceWindow.repeat
|
||||
)}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
{maintenanceWindow.repeat === 0
|
||||
? "N/A"
|
||||
: formatDurationRounded(maintenanceWindow.repeat)}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<ActionsMenu
|
||||
maintenanceWindow={maintenanceWindow}
|
||||
updateCallback={updateCallback}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
);
|
||||
})}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
<Stack
|
||||
direction="row"
|
||||
alignItems="center"
|
||||
justifyContent="space-between"
|
||||
px={theme.spacing(4)}
|
||||
>
|
||||
<Typography px={theme.spacing(2)} variant="body2" sx={{ opacity: 0.7 }}>
|
||||
Showing {getRange()} of {maintenanceWindowCount} maintenance window(s)
|
||||
</Typography>
|
||||
<TablePagination
|
||||
component="div"
|
||||
count={maintenanceWindowCount}
|
||||
page={page}
|
||||
onPageChange={handleChangePage}
|
||||
rowsPerPage={rowsPerPage}
|
||||
rowsPerPageOptions={[5, 10, 15, 25]}
|
||||
onRowsPerPageChange={handleChangeRowsPerPage}
|
||||
ActionsComponent={TablePaginationActions}
|
||||
labelRowsPerPage="Rows per page"
|
||||
labelDisplayedRows={({ page, count }) =>
|
||||
`Page ${page + 1} of ${Math.max(0, Math.ceil(count / rowsPerPage))}`
|
||||
}
|
||||
slotProps={{
|
||||
select: {
|
||||
MenuProps: {
|
||||
keepMounted: true,
|
||||
disableScrollLock: true,
|
||||
PaperProps: {
|
||||
className: "pagination-dropdown",
|
||||
sx: {
|
||||
mt: 0,
|
||||
mb: theme.spacing(2),
|
||||
},
|
||||
},
|
||||
transformOrigin: { vertical: "bottom", horizontal: "left" },
|
||||
anchorOrigin: { vertical: "top", horizontal: "left" },
|
||||
sx: { mt: theme.spacing(-2) },
|
||||
},
|
||||
inputProps: { id: "pagination-dropdown" },
|
||||
IconComponent: SelectorVertical,
|
||||
sx: {
|
||||
ml: theme.spacing(4),
|
||||
mr: theme.spacing(12),
|
||||
minWidth: theme.spacing(20),
|
||||
textAlign: "left",
|
||||
"&.Mui-focused > div": {
|
||||
backgroundColor: theme.palette.background.main,
|
||||
},
|
||||
},
|
||||
},
|
||||
}}
|
||||
sx={{
|
||||
mt: theme.spacing(6),
|
||||
color: theme.palette.text.secondary,
|
||||
"& svg path": {
|
||||
stroke: theme.palette.text.tertiary,
|
||||
strokeWidth: 1.3,
|
||||
},
|
||||
"& .MuiSelect-select": {
|
||||
border: 1,
|
||||
borderColor: theme.palette.border.light,
|
||||
borderRadius: theme.shape.borderRadius,
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</Stack>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
MaintenanceTable.propTypes = {
|
||||
isAdmin: PropTypes.bool,
|
||||
page: PropTypes.number,
|
||||
setPage: PropTypes.func,
|
||||
rowsPerPage: PropTypes.number,
|
||||
setRowsPerPage: PropTypes.func,
|
||||
sort: PropTypes.object,
|
||||
setSort: PropTypes.func,
|
||||
maintenanceWindows: PropTypes.array,
|
||||
maintenanceWindowCount: PropTypes.number,
|
||||
updateCallback: PropTypes.func,
|
||||
};
|
||||
|
||||
const MemoizedMaintenanceTable = memo(MaintenanceTable);
|
||||
export default MemoizedMaintenanceTable;
|
||||
@@ -1,36 +0,0 @@
|
||||
.maintenance-checklist-main {
|
||||
max-width: 350px;
|
||||
min-height: 390px;
|
||||
margin: auto;
|
||||
padding: 5px;
|
||||
}
|
||||
|
||||
.maintenance-image {
|
||||
width: 280px;
|
||||
height: 150px;
|
||||
background-color: whitesmoke;
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
.maintenance-title {
|
||||
color: var(--secondary-color-light);
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
margin-top: 40px;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.checklist-item {
|
||||
font-size: 13px;
|
||||
color: var(--secondary-color-light);
|
||||
display: flex;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.checklist-item-text {
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
.maintenance-checklist-main .maintenance-checklist-button {
|
||||
margin-top: 30px;
|
||||
}
|
||||
@@ -1,10 +1,48 @@
|
||||
import { Box } from "@mui/material";
|
||||
import { Box, Stack, Button } from "@mui/material";
|
||||
import { useTheme } from "@emotion/react";
|
||||
import Fallback from "../../Components/Fallback";
|
||||
import { useState, useEffect } from "react";
|
||||
import "./index.css";
|
||||
import MaintenanceTable from "./MaintenanceTable";
|
||||
import { useSelector } from "react-redux";
|
||||
import { networkService } from "../../main";
|
||||
import Breadcrumbs from "../../Components/Breadcrumbs";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
|
||||
const Maintenance = ({ isAdmin }) => {
|
||||
const theme = useTheme();
|
||||
const navigate = useNavigate();
|
||||
const { authToken } = useSelector((state) => state.auth);
|
||||
const { rowsPerPage } = useSelector((state) => state.ui.maintenance);
|
||||
|
||||
const [maintenanceWindows, setMaintenanceWindows] = useState([]);
|
||||
const [maintenanceWindowCount, setMaintenanceWindowCount] = useState(0);
|
||||
const [page, setPage] = useState(0);
|
||||
const [sort, setSort] = useState({});
|
||||
const [updateTrigger, setUpdateTrigger] = useState(false);
|
||||
|
||||
const handleActionMenuDelete = () => {
|
||||
setUpdateTrigger((prev) => !prev);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const fetchMaintenanceWindows = async () => {
|
||||
try {
|
||||
const response = await networkService.getMaintenanceWindowsByTeamId({
|
||||
authToken: authToken,
|
||||
page: page,
|
||||
rowsPerPage: rowsPerPage,
|
||||
});
|
||||
const { maintenanceWindows, maintenanceWindowCount } =
|
||||
response.data.data;
|
||||
setMaintenanceWindows(maintenanceWindows);
|
||||
setMaintenanceWindowCount(maintenanceWindowCount);
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
}
|
||||
};
|
||||
fetchMaintenanceWindows();
|
||||
}, [authToken, page, rowsPerPage, updateTrigger]);
|
||||
|
||||
return (
|
||||
<Box
|
||||
@@ -21,16 +59,52 @@ const Maintenance = ({ isAdmin }) => {
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Fallback
|
||||
title="maintenance window"
|
||||
checks={[
|
||||
"Mark your maintenance periods",
|
||||
"Eliminate any misunderstandings",
|
||||
"Stop sending alerts in maintenance windows",
|
||||
]}
|
||||
link="/maintenance/create"
|
||||
isAdmin={isAdmin}
|
||||
/>
|
||||
{maintenanceWindows.length > 0 && (
|
||||
<Stack gap={theme.spacing(8)}>
|
||||
<Stack
|
||||
direction="row"
|
||||
justifyContent="space-between"
|
||||
alignItems="center"
|
||||
mt={theme.spacing(5)}
|
||||
>
|
||||
<Breadcrumbs
|
||||
list={[{ name: "maintenance", path: "/maintenance" }]}
|
||||
/>
|
||||
<Button
|
||||
variant="contained"
|
||||
color="primary"
|
||||
onClick={() => {
|
||||
navigate("/maintenance/create");
|
||||
}}
|
||||
sx={{ fontWeight: 500 }}
|
||||
>
|
||||
Create maintenance window
|
||||
</Button>
|
||||
</Stack>
|
||||
<MaintenanceTable
|
||||
page={page}
|
||||
setPage={setPage}
|
||||
rowsPerPage={rowsPerPage}
|
||||
sort={sort}
|
||||
setSort={setSort}
|
||||
maintenanceWindows={maintenanceWindows}
|
||||
maintenanceWindowCount={maintenanceWindowCount}
|
||||
updateCallback={handleActionMenuDelete}
|
||||
/>
|
||||
</Stack>
|
||||
)}
|
||||
{maintenanceWindows.length === 0 && (
|
||||
<Fallback
|
||||
title="maintenance window"
|
||||
checks={[
|
||||
"Mark your maintenance periods",
|
||||
"Eliminate any misunderstandings",
|
||||
"Stop sending alerts in maintenance windows",
|
||||
]}
|
||||
link="/maintenance/create"
|
||||
isAdmin={true}
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,34 +0,0 @@
|
||||
.create-monitor p.MuiTypography-root,
|
||||
.create-monitor button.MuiButtonBase-root {
|
||||
font-size: var(--env-var-font-size-medium);
|
||||
}
|
||||
.create-monitor h6.MuiTypography-root,
|
||||
.create-monitor .MuiBox-root .field + p.MuiTypography-root {
|
||||
font-size: var(--env-var-font-size-small-plus);
|
||||
opacity: 0.8;
|
||||
}
|
||||
.create-monitor button.MuiButtonBase-root {
|
||||
height: 34px;
|
||||
}
|
||||
|
||||
.create-monitor .error-container {
|
||||
position: relative;
|
||||
}
|
||||
.create-monitor .error-container p.MuiTypography-root.input-error {
|
||||
opacity: 0.8;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
}
|
||||
.create-monitor .MuiStack-root:has(span.MuiTypography-root.input-error) {
|
||||
position: relative;
|
||||
}
|
||||
.create-monitor span.MuiTypography-root.input-error {
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
}
|
||||
|
||||
.create-monitor .MuiStack-root .MuiButtonGroup-root button {
|
||||
font-size: var(--env-var-font-size-small);
|
||||
height: 28px;
|
||||
padding: 8px;
|
||||
}
|
||||
@@ -112,7 +112,7 @@ const CreateMonitor = () => {
|
||||
{ [name]: value },
|
||||
{ abortEarly: false }
|
||||
);
|
||||
|
||||
console.log(error);
|
||||
setErrors((prev) => {
|
||||
const updatedErrors = { ...prev };
|
||||
if (error) updatedErrors[name] = error.details[0].message;
|
||||
|
||||
@@ -59,18 +59,19 @@ const Monitors = ({ isAdmin }) => {
|
||||
mt={theme.spacing(5)}
|
||||
>
|
||||
<Greeting type="uptime" />
|
||||
{monitorState?.monitorsSummary?.monitors?.length !== 0 && (
|
||||
<Button
|
||||
variant="contained"
|
||||
color="primary"
|
||||
onClick={() => {
|
||||
navigate("/monitors/create");
|
||||
}}
|
||||
sx={{ fontWeight: 500 }}
|
||||
>
|
||||
Create monitor
|
||||
</Button>
|
||||
)}
|
||||
{isAdmin &&
|
||||
monitorState?.monitorsSummary?.monitors?.length !== 0 && (
|
||||
<Button
|
||||
variant="contained"
|
||||
color="primary"
|
||||
onClick={() => {
|
||||
navigate("/monitors/create");
|
||||
}}
|
||||
sx={{ fontWeight: 500 }}
|
||||
>
|
||||
Create monitor
|
||||
</Button>
|
||||
)}
|
||||
</Stack>
|
||||
</Box>
|
||||
{isAdmin && monitorState?.monitorsSummary?.monitors?.length === 0 && (
|
||||
@@ -134,9 +135,9 @@ const Monitors = ({ isAdmin }) => {
|
||||
</Box>
|
||||
<Box width="25%" minWidth={150} ml="auto">
|
||||
<Search
|
||||
options={monitorState?.monitorsSummary.monitors}
|
||||
options={monitorState?.monitorsSummary?.monitors ?? []}
|
||||
filteredBy="name"
|
||||
value={search}
|
||||
inputValue={search}
|
||||
handleInputChange={handleSearch}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
@@ -244,7 +244,7 @@ const PageSpeedConfigure = () => {
|
||||
},
|
||||
}}
|
||||
>
|
||||
Editting...
|
||||
Editing...
|
||||
</Typography>
|
||||
</Stack>
|
||||
</Box>
|
||||
|
||||
@@ -32,7 +32,7 @@ import PieChart from "./Charts/PieChart";
|
||||
import useUtils from "../../Monitors/utils";
|
||||
import "./index.css";
|
||||
|
||||
const PageSpeedDetails = () => {
|
||||
const PageSpeedDetails = ({ isAdmin }) => {
|
||||
const theme = useTheme();
|
||||
const navigate = useNavigate();
|
||||
const { statusColor, pagespeedStatusMsg, determineState } = useUtils();
|
||||
@@ -54,7 +54,7 @@ const PageSpeedDetails = () => {
|
||||
normalize: null,
|
||||
});
|
||||
setMonitor(res?.data?.data ?? {});
|
||||
setAudits(res?.data?.data?.checks?.[0]?.audits ?? []);
|
||||
setAudits(res?.data?.data?.checks?.[0]?.audits ?? {});
|
||||
} catch (error) {
|
||||
logger.error(logger);
|
||||
navigate("/not-found", { replace: true });
|
||||
@@ -157,25 +157,27 @@ const PageSpeedDetails = () => {
|
||||
</Typography>
|
||||
</Stack>
|
||||
</Box>
|
||||
<Button
|
||||
variant="contained"
|
||||
color="secondary"
|
||||
onClick={() => navigate(`/pagespeed/configure/${monitorId}`)}
|
||||
sx={{
|
||||
ml: "auto",
|
||||
alignSelf: "flex-end",
|
||||
px: theme.spacing(5),
|
||||
"& svg": {
|
||||
mr: theme.spacing(3),
|
||||
"& path": {
|
||||
stroke: theme.palette.other.icon,
|
||||
{isAdmin && (
|
||||
<Button
|
||||
variant="contained"
|
||||
color="secondary"
|
||||
onClick={() => navigate(`/pagespeed/configure/${monitorId}`)}
|
||||
sx={{
|
||||
ml: "auto",
|
||||
alignSelf: "flex-end",
|
||||
px: theme.spacing(5),
|
||||
"& svg": {
|
||||
mr: theme.spacing(3),
|
||||
"& path": {
|
||||
stroke: theme.palette.other.icon,
|
||||
},
|
||||
},
|
||||
},
|
||||
}}
|
||||
>
|
||||
<SettingsIcon />
|
||||
Configure
|
||||
</Button>
|
||||
}}
|
||||
>
|
||||
<SettingsIcon />
|
||||
Configure
|
||||
</Button>
|
||||
)}
|
||||
</Stack>
|
||||
<Stack direction="row" gap={theme.spacing(8)}>
|
||||
<StatBox>
|
||||
@@ -393,6 +395,7 @@ const PageSpeedDetails = () => {
|
||||
};
|
||||
|
||||
PageSpeedDetails.propTypes = {
|
||||
isAdmin: PropTypes.bool,
|
||||
push: PropTypes.func,
|
||||
};
|
||||
|
||||
|
||||
@@ -87,14 +87,16 @@ const PageSpeed = ({ isAdmin }) => {
|
||||
mt={theme.spacing(5)}
|
||||
>
|
||||
<Greeting type="pagespeed" />
|
||||
<Button
|
||||
variant="contained"
|
||||
color="primary"
|
||||
onClick={() => navigate("/pagespeed/create")}
|
||||
sx={{ whiteSpace: "nowrap" }}
|
||||
>
|
||||
Create new
|
||||
</Button>
|
||||
{isAdmin && (
|
||||
<Button
|
||||
variant="contained"
|
||||
color="primary"
|
||||
onClick={() => navigate("/pagespeed/create")}
|
||||
sx={{ whiteSpace: "nowrap" }}
|
||||
>
|
||||
Create new
|
||||
</Button>
|
||||
)}
|
||||
</Stack>
|
||||
</Box>
|
||||
<Grid container spacing={theme.spacing(12)}>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useTheme } from "@emotion/react";
|
||||
import { Box, Stack, Typography } from "@mui/material";
|
||||
import { Box, Stack, Typography, Button } from "@mui/material";
|
||||
import Field from "../../Components/Inputs/Field";
|
||||
import Link from "../../Components/Link";
|
||||
import Select from "../../Components/Inputs/Select";
|
||||
@@ -21,6 +21,7 @@ import { useState } from "react";
|
||||
import { ConfigBox } from "./styled";
|
||||
import { networkService } from "../../main";
|
||||
import { settingsValidation } from "../../Validation/validation";
|
||||
import { useNavigate } from "react-router";
|
||||
|
||||
const SECONDS_PER_DAY = 86400;
|
||||
|
||||
@@ -33,10 +34,11 @@ const Settings = ({ isAdmin }) => {
|
||||
const { timezone } = useSelector((state) => state.ui);
|
||||
const [checksIsLoading, setChecksIsLoading] = useState(false);
|
||||
const [form, setForm] = useState({
|
||||
ttl: (checkTTL / SECONDS_PER_DAY).toString(),
|
||||
ttl: checkTTL ? (checkTTL / SECONDS_PER_DAY).toString() : 0,
|
||||
});
|
||||
const [errors, setErrors] = useState({});
|
||||
const dispatch = useDispatch();
|
||||
const navigate = useNavigate();
|
||||
|
||||
const handleChange = (event) => {
|
||||
const { value, id } = event.target;
|
||||
@@ -162,7 +164,7 @@ const Settings = ({ isAdmin }) => {
|
||||
>
|
||||
<ConfigBox>
|
||||
<Box>
|
||||
<Typography component="h1">General Settings</Typography>
|
||||
<Typography component="h1">General settings</Typography>
|
||||
<Typography sx={{ mt: theme.spacing(2), mb: theme.spacing(2) }}>
|
||||
<Typography component="span">Display timezone</Typography>- The
|
||||
timezone of the dashboard you publicly display.
|
||||
@@ -216,7 +218,7 @@ const Settings = ({ isAdmin }) => {
|
||||
{isAdmin && (
|
||||
<ConfigBox>
|
||||
<Box>
|
||||
<Typography component="h1">Demo Monitors</Typography>
|
||||
<Typography component="h1">Demo monitors</Typography>
|
||||
<Typography sx={{ mt: theme.spacing(2) }}>
|
||||
Here you can add and remove demo monitors
|
||||
</Typography>
|
||||
@@ -249,6 +251,28 @@ const Settings = ({ isAdmin }) => {
|
||||
</Stack>
|
||||
</ConfigBox>
|
||||
)}
|
||||
{isAdmin && (
|
||||
<ConfigBox>
|
||||
<Box>
|
||||
<Typography component="h1">Advanced settings</Typography>
|
||||
<Typography sx={{ mt: theme.spacing(2) }}>
|
||||
Click here to modify advanced settings
|
||||
</Typography>
|
||||
</Box>
|
||||
<Stack gap={theme.spacing(20)}>
|
||||
<Box>
|
||||
<Button
|
||||
variant="contained"
|
||||
onClick={() => {
|
||||
navigate("/advanced-settings");
|
||||
}}
|
||||
>
|
||||
Advanced Settings
|
||||
</Button>
|
||||
</Box>
|
||||
</Stack>
|
||||
</ConfigBox>
|
||||
)}
|
||||
<ConfigBox>
|
||||
<Box>
|
||||
<Typography component="h1">About</Typography>
|
||||
|
||||
@@ -1,30 +1,56 @@
|
||||
const LOG_LEVEL = import.meta.env.VITE_APP_LOG_LEVEL;
|
||||
class Logger {
|
||||
constructor(logLevel) {
|
||||
const NO_OP = () => {};
|
||||
import store from "../store";
|
||||
const LOG_LEVEL = import.meta.env.VITE_APP_LOG_LEVEL || "debug";
|
||||
const NO_OP = () => {};
|
||||
|
||||
class Logger {
|
||||
constructor() {
|
||||
let logLevel = LOG_LEVEL;
|
||||
this.unsubscribe = store.subscribe(() => {
|
||||
const state = store.getState();
|
||||
logLevel = state.settings.logLevel || "debug";
|
||||
this.updateLogLevel(logLevel);
|
||||
});
|
||||
}
|
||||
|
||||
updateLogLevel(logLevel) {
|
||||
if (logLevel === "none") {
|
||||
this.info = NO_OP;
|
||||
this.error = NO_OP;
|
||||
this.warn = NO_OP;
|
||||
this.log = NO_OP;
|
||||
return;
|
||||
}
|
||||
|
||||
this.error = console.error.bind(console);
|
||||
|
||||
if (logLevel === "error") {
|
||||
this.error = console.error.bind(console);
|
||||
this.info = NO_OP;
|
||||
this.warn = NO_OP;
|
||||
this.log = NO_OP;
|
||||
return;
|
||||
}
|
||||
this.warn = console.warn.bind(console);
|
||||
|
||||
if (logLevel === "warn") {
|
||||
this.error = console.error.bind(console);
|
||||
this.warn = console.warn.bind(console);
|
||||
this.info = NO_OP;
|
||||
this.log = NO_OP;
|
||||
return;
|
||||
}
|
||||
this.log = console.log.bind(console);
|
||||
|
||||
if (logLevel === "info") {
|
||||
this.error = console.error.bind(console);
|
||||
this.warn = console.warn.bind(console);
|
||||
this.info = console.info.bind(console);
|
||||
this.log = NO_OP;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
cleanup() {
|
||||
if (this.unsubscribe) {
|
||||
this.unsubscribe();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const logger = new Logger(LOG_LEVEL);
|
||||
export const logger = new Logger();
|
||||
@@ -1,22 +1,51 @@
|
||||
import axios from "axios";
|
||||
const BASE_URL = import.meta.env.VITE_APP_API_BASE_URL;
|
||||
const FALLBACK_BASE_URL = "http://localhost:5000/api/v1";
|
||||
import { clearAuthState } from "../Features/Auth/authSlice";
|
||||
import { clearUptimeMonitorState } from "../Features/UptimeMonitors/uptimeMonitorsSlice";
|
||||
import { logger } from "./Logger";
|
||||
class NetworkService {
|
||||
constructor(store) {
|
||||
constructor(store, dispatch, navigate) {
|
||||
this.store = store;
|
||||
this.axiosInstance = axios.create({ baseURL: BASE_URL });
|
||||
this.dispatch = dispatch;
|
||||
this.navigate = navigate;
|
||||
let baseURL = BASE_URL;
|
||||
this.axiosInstance = axios.create();
|
||||
this.setBaseUrl(baseURL);
|
||||
this.unsubscribe = store.subscribe(() => {
|
||||
const state = store.getState();
|
||||
if (BASE_URL !== undefined) {
|
||||
baseURL = BASE_URL;
|
||||
} else if (state?.settings?.apiBaseUrl ?? null) {
|
||||
baseURL = state.settings.apiBaseUrl;
|
||||
} else {
|
||||
baseURL = FALLBACK_BASE_URL;
|
||||
}
|
||||
this.setBaseUrl(baseURL);
|
||||
});
|
||||
this.axiosInstance.interceptors.response.use(
|
||||
(response) => response,
|
||||
(error) => {
|
||||
logger.error(error);
|
||||
if (error.response && error.response.status === 401) {
|
||||
logger.error("Invalid token received");
|
||||
dispatch(clearAuthState());
|
||||
dispatch(clearUptimeMonitorState());
|
||||
navigate("/login");
|
||||
}
|
||||
return Promise.reject(error);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
setBaseUrl = (url) => {
|
||||
this.axiosInstance.defaults.baseURL = url;
|
||||
};
|
||||
|
||||
cleanup() {
|
||||
if (this.unsubscribe) {
|
||||
this.unsubscribe();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* ************************************
|
||||
@@ -101,7 +130,7 @@ class NetworkService {
|
||||
* @param {Object} config - The configuration object.
|
||||
* @param {string} config.authToken - The authorization token to be used in the request header.
|
||||
* @param {string} config.teamId - The ID of the team whose monitors are to be retrieved.
|
||||
* @param {number} [config.limit] - The maximum number of monitors to retrieve.
|
||||
* @param {number} [config.limit] - The maximum number of checks to retrieve. 0 for all, -1 for none
|
||||
* @param {Array<string>} [config.types] - The types of monitors to retrieve.
|
||||
* @param {string} [config.status] - The status of the monitors to retrieve.
|
||||
* @param {string} [config.checkOrder] - The order in which to sort the retrieved monitors.
|
||||
@@ -609,7 +638,7 @@ class NetworkService {
|
||||
*/
|
||||
async updateChecksTTL(config) {
|
||||
return this.axiosInstance.put(
|
||||
`/checks/ttl`,
|
||||
`/checks/team/ttl`,
|
||||
{ ttl: config.ttl },
|
||||
{
|
||||
headers: {
|
||||
@@ -619,6 +648,197 @@ class NetworkService {
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* ************************************
|
||||
* Get app settings
|
||||
* ************************************
|
||||
*
|
||||
* @async
|
||||
* @param {Object} config - The configuration object.
|
||||
* @param {string} config.authToken - The authorization token to be used in the request header.
|
||||
* @returns {Promise<AxiosResponse>} The response from the axios GET request.
|
||||
*
|
||||
*/
|
||||
|
||||
async getAppSettings(config) {
|
||||
return this.axiosInstance.get("/settings", {
|
||||
headers: {
|
||||
Authorization: `Bearer ${config.authToken}`,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* ************************************
|
||||
* Create a new monitor
|
||||
* ************************************
|
||||
*
|
||||
* @async
|
||||
* @param {Object} config - The configuration object.
|
||||
* @param {string} config.authToken - The authorization token to be used in the request header.
|
||||
* @param {Object} config.settings - The monitor object to be sent in the request body.
|
||||
* @returns {Promise<AxiosResponse>} The response from the axios POST request.
|
||||
*/
|
||||
async updateAppSettings(config) {
|
||||
return this.axiosInstance.put(`/settings`, config.settings, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${config.authToken}`,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* ************************************
|
||||
* Creates a maintenance window
|
||||
* ************************************
|
||||
*
|
||||
* @async
|
||||
* @param {Object} config - The configuration object.
|
||||
* @param {string} config.authToken - The authorization token to be used in the request header.
|
||||
* @param {Object} config.maintenanceWindow - The maintenance window object to be sent in the request body.
|
||||
* @returns {Promise<AxiosResponse>} The response from the axios POST request.
|
||||
*
|
||||
*/
|
||||
|
||||
async createMaintenanceWindow(config) {
|
||||
return this.axiosInstance.post(
|
||||
`/maintenance-window`,
|
||||
config.maintenanceWindow,
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${config.authToken}`,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* ************************************
|
||||
* Edits a maintenance window
|
||||
* ************************************
|
||||
*
|
||||
* @async
|
||||
* @param {Object} config - The configuration object.
|
||||
* @param {string} config.authToken - The authorization token to be used in the request header.
|
||||
* @param {Object} config.maintenanceWindowId - The maintenance window id.
|
||||
* @param {Object} config.maintenanceWindow - The maintenance window object to be sent in the request body.
|
||||
* @returns {Promise<AxiosResponse>} The response from the axios POST request.
|
||||
*
|
||||
*/
|
||||
|
||||
async editMaintenanceWindow(config) {
|
||||
return this.axiosInstance.put(
|
||||
`/maintenance-window/${config.maintenanceWindowId}`,
|
||||
config.maintenanceWindow,
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${config.authToken}`,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* ************************************
|
||||
* Get maintenance window by id
|
||||
* ************************************
|
||||
*
|
||||
* @async
|
||||
* @param {Object} config - The configuration object.
|
||||
* @param {string} config.authToken - The authorization token to be used in the request header.
|
||||
* @param {string} [config.maintenanceWindowId] - The id of the maintenance window to delete.
|
||||
* @returns {Promise<AxiosResponse>} The response from the axios POST request.
|
||||
*
|
||||
*/
|
||||
|
||||
async getMaintenanceWindowById(config) {
|
||||
const { authToken, maintenanceWindowId } = config;
|
||||
return this.axiosInstance.get(
|
||||
`/maintenance-window/${maintenanceWindowId}`,
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${authToken}`,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* ************************************
|
||||
* Get maintenance windows by teamId
|
||||
* ************************************
|
||||
*
|
||||
* @async
|
||||
* @param {Object} config - The configuration object.
|
||||
* @param {string} config.authToken - The authorization token to be used in the request header.
|
||||
* @param {string} [config.active] - The status of the maintenance windows to retrieve.
|
||||
* @param {number} [config.page] - The page number for pagination.
|
||||
* @param {number} [config.rowsPerPage] - The number of rows per page for pagination.
|
||||
* @param {string} [config.field] - The field to sort by.
|
||||
* @param {string} [config.order] - The order in which to sort the field.
|
||||
* @returns {Promise<AxiosResponse>} The response from the axios POST request.
|
||||
*
|
||||
*/
|
||||
|
||||
async getMaintenanceWindowsByTeamId(config) {
|
||||
const { authToken, active, page, rowsPerPage, field, order } = config;
|
||||
const params = new URLSearchParams();
|
||||
|
||||
if (active) params.append("status", active);
|
||||
if (page) params.append("page", page);
|
||||
if (rowsPerPage) params.append("rowsPerPage", rowsPerPage);
|
||||
if (field) params.append("field", field);
|
||||
if (order) params.append("order", order);
|
||||
|
||||
return this.axiosInstance.get(
|
||||
`/maintenance-window/team?${params.toString()}`,
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${authToken}`,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
}
|
||||
);
|
||||
}
|
||||
/**
|
||||
* ************************************
|
||||
* Delete maintenance window by id
|
||||
* ************************************
|
||||
*
|
||||
* @async
|
||||
* @param {Object} config - The configuration object.
|
||||
* @param {string} config.authToken - The authorization token to be used in the request header.
|
||||
* @param {string} [config.maintenanceWindowId] - The id of the maintenance window to delete.
|
||||
* @returns {Promise<AxiosResponse>} The response from the axios POST request.
|
||||
*
|
||||
*/
|
||||
|
||||
async deleteMaintenanceWindow(config) {
|
||||
const { authToken, maintenanceWindowId } = config;
|
||||
return this.axiosInstance.delete(
|
||||
`/maintenance-window/${maintenanceWindowId}`,
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${authToken}`,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default NetworkService;
|
||||
|
||||
let networkService;
|
||||
|
||||
export const setNetworkService = (service) => {
|
||||
networkService = service;
|
||||
};
|
||||
export { networkService };
|
||||
@@ -0,0 +1,15 @@
|
||||
import { useDispatch } from "react-redux";
|
||||
import { useNavigate } from "react-router";
|
||||
import { setNetworkService } from "./NetworkService";
|
||||
import NetworkService from "./NetworkService";
|
||||
import { store } from "../store";
|
||||
|
||||
const NetworkServiceProvider = ({ children }) => {
|
||||
const dispatch = useDispatch();
|
||||
const navigate = useNavigate();
|
||||
const networkService = new NetworkService(store, dispatch, navigate);
|
||||
setNetworkService(networkService);
|
||||
return children;
|
||||
};
|
||||
|
||||
export default NetworkServiceProvider;
|
||||
@@ -4,6 +4,12 @@ import timezone from "dayjs/plugin/timezone";
|
||||
dayjs.extend(utc);
|
||||
dayjs.extend(timezone);
|
||||
|
||||
export const MS_PER_SECOND = 1000;
|
||||
export const MS_PER_MINUTE = 60 * MS_PER_SECOND;
|
||||
export const MS_PER_HOUR = 60 * MS_PER_MINUTE;
|
||||
export const MS_PER_DAY = 24 * MS_PER_HOUR;
|
||||
export const MS_PER_WEEK = MS_PER_DAY * 7;
|
||||
|
||||
export const formatDuration = (ms) => {
|
||||
const seconds = Math.floor(ms / 1000);
|
||||
const minutes = Math.floor(seconds / 60);
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import joi from "joi";
|
||||
import dayjs from "dayjs";
|
||||
|
||||
const nameSchema = joi
|
||||
.string()
|
||||
@@ -77,6 +78,7 @@ const credentials = joi.object({
|
||||
}),
|
||||
role: joi.array(),
|
||||
teamId: joi.string().allow("").optional(),
|
||||
inviteToken: joi.string().allow(""),
|
||||
});
|
||||
|
||||
const monitorValidation = joi.object({
|
||||
@@ -112,50 +114,33 @@ const imageValidation = joi.object({
|
||||
}),
|
||||
});
|
||||
|
||||
const maintenanceWindowValidation = joi.object({
|
||||
repeat: joi.number().valid(1, 2, 3).required().messages({
|
||||
"number.base": "Repeat must be a number.",
|
||||
"any.only": "Repeat must be one of [1, 2, 3].",
|
||||
"any.required": "Repeat is required.",
|
||||
}),
|
||||
date: joi.date().required().messages({
|
||||
"date.base": "Date must be a valid date.",
|
||||
"any.required": "Date is required.",
|
||||
}),
|
||||
startTime: joi.string().required().messages({
|
||||
"string.base": "Start time must be a valid time.",
|
||||
"any.required": "Start time is required.",
|
||||
}),
|
||||
duration: joi.number().required().messages({
|
||||
"number.empty": "duration is required.",
|
||||
"number.base": "Duration must be a number.",
|
||||
"any.required": "Duration is required.",
|
||||
}),
|
||||
unit: joi.string().valid("minutes", "hours", "days").required().messages({
|
||||
"string.base": "Unit must be a string.",
|
||||
"any.only": "Unit must be one of ['minutes', 'hours', 'days'].",
|
||||
"any.required": "Unit is required.",
|
||||
}),
|
||||
displayName: joi.string().max(50).required().messages({
|
||||
"string.empty": "Display name is required.",
|
||||
"string.max": "Display name must be less than 50 characters long",
|
||||
}),
|
||||
addMonitors: joi.string().max(50).required().messages({
|
||||
"string.empty": "Add monitors is required.",
|
||||
"string.max": "Add monitors must be less than 50 characters long",
|
||||
}),
|
||||
});
|
||||
|
||||
const settingsValidation = joi.object({
|
||||
ttl: joi.number().required().messages({
|
||||
"string.empty": "TTL is required",
|
||||
}),
|
||||
});
|
||||
|
||||
const dayjsValidator = (value, helpers) => {
|
||||
if (!dayjs(value).isValid()) {
|
||||
return helpers.error("any.invalid");
|
||||
}
|
||||
return value;
|
||||
};
|
||||
|
||||
const maintenanceWindowValidation = joi.object({
|
||||
repeat: joi.string(),
|
||||
startDate: joi.custom(dayjsValidator, "Day.js date validation"),
|
||||
startTime: joi.custom(dayjsValidator, "Day.js date validation"),
|
||||
duration: joi.number(),
|
||||
durationUnit: joi.string(),
|
||||
name: joi.string(),
|
||||
monitors: joi.array().min(1),
|
||||
});
|
||||
|
||||
export {
|
||||
credentials,
|
||||
imageValidation,
|
||||
monitorValidation,
|
||||
maintenanceWindowValidation,
|
||||
settingsValidation,
|
||||
maintenanceWindowValidation,
|
||||
};
|
||||
@@ -0,0 +1,3 @@
|
||||
<svg width="18" height="20" viewBox="0 0 18 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M16.5 8.33342H1.5M12.3333 1.66675V5.00008M5.66667 1.66675V5.00008M5.5 18.3334H12.5C13.9001 18.3334 14.6002 18.3334 15.135 18.0609C15.6054 17.8212 15.9878 17.4388 16.2275 16.9684C16.5 16.4336 16.5 15.7335 16.5 14.3334V7.33342C16.5 5.93328 16.5 5.23322 16.2275 4.69844C15.9878 4.22803 15.6054 3.84558 15.135 3.6059C14.6002 3.33341 13.9001 3.33341 12.5 3.33341H5.5C4.09987 3.33341 3.3998 3.33341 2.86502 3.6059C2.39462 3.84558 2.01217 4.22803 1.77248 4.69844C1.5 5.23322 1.5 5.93328 1.5 7.33341V14.3334C1.5 15.7335 1.5 16.4336 1.77248 16.9684C2.01217 17.4388 2.39462 17.8212 2.86502 18.0609C3.3998 18.3334 4.09987 18.3334 5.5 18.3334Z" stroke="#344054" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 830 B |
+8
-5
@@ -1,18 +1,21 @@
|
||||
import React from "react";
|
||||
import ReactDOM from "react-dom/client";
|
||||
import App from "./App.jsx";
|
||||
import "./index.css";
|
||||
import { BrowserRouter as Router, HashRouter } from "react-router-dom";
|
||||
import { BrowserRouter as Router } from "react-router-dom";
|
||||
import { Provider } from "react-redux";
|
||||
import { persistor, store } from "./store";
|
||||
import { PersistGate } from "redux-persist/integration/react";
|
||||
import NetworkService from "./Utils/NetworkService.js";
|
||||
export const networkService = new NetworkService(store);
|
||||
import NetworkServiceProvider from "./Utils/NetworkServiceProvider.jsx";
|
||||
import { networkService } from "./Utils/NetworkService";
|
||||
export { networkService };
|
||||
|
||||
ReactDOM.createRoot(document.getElementById("root")).render(
|
||||
<Provider store={store}>
|
||||
<PersistGate loading={null} persistor={persistor}>
|
||||
<Router>
|
||||
<App />
|
||||
<NetworkServiceProvider>
|
||||
<App />
|
||||
</NetworkServiceProvider>
|
||||
</Router>
|
||||
</PersistGate>
|
||||
</Provider>
|
||||
|
||||
+4
-1
@@ -4,6 +4,7 @@ import uptimeMonitorsReducer from "./Features/UptimeMonitors/uptimeMonitorsSlice
|
||||
import pageSpeedMonitorReducer from "./Features/PageSpeedMonitor/pageSpeedMonitorSlice";
|
||||
import authReducer from "./Features/Auth/authSlice";
|
||||
import uiReducer from "./Features/UI/uiSlice";
|
||||
import settingsReducer from "./Features/Settings/settingsSlice";
|
||||
import storage from "redux-persist/lib/storage";
|
||||
import { persistReducer, persistStore, createTransform } from "redux-persist";
|
||||
|
||||
@@ -21,7 +22,7 @@ const authTransform = createTransform(
|
||||
const persistConfig = {
|
||||
key: "root",
|
||||
storage,
|
||||
whitielist: ["auth", "monitors", "pageSpeed", "ui"],
|
||||
whitelist: ["auth", "monitors", "pageSpeed", "ui", "settings"],
|
||||
transforms: [authTransform],
|
||||
};
|
||||
|
||||
@@ -30,6 +31,7 @@ const rootReducer = combineReducers({
|
||||
auth: authReducer,
|
||||
pageSpeedMonitors: pageSpeedMonitorReducer,
|
||||
ui: uiReducer,
|
||||
settings: settingsReducer,
|
||||
});
|
||||
|
||||
const persistedReducer = persistReducer(persistConfig, rootReducer);
|
||||
@@ -49,3 +51,4 @@ export const store = configureStore({
|
||||
});
|
||||
|
||||
export const persistor = persistStore(store);
|
||||
export default store;
|
||||
+7
-3
@@ -1,7 +1,11 @@
|
||||
*.sh
|
||||
!quickstart.sh
|
||||
!build_images.sh
|
||||
mongo/data/*
|
||||
redis/data/*
|
||||
dev/mongo/data/*
|
||||
dev/redis/data/*
|
||||
dist/mongo/data/*
|
||||
dist/redis/data/*
|
||||
prod/mongo/data/*
|
||||
prod/redis/data/*
|
||||
*.env
|
||||
certbot/*
|
||||
prod/certbot/*
|
||||
@@ -1,22 +0,0 @@
|
||||
FROM node:20-alpine as build
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY ./Client/package*.json ./
|
||||
|
||||
RUN npm install
|
||||
|
||||
COPY ./Client .
|
||||
|
||||
RUN npm run build
|
||||
|
||||
# RUN npm install -g serve
|
||||
|
||||
# CMD ["serve","-s", "dist", "-l", "5173"]
|
||||
|
||||
FROM nginx:1.27.1-alpine
|
||||
|
||||
# COPY ./Docker/nginx/default.conf /etc/nginx/conf.d/default.conf
|
||||
COPY --from=build /app/dist /usr/share/nginx/html
|
||||
|
||||
CMD ["nginx", "-g", "daemon off;"]
|
||||
@@ -2,19 +2,19 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Change directory to root Server directory for correct Docker Context
|
||||
cd ..
|
||||
cd ../..
|
||||
|
||||
#Client
|
||||
client="./Docker/client.Dockerfile"
|
||||
client="./Docker/dev/client.Dockerfile"
|
||||
|
||||
# MongoDB
|
||||
mongoDB="./Docker/mongoDB.Dockerfile"
|
||||
mongoDB="./Docker/dev/mongoDB.Dockerfile"
|
||||
|
||||
# Redis
|
||||
redis="./Docker/redis.Dockerfile"
|
||||
redis="./Docker/dev/redis.Dockerfile"
|
||||
|
||||
# Server
|
||||
server="./Docker/server.Dockerfile"
|
||||
server="./Docker/dev/server.Dockerfile"
|
||||
|
||||
docker build -f $client -t uptime_client .
|
||||
docker build -f $mongoDB -t uptime_database_mongo .
|
||||
@@ -0,0 +1,15 @@
|
||||
FROM node:20-alpine AS build
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY ../../Client/package*.json ./
|
||||
|
||||
RUN npm install
|
||||
|
||||
COPY ../../Client .
|
||||
|
||||
RUN npm run build
|
||||
|
||||
RUN npm install -g serve
|
||||
|
||||
CMD ["serve","-s", "dist", "-l", "5173"]
|
||||
@@ -0,0 +1,30 @@
|
||||
services:
|
||||
client:
|
||||
image: uptime_client:latest
|
||||
ports:
|
||||
- "5173:5173"
|
||||
|
||||
depends_on:
|
||||
- server
|
||||
server:
|
||||
image: uptime_server:latest
|
||||
ports:
|
||||
- "5000:5000"
|
||||
env_file:
|
||||
- server.env
|
||||
depends_on:
|
||||
- redis
|
||||
- mongodb
|
||||
redis:
|
||||
image: uptime_redis:latest
|
||||
ports:
|
||||
- "6379:6379"
|
||||
volumes:
|
||||
- ./redis/data:/data
|
||||
mongodb:
|
||||
image: uptime_database_mongo:latest
|
||||
command: ["mongod", "--quiet"]
|
||||
ports:
|
||||
- "27017:27017"
|
||||
volumes:
|
||||
- ./mongo/data:/data/db
|
||||
@@ -0,0 +1,3 @@
|
||||
FROM mongo
|
||||
EXPOSE 27017
|
||||
CMD ["mongod"]
|
||||
File renamed without changes.
@@ -0,0 +1,13 @@
|
||||
FROM node:20-alpine
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY ../../Server/package*.json ./
|
||||
|
||||
RUN npm install
|
||||
|
||||
COPY ../../Server/ ./
|
||||
|
||||
EXPOSE 5000
|
||||
|
||||
CMD ["node", "index.js"]
|
||||
+24
@@ -0,0 +1,24 @@
|
||||
|
||||
#!/bin/bash
|
||||
|
||||
# Change directory to root Server directory for correct Docker Context
|
||||
cd ../..
|
||||
|
||||
#Client
|
||||
client="./Docker/dist/client.Dockerfile"
|
||||
|
||||
# MongoDB
|
||||
mongoDB="./Docker/dist/mongoDB.Dockerfile"
|
||||
|
||||
# Redis
|
||||
redis="./Docker/dist/redis.Dockerfile"
|
||||
|
||||
# Server
|
||||
server="./Docker/dist/server.Dockerfile"
|
||||
|
||||
docker build -f $client -t dist_uptime_client .
|
||||
docker build -f $mongoDB -t dist_uptime_database_mongo .
|
||||
docker build -f $redis -t dist_uptime_redis .
|
||||
docker build -f $server -t dist_uptime_server .
|
||||
|
||||
echo "All images built"
|
||||
Vendored
+19
@@ -0,0 +1,19 @@
|
||||
FROM node:20-alpine AS build
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY ../../Client/package*.json ./
|
||||
|
||||
RUN npm install
|
||||
|
||||
COPY ../../Client .
|
||||
|
||||
RUN npm run build
|
||||
|
||||
|
||||
FROM nginx:1.27.1-alpine
|
||||
|
||||
COPY ./Docker/dist/nginx/conf.d/default.conf /etc/nginx/conf.d/default.conf
|
||||
COPY --from=build /app/dist /usr/share/nginx/html
|
||||
|
||||
CMD ["nginx", "-g", "daemon off;"]
|
||||
@@ -6,17 +6,16 @@ services:
|
||||
- "443:443"
|
||||
depends_on:
|
||||
- server
|
||||
volumes:
|
||||
- ./nginx/conf.d:/etc/nginx/conf.d/:ro
|
||||
server:
|
||||
image: bluewaveuptime/uptime_server:latest
|
||||
ports:
|
||||
- "5000:5000"
|
||||
env_file:
|
||||
- server.env
|
||||
depends_on:
|
||||
- redis
|
||||
- mongodb
|
||||
environment:
|
||||
- DB_CONNECTION_STRING=mongodb://mongodb:27017/uptime_db
|
||||
- REDIS_HOST=redis
|
||||
redis:
|
||||
image: bluewaveuptime/uptime_redis:latest
|
||||
ports:
|
||||
@@ -25,10 +24,8 @@ services:
|
||||
- ./redis/data:/data
|
||||
mongodb:
|
||||
image: bluewaveuptime/uptime_database_mongo:latest
|
||||
command: ["mongod", "--quiet", "--auth"]
|
||||
ports:
|
||||
- "27017:27017"
|
||||
volumes:
|
||||
- ./mongo/data:/data/db
|
||||
env_file:
|
||||
- mongo.env
|
||||
command: ["mongod", "--quiet"]
|
||||
ports:
|
||||
- "27017:27017"
|
||||
Vendored
+3
@@ -0,0 +1,3 @@
|
||||
FROM mongo
|
||||
EXPOSE 27017
|
||||
CMD ["mongod"]
|
||||
+35
@@ -0,0 +1,35 @@
|
||||
server {
|
||||
listen 80;
|
||||
listen [::]:80;
|
||||
|
||||
server_name uptime-demo.bluewavelabs.ca;
|
||||
server_tokens off;
|
||||
|
||||
location /.well-known/acme-challenge/ {
|
||||
root /var/www/certbot;
|
||||
}
|
||||
|
||||
location / {
|
||||
root /usr/share/nginx/html;
|
||||
index index.html index.htm;
|
||||
try_files $uri $uri/ /index.html;
|
||||
}
|
||||
|
||||
location /api/ {
|
||||
proxy_pass http://server:5000/api/;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
}
|
||||
|
||||
location /api-docs/ {
|
||||
proxy_pass http://server:5000/api-docs/;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
}
|
||||
}
|
||||
Vendored
+2
@@ -0,0 +1,2 @@
|
||||
FROM redis
|
||||
EXPOSE 6379
|
||||
Vendored
+13
@@ -0,0 +1,13 @@
|
||||
FROM node:20-alpine
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY ../../Server/package*.json ./
|
||||
|
||||
RUN npm install
|
||||
|
||||
COPY ../../Server/ ./
|
||||
|
||||
EXPOSE 5000
|
||||
|
||||
CMD ["node", "index.js"]
|
||||
@@ -1,4 +0,0 @@
|
||||
FROM mongo
|
||||
COPY ./Docker/mongo/init/create_users.js /docker-entrypoint-initdb.d/
|
||||
EXPOSE 27017
|
||||
CMD ["mongod"]
|
||||
Executable
+24
@@ -0,0 +1,24 @@
|
||||
|
||||
#!/bin/bash
|
||||
|
||||
# Change directory to root directory for correct Docker Context
|
||||
cd ../..
|
||||
|
||||
#Client
|
||||
client="./Docker/prod/client.Dockerfile"
|
||||
|
||||
# MongoDB
|
||||
mongoDB="./Docker/prod/mongoDB.Dockerfile"
|
||||
|
||||
# Redis
|
||||
redis="./Docker/prod/redis.Dockerfile"
|
||||
|
||||
# Server
|
||||
server="./Docker/prod/server.Dockerfile"
|
||||
|
||||
docker build -f $client -t uptime_client .
|
||||
docker build -f $mongoDB -t uptime_database_mongo .
|
||||
docker build -f $redis -t uptime_redis .
|
||||
docker build -f $server -t uptime_server .
|
||||
|
||||
echo "All images built"
|
||||
@@ -0,0 +1,17 @@
|
||||
FROM node:20-alpine AS build
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY ../../Client/package*.json ./
|
||||
|
||||
RUN npm install
|
||||
|
||||
COPY ../../Client .
|
||||
|
||||
RUN npm run build
|
||||
|
||||
FROM nginx:1.27.1-alpine
|
||||
|
||||
COPY --from=build /app/dist /usr/share/nginx/html
|
||||
|
||||
CMD ["nginx", "-g", "daemon off;"]
|
||||
@@ -40,4 +40,4 @@ services:
|
||||
- ./mongo/data:/data/db
|
||||
- ./mongo/init/create_users.js:/docker-entrypoint-initdb.d/create_users.js
|
||||
env_file:
|
||||
- mongo.env
|
||||
- mongo.env
|
||||
File renamed without changes.
@@ -0,0 +1,4 @@
|
||||
FROM mongo
|
||||
COPY ./Docker/prod/mongo/init/create_users.js /docker-entrypoint-initdb.d/
|
||||
EXPOSE 27017
|
||||
CMD ["mongod"]
|
||||
@@ -23,6 +23,15 @@ server {
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
}
|
||||
|
||||
location /api-docs/ {
|
||||
proxy_pass http://server:5000/api-docs/;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
}
|
||||
}
|
||||
|
||||
server {
|
||||
@@ -48,4 +57,13 @@ server {
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
}
|
||||
|
||||
location /api-docs/ {
|
||||
proxy_pass http://server:5000/api-docs/;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
FROM redis
|
||||
EXPOSE 6379
|
||||
File renamed without changes.
@@ -1,123 +0,0 @@
|
||||
default_server_base_url="http://localhost:5000/api/v1"
|
||||
default_client_host="http://localhost"
|
||||
default_jwt_secret="my_secret"
|
||||
default_db_type="MongoDB"
|
||||
default_redis_host="redis"
|
||||
default_redis_port=6379
|
||||
default_token_ttl="99d"
|
||||
|
||||
default_db_username="uptime_user"
|
||||
default_db_password="uptime_password"
|
||||
|
||||
default_system_email_host="smtp.gmail.com"
|
||||
default_system_email_port=465
|
||||
|
||||
|
||||
echo "Welcome to the Uptime Monitor Setup Script! \n"
|
||||
echo
|
||||
|
||||
echo "Configuring server"
|
||||
printf '%*s\n' "${COLUMNS:-$(tput cols)}" '' | tr ' ' '*'
|
||||
echo
|
||||
|
||||
db_type=$default_db_type
|
||||
redis_host=$default_redis_host
|
||||
redis_port=$default_redis_port
|
||||
jwt_secret=$default_jwt_secret
|
||||
|
||||
|
||||
read -p "Enter a username for your database [$default_db_username]: " db_username
|
||||
db_username="${db_username:-$default_db_username}"
|
||||
|
||||
read -p "Enter a password for your database [$default_db_password]: " db_password
|
||||
db_password="${db_password:-$default_db_password}"
|
||||
|
||||
read -p "Enter your system email host [$default_system_email_host]: " system_email_host
|
||||
system_email_host="${system_email_host:-$default_system_email_host}"
|
||||
echo "System email host: $system_email_host"
|
||||
echo
|
||||
|
||||
read -p "Enter your DB connection string [$default_db_connection_string]: " db_connection_string
|
||||
db_connection_string="mongodb://${db_username}:${db_password}@mongodb:27017/uptime_db"
|
||||
echo "DB connection string: $db_connection_string"
|
||||
echo
|
||||
|
||||
read -p "Enter your system email port [$default_system_email_port]: " system_email_port
|
||||
system_email_port="${system_email_port:-$default_system_email_port}"
|
||||
echo "System email port: $system_email_port"
|
||||
echo
|
||||
|
||||
|
||||
read -p "Enter your system email address: " system_email_address
|
||||
echo "System email address: $system_email_address"
|
||||
echo
|
||||
|
||||
read -p "Enter your system email password: " system_email_password
|
||||
echo "System email password: $system_email_password"
|
||||
echo
|
||||
|
||||
read -p "Enter your Token TTL [$default_token_ttl]: " token_ttl
|
||||
token_ttl="${token_ttl:-$default_token_ttl}"
|
||||
echo "Token TTL: $token_ttl"
|
||||
echo
|
||||
|
||||
read -p "Enter your Pagespeed API key: " pagespeed_api_key
|
||||
echo "Pagespeed API key: $pagespeed_api_key"
|
||||
echo
|
||||
|
||||
echo "Writing to ./server.env"
|
||||
echo
|
||||
|
||||
{
|
||||
echo "CLIENT_HOST=\"$client_host\""
|
||||
echo "JWT_SECRET=\"$jwt_secret\""
|
||||
echo "DB_TYPE=\"$db_type\""
|
||||
echo "DB_CONNECTION_STRING=\"$db_connection_string\""
|
||||
echo "REDIS_HOST=\"$redis_host\""
|
||||
echo "REDIS_PORT=$redis_port"
|
||||
echo "SYSTEM_EMAIL_HOST=\"$system_email_host\""
|
||||
echo "SYSTEM_EMAIL_PORT=$system_email_port"
|
||||
echo "SYSTEM_EMAIL_ADDRESS=\"$system_email_address\""
|
||||
echo "SYSTEM_EMAIL_PASSWORD=\"$system_email_password\""
|
||||
echo "TOKEN_TTL=\"$token_ttl\""
|
||||
echo "PAGESPEED_API_KEY=\"$pagespeed_api_key\""
|
||||
} > ./server.env
|
||||
|
||||
|
||||
echo
|
||||
{
|
||||
echo USERNAME_ENV_VAR=${db_username}
|
||||
echo PASSWORD_ENV_VAR=${db_password}
|
||||
} > ./mongo.env
|
||||
|
||||
mkdir -p ./nginx/conf.d/
|
||||
|
||||
|
||||
cat <<EOL > ./nginx/conf.d/default.conf
|
||||
server {
|
||||
listen 80;
|
||||
listen [::]:80;
|
||||
|
||||
server_name uptime-demo.bluewavelabs.ca;
|
||||
server_tokens off;
|
||||
|
||||
location /.well-known/acme-challenge/ {
|
||||
root /var/www/certbot;
|
||||
}
|
||||
|
||||
location / {
|
||||
root /usr/share/nginx/html;
|
||||
index index.html index.htm;
|
||||
try_files \$uri \$uri/ /index.html;
|
||||
}
|
||||
|
||||
location /api/ {
|
||||
proxy_pass http://server:5000/api/;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host \$host;
|
||||
proxy_set_header X-Real-IP \$remote_addr;
|
||||
proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto \$scheme;
|
||||
}
|
||||
}
|
||||
EOL
|
||||
@@ -2,3 +2,5 @@ node_modules
|
||||
.env
|
||||
*.log
|
||||
*.sh
|
||||
.nyc_output
|
||||
coverage
|
||||
@@ -0,0 +1,8 @@
|
||||
module.exports = {
|
||||
require: ["chai/register-expect.js"], // Include Chai's "expect" interface globally
|
||||
spec: "tests/**/*.test.js", // Specify test files
|
||||
timeout: 5000, // Set test-case timeout in milliseconds
|
||||
recursive: true, // Include subdirectories
|
||||
reporter: "spec", // Use the "spec" reporter
|
||||
exit: true, // Force Mocha to quit after tests complete
|
||||
};
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"all": true,
|
||||
"include": ["**/*.js"],
|
||||
"exclude": ["**/*.test.js"],
|
||||
"reporter": ["html", "text", "lcov"],
|
||||
"sourceMap": false,
|
||||
"instrument": true
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
const PORT = process.env.PORT || 5000;
|
||||
const PORT = 5000;
|
||||
|
||||
const connectDbAndRunServer = async (app, db) => {
|
||||
try {
|
||||
|
||||
@@ -1,192 +0,0 @@
|
||||
const logger = require("../utils/logger");
|
||||
require("dotenv").config();
|
||||
const {
|
||||
createAlertParamValidation,
|
||||
createAlertBodyValidation,
|
||||
getAlertsByUserIdParamValidation,
|
||||
getAlertsByMonitorIdParamValidation,
|
||||
getAlertByIdParamValidation,
|
||||
editAlertParamValidation,
|
||||
editAlertBodyValidation,
|
||||
deleteAlertParamValidation,
|
||||
} = require("../validation/joi");
|
||||
const { successMessages } = require("../utils/messages");
|
||||
const SERVICE_NAME = "alertController";
|
||||
|
||||
/**
|
||||
* Creates an alert for a monitor
|
||||
* @async
|
||||
* @param {Express.Request} req
|
||||
* @param {Express.Response} res
|
||||
* @returns {Promise<Express.Response>}
|
||||
* @throws {Error}
|
||||
*/
|
||||
const createAlert = async (req, res, next) => {
|
||||
try {
|
||||
await createAlertParamValidation.validateAsync(req.params);
|
||||
await createAlertBodyValidation.validateAsync(req.body);
|
||||
} catch (error) {
|
||||
error.status = 422;
|
||||
error.message =
|
||||
error.details?.[0]?.message || error.message || "Validation Error";
|
||||
error.service = SERVICE_NAME;
|
||||
next(error);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const alertData = { ...req.body };
|
||||
const alert = await req.db.createAlert(alertData);
|
||||
return res
|
||||
.status(200)
|
||||
.json({ success: true, msg: successMessages.ALERT_CREATE, data: alert });
|
||||
} catch (error) {
|
||||
error.service === undefined ? (error.service = SERVICE_NAME) : null;
|
||||
error.method === undefined ? (error.method = "createAlert") : null;
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Gets all alerts for a user
|
||||
* @async
|
||||
* @param {Express.Request} req
|
||||
* @param {Express.Response} res
|
||||
* @returns {Promise<Express.Response>}
|
||||
* @throws {Error}
|
||||
*/
|
||||
const getAlertsByUserId = async (req, res, next) => {
|
||||
try {
|
||||
await getAlertsByUserIdParamValidation.validateAsync(req.params);
|
||||
} catch (error) {
|
||||
error.status = 422;
|
||||
error.message =
|
||||
error.details?.[0]?.message || error.message || "Validation Error";
|
||||
error.service = SERVICE_NAME;
|
||||
next(error);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const alerts = await req.db.getAlertsByUserId(req.params.userId);
|
||||
return res.status(200).json({
|
||||
success: true,
|
||||
msg: successMessages.ALERT_GET_BY_USER,
|
||||
data: alerts,
|
||||
});
|
||||
} catch (error) {
|
||||
error.service = SERVICE_NAME;
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
|
||||
const getAlertsByMonitorId = async (req, res, next) => {
|
||||
try {
|
||||
await getAlertsByMonitorIdParamValidation.validateAsync(req.params);
|
||||
} catch (error) {
|
||||
error.status = 422;
|
||||
error.message =
|
||||
error.details?.[0]?.message || error.message || "Validation Error";
|
||||
error.service = SERVICE_NAME;
|
||||
next(error);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const alerts = await req.db.getAlertsByMonitorId(req.params.monitorId);
|
||||
return res.status(200).json({
|
||||
success: true,
|
||||
msg: successMessages.ALERT_GET_BY_MONITOR,
|
||||
data: alerts,
|
||||
});
|
||||
} catch (error) {
|
||||
error.service = SERVICE_NAME;
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
|
||||
const getAlertById = async (req, res, next) => {
|
||||
try {
|
||||
await getAlertByIdParamValidation.validateAsync(req.params);
|
||||
} catch (error) {
|
||||
error.status = 422;
|
||||
error.message =
|
||||
error.details?.[0]?.message || error.message || "Validation Error";
|
||||
error.service = SERVICE_NAME;
|
||||
next(error);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const alert = await req.db.getAlertById(req.params.alertId);
|
||||
return res.status(200).json({
|
||||
success: true,
|
||||
msg: successMessages.ALERT_GET_BY_ID,
|
||||
data: alert,
|
||||
});
|
||||
} catch (error) {
|
||||
error.service = SERVICE_NAME;
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
|
||||
const editAlert = async (req, res, next) => {
|
||||
try {
|
||||
await editAlertParamValidation.validateAsync(req.params);
|
||||
await editAlertBodyValidation.validateAsync(req.body);
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
error.status = 422;
|
||||
error.message =
|
||||
error.details?.[0]?.message || error.message || "Validation Error";
|
||||
error.service = SERVICE_NAME;
|
||||
next(error);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const alertData = { ...req.body };
|
||||
const alert = await req.db.editAlert(req.params.alertId, alertData);
|
||||
return res.status(200).json({
|
||||
success: true,
|
||||
msg: successMessages.ALERT_EDIT,
|
||||
data: alert,
|
||||
});
|
||||
} catch (error) {
|
||||
error.service = SERVICE_NAME;
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
const deleteAlert = async (req, res, next) => {
|
||||
try {
|
||||
await deleteAlertParamValidation.validateAsync(req.params);
|
||||
} catch (error) {
|
||||
error.status = 422;
|
||||
error.message =
|
||||
error.details?.[0]?.message || error.message || "Validation Error";
|
||||
error.service = SERVICE_NAME;
|
||||
next(error);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const deleted = await req.db.deleteAlert(req.params.alertId);
|
||||
return res.status(200).json({
|
||||
success: true,
|
||||
msg: successMessages.ALERT_DELETE,
|
||||
data: deleted,
|
||||
});
|
||||
} catch (error) {
|
||||
error.service = SERVICE_NAME;
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
createAlert,
|
||||
getAlertsByUserId,
|
||||
getAlertsByMonitorId,
|
||||
getAlertById,
|
||||
editAlert,
|
||||
deleteAlert,
|
||||
};
|
||||
@@ -1,4 +1,3 @@
|
||||
const express = require("express");
|
||||
const {
|
||||
registrationBodyValidation,
|
||||
loginValidation,
|
||||
@@ -7,67 +6,67 @@ const {
|
||||
recoveryValidation,
|
||||
recoveryTokenValidation,
|
||||
newPasswordValidation,
|
||||
deleteUserParamValidation,
|
||||
inviteRoleValidation,
|
||||
inviteBodyValidation,
|
||||
inviteVerifciationBodyValidation,
|
||||
} = require("../validation/joi");
|
||||
const logger = require("../utils/logger");
|
||||
require("dotenv").config();
|
||||
const { errorMessages, successMessages } = require("../utils/messages");
|
||||
var jwt = require("jsonwebtoken");
|
||||
const SERVICE_NAME = "auth";
|
||||
const jwt = require("jsonwebtoken");
|
||||
const SERVICE_NAME = "AuthController";
|
||||
const { getTokenFromHeaders } = require("../utils/utils");
|
||||
const crypto = require("crypto");
|
||||
const { handleValidationError, handleError } = require("./controllerUtils");
|
||||
|
||||
/**
|
||||
* Creates and returns JWT token with an arbitrary payload
|
||||
* @function
|
||||
* @param {Object} payload
|
||||
* @param {Object} appSettings
|
||||
* @returns {String}
|
||||
* @throws {Error}
|
||||
*/
|
||||
const issueToken = (payload) => {
|
||||
//TODO Add proper expiration date
|
||||
const tokenTTL = process.env.TOKEN_TTL ? process.env.TOKEN_TTL : "2h";
|
||||
return jwt.sign(payload, process.env.JWT_SECRET, { expiresIn: tokenTTL });
|
||||
const issueToken = (payload, appSettings) => {
|
||||
try {
|
||||
const tokenTTL = appSettings.jwtTTL ? appSettings.jwtTTL : "2h";
|
||||
return jwt.sign(payload, appSettings.jwtSecret, { expiresIn: tokenTTL });
|
||||
} catch (error) {
|
||||
throw handleError(error, SERVICE_NAME, "issueToken");
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* @function
|
||||
* @param {express.Request} req
|
||||
* @param {express.Response} res
|
||||
* @returns {{success: Boolean, msg: String}}
|
||||
* Registers a new user. If the user is the first account, a JWT secret is created. If not, an invite token is required.
|
||||
* @async
|
||||
* @param {Object} req - The Express request object.
|
||||
* @property {Object} req.body - The body of the request.
|
||||
* @property {string} req.body.inviteToken - The invite token for registration.
|
||||
* @property {Object} req.file - The file object for the user's profile image.
|
||||
* @param {Object} res - The Express response object.
|
||||
* @param {function} next - The next middleware function.
|
||||
* @returns {Object} The response object with a success status, a message indicating the creation of the user, the created user data, and a JWT token.
|
||||
* @throws {Error} If there is an error during the process, especially if there is a validation error (422).
|
||||
*/
|
||||
const registerController = async (req, res, next) => {
|
||||
const registerUser = async (req, res, next) => {
|
||||
// joi validation
|
||||
try {
|
||||
await registrationBodyValidation.validateAsync(req.body);
|
||||
} catch (error) {
|
||||
error.status = 422;
|
||||
error.service = SERVICE_NAME;
|
||||
error.message =
|
||||
error.details?.[0]?.message || error.message || "Validation Error";
|
||||
next(error);
|
||||
const validationError = handleValidationError(error, SERVICE_NAME);
|
||||
next(validationError);
|
||||
return;
|
||||
}
|
||||
|
||||
// TODO Can there be more than one admin?
|
||||
// // Check if an admin user exists, if so, error
|
||||
// try {
|
||||
// const admin = await req.db.checkAdmin(req, res);
|
||||
// console.log(admin);
|
||||
// if (admin === true) {
|
||||
// throw new Error(errorMessages.AUTH_ADMIN_EXISTS);
|
||||
// }
|
||||
// } catch (error) {
|
||||
// console.log("WEEEEEEE", error.message);
|
||||
// error.service = SERVICE_NAME;
|
||||
// error.status = 403;
|
||||
// next(error);
|
||||
// return;
|
||||
// }
|
||||
|
||||
// Create a new user
|
||||
try {
|
||||
const { inviteToken } = req.body;
|
||||
// If superAdmin exists, a token should be attached to all further register requests
|
||||
const superAdminExists = await req.db.checkSuperadmin(req, res);
|
||||
if (superAdminExists) {
|
||||
await req.db.getInviteTokenAndDelete(inviteToken);
|
||||
} else {
|
||||
// This is the first account, create JWT secret to use if one is not supplied by env
|
||||
const jwtSecret = crypto.randomBytes(64).toString("hex");
|
||||
await req.db.updateAppSettings({ jwtSecret });
|
||||
}
|
||||
|
||||
const newUser = await req.db.insertUser({ ...req.body }, req.file);
|
||||
logger.info(successMessages.AUTH_CREATE_USER, {
|
||||
service: SERVICE_NAME,
|
||||
@@ -78,8 +77,8 @@ const registerController = async (req, res, next) => {
|
||||
delete userForToken.profileImage;
|
||||
delete userForToken.avatarImage;
|
||||
|
||||
const token = issueToken(userForToken);
|
||||
|
||||
const appSettings = await req.settingsService.getSettings();
|
||||
const token = issueToken(userForToken, appSettings);
|
||||
req.emailService
|
||||
.buildAndSendEmail(
|
||||
"welcomeEmailTemplate",
|
||||
@@ -100,28 +99,28 @@ const registerController = async (req, res, next) => {
|
||||
data: { user: newUser, token: token },
|
||||
});
|
||||
} catch (error) {
|
||||
error.service === undefined ? (error.service = SERVICE_NAME) : null;
|
||||
error.method === undefined ? (error.method = "registerController") : null;
|
||||
next(error);
|
||||
next(handleError(error, SERVICE_NAME, "registerController"));
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns JWT with User info
|
||||
* Logs in a user by validating the user's credentials and issuing a JWT token.
|
||||
* @async
|
||||
* @param {Express.Request} req
|
||||
* @param {Express.Response} res
|
||||
* @returns {Promise<Express.Response>}
|
||||
* @param {Object} req - The Express request object.
|
||||
* @property {Object} req.body - The body of the request.
|
||||
* @property {string} req.body.email - The email of the user.
|
||||
* @property {string} req.body.password - The password of the user.
|
||||
* @param {Object} res - The Express response object.
|
||||
* @param {function} next - The next middleware function.
|
||||
* @returns {Object} The response object with a success status, a message indicating the login of the user, the user data (without password and avatar image), and a JWT token.
|
||||
* @throws {Error} If there is an error during the process, especially if there is a validation error (422) or the password is incorrect.
|
||||
*/
|
||||
const loginController = async (req, res, next) => {
|
||||
const loginUser = async (req, res, next) => {
|
||||
try {
|
||||
await loginValidation.validateAsync(req.body);
|
||||
} catch (error) {
|
||||
error.status = 422;
|
||||
error.service = SERVICE_NAME;
|
||||
error.message =
|
||||
error.details?.[0]?.message || error.message || "Validation Error";
|
||||
next(error);
|
||||
const validationError = handleValidationError(error, SERVICE_NAME);
|
||||
next(validationError);
|
||||
return;
|
||||
}
|
||||
try {
|
||||
@@ -132,7 +131,8 @@ const loginController = async (req, res, next) => {
|
||||
// Compare password
|
||||
const match = await user.comparePassword(password);
|
||||
if (match !== true) {
|
||||
throw new Error(errorMessages.AUTH_INCORRECT_PASSWORD);
|
||||
next(new Error(errorMessages.AUTH_INCORRECT_PASSWORD));
|
||||
return;
|
||||
}
|
||||
|
||||
// Remove password from user object. Should this be abstracted to DB layer?
|
||||
@@ -141,8 +141,8 @@ const loginController = async (req, res, next) => {
|
||||
delete userWithoutPassword.avatarImage;
|
||||
|
||||
// Happy path, return token
|
||||
const token = issueToken(userWithoutPassword);
|
||||
|
||||
const appSettings = await req.settingsService.getSettings();
|
||||
const token = issueToken(userWithoutPassword, appSettings);
|
||||
// reset avatar image
|
||||
userWithoutPassword.avatarImage = user.avatarImage;
|
||||
|
||||
@@ -152,23 +152,31 @@ const loginController = async (req, res, next) => {
|
||||
data: { user: userWithoutPassword, token: token },
|
||||
});
|
||||
} catch (error) {
|
||||
error.status = 500;
|
||||
error.service === undefined ? (error.service = SERVICE_NAME) : null;
|
||||
error.method === undefined ? (error.method = "loginController") : null;
|
||||
next(error);
|
||||
next(handleError(error, SERVICE_NAME, "loginController"));
|
||||
}
|
||||
};
|
||||
|
||||
const userEditController = async (req, res, next) => {
|
||||
/**
|
||||
* Edits a user's information. If the user wants to change their password, the current password is checked before updating to the new password.
|
||||
* @async
|
||||
* @param {Object} req - The Express request object.
|
||||
* @property {Object} req.params - The parameters of the request.
|
||||
* @property {string} req.params.userId - The ID of the user to be edited.
|
||||
* @property {Object} req.body - The body of the request.
|
||||
* @property {string} req.body.password - The current password of the user.
|
||||
* @property {string} req.body.newPassword - The new password of the user.
|
||||
* @param {Object} res - The Express response object.
|
||||
* @param {function} next - The next middleware function.
|
||||
* @returns {Object} The response object with a success status, a message indicating the update of the user, and the updated user data.
|
||||
* @throws {Error} If there is an error during the process, especially if there is a validation error (422), the user is unauthorized (401), or the password is incorrect (403).
|
||||
*/
|
||||
const editUser = async (req, res, next) => {
|
||||
try {
|
||||
await editUserParamValidation.validateAsync(req.params);
|
||||
await editUserBodyValidation.validateAsync(req.body);
|
||||
} catch (error) {
|
||||
error.status = 422;
|
||||
error.service = SERVICE_NAME;
|
||||
error.message =
|
||||
error.details?.[0]?.message || error.message || "Validation Error";
|
||||
next(error);
|
||||
const validationError = handleValidationError(error, SERVICE_NAME);
|
||||
next(validationError);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -184,14 +192,11 @@ const userEditController = async (req, res, next) => {
|
||||
try {
|
||||
// Change Password check
|
||||
if (req.body.password && req.body.newPassword) {
|
||||
const newPassword = req.body.newPassword;
|
||||
const password = req.body.password;
|
||||
|
||||
// Compare password
|
||||
// Get token from headers
|
||||
const token = getTokenFromHeaders(req.headers);
|
||||
// Get email from token
|
||||
const { email } = jwt.verify(token, process.env.JWT_SECRET);
|
||||
const { jwtSecret } = req.settingsService.getSettings();
|
||||
const { email } = jwt.verify(token, jwtSecret);
|
||||
// Add user email to body for DB operation
|
||||
req.body.email = email;
|
||||
// Get user
|
||||
@@ -202,7 +207,8 @@ const userEditController = async (req, res, next) => {
|
||||
if (!match) {
|
||||
const error = new Error(errorMessages.AUTH_INCORRECT_PASSWORD);
|
||||
error.status = 403;
|
||||
throw error;
|
||||
next(error);
|
||||
return;
|
||||
}
|
||||
// If a match, update the password
|
||||
req.body.password = req.body.newPassword;
|
||||
@@ -215,96 +221,20 @@ const userEditController = async (req, res, next) => {
|
||||
data: updatedUser,
|
||||
});
|
||||
} catch (error) {
|
||||
error.service === undefined ? (error.service = SERVICE_NAME) : null;
|
||||
error.method === undefined ? (error.method = "userEditController") : null;
|
||||
next(error);
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
const inviteController = async (req, res, next) => {
|
||||
try {
|
||||
// Only admins can invite
|
||||
const token = getTokenFromHeaders(req.headers);
|
||||
const { role, firstname, teamId } = jwt.decode(token);
|
||||
req.body.teamId = teamId;
|
||||
try {
|
||||
await inviteRoleValidation.validateAsync({ roles: role });
|
||||
await inviteBodyValidation.validateAsync(req.body);
|
||||
} catch (error) {
|
||||
error.status = 422;
|
||||
error.service = SERVICE_NAME;
|
||||
error.message =
|
||||
error.details?.[0]?.message || error.message || "Validation Error";
|
||||
next(error);
|
||||
return;
|
||||
}
|
||||
|
||||
const inviteToken = await req.db.requestInviteToken(req, res);
|
||||
req.emailService
|
||||
.buildAndSendEmail(
|
||||
"employeeActivationTemplate",
|
||||
{
|
||||
name: firstname,
|
||||
link: `${process.env.CLIENT_HOST}/register/${inviteToken.token}`,
|
||||
},
|
||||
req.body.email,
|
||||
"Welcome to Uptime Monitor"
|
||||
)
|
||||
.catch((error) => {
|
||||
logger.error("Error sending invite email", {
|
||||
service: SERVICE_NAME,
|
||||
error: error.message,
|
||||
});
|
||||
});
|
||||
|
||||
return res
|
||||
.status(200)
|
||||
.json({ success: true, msg: "Invite sent", data: inviteToken });
|
||||
} catch (error) {
|
||||
error.service === undefined ? (error.service = SERVICE_NAME) : null;
|
||||
error.method === undefined ? (error.method = "inviteController") : null;
|
||||
next(error);
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
const inviteVerifyController = async (req, res, next) => {
|
||||
try {
|
||||
await inviteVerifciationBodyValidation.validateAsync(req.body);
|
||||
} catch (error) {
|
||||
error.status = 422;
|
||||
error.service = SERVICE_NAME;
|
||||
error.message =
|
||||
error.details?.[0]?.message || error.message || "Validation Error";
|
||||
next(error);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const invite = await req.db.getInviteToken(req, res);
|
||||
res
|
||||
.status(200)
|
||||
.json({ status: "success", msg: "Invite verified", data: invite });
|
||||
} catch (error) {
|
||||
error.service === undefined ? (error.service = SERVICE_NAME) : null;
|
||||
error.method === undefined
|
||||
? (error.method = "inviteVerifyController")
|
||||
: null;
|
||||
next(error);
|
||||
return;
|
||||
next(handleError(error, SERVICE_NAME, "userEditController"));
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Checks to see if an admin account exists
|
||||
* Checks if a superadmin account exists in the database.
|
||||
* @async
|
||||
* @param {Express.Request} req
|
||||
* @param {Express.Response} res
|
||||
* @returns {Promise<Express.Response>}
|
||||
* @param {Object} req - The Express request object.
|
||||
* @param {Object} res - The Express response object.
|
||||
* @param {function} next - The next middleware function.
|
||||
* @returns {Object} The response object with a success status, a message indicating the existence of a superadmin, and a boolean indicating the existence of a superadmin.
|
||||
* @throws {Error} If there is an error during the process.
|
||||
*/
|
||||
|
||||
const checkSuperadminController = async (req, res) => {
|
||||
const checkSuperAdminExists = async (req, res, next) => {
|
||||
try {
|
||||
const superAdminExists = await req.db.checkSuperadmin(req, res);
|
||||
return res.status(200).json({
|
||||
@@ -313,33 +243,27 @@ const checkSuperadminController = async (req, res) => {
|
||||
data: superAdminExists,
|
||||
});
|
||||
} catch (error) {
|
||||
error.service === undefined ? (error.service = SERVICE_NAME) : null;
|
||||
error.method === undefined
|
||||
? (error.method = "checkSuperadminController")
|
||||
: null;
|
||||
next(error);
|
||||
return;
|
||||
next(handleError(error, SERVICE_NAME, "checkSuperadminController"));
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns a recovery token
|
||||
* Requests a recovery token for a user. The user's email is validated and a recovery token is created and sent via email.
|
||||
* @async
|
||||
* @param {Express.Request} req
|
||||
* @property {Object} req.body
|
||||
* @property {string} req.body.email
|
||||
* @property {EmailService} req.body.emailService
|
||||
* @returns {Promise<Express.Response>}
|
||||
* @param {Object} req - The Express request object.
|
||||
* @property {Object} req.body - The body of the request.
|
||||
* @property {string} req.body.email - The email of the user requesting recovery.
|
||||
* @param {Object} res - The Express response object.
|
||||
* @param {function} next - The next middleware function.
|
||||
* @returns {Object} The response object with a success status, a message indicating the creation of the recovery token, and the message ID of the sent email.
|
||||
* @throws {Error} If there is an error during the process, especially if there is a validation error (422).
|
||||
*/
|
||||
const recoveryRequestController = async (req, res, next) => {
|
||||
const requestRecovery = async (req, res, next) => {
|
||||
try {
|
||||
await recoveryValidation.validateAsync(req.body);
|
||||
} catch (error) {
|
||||
error.status = 422;
|
||||
error.service = SERVICE_NAME;
|
||||
error.message =
|
||||
error.details?.[0]?.message || error.message || "Validation Error";
|
||||
next(error);
|
||||
const validationError = handleValidationError(error, SERVICE_NAME);
|
||||
next(validationError);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -350,13 +274,18 @@ const recoveryRequestController = async (req, res, next) => {
|
||||
const recoveryToken = await req.db.requestRecoveryToken(req, res);
|
||||
const name = user.firstName;
|
||||
const email = req.body.email;
|
||||
const url = `${process.env.CLIENT_HOST}/set-new-password/${recoveryToken.token}`;
|
||||
const { clientHost } = req.settingsService.getSettings();
|
||||
const url = `${clientHost}/set-new-password/${recoveryToken.token}`;
|
||||
|
||||
const msgId = await req.emailService.buildAndSendEmail(
|
||||
"passwordResetTemplate",
|
||||
{ name, email, url },
|
||||
{
|
||||
name,
|
||||
email,
|
||||
url,
|
||||
},
|
||||
email,
|
||||
"Bluewaves Uptime Password Resest"
|
||||
"Bluewave Uptime Password Reset"
|
||||
);
|
||||
|
||||
return res.status(200).json({
|
||||
@@ -366,88 +295,73 @@ const recoveryRequestController = async (req, res, next) => {
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
error.service === undefined ? (error.service = SERVICE_NAME) : null;
|
||||
error.method === undefined
|
||||
? (error.method = "recoveryRequestController")
|
||||
: null;
|
||||
next(error);
|
||||
return;
|
||||
next(handleError(error, SERVICE_NAME, "recoveryRequestController"));
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns a recovery token
|
||||
* Validates a recovery token. The recovery token is validated and if valid, a success message is returned.
|
||||
* @async
|
||||
* @param {Express.Request} req
|
||||
* @property {Object} req.body
|
||||
* @property {string} req.body.token
|
||||
* @param {Express.Response} res
|
||||
* @returns {Promise<Express.Response>}
|
||||
* @param {Object} req - The Express request object.
|
||||
* @property {Object} req.body - The body of the request.
|
||||
* @property {string} req.body.token - The recovery token to be validated.
|
||||
* @param {Object} res - The Express response object.
|
||||
* @param {function} next - The next middleware function.
|
||||
* @returns {Object} The response object with a success status and a message indicating the validation of the recovery token.
|
||||
* @throws {Error} If there is an error during the process, especially if there is a validation error (422).
|
||||
*/
|
||||
const validateRecoveryTokenController = async (req, res, next) => {
|
||||
const validateRecovery = async (req, res, next) => {
|
||||
try {
|
||||
await recoveryTokenValidation.validateAsync(req.body);
|
||||
} catch (error) {
|
||||
error.status = 422;
|
||||
error.service = SERVICE_NAME;
|
||||
error.message =
|
||||
error.details?.[0]?.message || error.message || "Validation Error";
|
||||
next(error);
|
||||
const validationError = handleValidationError(error, SERVICE_NAME);
|
||||
next(validationError);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await req.db.validateRecoveryToken(req, res);
|
||||
// TODO Redirect user to reset password after validating token
|
||||
return res.status(200).json({
|
||||
success: true,
|
||||
msg: successMessages.AUTH_VERIFY_RECOVERY_TOKEN,
|
||||
});
|
||||
} catch (error) {
|
||||
error.service === undefined ? (error.service = SERVICE_NAME) : null;
|
||||
error.method === undefined
|
||||
? (error.method = "validateRecoveryTokenController")
|
||||
: null;
|
||||
next(error);
|
||||
return;
|
||||
next(handleError(error, SERVICE_NAME, "validateRecoveryTokenController"));
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns an updated user
|
||||
* Resets a user's password. The new password is validated and if valid, the user's password is updated in the database and a new JWT token is issued.
|
||||
* @async
|
||||
* @param {Express.Request} req
|
||||
* @property {Object} req.body
|
||||
* @property {string} req.body.token
|
||||
* @property {string} req.body.password
|
||||
* @param {Express.Response} res
|
||||
* @returns {Promise<Express.Response>}
|
||||
* @param {Object} req - The Express request object.
|
||||
* @property {Object} req.body - The body of the request.
|
||||
* @property {string} req.body.token - The recovery token.
|
||||
* @property {string} req.body.password - The new password of the user.
|
||||
* @param {Object} res - The Express response object.
|
||||
* @param {function} next - The next middleware function.
|
||||
* @returns {Object} The response object with a success status, a message indicating the reset of the password, the updated user data (without password and avatar image), and a new JWT token.
|
||||
* @throws {Error} If there is an error during the process, especially if there is a validation error (422).
|
||||
*/
|
||||
const resetPasswordController = async (req, res, next) => {
|
||||
const resetPassword = async (req, res, next) => {
|
||||
try {
|
||||
await newPasswordValidation.validateAsync(req.body);
|
||||
} catch (error) {
|
||||
error.status = 422;
|
||||
error.service = SERVICE_NAME;
|
||||
error.message =
|
||||
error.details?.[0]?.message || error.message || "Validation Error";
|
||||
next(error);
|
||||
const validationError = handleValidationError(error, SERVICE_NAME);
|
||||
next(validationError);
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const user = await req.db.resetPassword(req, res);
|
||||
const token = issueToken(user._doc);
|
||||
|
||||
const appSettings = await req.settingsService.getSettings();
|
||||
const token = issueToken(user._doc, appSettings);
|
||||
res.status(200).json({
|
||||
success: true,
|
||||
msg: successMessages.AUTH_RESET_PASSWORD,
|
||||
data: { user, token },
|
||||
});
|
||||
} catch (error) {
|
||||
error.service === undefined ? (error.service = SERVICE_NAME) : null;
|
||||
error.method === undefined
|
||||
? (error.method = "resetPasswordController")
|
||||
: null;
|
||||
next(error);
|
||||
next(handleError(error, SERVICE_NAME, "resetPasswordController"));
|
||||
}
|
||||
};
|
||||
|
||||
@@ -460,16 +374,17 @@ const resetPasswordController = async (req, res, next) => {
|
||||
* @returns {Object} The response object with success status and message.
|
||||
* @throws {Error} If user validation fails or user is not found in the database.
|
||||
*/
|
||||
const deleteUserController = async (req, res, next) => {
|
||||
const deleteUser = async (req, res, next) => {
|
||||
try {
|
||||
const token = getTokenFromHeaders(req.headers);
|
||||
const decodedToken = jwt.decode(token);
|
||||
const { _id, email } = decodedToken;
|
||||
const { email } = decodedToken;
|
||||
|
||||
// Check if the user exists
|
||||
const user = await req.db.getUserByEmail(email);
|
||||
if (!user) {
|
||||
throw new Error(errorMessages.DB_USER_NOT_FOUND);
|
||||
next(new Error(errorMessages.DB_USER_NOT_FOUND));
|
||||
return;
|
||||
}
|
||||
|
||||
// 1. Find all the monitors associated with the team ID if superadmin
|
||||
@@ -477,61 +392,56 @@ const deleteUserController = async (req, res, next) => {
|
||||
const result = await req.db.getMonitorsByTeamId({
|
||||
params: { teamId: user.teamId },
|
||||
});
|
||||
if (user.role.includes("superadmin") && result?.monitors.length > 0) {
|
||||
|
||||
if (user.role.includes("superadmin")) {
|
||||
//2. Remove all jobs, delete checks and alerts
|
||||
await Promise.all(
|
||||
monitors.map(async (monitor) => {
|
||||
await req.jobQueue.deleteJob(monitor);
|
||||
result?.monitors.length > 0 &&
|
||||
(await Promise.all(
|
||||
result.monitors.map(async (monitor) => {
|
||||
await req.jobQueue.deleteJob(monitor);
|
||||
await req.db.deleteChecks(monitor._id);
|
||||
await req.db.deletePageSpeedChecksByMonitorId(monitor._id);
|
||||
await req.db.deleteNotificationsByMonitorId(monitor._id);
|
||||
})
|
||||
));
|
||||
|
||||
await req.db.deleteChecks(monitor._id);
|
||||
await req.db.deleteAlertByMonitorId(monitor._id);
|
||||
await req.db.deletePageSpeedChecksByMonitorId(monitor._id);
|
||||
await req.db.deleteNotificationsByMonitorId(monitor._id);
|
||||
})
|
||||
);
|
||||
|
||||
// 3. Delete each monitor
|
||||
// 3. Delete team
|
||||
await req.db.deleteTeam(user.teamId);
|
||||
// 4. Delete all other team members
|
||||
await req.db.deleteAllOtherUsers();
|
||||
// 5. Delete each monitor
|
||||
await req.db.deleteMonitorsByUserId(user._id);
|
||||
}
|
||||
// 4. Delete the user by id
|
||||
// 6. Delete the user by id
|
||||
await req.db.deleteUser(user._id);
|
||||
|
||||
return res.status(200).json({
|
||||
success: true,
|
||||
msg: successMessages.AUTH_DELETE_USER,
|
||||
});
|
||||
} catch (error) {
|
||||
error.service === undefined ? (error.service = SERVICE_NAME) : null;
|
||||
error.method === undefined ? (error.method = "deleteUserController") : null;
|
||||
next(error);
|
||||
next(handleError(error, SERVICE_NAME, "deleteUserController"));
|
||||
}
|
||||
};
|
||||
|
||||
const getAllUsersController = async (req, res) => {
|
||||
const getAllUsers = async (req, res) => {
|
||||
try {
|
||||
const allUsers = await req.db.getAllUsers(req, res);
|
||||
res
|
||||
.status(200)
|
||||
.json({ success: true, msg: "Got all users", data: allUsers });
|
||||
} catch (error) {
|
||||
error.service === undefined ? (error.service = SERVICE_NAME) : null;
|
||||
error.method === undefined
|
||||
? (error.method = "getAllUsersController")
|
||||
: null;
|
||||
next(error);
|
||||
next(handleError(error, SERVICE_NAME, "getAllUsersController"));
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
registerController,
|
||||
loginController,
|
||||
userEditController,
|
||||
inviteController,
|
||||
inviteVerifyController,
|
||||
checkSuperadminController,
|
||||
recoveryRequestController,
|
||||
validateRecoveryTokenController,
|
||||
resetPasswordController,
|
||||
deleteUserController,
|
||||
getAllUsersController,
|
||||
registerUser,
|
||||
loginUser,
|
||||
editUser,
|
||||
checkSuperadminExists: checkSuperAdminExists,
|
||||
requestRecovery,
|
||||
validateRecovery,
|
||||
resetPassword,
|
||||
deleteUser,
|
||||
getAllUsers,
|
||||
};
|
||||
@@ -13,17 +13,14 @@ const { successMessages } = require("../utils/messages");
|
||||
const jwt = require("jsonwebtoken");
|
||||
const { getTokenFromHeaders } = require("../utils/utils");
|
||||
const SERVICE_NAME = "checkController";
|
||||
const { handleValidationError, handleError } = require("./controllerUtils");
|
||||
|
||||
const createCheck = async (req, res, next) => {
|
||||
try {
|
||||
await createCheckParamValidation.validateAsync(req.params);
|
||||
await createCheckBodyValidation.validateAsync(req.body);
|
||||
} catch (error) {
|
||||
error.status = 422;
|
||||
error.service = SERVICE_NAME;
|
||||
error.message =
|
||||
error.details?.[0]?.message || error.message || "Validation Error";
|
||||
next(error);
|
||||
next(handleValidationError(error, SERVICE_NAME));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -34,9 +31,7 @@ const createCheck = async (req, res, next) => {
|
||||
.status(200)
|
||||
.json({ success: true, msg: successMessages.CHECK_CREATE, data: check });
|
||||
} catch (error) {
|
||||
error.service === undefined ? (error.serivce = SERVICE_NAME) : null;
|
||||
error.method === undefined ? (error.method = "createCheck") : null;
|
||||
next(error);
|
||||
next(handleError(error, SERVICE_NAME, "createCheck"));
|
||||
}
|
||||
};
|
||||
|
||||
@@ -45,11 +40,7 @@ const getChecks = async (req, res, next) => {
|
||||
await getChecksParamValidation.validateAsync(req.params);
|
||||
await getChecksQueryValidation.validateAsync(req.query);
|
||||
} catch (error) {
|
||||
error.status = 422;
|
||||
error.service = SERVICE_NAME;
|
||||
error.message =
|
||||
error.details?.[0]?.message || error.message || "Validation Error";
|
||||
next(error);
|
||||
next(handleValidationError(error, SERVICE_NAME));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -62,9 +53,7 @@ const getChecks = async (req, res, next) => {
|
||||
data: { checksCount, checks },
|
||||
});
|
||||
} catch (error) {
|
||||
error.service === undefined ? (error.serivce = SERVICE_NAME) : null;
|
||||
error.method === undefined ? (error.method = "getChecks") : null;
|
||||
next(error);
|
||||
next(handleError(error, SERVICE_NAME, "getChecks"));
|
||||
}
|
||||
};
|
||||
|
||||
@@ -73,11 +62,7 @@ const getTeamChecks = async (req, res, next) => {
|
||||
await getTeamChecksParamValidation.validateAsync(req.params);
|
||||
await getTeamChecksQueryValidation.validateAsync(req.query);
|
||||
} catch (error) {
|
||||
error.status = 422;
|
||||
error.service = SERVICE_NAME;
|
||||
error.message =
|
||||
error.details?.[0]?.message || error.message || "Validation Error";
|
||||
next(error);
|
||||
next(handleValidationError(error, SERVICE_NAME));
|
||||
return;
|
||||
}
|
||||
try {
|
||||
@@ -88,9 +73,7 @@ const getTeamChecks = async (req, res, next) => {
|
||||
data: checkData,
|
||||
});
|
||||
} catch (error) {
|
||||
error.service === undefined ? (error.serivce = SERVICE_NAME) : null;
|
||||
error.method === undefined ? (error.method = "getTeamChecks") : null;
|
||||
next(error);
|
||||
next(handleError(error, SERVICE_NAME, "getTeamChecks"));
|
||||
}
|
||||
};
|
||||
|
||||
@@ -98,11 +81,7 @@ const deleteChecks = async (req, res, next) => {
|
||||
try {
|
||||
await deleteChecksParamValidation.validateAsync(req.params);
|
||||
} catch (error) {
|
||||
error.status = 422;
|
||||
error.service = SERVICE_NAME;
|
||||
error.message =
|
||||
error.details?.[0]?.message || error.message || "Validation Error";
|
||||
next(error);
|
||||
next(handleValidationError(error, SERVICE_NAME));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -114,9 +93,7 @@ const deleteChecks = async (req, res, next) => {
|
||||
data: { deletedCount },
|
||||
});
|
||||
} catch (error) {
|
||||
error.service === undefined ? (error.serivce = SERVICE_NAME) : null;
|
||||
error.method === undefined ? (error.method = "deleteChecks") : null;
|
||||
next(error);
|
||||
next(handleError(error, SERVICE_NAME, "deleteChecks"));
|
||||
}
|
||||
};
|
||||
|
||||
@@ -124,10 +101,7 @@ const deleteChecksByTeamId = async (req, res, next) => {
|
||||
try {
|
||||
await deleteChecksByTeamIdParamValidation.validateAsync(req.params);
|
||||
} catch (error) {
|
||||
error.service === undefined ? (error.service = SERVICE_NAME) : null;
|
||||
error.method === undefined ? (error.method = "deleteChecksByTeam") : null;
|
||||
error.status = 422;
|
||||
next(error);
|
||||
next(handleValidationError(error, SERVICE_NAME));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -139,9 +113,7 @@ const deleteChecksByTeamId = async (req, res, next) => {
|
||||
data: { deletedCount },
|
||||
});
|
||||
} catch (error) {
|
||||
error.service === undefined ? (error.service = SERVICE_NAME) : null;
|
||||
error.method === undefined ? (error.method = "deleteChecksByTeamId") : null;
|
||||
next(error);
|
||||
next(handleError(error, SERVICE_NAME, "deleteChecksByTeamId"));
|
||||
}
|
||||
};
|
||||
|
||||
@@ -151,19 +123,15 @@ const updateChecksTTL = async (req, res, next) => {
|
||||
try {
|
||||
await updateChecksTTLBodyValidation.validateAsync(req.body);
|
||||
} catch (error) {
|
||||
error.status = 422;
|
||||
error.service === undefined ? (error.service = SERVICE_NAME) : null;
|
||||
error.method === undefined ? (error.method = "updateChecksTTL") : null;
|
||||
error.message =
|
||||
error.details?.[0]?.message || error.message || "Validation Error";
|
||||
next(error);
|
||||
next(handleValidationError(error, SERVICE_NAME));
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// Get user's teamId
|
||||
const token = getTokenFromHeaders(req.headers);
|
||||
const { teamId } = jwt.verify(token, process.env.JWT_SECRET);
|
||||
const { jwtSecret } = req.settingsService.getSettings();
|
||||
const { teamId } = jwt.verify(token, jwtSecret);
|
||||
const ttl = parseInt(req.body.ttl, 10) * SECONDS_PER_DAY;
|
||||
await req.db.updateChecksTTL(teamId, ttl);
|
||||
return res.status(200).json({
|
||||
@@ -171,9 +139,7 @@ const updateChecksTTL = async (req, res, next) => {
|
||||
msg: successMessages.CHECK_UPDATE_TTL,
|
||||
});
|
||||
} catch (error) {
|
||||
error.service === undefined ? (error.service = SERVICE_NAME) : null;
|
||||
error.method === undefined ? (error.method = "updateTTL") : null;
|
||||
next(error);
|
||||
next(handleError(error, SERVICE_NAME, "updateTTL"));
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
const handleValidationError = (error, serviceName) => {
|
||||
error.status = 422;
|
||||
error.service = serviceName;
|
||||
error.message =
|
||||
error.details?.[0]?.message || error.message || "Validation Error";
|
||||
return error;
|
||||
};
|
||||
|
||||
const handleError = (error, serviceName, method, code = 500) => {
|
||||
error.code === undefined ? (error.code = code) : null;
|
||||
error.service === undefined ? (error.service = serviceName) : null;
|
||||
error.method === undefined ? (error.method = method) : null;
|
||||
return error;
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
handleValidationError,
|
||||
handleError,
|
||||
};
|
||||
@@ -1,12 +1,12 @@
|
||||
const {
|
||||
inviteRoleValidation,
|
||||
inviteBodyValidation,
|
||||
inviteVerifciationBodyValidation,
|
||||
inviteVerificationBodyValidation,
|
||||
} = require("../validation/joi");
|
||||
const logger = require("../utils/logger");
|
||||
require("dotenv").config();
|
||||
const { errorMessages, successMessages } = require("../utils/messages");
|
||||
var jwt = require("jsonwebtoken");
|
||||
const jwt = require("jsonwebtoken");
|
||||
const { handleError, handleValidationError } = require("./controllerUtils");
|
||||
const SERVICE_NAME = "inviteController";
|
||||
|
||||
const getTokenFromHeaders = (headers) => {
|
||||
@@ -20,7 +20,20 @@ const getTokenFromHeaders = (headers) => {
|
||||
return parts[1];
|
||||
};
|
||||
|
||||
const inviteController = async (req, res, next) => {
|
||||
/**
|
||||
* Issues an invitation to a new user. Only admins can invite new users. An invitation token is created and sent via email.
|
||||
* @async
|
||||
* @param {Object} req - The Express request object.
|
||||
* @property {Object} req.headers - The headers of the request.
|
||||
* @property {string} req.headers.authorization - The authorization header containing the JWT token.
|
||||
* @property {Object} req.body - The body of the request.
|
||||
* @property {string} req.body.email - The email of the user to be invited.
|
||||
* @param {Object} res - The Express response object.
|
||||
* @param {function} next - The next middleware function.
|
||||
* @returns {Object} The response object with a success status, a message indicating the sending of the invitation, and the invitation token.
|
||||
* @throws {Error} If there is an error during the process, especially if there is a validation error (422).
|
||||
*/
|
||||
const issueInvitation = async (req, res, next) => {
|
||||
try {
|
||||
// Only admins can invite
|
||||
const token = getTokenFromHeaders(req.headers);
|
||||
@@ -30,21 +43,18 @@ const inviteController = async (req, res, next) => {
|
||||
await inviteRoleValidation.validateAsync({ roles: role });
|
||||
await inviteBodyValidation.validateAsync(req.body);
|
||||
} catch (error) {
|
||||
error.status = 422;
|
||||
error.service = SERVICE_NAME;
|
||||
error.message =
|
||||
error.details?.[0]?.message || error.message || "Validation Error";
|
||||
next(error);
|
||||
next(handleValidationError(error, SERVICE_NAME));
|
||||
return;
|
||||
}
|
||||
|
||||
const inviteToken = await req.db.requestInviteToken({ ...req.body });
|
||||
const { clientHost } = req.settingsService.getSettings();
|
||||
req.emailService
|
||||
.buildAndSendEmail(
|
||||
"employeeActivationTemplate",
|
||||
{
|
||||
name: firstname,
|
||||
link: `${process.env.CLIENT_HOST}/register/${inviteToken.token}`,
|
||||
link: `${clientHost}/register/${inviteToken.token}`,
|
||||
},
|
||||
req.body.email,
|
||||
"Welcome to Uptime Monitor"
|
||||
@@ -60,22 +70,15 @@ const inviteController = async (req, res, next) => {
|
||||
.status(200)
|
||||
.json({ success: true, msg: "Invite sent", data: inviteToken });
|
||||
} catch (error) {
|
||||
error.service === undefined ? (error.service = SERVICE_NAME) : null;
|
||||
error.method === undefined ? (error.method = "inviteController") : null;
|
||||
next(error);
|
||||
return;
|
||||
next(handleError(error, SERVICE_NAME, "inviteController"));
|
||||
}
|
||||
};
|
||||
|
||||
const inviteVerifyController = async (req, res, next) => {
|
||||
try {
|
||||
await inviteVerifciationBodyValidation.validateAsync(req.body);
|
||||
await inviteVerificationBodyValidation.validateAsync(req.body);
|
||||
} catch (error) {
|
||||
error.status = 422;
|
||||
error.service = SERVICE_NAME;
|
||||
error.message =
|
||||
error.details?.[0]?.message || error.message || "Validation Error";
|
||||
next(error);
|
||||
next(handleValidationError(error, SERVICE_NAME));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -85,16 +88,11 @@ const inviteVerifyController = async (req, res, next) => {
|
||||
.status(200)
|
||||
.json({ status: "success", msg: "Invite verified", data: invite });
|
||||
} catch (error) {
|
||||
error.service === undefined ? (error.service = SERVICE_NAME) : null;
|
||||
error.method === undefined
|
||||
? (error.method = "inviteVerifyController")
|
||||
: null;
|
||||
next(error);
|
||||
return;
|
||||
next(handleError(error, SERVICE_NAME, "inviteVerifyController"));
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
inviteController,
|
||||
inviteController: issueInvitation,
|
||||
inviteVerifyController,
|
||||
};
|
||||
@@ -1,81 +1,96 @@
|
||||
const {
|
||||
createMaintenanceWindowParamValidation,
|
||||
createMaintenanceWindowBodyValidation,
|
||||
getMaintenanceWindowsByUserIdParamValidation,
|
||||
editMaintenanceWindowByIdParamValidation,
|
||||
editMaintenanceByIdWindowBodyValidation,
|
||||
getMaintenanceWindowByIdParamValidation,
|
||||
getMaintenanceWindowsByMonitorIdParamValidation,
|
||||
getMaintenanceWindowsByTeamIdQueryValidation,
|
||||
deleteMaintenanceWindowByIdParamValidation,
|
||||
} = require("../validation/joi");
|
||||
|
||||
const jwt = require("jsonwebtoken");
|
||||
const { getTokenFromHeaders } = require("../utils/utils");
|
||||
const { successMessages } = require("../utils/messages");
|
||||
const { handleValidationError, handleError } = require("./controllerUtils");
|
||||
const SERVICE_NAME = "maintenanceWindowController";
|
||||
const { errorMessages, successMessages } = require("../utils/messages");
|
||||
|
||||
const createMaintenanceWindow = async (req, res, next) => {
|
||||
const createMaintenanceWindows = async (req, res, next) => {
|
||||
try {
|
||||
await createMaintenanceWindowParamValidation.validateAsync(req.params);
|
||||
await createMaintenanceWindowBodyValidation.validateAsync(req.body);
|
||||
} catch (error) {
|
||||
error.status = 422;
|
||||
error.service = SERVICE_NAME;
|
||||
error.message =
|
||||
error.details?.[0]?.message || error.message || "Validation Error";
|
||||
next(error);
|
||||
next(handleValidationError(error, SERVICE_NAME));
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const data = {
|
||||
monitorId: req.params.monitorId,
|
||||
...req.body,
|
||||
};
|
||||
|
||||
if (data.oneTime === true) {
|
||||
data.expiry = data.end;
|
||||
}
|
||||
|
||||
const maintenanceWindow = await req.db.createMaintenanceWindow(data);
|
||||
|
||||
const token = getTokenFromHeaders(req.headers);
|
||||
const { jwtSecret } = req.settingsService.getSettings();
|
||||
const { teamId } = jwt.verify(token, jwtSecret);
|
||||
const monitorIds = req.body.monitors;
|
||||
const dbTransactions = monitorIds.map((monitorId) => {
|
||||
return req.db.createMaintenanceWindow({
|
||||
teamId,
|
||||
monitorId,
|
||||
name: req.body.name,
|
||||
active: req.body.active ? req.body.active : true,
|
||||
repeat: req.body.repeat,
|
||||
start: req.body.start,
|
||||
end: req.body.end,
|
||||
});
|
||||
});
|
||||
await Promise.all(dbTransactions);
|
||||
return res.status(201).json({
|
||||
success: true,
|
||||
msg: successMessages.MAINTENANCE_WINDOW_CREATE,
|
||||
data: maintenanceWindow,
|
||||
});
|
||||
} catch (error) {
|
||||
error.service === undefined ? (error.service = SERVICE_NAME) : null;
|
||||
error.method === undefined
|
||||
? (error.method = "createMaintenanceWindow")
|
||||
: null;
|
||||
next(error);
|
||||
next(handleError(error, SERVICE_NAME, "createMaintenanceWindow"));
|
||||
}
|
||||
};
|
||||
|
||||
const getMaintenanceWindowsByUserId = async (req, res, next) => {
|
||||
const getMaintenanceWindowById = async (req, res, next) => {
|
||||
try {
|
||||
await getMaintenanceWindowsByUserIdParamValidation.validateAsync(
|
||||
req.params
|
||||
);
|
||||
await getMaintenanceWindowByIdParamValidation.validateAsync(req.params);
|
||||
} catch (error) {
|
||||
error.status = 422;
|
||||
error.service = SERVICE_NAME;
|
||||
error.message =
|
||||
error.details?.[0]?.message || error.message || "Validation Error";
|
||||
next(error);
|
||||
next(handleValidationError(error, SERVICE_NAME));
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const maintenanceWindows = await req.db.getMaintenanceWindowsByUserId(
|
||||
req.params.userId
|
||||
const maintenanceWindow = await req.db.getMaintenanceWindowById(
|
||||
req.params.id
|
||||
);
|
||||
return res.status(200).json({
|
||||
success: true,
|
||||
msg: successMessages.MAINTENANCE_WINDOW_GET_BY_ID,
|
||||
data: maintenanceWindow,
|
||||
});
|
||||
} catch (error) {
|
||||
next(handleError(error, SERVICE_NAME, "getMaintenanceWindowById"));
|
||||
}
|
||||
};
|
||||
|
||||
const getMaintenanceWindowsByTeamId = async (req, res, next) => {
|
||||
try {
|
||||
await getMaintenanceWindowsByTeamIdQueryValidation.validateAsync(req.query);
|
||||
} catch (error) {
|
||||
next(handleValidationError(error, SERVICE_NAME));
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const token = getTokenFromHeaders(req.headers);
|
||||
const { jwtSecret } = req.settingsService.getSettings();
|
||||
const { teamId } = jwt.verify(token, jwtSecret);
|
||||
const maintenanceWindows = await req.db.getMaintenanceWindowsByTeamId(
|
||||
teamId,
|
||||
req.query
|
||||
);
|
||||
|
||||
return res.status(201).json({
|
||||
success: true,
|
||||
msg: successMessages.MAINTEANCE_WINDOW_GET_BY_USER,
|
||||
msg: successMessages.MAINTENANCE_WINDOW_GET_BY_TEAM,
|
||||
data: maintenanceWindows,
|
||||
});
|
||||
} catch (error) {
|
||||
error.service === undefined ? (error.service = SERVICE_NAME) : null;
|
||||
error.method === undefined
|
||||
? (error.method = "getMaintenanceWindowsByUserId")
|
||||
: null;
|
||||
next(error);
|
||||
next(handleError(error, SERVICE_NAME, "getMaintenanceWindowsByUserId"));
|
||||
}
|
||||
};
|
||||
|
||||
@@ -85,11 +100,7 @@ const getMaintenanceWindowsByMonitorId = async (req, res, next) => {
|
||||
req.params
|
||||
);
|
||||
} catch (error) {
|
||||
error.status = 422;
|
||||
error.service = SERVICE_NAME;
|
||||
error.message =
|
||||
error.details?.[0]?.message || error.message || "Validation Error";
|
||||
next(error);
|
||||
next(handleValidationError(error, SERVICE_NAME));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -100,19 +111,60 @@ const getMaintenanceWindowsByMonitorId = async (req, res, next) => {
|
||||
|
||||
return res.status(201).json({
|
||||
success: true,
|
||||
msg: successMessages.MAINTEANCE_WINDOW_GET_BY_USER,
|
||||
msg: successMessages.MAINTENANCE_WINDOW_GET_BY_USER,
|
||||
data: maintenanceWindows,
|
||||
});
|
||||
} catch (error) {
|
||||
error.service === undefined ? (error.service = SERVICE_NAME) : null;
|
||||
error.method === undefined
|
||||
? (error.method = "getMaintenanceWindowsByMonitorId")
|
||||
: null;
|
||||
next(error);
|
||||
next(handleError(error, SERVICE_NAME, "getMaintenanceWindowsByMonitorId"));
|
||||
}
|
||||
};
|
||||
module.exports = {
|
||||
createMaintenanceWindow,
|
||||
getMaintenanceWindowsByUserId,
|
||||
getMaintenanceWindowsByMonitorId,
|
||||
|
||||
const deleteMaintenanceWindow = async (req, res, next) => {
|
||||
try {
|
||||
await deleteMaintenanceWindowByIdParamValidation.validateAsync(req.params);
|
||||
} catch (error) {
|
||||
next(handleValidationError(error, SERVICE_NAME));
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await req.db.deleteMaintenanceWindowById(req.params.id);
|
||||
return res.status(201).json({
|
||||
success: true,
|
||||
msg: successMessages.MAINTENANCE_WINDOW_DELETE,
|
||||
});
|
||||
} catch (error) {
|
||||
next(handleError(error, SERVICE_NAME, "deleteMaintenanceWindow"));
|
||||
}
|
||||
};
|
||||
|
||||
const editMaintenanceWindow = async (req, res, next) => {
|
||||
try {
|
||||
await editMaintenanceWindowByIdParamValidation.validateAsync(req.params);
|
||||
await editMaintenanceByIdWindowBodyValidation.validateAsync(req.body);
|
||||
} catch (error) {
|
||||
next(handleValidationError(error, SERVICE_NAME));
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const editedMaintenanceWindow = await req.db.editMaintenanceWindowById(
|
||||
req.params.id,
|
||||
req.body
|
||||
);
|
||||
return res.status(200).json({
|
||||
success: true,
|
||||
msg: successMessages.MAINTENANCE_WINDOW_EDIT,
|
||||
data: editedMaintenanceWindow,
|
||||
});
|
||||
} catch (error) {
|
||||
next(handleError(error, SERVICE_NAME, "editMaintenanceWindow"));
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
createMaintenanceWindows,
|
||||
getMaintenanceWindowById,
|
||||
getMaintenanceWindowsByTeamId,
|
||||
getMaintenanceWindowsByMonitorId,
|
||||
deleteMaintenanceWindow,
|
||||
editMaintenanceWindow,
|
||||
};
|
||||
@@ -11,8 +11,6 @@ const {
|
||||
getMonitorStatsByIdParamValidation,
|
||||
getMonitorStatsByIdQueryValidation,
|
||||
getCertificateParamValidation,
|
||||
getMonitorAggregateStatsParamValidation,
|
||||
getMonitorAggregateStatsQueryValidation,
|
||||
} = require("../validation/joi");
|
||||
|
||||
const sslChecker = require("ssl-checker");
|
||||
@@ -20,12 +18,15 @@ const SERVICE_NAME = "monitorController";
|
||||
const { errorMessages, successMessages } = require("../utils/messages");
|
||||
const jwt = require("jsonwebtoken");
|
||||
const { getTokenFromHeaders } = require("../utils/utils");
|
||||
const logger = require("../utils/logger");
|
||||
const { handleError, handleValidationError } = require("./controllerUtils");
|
||||
|
||||
/**
|
||||
* Returns all monitors
|
||||
* @async
|
||||
* @param {Express.Request} req
|
||||
* @param {Express.Response} res
|
||||
* @param {function} next
|
||||
* @returns {Promise<Express.Response>}
|
||||
* @throws {Error}
|
||||
*/
|
||||
@@ -38,51 +39,7 @@ const getAllMonitors = async (req, res, next) => {
|
||||
data: monitors,
|
||||
});
|
||||
} catch (error) {
|
||||
error.service === undefined ? (error.service = SERVICE_NAME) : null;
|
||||
error.method === undefined ? (error.method = "getAllMonitors") : null;
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns agregate stats for a monitor
|
||||
* @async
|
||||
* @param {Express.Request} req
|
||||
* @param {Express.Response} res
|
||||
* @returns {Promise<Express.Response>}
|
||||
* @throws {Error}
|
||||
*/
|
||||
|
||||
const getMonitorAggregateStats = async (req, res, next) => {
|
||||
try {
|
||||
await getMonitorAggregateStatsParamValidation.validateAsync(req.params);
|
||||
await getMonitorAggregateStatsQueryValidation.validateAsync(req.query);
|
||||
} catch (error) {
|
||||
error.status = 422;
|
||||
error.message =
|
||||
error.details?.[0]?.message || error.message || "Validation Error";
|
||||
next(error);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const { monitorId } = req.params;
|
||||
const dateRange = req.query.dateRange;
|
||||
const aggregateStats = await req.db.getMonitorAggregateStats(
|
||||
monitorId,
|
||||
dateRange
|
||||
);
|
||||
return res.json({
|
||||
success: true,
|
||||
msg: successMessages.MONTIOR_STATS_BY_ID,
|
||||
data: aggregateStats,
|
||||
});
|
||||
} catch (error) {
|
||||
error.service === undefined ? (error.service = SERVICE_NAME) : null;
|
||||
error.method === undefined
|
||||
? (error.method = "getMonitorAggregateStats")
|
||||
: null;
|
||||
next(error);
|
||||
next(handleError(error, SERVICE_NAME, "getAllMonitors"));
|
||||
}
|
||||
};
|
||||
|
||||
@@ -91,6 +48,7 @@ const getMonitorAggregateStats = async (req, res, next) => {
|
||||
* @async
|
||||
* @param {Express.Request} req
|
||||
* @param {Express.Response} res
|
||||
* @param {function} next
|
||||
* @returns {Promise<Express.Response>}
|
||||
* @throws {Error}
|
||||
*/
|
||||
@@ -99,10 +57,7 @@ const getMonitorStatsById = async (req, res, next) => {
|
||||
await getMonitorStatsByIdParamValidation.validateAsync(req.params);
|
||||
await getMonitorStatsByIdQueryValidation.validateAsync(req.query);
|
||||
} catch (error) {
|
||||
error.status = 422;
|
||||
error.message =
|
||||
error.details?.[0]?.message || error.message || "Validation Error";
|
||||
next(error);
|
||||
next(handleValidationError(error, SERVICE_NAME));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -110,13 +65,11 @@ const getMonitorStatsById = async (req, res, next) => {
|
||||
const monitorStats = await req.db.getMonitorStatsById(req);
|
||||
return res.json({
|
||||
success: true,
|
||||
msg: successMessages.MONTIOR_STATS_BY_ID,
|
||||
msg: successMessages.MONITOR_STATS_BY_ID,
|
||||
data: monitorStats,
|
||||
});
|
||||
} catch (error) {
|
||||
error.service === undefined ? (error.service = SERVICE_NAME) : null;
|
||||
error.method === undefined ? (error.method = "getMonitorStatsById") : null;
|
||||
next(error);
|
||||
next(handleError(error, SERVICE_NAME, "getMonitorStatsById"));
|
||||
}
|
||||
};
|
||||
|
||||
@@ -124,10 +77,7 @@ const getMonitorCertificate = async (req, res, next) => {
|
||||
try {
|
||||
await getCertificateParamValidation.validateAsync(req.params);
|
||||
} catch (error) {
|
||||
error.status = 422;
|
||||
error.message =
|
||||
error.details?.[0]?.message || error.message || "Validation Error";
|
||||
next(error);
|
||||
next(handleValidationError(error, SERVICE_NAME));
|
||||
}
|
||||
|
||||
try {
|
||||
@@ -151,31 +101,27 @@ const getMonitorCertificate = async (req, res, next) => {
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
error.service === undefined ? (error.service = SERVICE_NAME) : null;
|
||||
error.method === undefined
|
||||
? (error.method = "getMonitorCertificate")
|
||||
: null;
|
||||
next(error);
|
||||
next(handleError(error, SERVICE_NAME, "getMonitorCertificate"));
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns monitor with matching ID
|
||||
* Retrieves a monitor by its ID.
|
||||
* @async
|
||||
* @param {Express.Request} req
|
||||
* @param {Express.Response} res
|
||||
* @returns {Promise<Express.Response>}
|
||||
* @throws {Error}
|
||||
* @param {Object} req - The Express request object.
|
||||
* @property {Object} req.params - The parameters of the request.
|
||||
* @property {string} req.params.monitorId - The ID of the monitor to be retrieved.
|
||||
* @param {Object} res - The Express response object.
|
||||
* @param {function} next - The next middleware function.
|
||||
* @returns {Object} The response object with a success status, a message, and the retrieved monitor data.
|
||||
* @throws {Error} If there is an error during the process, especially if the monitor is not found (404) or if there is a validation error (422).
|
||||
*/
|
||||
const getMonitorById = async (req, res, next) => {
|
||||
try {
|
||||
await getMonitorByIdParamValidation.validateAsync(req.params);
|
||||
await getMonitorByIdQueryValidation.validateAsync(req.query);
|
||||
} catch (error) {
|
||||
error.status = 422;
|
||||
error.message =
|
||||
error.details?.[0]?.message || error.message || "Validation Error";
|
||||
next(error);
|
||||
next(handleValidationError(error, SERVICE_NAME));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -184,45 +130,39 @@ const getMonitorById = async (req, res, next) => {
|
||||
if (!monitor) {
|
||||
const error = new Error(errorMessages.MONITOR_GET_BY_ID);
|
||||
error.status = 404;
|
||||
throw error;
|
||||
}
|
||||
|
||||
return res.json({
|
||||
success: true,
|
||||
msg: successMessages.MONTIOR_GET_BY_ID,
|
||||
msg: successMessages.MONITOR_GET_BY_ID,
|
||||
data: monitor,
|
||||
});
|
||||
} catch (error) {
|
||||
error.service === undefined ? (error.service = SERVICE_NAME) : null;
|
||||
error.method === undefined ? (error.method = "getMonitorById") : null;
|
||||
next(error);
|
||||
next(handleError(error, SERVICE_NAME, "getMonitorById"));
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns all monitors and a sumamry for a team with TeamID
|
||||
* Retrieves all monitors and a summary for a team based on the team ID.
|
||||
* @async
|
||||
* @param {Express.Request} req
|
||||
* @param {Express.Response} res
|
||||
* @returns {Promise<Express.Response>}
|
||||
* @throws {Error}
|
||||
* @param {Object} req - The Express request object.
|
||||
* @property {Object} req.params - The parameters of the request.
|
||||
* @property {string} req.params.teamId - The ID of the team.
|
||||
* @property {Object} req.query - The query parameters of the request.
|
||||
* @property {string} req.query.type - The type of the request.
|
||||
* @param {Object} res - The Express response object.
|
||||
* @param {function} next - The next middleware function.
|
||||
* @returns {Object} The response object with a success status, a message, and the data containing the monitors and summary for the team.
|
||||
* @throws {Error} If there is an error during the process, especially if there is a validation error (422).
|
||||
*/
|
||||
|
||||
const getMonitorsAndSummaryByTeamId = async (req, res, next) => {
|
||||
try {
|
||||
await getMonitorsAndSummaryByTeamIdParamValidation.validateAsync(
|
||||
req.params
|
||||
);
|
||||
await getMonitorsAndSummaryByTeamIdQueryValidation.validateAsync(req.query);
|
||||
//validation
|
||||
} catch (error) {
|
||||
error.status = 422;
|
||||
error.service = SERVICE_NAME;
|
||||
error.method === undefined &&
|
||||
error.method === "getMonitorsAndSummaryByTeamId";
|
||||
error.message =
|
||||
error.details?.[0]?.message || error.message || "Validation Error";
|
||||
next(error);
|
||||
next(handleValidationError(error, SERVICE_NAME));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -239,32 +179,28 @@ const getMonitorsAndSummaryByTeamId = async (req, res, next) => {
|
||||
data: monitorsSummary,
|
||||
});
|
||||
} catch (error) {
|
||||
error.service === undefined ? (error.service = SERVICE_NAME) : null;
|
||||
error.method === undefined
|
||||
? (error.method = "getMonitorsAndSummaryByTeamId")
|
||||
: null;
|
||||
next(error);
|
||||
next(handleError(error, SERVICE_NAME, "getMonitorsAndSummaryByTeamId"));
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns all monitors belong to team with TeamID
|
||||
* Retrieves all monitors associated with a team by the team's ID.
|
||||
* @async
|
||||
* @param {Express.Request} req
|
||||
* @param {Express.Response} res
|
||||
* @returns {Promise<Express.Response>}
|
||||
* @throws {Error}
|
||||
* @param {Object} req - The Express request object.
|
||||
* @property {Object} req.params - The parameters of the request.
|
||||
* @property {string} req.params.teamId - The ID of the team.
|
||||
* @property {Object} req.query - The query parameters of the request.
|
||||
* @param {Object} res - The Express response object.
|
||||
* @param {function} next - The next middleware function.
|
||||
* @returns {Object} The response object with a success status, a message, and the data containing the monitors for the team.
|
||||
* @throws {Error} If there is an error during the process, especially if there is a validation error (422).
|
||||
*/
|
||||
const getMonitorsByTeamId = async (req, res, next) => {
|
||||
try {
|
||||
await getMonitorsByTeamIdValidation.validateAsync(req.params);
|
||||
await getMonitorsByTeamIdQueryValidation.validateAsync(req.query);
|
||||
} catch (error) {
|
||||
error.status = 422;
|
||||
error.service = SERVICE_NAME;
|
||||
error.message =
|
||||
error.details?.[0]?.message || error.message || "Validation Error";
|
||||
next(error);
|
||||
next(handleValidationError(error, SERVICE_NAME));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -277,31 +213,27 @@ const getMonitorsByTeamId = async (req, res, next) => {
|
||||
data: monitors,
|
||||
});
|
||||
} catch (error) {
|
||||
error.service === undefined ? (error.service = SERVICE_NAME) : null;
|
||||
error.method === undefined ? (error.method = "getMonitorsByTeamId") : null;
|
||||
next(handleError(error, SERVICE_NAME, "getMonitorsByTeamId"));
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Creates a new monitor
|
||||
* Creates a new monitor and adds it to the job queue.
|
||||
* @async
|
||||
* @param {Express.Request} req
|
||||
* @param {Express.Response} res
|
||||
* @returns {Promise<Express.Response>}
|
||||
* @throws {Error}
|
||||
* @param {Object} req - The Express request object.
|
||||
* @property {Object} req.body - The body of the request.
|
||||
* @property {Array} req.body.notifications - The notifications associated with the monitor.
|
||||
* @param {Object} res - The Express response object.
|
||||
* @param {function} next - The next middleware function.
|
||||
* @returns {Object} The response object with a success status, a message indicating the creation of the monitor, and the created monitor data.
|
||||
* @throws {Error} If there is an error during the process, especially if there is a validation error (422).
|
||||
*/
|
||||
|
||||
const createMonitor = async (req, res, next) => {
|
||||
try {
|
||||
await createMonitorBodyValidation.validateAsync(req.body);
|
||||
} catch (error) {
|
||||
error.status = 422;
|
||||
error.service = SERVICE_NAME;
|
||||
error.message =
|
||||
error.details?.[0]?.message || error.message || "Validation Error";
|
||||
|
||||
next(error);
|
||||
next(handleValidationError(error, SERVICE_NAME));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -310,13 +242,12 @@ const createMonitor = async (req, res, next) => {
|
||||
const monitor = await req.db.createMonitor(req, res);
|
||||
|
||||
if (notifications && notifications.length !== 0) {
|
||||
const setNotifications = await Promise.all(
|
||||
monitor.notifications = await Promise.all(
|
||||
notifications.map(async (notification) => {
|
||||
notification.monitorId = monitor._id;
|
||||
await req.db.createNotification(notification);
|
||||
})
|
||||
);
|
||||
monitor.notifications = setNotifications;
|
||||
await monitor.save();
|
||||
}
|
||||
// Add monitor to job queue
|
||||
@@ -327,62 +258,71 @@ const createMonitor = async (req, res, next) => {
|
||||
data: monitor,
|
||||
});
|
||||
} catch (error) {
|
||||
error.service === undefined ? (error.service = SERVICE_NAME) : null;
|
||||
error.method === undefined ? (error.method = "createMonitor") : null;
|
||||
next(error);
|
||||
next(handleError(error, SERVICE_NAME, "createMonitor"));
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Delete a monitor by ID
|
||||
* Deletes a monitor by its ID and also deletes associated checks, alerts, and notifications.
|
||||
* @async
|
||||
* @param {Express.Request} req
|
||||
* @param {Express.Response} res
|
||||
* @returns {Promise<Express.Response>}
|
||||
* @throws {Error}
|
||||
* @param {Object} req - The Express request object.
|
||||
* @property {Object} req.params - The parameters of the request.
|
||||
* @property {string} req.params.monitorId - The ID of the monitor to be deleted.
|
||||
* @param {Object} res - The Express response object.
|
||||
* @param {function} next - The next middleware function.
|
||||
* @returns {Object} The response object with a success status and a message indicating the deletion of the monitor.
|
||||
* @throws {Error} If there is an error during the process, especially if there is a validation error (422) or an error in deleting associated records.
|
||||
*/
|
||||
|
||||
const deleteMonitor = async (req, res, next) => {
|
||||
try {
|
||||
await getMonitorByIdParamValidation.validateAsync(req.params);
|
||||
} catch (error) {
|
||||
error.status = 422;
|
||||
error.service = SERVICE_NAME;
|
||||
error.message =
|
||||
error.details?.[0]?.message || error.message || "Validation Error";
|
||||
next(error);
|
||||
next(handleValidationError(error, SERVICE_NAME));
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const monitor = await req.db.deleteMonitor(req, res, next);
|
||||
// Delete associated checks,alerts,and notifications
|
||||
await req.jobQueue.deleteJob(monitor);
|
||||
await req.db.deleteChecks(monitor._id);
|
||||
await req.db.deleteAlertByMonitorId(monitor._id);
|
||||
await req.db.deletePageSpeedChecksByMonitorId(monitor._id);
|
||||
await req.db.deleteNotificationsByMonitorId(monitor._id);
|
||||
|
||||
/**
|
||||
* TODO
|
||||
* We should remove all checks and alerts associated with this monitor
|
||||
* when it is deleted so there is no orphaned data
|
||||
* We also need to make sure to stop all running services for this monitor
|
||||
*/
|
||||
try {
|
||||
await req.jobQueue.deleteJob(monitor);
|
||||
await req.db.deleteChecks(monitor._id);
|
||||
await req.db.deletePageSpeedChecksByMonitorId(monitor._id);
|
||||
await req.db.deleteNotificationsByMonitorId(monitor._id);
|
||||
} catch (error) {
|
||||
logger.error(
|
||||
`Error deleting associated records for monitor ${monitor._id} with name ${monitor.name}`,
|
||||
{
|
||||
method: "deleteMonitor",
|
||||
service: SERVICE_NAME,
|
||||
error: error.message,
|
||||
}
|
||||
);
|
||||
}
|
||||
return res
|
||||
.status(200)
|
||||
.json({ success: true, msg: successMessages.MONITOR_DELETE });
|
||||
} catch (error) {
|
||||
error.service === undefined ? (error.service = SERVICE_NAME) : null;
|
||||
error.method === undefined ? (error.method = "deleteMonitor") : null;
|
||||
next(error);
|
||||
next(handleError(error, SERVICE_NAME, "deleteMonitor"));
|
||||
}
|
||||
};
|
||||
|
||||
const deleteAllMonitors = async (req, res) => {
|
||||
/**
|
||||
* Deletes all monitors associated with a team.
|
||||
* @async
|
||||
* @param {Object} req - The Express request object.
|
||||
* @property {Object} req.headers - The headers of the request.
|
||||
* @property {string} req.headers.authorization - The authorization header containing the JWT token.
|
||||
* @param {Object} res - The Express response object.
|
||||
* @param {function} next
|
||||
* @returns {Object} The response object with a success status and a message indicating the number of deleted monitors.
|
||||
* @throws {Error} If there is an error during the deletion process.
|
||||
*/
|
||||
const deleteAllMonitors = async (req, res, next) => {
|
||||
try {
|
||||
const token = getTokenFromHeaders(req.headers);
|
||||
const { teamId } = jwt.verify(token, process.env.JWT_SECRET);
|
||||
const { jwtSecret } = req.settingsService.getSettings();
|
||||
const { teamId } = jwt.verify(token, jwtSecret);
|
||||
const { monitors, deletedCount } = await req.db.deleteAllMonitors(teamId);
|
||||
await monitors.forEach(async (monitor) => {
|
||||
await req.jobQueue.deleteJob(monitor);
|
||||
@@ -396,30 +336,29 @@ const deleteAllMonitors = async (req, res) => {
|
||||
.status(200)
|
||||
.json({ success: true, msg: `Deleted ${deletedCount} monitors` });
|
||||
} catch (error) {
|
||||
error.service === undefined ? (error.service = SERVICE_NAME) : null;
|
||||
error.method === undefined ? (error.method = "deleteAllMonitors") : null;
|
||||
next(error);
|
||||
next(handleError(error, SERVICE_NAME, "deleteAllMonitors"));
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Edit a monitor by ID
|
||||
* Edits a monitor by its ID, updates its notifications, and updates its job in the job queue.
|
||||
* @async
|
||||
* @param {Express.Request} req
|
||||
* @param {Express.Response} res
|
||||
* @returns {Promise<Express.Response>}
|
||||
* @throws {Error}
|
||||
* @param {Object} req - The Express request object.
|
||||
* @property {Object} req.params - The parameters of the request.
|
||||
* @property {string} req.params.monitorId - The ID of the monitor to be edited.
|
||||
* @property {Object} req.body - The body of the request.
|
||||
* @property {Array} req.body.notifications - The notifications to be associated with the monitor.
|
||||
* @param {Object} res - The Express response object.
|
||||
* @param {function} next - The next middleware function.
|
||||
* @returns {Object} The response object with a success status, a message indicating the editing of the monitor, and the edited monitor data.
|
||||
* @throws {Error} If there is an error during the process, especially if there is a validation error (422).
|
||||
*/
|
||||
const editMonitor = async (req, res, next) => {
|
||||
try {
|
||||
await getMonitorByIdParamValidation.validateAsync(req.params);
|
||||
await editMonitorBodyValidation.validateAsync(req.body);
|
||||
} catch (error) {
|
||||
error.status = 422;
|
||||
error.service = SERVICE_NAME;
|
||||
error.message =
|
||||
error.details?.[0]?.message || error.message || "Validation Error";
|
||||
next(error);
|
||||
next(handleValidationError(error, SERVICE_NAME));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -453,21 +392,26 @@ const editMonitor = async (req, res, next) => {
|
||||
data: editedMonitor,
|
||||
});
|
||||
} catch (error) {
|
||||
error.service === undefined ? (error.service = SERVICE_NAME) : null;
|
||||
error.method === undefined ? (error.method = "editMonitor") : null;
|
||||
next(error);
|
||||
next(handleError(error, SERVICE_NAME, "editMonitor"));
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Pauses or resumes a monitor based on its current state.
|
||||
* @async
|
||||
* @param {Object} req - The Express request object.
|
||||
* @property {Object} req.params - The parameters of the request.
|
||||
* @property {string} req.params.monitorId - The ID of the monitor to be paused or resumed.
|
||||
* @param {Object} res - The Express response object.
|
||||
* @param {function} next - The next middleware function.
|
||||
* @returns {Object} The response object with a success status, a message indicating the new state of the monitor, and the updated monitor data.
|
||||
* @throws {Error} If there is an error during the process.
|
||||
*/
|
||||
const pauseMonitor = async (req, res, next) => {
|
||||
try {
|
||||
await pauseMonitorParamValidation.validateAsync(req.params);
|
||||
} catch (error) {
|
||||
error.status = 422;
|
||||
error.service = SERVICE_NAME;
|
||||
error.message =
|
||||
error.details?.[0]?.message || error.message || "Validation Error";
|
||||
next(error);
|
||||
next(handleValidationError(error, SERVICE_NAME));
|
||||
}
|
||||
|
||||
try {
|
||||
@@ -488,35 +432,43 @@ const pauseMonitor = async (req, res, next) => {
|
||||
data: monitor,
|
||||
});
|
||||
} catch (error) {
|
||||
error.service === undefined ? (error.service = SERVICE_NAME) : null;
|
||||
error.method === undefined ? (error.method = "pauseMonitor") : null;
|
||||
next(error);
|
||||
next(handleError(error, SERVICE_NAME, "pauseMonitor"));
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Adds demo monitors for a team.
|
||||
* @async
|
||||
* @param {Object} req - The Express request object.
|
||||
* @property {Object} req.headers - The headers of the request.
|
||||
* @property {string} req.headers.authorization - The authorization header containing the JWT token.
|
||||
* @param {Object} res - The Express response object.
|
||||
* @param {function} next - The next middleware function.
|
||||
* @returns {Object} The response object with a success status, a message indicating the addition of demo monitors, and the number of demo monitors added.
|
||||
* @throws {Error} If there is an error during the process.
|
||||
*/
|
||||
const addDemoMonitors = async (req, res, next) => {
|
||||
try {
|
||||
const token = getTokenFromHeaders(req.headers);
|
||||
const { _id, teamId } = jwt.verify(token, process.env.JWT_SECRET);
|
||||
const { jwtSecret } = req.settingsService.getSettings();
|
||||
const { _id, teamId } = jwt.verify(token, jwtSecret);
|
||||
const demoMonitors = await req.db.addDemoMonitors(_id, teamId);
|
||||
await demoMonitors.forEach(async (monitor) => {
|
||||
await req.jobQueue.addJob(monitor._id, monitor);
|
||||
});
|
||||
await Promise.all(
|
||||
demoMonitors.map((monitor) => req.jobQueue.addJob(monitor._id, monitor))
|
||||
);
|
||||
|
||||
return res.status(200).json({
|
||||
success: true,
|
||||
message: successMessages.MONITOR_DEMO_ADDED,
|
||||
data: demoMonitors.length,
|
||||
});
|
||||
} catch (error) {
|
||||
error.service === undefined ? (error.service = SERVICE_NAME) : null;
|
||||
error.method === undefined ? (error.method = "addDemoMonitors") : null;
|
||||
next(error);
|
||||
next(handleError(error, SERVICE_NAME, "addDemoMonitors"));
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
getAllMonitors,
|
||||
getMonitorAggregateStats,
|
||||
getMonitorStatsById,
|
||||
getMonitorCertificate,
|
||||
getMonitorById,
|
||||
|
||||
@@ -1,129 +0,0 @@
|
||||
const PageSpeedService = require("../service/pageSpeedService");
|
||||
const { successMessages } = require("../utils/messages");
|
||||
const pageSpeedService = new PageSpeedService();
|
||||
const SERVICE_NAME = "pagespeedCheckController";
|
||||
const {
|
||||
getPageSpeedCheckParamValidation,
|
||||
createPageSpeedCheckParamValidation,
|
||||
createPageSpeedCheckBodyValidation,
|
||||
deletePageSpeedCheckParamValidation,
|
||||
} = require("../validation/joi");
|
||||
|
||||
/**
|
||||
* Gets all PageSpeedChecks for a monitor
|
||||
* @async
|
||||
* @param {Express.Request} req - The request object
|
||||
* @param {Express.Response} res - The response object
|
||||
* @param {Function} next - The next middleware function
|
||||
* @returns {Promise<Express.Response>}
|
||||
*/
|
||||
const getPageSpeedChecks = async (req, res, next) => {
|
||||
try {
|
||||
// Validate monitorId parameter
|
||||
await getPageSpeedCheckParamValidation.validateAsync(req.params);
|
||||
} catch (error) {
|
||||
error.status = 422;
|
||||
error.service = SERVICE_NAME;
|
||||
error.message =
|
||||
error.details?.[0]?.message || error.message || "Validation Error";
|
||||
next(error);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
return res.status(200).json({ msg: "Hit getPageSpeedChecks" });
|
||||
} catch (error) {
|
||||
error.service === undefined ? (error.service = SERVICE_NAME) : null;
|
||||
error.method === undefined ? (error.method = "getPageSpeedChecks") : null;
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Creates a new PageSpeedCheck for a monitor
|
||||
* @async
|
||||
* @param {Express.Request} req - The request object
|
||||
* @param {Express.Response} res - The response object
|
||||
* @param {Function} next - The next middleware function
|
||||
* @returns {Promise<Express.Response>}
|
||||
*/
|
||||
const createPageSpeedCheck = async (req, res, next) => {
|
||||
try {
|
||||
await createPageSpeedCheckParamValidation.validateAsync(req.params);
|
||||
await createPageSpeedCheckBodyValidation.validateAsync(req.body);
|
||||
} catch (error) {
|
||||
error.status = 422;
|
||||
error.service = SERVICE_NAME;
|
||||
error.message =
|
||||
error.details?.[0]?.message || error.message || "Validation Error";
|
||||
next(error);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const { monitorId } = req.params;
|
||||
const { url } = req.body;
|
||||
|
||||
// Run the PageSpeed check
|
||||
const pageSpeedResults = await pageSpeedService.runPageSpeedCheck(url);
|
||||
|
||||
// Extract categories scores
|
||||
const categories = pageSpeedResults.lighthouseResult?.categories;
|
||||
|
||||
if (!categories) {
|
||||
throw new Error("Categories not found in PageSpeed results");
|
||||
}
|
||||
|
||||
const newPageSpeedCheck = await pageSpeedService.createPageSpeedCheck({
|
||||
monitorId,
|
||||
accessibility: (categories.accessibility?.score || 0) * 100,
|
||||
bestPractices: (categories["best-practices"]?.score || 0) * 100,
|
||||
seo: (categories.seo?.score || 0) * 100,
|
||||
performance: (categories.performance?.score || 0) * 100,
|
||||
});
|
||||
|
||||
return res.status(201).json({
|
||||
msg: successMessages.CREATED,
|
||||
data: newPageSpeedCheck,
|
||||
});
|
||||
} catch (error) {
|
||||
error.service === undefined ? (error.service = SERVICE_NAME) : null;
|
||||
error.method === undefined ? (error.method = "createPageSpeedCheck") : null;
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Deletes all PageSpeedChecks for a monitor
|
||||
* @async
|
||||
* @param {Express.Request} req - The request object
|
||||
* @param {Express.Response} res - The response object
|
||||
* @param {Function} next - The next middleware function
|
||||
* @returns {Promise<Express.Response>}
|
||||
*/
|
||||
const deletePageSpeedCheck = async (req, res, next) => {
|
||||
try {
|
||||
// Validate monitorId parameter
|
||||
await deletePageSpeedCheckParamValidation.validateAsync(req.params);
|
||||
} catch (error) {
|
||||
error.status = 422;
|
||||
error.service = SERVICE_NAME;
|
||||
error.message =
|
||||
error.details?.[0]?.message || error.message || "Validation Error";
|
||||
next(error);
|
||||
return;
|
||||
}
|
||||
try {
|
||||
return res.status(200).json({ msg: "Hit deletePageSpeedCheck" });
|
||||
} catch (error) {
|
||||
error.service === undefined ? (error.service = SERVICE_NAME) : null;
|
||||
error.method === undefined ? (error.method = "deletePageSpeedCheck") : null;
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
getPageSpeedChecks,
|
||||
createPageSpeedCheck,
|
||||
deletePageSpeedCheck,
|
||||
};
|
||||
@@ -1,3 +1,5 @@
|
||||
const { handleError } = require("./controllerUtils");
|
||||
|
||||
const SERVICE_NAME = "JobQueueController";
|
||||
|
||||
const getMetrics = async (req, res, next) => {
|
||||
@@ -7,9 +9,7 @@ const getMetrics = async (req, res, next) => {
|
||||
.status(200)
|
||||
.json({ success: true, msg: "Metrics retrieved", data: metrics });
|
||||
} catch (error) {
|
||||
error.service === undefined ? (error.service = SERVICE_NAME) : null;
|
||||
error.method === undefined ? (error.method = "getMetrics") : null;
|
||||
next(error);
|
||||
next(handleError(error, SERVICE_NAME, "getMetrics"));
|
||||
return;
|
||||
}
|
||||
};
|
||||
@@ -19,9 +19,7 @@ const getJobs = async (req, res, next) => {
|
||||
const jobs = await req.jobQueue.getJobStats();
|
||||
return res.status(200).json({ jobs });
|
||||
} catch (error) {
|
||||
error.service === undefined ? (error.service = SERVICE_NAME) : null;
|
||||
error.method === undefined ? (error.method = "getJobs") : null;
|
||||
next(error);
|
||||
next(handleError(error, SERVICE_NAME, "getJobs"));
|
||||
return;
|
||||
}
|
||||
};
|
||||
@@ -31,21 +29,17 @@ const addJob = async (req, res, next) => {
|
||||
await req.jobQueue.addJob(Math.random().toString(36).substring(7));
|
||||
return res.send("Added job");
|
||||
} catch (error) {
|
||||
error.service === undefined ? (error.service = SERVICE_NAME) : null;
|
||||
error.method === undefined ? (error.method = "addJob") : null;
|
||||
next(error);
|
||||
next(handleError(error, SERVICE_NAME, "addJob"));
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
const obliterateQueue = async (req, res, next) => {
|
||||
try {
|
||||
const obliterated = await req.jobQueue.obliterate();
|
||||
return res.status(200).send("Obliterated jobs");
|
||||
await req.jobQueue.obliterate();
|
||||
return res.status(200).send("Obliterated queue");
|
||||
} catch (error) {
|
||||
error.service === undefined ? (error.service = SERVICE_NAME) : null;
|
||||
error.method === undefined ? (error.method = "obliterateQueue") : null;
|
||||
next(error);
|
||||
next(handleError(error, SERVICE_NAME, "obliterateQueue"));
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
const { successMessages } = require("../utils/messages");
|
||||
const SERVICE_NAME = "SettingsController";
|
||||
const { updateAppSettingsBodyValidation } = require("../validation/joi");
|
||||
const { handleValidationError, handleError } = require("./controllerUtils");
|
||||
|
||||
const getAppSettings = async (req, res, next) => {
|
||||
try {
|
||||
const settings = { ...(await req.settingsService.getSettings()) };
|
||||
delete settings.jwtSecret;
|
||||
return res.status(200).json({
|
||||
success: true,
|
||||
msg: successMessages.GET_APP_SETTINGS,
|
||||
data: settings,
|
||||
});
|
||||
} catch (error) {
|
||||
next(handleError(error, SERVICE_NAME, "getAppSettings"));
|
||||
}
|
||||
};
|
||||
|
||||
const updateAppSettings = async (req, res, next) => {
|
||||
try {
|
||||
await updateAppSettingsBodyValidation.validateAsync(req.body);
|
||||
} catch (error) {
|
||||
next(handleValidationError(error, SERVICE_NAME));
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await req.db.updateAppSettings(req.body);
|
||||
const updatedSettings = { ...(await req.settingsService.reloadSettings()) };
|
||||
delete updatedSettings.jwtSecret;
|
||||
return res.status(200).json({
|
||||
success: true,
|
||||
msg: successMessages.UPDATE_APP_SETTINGS,
|
||||
data: updatedSettings,
|
||||
});
|
||||
} catch (error) {
|
||||
next(handleError(error, SERVICE_NAME, "updateAppSettings"));
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
getAppSettings,
|
||||
updateAppSettings,
|
||||
};
|
||||
+2
-2
@@ -20,8 +20,8 @@
|
||||
// The rest of the code is the same, as all the `db` methods are standardized.
|
||||
// **************************
|
||||
|
||||
const Monitor = require("../models/Monitor");
|
||||
const UserModel = require("../models/user");
|
||||
const Monitor = require("./models/Monitor");
|
||||
const UserModel = require("./models/User");
|
||||
const bcrypt = require("bcrypt");
|
||||
|
||||
let FAKE_MONITOR_DATA = [];
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
const mongoose = require("mongoose");
|
||||
|
||||
const AppSettingsSchema = mongoose.Schema(
|
||||
{
|
||||
apiBaseUrl: {
|
||||
type: String,
|
||||
required: true,
|
||||
default: "http://localhost:5000/api/v1",
|
||||
},
|
||||
logLevel: {
|
||||
type: String,
|
||||
default: "debug",
|
||||
enum: ["debug", "none", "error", "warn"],
|
||||
},
|
||||
clientHost: {
|
||||
type: String,
|
||||
required: true,
|
||||
default: "http://localhost:5173",
|
||||
},
|
||||
jwtSecret: {
|
||||
type: String,
|
||||
required: true,
|
||||
default: "my_secret",
|
||||
},
|
||||
dbType: {
|
||||
type: String,
|
||||
required: true,
|
||||
default: "MongoDB",
|
||||
},
|
||||
dbConnectionString: {
|
||||
type: String,
|
||||
required: true,
|
||||
default: "mongodb://localhost:27017/uptime_db",
|
||||
},
|
||||
redisHost: {
|
||||
type: String,
|
||||
required: true,
|
||||
default: "127.0.0.1",
|
||||
},
|
||||
redisPort: {
|
||||
type: Number,
|
||||
default: "6379",
|
||||
},
|
||||
jwtTTL: {
|
||||
type: String,
|
||||
required: true,
|
||||
default: "99d",
|
||||
},
|
||||
pagespeedApiKey: {
|
||||
type: String,
|
||||
default: "",
|
||||
},
|
||||
systemEmailHost: {
|
||||
type: String,
|
||||
default: "smtp.gmail.com",
|
||||
},
|
||||
systemEmailPort: {
|
||||
type: Number,
|
||||
default: 465,
|
||||
},
|
||||
systemEmailAddress: {
|
||||
type: String,
|
||||
default: "",
|
||||
},
|
||||
systemEmailPassword: {
|
||||
type: String,
|
||||
default: "",
|
||||
},
|
||||
singleton: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
unique: true,
|
||||
default: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
timestamps: true,
|
||||
}
|
||||
);
|
||||
|
||||
module.exports = mongoose.model("AppSettings", AppSettingsSchema);
|
||||
@@ -1,6 +1,6 @@
|
||||
const mongoose = require("mongoose");
|
||||
const EmailService = require("../service/emailService");
|
||||
const Notification = require("../models/Notification");
|
||||
const EmailService = require("../../service/emailService");
|
||||
const Notification = require("./Notification");
|
||||
|
||||
/**
|
||||
* Check Schema for MongoDB collection.
|
||||
File renamed without changes.
@@ -6,7 +6,7 @@ const mongoose = require("mongoose");
|
||||
* @typedef {Object} MaintenanceWindow
|
||||
* @property {mongoose.Schema.Types.ObjectId} monitorId - The ID of the monitor. This is a reference to the Monitor model and is immutable.
|
||||
* @property {Boolean} active - Indicates whether the maintenance window is active.
|
||||
* @property {Boolean} oneTime - Indicates whether the maintenance window is a one-time event.
|
||||
* @property {Number} repeat - Indicates how often this maintenance window should repeat.
|
||||
* @property {Date} start - The start date and time of the maintenance window.
|
||||
* @property {Date} end - The end date and time of the maintenance window.
|
||||
* @property {Date} expiry - The expiry date and time of the maintenance window. This is used for MongoDB's TTL index to automatically delete the document at this time. This field is set to the same value as `end` when `oneTime` is `true`.
|
||||
@@ -16,12 +16,12 @@ const mongoose = require("mongoose");
|
||||
* let maintenanceWindow = new MaintenanceWindow({
|
||||
* monitorId: monitorId,
|
||||
* active: active,
|
||||
* oneTime: oneTime,
|
||||
* repeat: repeat,
|
||||
* start: start,
|
||||
* end: end,
|
||||
* });
|
||||
*
|
||||
* if (oneTime) {
|
||||
* if (repeat === 0) {
|
||||
* maintenanceWindow.expiry = end;
|
||||
* }
|
||||
*
|
||||
@@ -34,16 +34,20 @@ const MaintenanceWindow = mongoose.Schema(
|
||||
ref: "Monitor",
|
||||
immutable: true,
|
||||
},
|
||||
userId: {
|
||||
teamId: {
|
||||
type: mongoose.Schema.Types.ObjectId,
|
||||
ref: "User",
|
||||
ref: "Team",
|
||||
immutable: true,
|
||||
},
|
||||
active: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
oneTime: {
|
||||
type: Boolean,
|
||||
name: {
|
||||
type: String,
|
||||
},
|
||||
repeat: {
|
||||
type: Number,
|
||||
},
|
||||
start: {
|
||||
type: Date,
|
||||
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
+40
-28
@@ -1,5 +1,6 @@
|
||||
const mongoose = require("mongoose");
|
||||
const UserModel = require("../../models/user");
|
||||
const UserModel = require("../models/User");
|
||||
const AppSettings = require("../models/AppSettings");
|
||||
|
||||
//****************************************
|
||||
// DB Connection
|
||||
@@ -7,7 +8,16 @@ const UserModel = require("../../models/user");
|
||||
|
||||
const connect = async () => {
|
||||
try {
|
||||
await mongoose.connect(process.env.DB_CONNECTION_STRING);
|
||||
const connectionString =
|
||||
process.env.DB_CONNECTION_STRING || "mongodb://localhost:27017/uptime_db";
|
||||
await mongoose.connect(connectionString);
|
||||
// If there are no AppSettings, create one
|
||||
let appSettings = await AppSettings.find();
|
||||
if (appSettings.length === 0) {
|
||||
appSettings = new AppSettings({});
|
||||
await appSettings.save();
|
||||
}
|
||||
|
||||
console.log("Connected to MongoDB");
|
||||
} catch (error) {
|
||||
console.error("Failed to connect to MongoDB");
|
||||
@@ -36,6 +46,8 @@ const {
|
||||
getUserByEmail,
|
||||
updateUser,
|
||||
deleteUser,
|
||||
deleteTeam,
|
||||
deleteAllOtherUsers,
|
||||
getAllUsers,
|
||||
logoutUser,
|
||||
} = require("./modules/userModule");
|
||||
@@ -47,6 +59,7 @@ const {
|
||||
const {
|
||||
requestInviteToken,
|
||||
getInviteToken,
|
||||
getInviteTokenAndDelete,
|
||||
} = require("./modules/inviteModule");
|
||||
|
||||
//****************************************
|
||||
@@ -100,48 +113,50 @@ const {
|
||||
updateChecksTTL,
|
||||
} = require("./modules/checkModule");
|
||||
|
||||
//****************************************
|
||||
// Alerts
|
||||
//****************************************
|
||||
|
||||
const {
|
||||
createAlert,
|
||||
getAlertsByUserId,
|
||||
getAlertsByMonitorId,
|
||||
getAlertById,
|
||||
editAlert,
|
||||
deleteAlert,
|
||||
deleteAlertByMonitorId,
|
||||
} = require("./modules/alertModule");
|
||||
|
||||
//****************************************
|
||||
// Maintenance Window
|
||||
//****************************************
|
||||
const {
|
||||
createMaintenanceWindow,
|
||||
getMaintenanceWindowsByUserId,
|
||||
getMaintenanceWindowById,
|
||||
getMaintenanceWindowsByTeamId,
|
||||
getMaintenanceWindowsByMonitorId,
|
||||
deleteMaintenaceWindowById,
|
||||
deleteMaintenanceWindowById,
|
||||
deleteMaintenanceWindowByMonitorId,
|
||||
deleteMaintenanceWindowByUserId,
|
||||
} = require("./modules/maintenaceWindowModule");
|
||||
editMaintenanceWindowById,
|
||||
} = require("./modules/maintenanceWindowModule");
|
||||
|
||||
//****************************************
|
||||
// Notifications
|
||||
//****************************************
|
||||
const {
|
||||
createNotification,
|
||||
getNotificationsByMonitorId,
|
||||
deleteNotificationsByMonitorId,
|
||||
} = require("./modules/notificationModule");
|
||||
|
||||
//****************************************
|
||||
// AppSettings
|
||||
//****************************************
|
||||
const {
|
||||
getAppSettings,
|
||||
updateAppSettings,
|
||||
} = require("./modules/settingsModule");
|
||||
|
||||
module.exports = {
|
||||
connect,
|
||||
insertUser,
|
||||
getUserByEmail,
|
||||
updateUser,
|
||||
deleteUser,
|
||||
deleteTeam,
|
||||
deleteAllOtherUsers,
|
||||
getAllUsers,
|
||||
logoutUser,
|
||||
requestInviteToken,
|
||||
getInviteToken,
|
||||
getInviteTokenAndDelete,
|
||||
requestRecoveryToken,
|
||||
validateRecoveryToken,
|
||||
resetPassword,
|
||||
@@ -163,24 +178,21 @@ module.exports = {
|
||||
deleteChecks,
|
||||
deleteChecksByTeamId,
|
||||
updateChecksTTL,
|
||||
createAlert,
|
||||
getAlertsByUserId,
|
||||
getAlertsByMonitorId,
|
||||
getAlertById,
|
||||
editAlert,
|
||||
deleteAlert,
|
||||
deleteAlertByMonitorId,
|
||||
deleteMonitorsByUserId,
|
||||
createPageSpeedCheck,
|
||||
getPageSpeedChecks,
|
||||
deletePageSpeedChecksByMonitorId,
|
||||
createMaintenanceWindow,
|
||||
getMaintenanceWindowsByUserId,
|
||||
getMaintenanceWindowsByTeamId,
|
||||
getMaintenanceWindowById,
|
||||
getMaintenanceWindowsByMonitorId,
|
||||
deleteMaintenaceWindowById,
|
||||
deleteMaintenanceWindowById,
|
||||
deleteMaintenanceWindowByMonitorId,
|
||||
deleteMaintenanceWindowByUserId,
|
||||
editMaintenanceWindowById,
|
||||
createNotification,
|
||||
getNotificationsByMonitorId,
|
||||
deleteNotificationsByMonitorId,
|
||||
getAppSettings,
|
||||
updateAppSettings,
|
||||
};
|
||||
@@ -1,140 +0,0 @@
|
||||
const Alert = require("../../../models/Alert");
|
||||
|
||||
/**
|
||||
* Creates an Alert associated with a Monitor and User
|
||||
* @async
|
||||
* @param {Object} alertData
|
||||
* @param {string} alertData.checkId
|
||||
* @param {string} alert.monitorId
|
||||
* @param {string} alert.userId
|
||||
* @param {boolean} alert.status
|
||||
* @param {string} alert.message
|
||||
* @param {boolean} alert.notifiedStatus
|
||||
* @param {boolean} alert.acknowledgeStatus
|
||||
* @returns {<Promise<Alert>>}
|
||||
* @throws {Error}
|
||||
*/
|
||||
const createAlert = async (alertData) => {
|
||||
try {
|
||||
const alert = await new Alert({ ...alertData }).save();
|
||||
return alert;
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Gets all alerts a User has set
|
||||
* @async
|
||||
* @param {string} userId
|
||||
* @returns {<Promise<Array<Alert>>}
|
||||
* @throws {Error}
|
||||
*/
|
||||
const getAlertsByUserId = async (userId) => {
|
||||
try {
|
||||
const alerts = await Alert.find({ userId });
|
||||
return alerts;
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Gets all alerts a for an associated Monitor
|
||||
* @async
|
||||
* @param {string} monitorId
|
||||
* @returns {<Promise<Array<Alert>>}
|
||||
* @throws {Error}
|
||||
*/
|
||||
const getAlertsByMonitorId = async (monitorId) => {
|
||||
try {
|
||||
const alerts = await Alert.find({ monitorId });
|
||||
return alerts;
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns an alert with specified ID
|
||||
* @async
|
||||
* @param {string} alertId
|
||||
* @returns {Promise<Alert>}
|
||||
* @throws {Error}
|
||||
*/
|
||||
const getAlertById = async (alertId) => {
|
||||
try {
|
||||
const alert = await Alert.findById(alertId);
|
||||
return alert;
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns an edited alert with the specified ID
|
||||
* @async
|
||||
* @param {string} alertId
|
||||
* @param {Object} alertData
|
||||
* @param {string} alertData.checkId
|
||||
* @param {string} alertData.monitorId
|
||||
* @param {string} alertData.userId
|
||||
* @param {boolean} alertData.status
|
||||
* @param {string} alertData.message
|
||||
* @param {boolean} alertData.notifiedStatus
|
||||
* @param {boolean} alertData.acknowledgeStatus
|
||||
* @param {Express.Response} res
|
||||
* @returns {Promise<Alert>>}
|
||||
* @throws {Error}
|
||||
*/
|
||||
const editAlert = async (alertId, alertData) => {
|
||||
try {
|
||||
const editedAlert = await Alert.findByIdAndUpdate(alertId, alertData, {
|
||||
new: true,
|
||||
});
|
||||
return editedAlert;
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Deletes an alert with the specified ID
|
||||
* @async
|
||||
* @param {string} alertId
|
||||
* @throws {Error}
|
||||
*/
|
||||
const deleteAlert = async (alertId) => {
|
||||
try {
|
||||
const result = await Alert.findByIdAndDelete(alertId);
|
||||
return result;
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Deletes alerts by monitor ID.
|
||||
*
|
||||
* @param {string} monitorId - The ID of the monitor.
|
||||
* @returns {Promise} A promise that resolves to the result of the delete operation.
|
||||
* @throws {Error} If an error occurs while deleting the alerts.
|
||||
*/
|
||||
const deleteAlertByMonitorId = async (monitorId) => {
|
||||
try {
|
||||
const result = await Alert.deleteMany({ monitorId });
|
||||
return result;
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
createAlert,
|
||||
getAlertsByUserId,
|
||||
getAlertsByMonitorId,
|
||||
getAlertById,
|
||||
editAlert,
|
||||
deleteAlert,
|
||||
deleteAlertByMonitorId,
|
||||
};
|
||||
@@ -1,6 +1,6 @@
|
||||
const Check = require("../../../models/Check");
|
||||
const Monitor = require("../../../models/Monitor");
|
||||
const User = require("../../../models/user");
|
||||
const Check = require("../../models/Check");
|
||||
const Monitor = require("../../models/Monitor");
|
||||
const User = require("../../models/User");
|
||||
const logger = require("../../../utils/logger");
|
||||
const SERVICE_NAME = "checkModule";
|
||||
const dateRangeLookup = {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
const InviteToken = require("../../../models/InviteToken");
|
||||
const InviteToken = require("../../models/InviteToken");
|
||||
const crypto = require("crypto");
|
||||
const { errorMessages } = require("../../../utils/messages");
|
||||
const SERVICE_NAME = "inviteModule";
|
||||
@@ -32,7 +32,7 @@ const requestInviteToken = async (userData) => {
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieves and deletes an invite token.
|
||||
* Retrieves an invite token
|
||||
*
|
||||
* This function searches for an invite token in the database and deletes it.
|
||||
* If the invite token is not found, it throws an error.
|
||||
@@ -42,6 +42,32 @@ const requestInviteToken = async (userData) => {
|
||||
* @throws {Error} If the invite token is not found or there is another error.
|
||||
*/
|
||||
const getInviteToken = async (token) => {
|
||||
try {
|
||||
const invite = await InviteToken.findOne({
|
||||
token,
|
||||
});
|
||||
if (invite === null) {
|
||||
throw new Error(errorMessages.AUTH_INVITE_NOT_FOUND);
|
||||
}
|
||||
return invite;
|
||||
} catch (error) {
|
||||
error.service = SERVICE_NAME;
|
||||
error.method = "getInviteToken";
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieves and deletes an invite token
|
||||
*
|
||||
* This function searches for an invite token in the database and deletes it.
|
||||
* If the invite token is not found, it throws an error.
|
||||
*
|
||||
* @param {string} token - The invite token to search for.
|
||||
* @returns {Promise<InviteToken>} The invite token data.
|
||||
* @throws {Error} If the invite token is not found or there is another error.
|
||||
*/
|
||||
const getInviteTokenAndDelete = async (token) => {
|
||||
try {
|
||||
const invite = await InviteToken.findOneAndDelete({
|
||||
token,
|
||||
@@ -60,4 +86,5 @@ const getInviteToken = async (token) => {
|
||||
module.exports = {
|
||||
requestInviteToken,
|
||||
getInviteToken,
|
||||
getInviteTokenAndDelete,
|
||||
};
|
||||
+73
-14
@@ -1,4 +1,4 @@
|
||||
const MaintenanceWindow = require("../../../models/MaintenanceWindow");
|
||||
const MaintenanceWindow = require("../../models/MaintenanceWindow");
|
||||
const SERVICE_NAME = "maintenanceWindowModule";
|
||||
|
||||
/**
|
||||
@@ -36,7 +36,7 @@ const createMaintenanceWindow = async (maintenanceWindowData) => {
|
||||
if (maintenanceWindowData.oneTime) {
|
||||
maintenanceWindow.expiry = maintenanceWindowData.end;
|
||||
}
|
||||
const result = maintenanceWindow.save();
|
||||
const result = await maintenanceWindow.save();
|
||||
return result;
|
||||
} catch (error) {
|
||||
error.service = SERVICE_NAME;
|
||||
@@ -45,22 +45,60 @@ const createMaintenanceWindow = async (maintenanceWindowData) => {
|
||||
}
|
||||
};
|
||||
|
||||
const getMaintenanceWindowById = async (maintenanceWindowId) => {
|
||||
try {
|
||||
const maintenanceWindow = await MaintenanceWindow.findById({
|
||||
_id: maintenanceWindowId,
|
||||
});
|
||||
return maintenanceWindow;
|
||||
} catch (error) {
|
||||
error.service = SERVICE_NAME;
|
||||
error.method = "getMaintenanceWindowById";
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Asynchronously retrieves all MaintenanceWindow documents associated with a specific user ID.
|
||||
* Asynchronously retrieves all MaintenanceWindow documents associated with a specific team ID.
|
||||
* @async
|
||||
* @function getMaintenanceWindowByUserId
|
||||
* @param {String} userId - The ID of the user.
|
||||
* @param {String} teamId - The ID of the team.
|
||||
* @param {Object} query - The request body.
|
||||
* @returns {Promise<Array<MaintenanceWindow>>} An array of MaintenanceWindow documents.
|
||||
* @throws {Error} If there is an error retrieving the documents.
|
||||
* @example
|
||||
* getMaintenanceWindowByUserId('userId')
|
||||
* getMaintenanceWindowByUserId(teamId)
|
||||
* .then(maintenanceWindows => console.log(maintenanceWindows))
|
||||
* .catch(error => console.error(error));
|
||||
*/
|
||||
const getMaintenanceWindowsByUserId = async (userId) => {
|
||||
const getMaintenanceWindowsByTeamId = async (teamId, query) => {
|
||||
try {
|
||||
const maintenanceWindows = await MaintenanceWindow.find({ userId: userId });
|
||||
return maintenanceWindows;
|
||||
let { active, page, rowsPerPage, field, order } = query || {};
|
||||
const maintenanceQuery = { teamId };
|
||||
|
||||
if (active !== undefined) maintenanceQuery.active = active;
|
||||
|
||||
const maintenanceWindowCount =
|
||||
await MaintenanceWindow.countDocuments(maintenanceQuery);
|
||||
|
||||
// Pagination
|
||||
let skip = 0;
|
||||
if (page && rowsPerPage) {
|
||||
skip = page * rowsPerPage;
|
||||
}
|
||||
|
||||
// Sorting
|
||||
let sort = {};
|
||||
if (field !== undefined && order !== undefined) {
|
||||
sort[field] = order === "asc" ? 1 : -1;
|
||||
}
|
||||
|
||||
const maintenanceWindows = await MaintenanceWindow.find(maintenanceQuery)
|
||||
.skip(skip)
|
||||
.limit(rowsPerPage)
|
||||
.sort(sort);
|
||||
|
||||
return { maintenanceWindows, maintenanceWindowCount };
|
||||
} catch (error) {
|
||||
error.service = SERVICE_NAME;
|
||||
error.method = "getMaintenanceWindowByUserId";
|
||||
@@ -96,23 +134,23 @@ const getMaintenanceWindowsByMonitorId = async (monitorId) => {
|
||||
/**
|
||||
* Asynchronously deletes a MaintenanceWindow document by its ID.
|
||||
* @async
|
||||
* @function deleteMaintenaceWindowById
|
||||
* @function deleteMaintenanceWindowById
|
||||
* @param {mongoose.Schema.Types.ObjectId} maintenanceWindowId - The ID of the MaintenanceWindow document to delete.
|
||||
* @returns {Promise<MaintenanceWindow>} The deleted MaintenanceWindow document.
|
||||
* @throws {Error} If there is an error deleting the document.
|
||||
* @example
|
||||
* deleteMaintenaceWindowById('maintenanceWindowId')
|
||||
* deleteMaintenanceWindowById('maintenanceWindowId')
|
||||
* .then(maintenanceWindow => console.log(maintenanceWindow))
|
||||
* .catch(error => console.error(error));
|
||||
*/
|
||||
const deleteMaintenaceWindowById = async (maintenanceWindowId) => {
|
||||
const deleteMaintenanceWindowById = async (maintenanceWindowId) => {
|
||||
try {
|
||||
const maintenanceWindow =
|
||||
await MaintenanceWindow.findByIdAndDelete(maintenanceWindowId);
|
||||
return maintenanceWindow;
|
||||
} catch (error) {
|
||||
error.service = SERVICE_NAME;
|
||||
error.method = "deleteMaintenaceWindowById";
|
||||
error.method = "deleteMaintenanceWindowById";
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
@@ -163,11 +201,32 @@ const deleteMaintenanceWindowByUserId = async (userId) => {
|
||||
}
|
||||
};
|
||||
|
||||
const editMaintenanceWindowById = async (
|
||||
maintenanceWindowId,
|
||||
maintenanceWindowData
|
||||
) => {
|
||||
console.log(maintenanceWindowData);
|
||||
try {
|
||||
const editedMaintenanceWindow = MaintenanceWindow.findByIdAndUpdate(
|
||||
maintenanceWindowId,
|
||||
maintenanceWindowData,
|
||||
{ new: true }
|
||||
);
|
||||
return editedMaintenanceWindow;
|
||||
} catch (error) {
|
||||
error.service = SERVICE_NAME;
|
||||
error.method = "editMaintenanceWindowById";
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
createMaintenanceWindow,
|
||||
getMaintenanceWindowsByUserId,
|
||||
getMaintenanceWindowById,
|
||||
getMaintenanceWindowsByTeamId,
|
||||
getMaintenanceWindowsByMonitorId,
|
||||
deleteMaintenaceWindowById,
|
||||
deleteMaintenanceWindowById,
|
||||
deleteMaintenanceWindowByMonitorId,
|
||||
deleteMaintenanceWindowByUserId,
|
||||
editMaintenanceWindowById,
|
||||
};
|
||||
@@ -1,8 +1,8 @@
|
||||
const Monitor = require("../../../models/Monitor");
|
||||
const Check = require("../../../models/Check");
|
||||
const PageSpeedCheck = require("../../../models/PageSpeedCheck");
|
||||
const Monitor = require("../../models/Monitor");
|
||||
const Check = require("../../models/Check");
|
||||
const PageSpeedCheck = require("../../models/PageSpeedCheck");
|
||||
const { errorMessages } = require("../../../utils/messages");
|
||||
const Notification = require("../../../models/Notification");
|
||||
const Notification = require("../../models/Notification");
|
||||
const { NormalizeData } = require("../../../utils/dataUtils");
|
||||
const demoMonitors = require("../../../utils/demoMonitors.json");
|
||||
const SERVICE_NAME = "monitorModule";
|
||||
@@ -147,7 +147,7 @@ const getMonitorStatsById = async (req) => {
|
||||
let { limit, sortOrder, dateRange, numToDisplay, normalize } = req.query;
|
||||
const monitor = await Monitor.findById(monitorId);
|
||||
if (monitor === null || monitor === undefined) {
|
||||
throw new Error(errorMessages.DB_FIND_MONTIOR_BY_ID(monitorId));
|
||||
throw new Error(errorMessages.DB_FIND_MONITOR_BY_ID(monitorId));
|
||||
}
|
||||
// This effectively removes limit, returning all checks
|
||||
if (limit === undefined) limit = 0;
|
||||
@@ -283,7 +283,7 @@ const getMonitorById = async (monitorId) => {
|
||||
try {
|
||||
const monitor = await Monitor.findById(monitorId);
|
||||
if (monitor === null || monitor === undefined) {
|
||||
throw new Error(errorMessages.DB_FIND_MONTIOR_BY_ID(monitorId));
|
||||
throw new Error(errorMessages.DB_FIND_MONITOR_BY_ID(monitorId));
|
||||
}
|
||||
// Get notifications
|
||||
const notifications = await Notification.find({
|
||||
@@ -481,7 +481,7 @@ const deleteMonitor = async (req, res) => {
|
||||
try {
|
||||
const monitor = await Monitor.findByIdAndDelete(monitorId);
|
||||
if (!monitor) {
|
||||
throw new Error(errorMessages.DB_FIND_MONTIOR_BY_ID(monitorId));
|
||||
throw new Error(errorMessages.DB_FIND_MONITOR_BY_ID(monitorId));
|
||||
}
|
||||
return monitor;
|
||||
} catch (error) {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
const Notification = require("../../../models/Notification");
|
||||
const Notification = require("../../models/Notification");
|
||||
const SERVICE_NAME = "notificationModule";
|
||||
/**
|
||||
* Creates a new notification.
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
const PageSpeedCheck = require("../../../models/PageSpeedCheck");
|
||||
const PageSpeedCheck = require("../../models/PageSpeedCheck");
|
||||
const SERVICE_NAME = "pageSpeedCheckModule";
|
||||
/**
|
||||
* Create a PageSpeed check for a monitor
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
const UserModel = require("../../../models/user");
|
||||
const RecoveryToken = require("../../../models/RecoveryToken");
|
||||
const UserModel = require("../../models/User");
|
||||
const RecoveryToken = require("../../models/RecoveryToken");
|
||||
const crypto = require("crypto");
|
||||
const { errorMessages } = require("../../../utils/messages");
|
||||
const SERVICE_NAME = "recoveryModule";
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
const AppSettings = require("../../models/AppSettings");
|
||||
const SERVICE_NAME = "SettingsModule";
|
||||
|
||||
const getAppSettings = async () => {
|
||||
try {
|
||||
const settings = AppSettings.findOne();
|
||||
return settings;
|
||||
} catch (error) {
|
||||
error.service = SERVICE_NAME;
|
||||
error.method = "getSettings";
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
const updateAppSettings = async (newSettings) => {
|
||||
try {
|
||||
const settings = await AppSettings.findOneAndUpdate(
|
||||
{},
|
||||
{ $set: newSettings },
|
||||
{ new: true }
|
||||
);
|
||||
return settings;
|
||||
} catch (error) {
|
||||
error.service = SERVICE_NAME;
|
||||
error.method = "updateAppSettings";
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
getAppSettings,
|
||||
updateAppSettings,
|
||||
};
|
||||
@@ -1,5 +1,5 @@
|
||||
const UserModel = require("../../../models/user");
|
||||
const TeamModel = require("../../../models/Team");
|
||||
const UserModel = require("../../models/User");
|
||||
const TeamModel = require("../../models/Team");
|
||||
const { errorMessages } = require("../../../utils/messages");
|
||||
const { GenerateAvatarImage } = require("../../../utils/imageProcessing");
|
||||
|
||||
@@ -35,6 +35,7 @@ const insertUser = async (userData, imageFile) => {
|
||||
email: userData.email,
|
||||
});
|
||||
userData.teamId = team._id;
|
||||
userData.checkTTL = 60 * 60 * 24 * 30;
|
||||
await team.save();
|
||||
}
|
||||
|
||||
@@ -156,6 +157,33 @@ const deleteUser = async (userId) => {
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Delete a user by ID
|
||||
* @async
|
||||
* @param {string} teamId
|
||||
* @returns {void}
|
||||
* @throws {Error}
|
||||
*/
|
||||
const deleteTeam = async (teamId) => {
|
||||
try {
|
||||
await TeamModel.findByIdAndDelete(teamId);
|
||||
} catch (error) {
|
||||
error.service = SERVICE_NAME;
|
||||
error.method = "deleteTeam";
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
const deleteAllOtherUsers = async () => {
|
||||
try {
|
||||
await UserModel.deleteMany({ role: { $ne: "superadmin" } });
|
||||
} catch (error) {
|
||||
error.service = SERVICE_NAME;
|
||||
error.method = "deleteAllOtherUsers";
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
const getAllUsers = async (req, res) => {
|
||||
try {
|
||||
const users = await UserModel.find()
|
||||
@@ -185,6 +213,8 @@ module.exports = {
|
||||
getUserByEmail,
|
||||
updateUser,
|
||||
deleteUser,
|
||||
deleteTeam,
|
||||
deleteAllOtherUsers,
|
||||
getAllUsers,
|
||||
logoutUser,
|
||||
};
|
||||
+30
-17
@@ -1,7 +1,10 @@
|
||||
const path = require("path");
|
||||
const fs = require("fs");
|
||||
const swaggerUi = require("swagger-ui-express");
|
||||
|
||||
const express = require("express");
|
||||
const helmet = require("helmet");
|
||||
const cors = require("cors");
|
||||
require("dotenv").config();
|
||||
const logger = require("./utils/logger");
|
||||
const { verifyJWT } = require("./middleware/verifyJWT");
|
||||
const { handleErrors } = require("./middleware/handleErrors");
|
||||
@@ -10,19 +13,21 @@ const authRouter = require("./routes/authRoute");
|
||||
const inviteRouter = require("./routes/inviteRoute");
|
||||
const monitorRouter = require("./routes/monitorRoute");
|
||||
const checkRouter = require("./routes/checkRoute");
|
||||
const alertRouter = require("./routes/alertRoute");
|
||||
const pageSpeedCheckRouter = require("./routes/pageSpeedCheckRoute");
|
||||
const maintenanceWindowRouter = require("./routes/maintenanceWindowRoute");
|
||||
const settingsRouter = require("./routes/settingsRoute");
|
||||
|
||||
const { connectDbAndRunServer } = require("./configs/db");
|
||||
const queueRouter = require("./routes/queueRoute");
|
||||
const JobQueue = require("./service/jobQueue");
|
||||
const NetworkService = require("./service/networkService");
|
||||
const EmailService = require("./service/emailService");
|
||||
const PageSpeedService = require("./service/pageSpeedService");
|
||||
const SettingsService = require("./service/settingsService");
|
||||
const SERVICE_NAME = "Server";
|
||||
let cleaningUp = false;
|
||||
|
||||
let cleaningUp = false;
|
||||
const openApiSpec = JSON.parse(
|
||||
fs.readFileSync(path.join(__dirname, "openapi.json"), "utf8")
|
||||
);
|
||||
// Need to wrap server setup in a function to handle async nature of JobQueue
|
||||
const startApp = async () => {
|
||||
// **************************
|
||||
@@ -42,9 +47,11 @@ const startApp = async () => {
|
||||
FakedDB: () => require("./db/FakeDb"),
|
||||
};
|
||||
|
||||
const db = DB_TYPE[process.env.DB_TYPE]
|
||||
? DB_TYPE[process.env.DB_TYPE]()
|
||||
: require("./db/FakeDb");
|
||||
// const db = DB_TYPE[process.env.DB_TYPE]
|
||||
// ? DB_TYPE[process.env.DB_TYPE]()
|
||||
// : require("./db/FakeDb");
|
||||
|
||||
const db = DB_TYPE.MongoDB();
|
||||
|
||||
const app = express();
|
||||
|
||||
@@ -55,7 +62,6 @@ const startApp = async () => {
|
||||
);
|
||||
app.use(express.json());
|
||||
app.use(helmet());
|
||||
|
||||
// **************************
|
||||
// Make DB accessible anywhere we have a Request object
|
||||
// By adding the DB to the request object, we can access it in any route
|
||||
@@ -66,20 +72,21 @@ const startApp = async () => {
|
||||
req.db = db;
|
||||
req.jobQueue = jobQueue;
|
||||
req.emailService = emailService;
|
||||
req.pageSpeedService = pageSpeedService;
|
||||
req.settingsService = settingsService;
|
||||
next();
|
||||
});
|
||||
|
||||
// Swagger UI
|
||||
app.use("/api-docs", swaggerUi.serve, swaggerUi.setup(openApiSpec));
|
||||
|
||||
//routes
|
||||
app.use("/api/v1/auth", authRouter);
|
||||
app.use("/api/v1/settings", verifyJWT, settingsRouter);
|
||||
app.use("/api/v1/invite", inviteRouter);
|
||||
app.use("/api/v1/monitors", verifyJWT, monitorRouter);
|
||||
app.use("/api/v1/checks", verifyJWT, checkRouter);
|
||||
app.use("/api/v1/alerts", verifyJWT, alertRouter);
|
||||
app.use("/api/v1/pagespeed", verifyJWT, pageSpeedCheckRouter);
|
||||
app.use("/api/v1/maintenance-window", verifyJWT, maintenanceWindowRouter);
|
||||
//Temporary route for testing, remove later
|
||||
app.use("/api/v1/job", queueRouter);
|
||||
app.use("/api/v1/queue", verifyJWT, queueRouter);
|
||||
|
||||
//health check
|
||||
app.use("/api/v1/healthy", (req, res) => {
|
||||
@@ -117,10 +124,16 @@ const startApp = async () => {
|
||||
|
||||
// Create services
|
||||
await connectDbAndRunServer(app, db);
|
||||
const emailService = new EmailService();
|
||||
const settingsService = new SettingsService();
|
||||
|
||||
await settingsService.loadSettings();
|
||||
const emailService = new EmailService(settingsService);
|
||||
const networkService = new NetworkService(db, emailService);
|
||||
const jobQueue = await JobQueue.createJobQueue(db, networkService);
|
||||
const pageSpeedService = new PageSpeedService();
|
||||
const jobQueue = await JobQueue.createJobQueue(
|
||||
db,
|
||||
networkService,
|
||||
settingsService
|
||||
);
|
||||
|
||||
const cleanup = async () => {
|
||||
if (cleaningUp) {
|
||||
|
||||
Loaded 100 of 164 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user