mirror of
https://github.com/OneUptime/oneuptime.git
synced 2026-04-06 08:42:13 +02:00
Compare commits
611 Commits
cert-manag
...
nativewind
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3658f0349e | ||
|
|
02133165d8 | ||
|
|
837b841352 | ||
|
|
10e344dad5 | ||
|
|
848bfb358f | ||
|
|
2ca1c64532 | ||
|
|
d9a79eafbd | ||
|
|
a1dc16f4b6 | ||
|
|
674613c0d6 | ||
|
|
e4e095798a | ||
|
|
2e07185584 | ||
|
|
144001981a | ||
|
|
cd096f66ea | ||
|
|
02cffe9f8b | ||
|
|
9c52e8966f | ||
|
|
9257a949fe | ||
|
|
7528ed8e0c | ||
|
|
98b6f3eac3 | ||
|
|
a2561b3ae8 | ||
|
|
368f5c6bbc | ||
|
|
0e2e30b0a9 | ||
|
|
2664a24875 | ||
|
|
40e486669f | ||
|
|
ae64cbc718 | ||
|
|
e14f691cc4 | ||
|
|
89c607a530 | ||
|
|
90f4e7418f | ||
|
|
79fed2da09 | ||
|
|
1dd41c103f | ||
|
|
988e85affc | ||
|
|
2810516987 | ||
|
|
679c1971a2 | ||
|
|
3abdbb7ef9 | ||
|
|
0d647f5dc1 | ||
|
|
bd1df491a2 | ||
|
|
feb872d05c | ||
|
|
64b6b99a21 | ||
|
|
8046c244b1 | ||
|
|
6928316ba0 | ||
|
|
36c74642f2 | ||
|
|
5a992e99c8 | ||
|
|
ff9230f878 | ||
|
|
9afa861ff2 | ||
|
|
6d7486e76d | ||
|
|
8529012b19 | ||
|
|
f704bd47a3 | ||
|
|
239f2fc34e | ||
|
|
5d5183b08e | ||
|
|
7cad0fab0f | ||
|
|
98e0d55ee3 | ||
|
|
2d8f0d7a58 | ||
|
|
643303cd7a | ||
|
|
dc692203be | ||
|
|
648c51d007 | ||
|
|
0e5b106333 | ||
|
|
40e9ea2ab6 | ||
|
|
2157e228b9 | ||
|
|
5c5534adb8 | ||
|
|
379297cd7e | ||
|
|
b3730e9708 | ||
|
|
2fbc44d5c3 | ||
|
|
12f05937af | ||
|
|
5ea440492b | ||
|
|
57b851a498 | ||
|
|
00f806b077 | ||
|
|
975c20a788 | ||
|
|
948e2d93c1 | ||
|
|
6de3c93745 | ||
|
|
1f63110561 | ||
|
|
8a94c35450 | ||
|
|
fdc97284a5 | ||
|
|
792ecfdbdc | ||
|
|
121b01dc08 | ||
|
|
1aa49071eb | ||
|
|
66bef1284a | ||
|
|
1526f708de | ||
|
|
53198e4486 | ||
|
|
a66bf2df2a | ||
|
|
34422721c3 | ||
|
|
b5008c2363 | ||
|
|
1cbab2be08 | ||
|
|
1bdcfd71f7 | ||
|
|
792271b146 | ||
|
|
3de5fbd35c | ||
|
|
6f8dc1ed59 | ||
|
|
92309d8fb2 | ||
|
|
d3070975cb | ||
|
|
69c0da5b17 | ||
|
|
1736690f01 | ||
|
|
78a92905e9 | ||
|
|
801c1b4ccb | ||
|
|
703b1dca51 | ||
|
|
e5ef97abd9 | ||
|
|
3053856990 | ||
|
|
b8e82e2801 | ||
|
|
2187fe63f0 | ||
|
|
b1f842f9e1 | ||
|
|
43e03fb61c | ||
|
|
2327ab84c2 | ||
|
|
93034b6018 | ||
|
|
e45022b5cb | ||
|
|
c022b70e6d | ||
|
|
5615ba2df7 | ||
|
|
268960bc5b | ||
|
|
508a713ecf | ||
|
|
36e50b591a | ||
|
|
05c1f95ba4 | ||
|
|
9d355691ae | ||
|
|
73c58186b6 | ||
|
|
eb8d3e4dfd | ||
|
|
987f30e5c7 | ||
|
|
b7a0dbf81b | ||
|
|
7368a0eb7c | ||
|
|
ef0b6f3e14 | ||
|
|
af8691f61d | ||
|
|
f7aee2e253 | ||
|
|
0a1b74d911 | ||
|
|
ab48da447d | ||
|
|
1cc0630939 | ||
|
|
5391ff4688 | ||
|
|
f6d96676fe | ||
|
|
cf02842ab1 | ||
|
|
b63fcf6b99 | ||
|
|
36069c1b4e | ||
|
|
d2846decce | ||
|
|
60a8a3f052 | ||
|
|
e2d15dc2e7 | ||
|
|
7cdefdeccd | ||
|
|
684b8822af | ||
|
|
231bc47942 | ||
|
|
965a497be3 | ||
|
|
f50a7fb99b | ||
|
|
50a5e75d1a | ||
|
|
84e838a055 | ||
|
|
6d6c78e974 | ||
|
|
778d5b7c6b | ||
|
|
8051146f41 | ||
|
|
86a359a230 | ||
|
|
c16dac65cc | ||
|
|
437c9ecdbc | ||
|
|
bf4eec2bdf | ||
|
|
08367f3c7f | ||
|
|
f5d077956a | ||
|
|
ca74005262 | ||
|
|
52c936935e | ||
|
|
2951600ed9 | ||
|
|
d12c8c778c | ||
|
|
77d4527a00 | ||
|
|
1ef3353155 | ||
|
|
2c635c0d1e | ||
|
|
44799f4625 | ||
|
|
4bc6e625d2 | ||
|
|
9ff773dd81 | ||
|
|
bf2c2afbb8 | ||
|
|
3efa3b374f | ||
|
|
f76b004e2a | ||
|
|
cb535bd114 | ||
|
|
9b09082d13 | ||
|
|
5ac6e7ba7b | ||
|
|
044183dbd7 | ||
|
|
fb13609c37 | ||
|
|
1c602b1ad3 | ||
|
|
1affb76085 | ||
|
|
0e02083dec | ||
|
|
a442b02337 | ||
|
|
3ab47328f6 | ||
|
|
57ff8c007b | ||
|
|
c83d1babcd | ||
|
|
1bc96d04fa | ||
|
|
e03a9b52ae | ||
|
|
a307a68ec1 | ||
|
|
1032f01278 | ||
|
|
83286aa34d | ||
|
|
543757a2b5 | ||
|
|
d0bbb12f92 | ||
|
|
066e785a53 | ||
|
|
2f46eadcd4 | ||
|
|
c472555f55 | ||
|
|
83c9255b98 | ||
|
|
1748a198c7 | ||
|
|
a4841f4b6e | ||
|
|
1c750d274e | ||
|
|
2dda68c462 | ||
|
|
6d5bc111ba | ||
|
|
0e1c8df7ab | ||
|
|
4b55cb3d84 | ||
|
|
eb66b57939 | ||
|
|
b53119d249 | ||
|
|
95c2db5ace | ||
|
|
8c4bc48c44 | ||
|
|
28f3b98d44 | ||
|
|
742da27d87 | ||
|
|
e2ba7dff8d | ||
|
|
3f6a58a087 | ||
|
|
3f2005bd34 | ||
|
|
651ba4247e | ||
|
|
ba71cf5980 | ||
|
|
442a4b935b | ||
|
|
5caf5e8991 | ||
|
|
b5e4545193 | ||
|
|
ce4cc70815 | ||
|
|
426dda60fe | ||
|
|
c6be5a9ebf | ||
|
|
eb19b8926b | ||
|
|
955a3f784d | ||
|
|
756dc88289 | ||
|
|
c116f9adc1 | ||
|
|
324f29edc3 | ||
|
|
03064308ef | ||
|
|
9dd4ac6f6b | ||
|
|
8b8f1ba530 | ||
|
|
31e77d2208 | ||
|
|
c6e78a3264 | ||
|
|
c474d3b1e5 | ||
|
|
3afbbfae5a | ||
|
|
92c3c619ee | ||
|
|
b65e28684e | ||
|
|
dabf1464f2 | ||
|
|
9f618acc31 | ||
|
|
3dd4caeae9 | ||
|
|
1345693175 | ||
|
|
b5a422e8aa | ||
|
|
122773b0ba | ||
|
|
cfc1d5d820 | ||
|
|
18d1df86d5 | ||
|
|
4b65366080 | ||
|
|
74bb6acd62 | ||
|
|
a47b0fe21d | ||
|
|
349e64e3a2 | ||
|
|
0f64b49f9c | ||
|
|
1f8d4967ee | ||
|
|
e8b580ab4c | ||
|
|
866183d95a | ||
|
|
ca64843de0 | ||
|
|
d7427b9abe | ||
|
|
77537999b0 | ||
|
|
263c341792 | ||
|
|
45b1dcbb7e | ||
|
|
43e344dfb1 | ||
|
|
e5b2e70ce2 | ||
|
|
602383a720 | ||
|
|
c10e691bf5 | ||
|
|
ab7c8c1df9 | ||
|
|
72236b025f | ||
|
|
1c5c9ee3ad | ||
|
|
352c923e74 | ||
|
|
d3ebbf1c88 | ||
|
|
411a455d54 | ||
|
|
4570d617b2 | ||
|
|
305094a8b2 | ||
|
|
b02d1da202 | ||
|
|
1407a0fce9 | ||
|
|
836fb91cbe | ||
|
|
4c819ca906 | ||
|
|
3bd7fb7c59 | ||
|
|
99e7193961 | ||
|
|
db10bba4d3 | ||
|
|
31a425d34d | ||
|
|
7870406295 | ||
|
|
77de0a1116 | ||
|
|
f2da31a6f9 | ||
|
|
d36577069f | ||
|
|
4cdf959a01 | ||
|
|
62adb3fd76 | ||
|
|
5bc6e67c21 | ||
|
|
d7e86a56e6 | ||
|
|
c2b569c13a | ||
|
|
6d6555396a | ||
|
|
13018c8169 | ||
|
|
82f462785b | ||
|
|
9f21725949 | ||
|
|
82bee44933 | ||
|
|
b67c702d4d | ||
|
|
e5017908ae | ||
|
|
eff87e1705 | ||
|
|
9fabdbbb2b | ||
|
|
56908674bb | ||
|
|
9fe3fb041e | ||
|
|
46d3bf527b | ||
|
|
06e11aa8d7 | ||
|
|
acd9a87694 | ||
|
|
9a4f799145 | ||
|
|
edd067f79f | ||
|
|
36b6b8423a | ||
|
|
81c558dc21 | ||
|
|
a23d585ec6 | ||
|
|
c193bd71d7 | ||
|
|
b1791602c8 | ||
|
|
e98059e4ee | ||
|
|
371ba8f414 | ||
|
|
a271ba2cd9 | ||
|
|
a337065c4f | ||
|
|
22479a6f9e | ||
|
|
29051c3010 | ||
|
|
aaa29c87ca | ||
|
|
0385dc7ac8 | ||
|
|
d55e354a07 | ||
|
|
20716b7c7e | ||
|
|
86143d1585 | ||
|
|
214915528b | ||
|
|
9b24ff50ec | ||
|
|
9caeb34f63 | ||
|
|
368f33db24 | ||
|
|
5d7a18cbe2 | ||
|
|
19f663d0fd | ||
|
|
d857024fbe | ||
|
|
027d766e03 | ||
|
|
e7599c2202 | ||
|
|
703b525310 | ||
|
|
534882bc17 | ||
|
|
bdb2170663 | ||
|
|
dffb11b304 | ||
|
|
4e5386fccf | ||
|
|
6c11fbf850 | ||
|
|
f713eb7546 | ||
|
|
0d1f07b5ae | ||
|
|
ffcaaf213f | ||
|
|
b05a0619fb | ||
|
|
cbd4d26189 | ||
|
|
1f24a79a8a | ||
|
|
94227a103d | ||
|
|
cd1bf5befe | ||
|
|
e50e75b009 | ||
|
|
9656fbdae4 | ||
|
|
1004251175 | ||
|
|
5c70aea851 | ||
|
|
25d5cc2a47 | ||
|
|
b6802bf949 | ||
|
|
5af14af52a | ||
|
|
fffe84526e | ||
|
|
e776186070 | ||
|
|
0f4f974d04 | ||
|
|
22ecea6381 | ||
|
|
d9ad2e3036 | ||
|
|
42e60213c6 | ||
|
|
6817a0759a | ||
|
|
963e2ffc6d | ||
|
|
5c2c50ecc6 | ||
|
|
1bb4e77075 | ||
|
|
1f22c231d6 | ||
|
|
4b3baa9ad3 | ||
|
|
503728345f | ||
|
|
958b6bf72c | ||
|
|
6bb5ec12f5 | ||
|
|
30b2b63b9b | ||
|
|
6c37290f49 | ||
|
|
b19302f8b9 | ||
|
|
acd0629791 | ||
|
|
3fa1aebceb | ||
|
|
1d6a645241 | ||
|
|
922e30f162 | ||
|
|
73beb80056 | ||
|
|
72fd74862c | ||
|
|
13d403ecf8 | ||
|
|
30bca619dd | ||
|
|
f969adba9c | ||
|
|
b42abc6e42 | ||
|
|
7ec183f9e9 | ||
|
|
d33c739372 | ||
|
|
d8e12daec5 | ||
|
|
2a54cfc527 | ||
|
|
c0241a2e20 | ||
|
|
e32d7cb368 | ||
|
|
4dec1290e8 | ||
|
|
04f6493a6d | ||
|
|
f113e84aa5 | ||
|
|
57f764b92a | ||
|
|
c4c7d10d16 | ||
|
|
00ba94f372 | ||
|
|
f85f41ffa9 | ||
|
|
21dfcdfa63 | ||
|
|
c818decfc8 | ||
|
|
1386fef470 | ||
|
|
4c0cbc17a2 | ||
|
|
3c42447b41 | ||
|
|
e31f616dc1 | ||
|
|
cfbb65f7ae | ||
|
|
52c42dae1e | ||
|
|
d838d377a0 | ||
|
|
3a6f8b4c95 | ||
|
|
1039bd9f0b | ||
|
|
5f84c7195c | ||
|
|
9597f66ab1 | ||
|
|
1562f8ee6a | ||
|
|
11b0477cd6 | ||
|
|
174694e040 | ||
|
|
f5664116b9 | ||
|
|
0eb502b77a | ||
|
|
1460521dc0 | ||
|
|
e73c8bca16 | ||
|
|
a0fef8df3d | ||
|
|
1821377dfa | ||
|
|
ac2c501058 | ||
|
|
3ce3d1ee65 | ||
|
|
ad17e49177 | ||
|
|
18389a0c31 | ||
|
|
9d04975759 | ||
|
|
3bb9b3d78b | ||
|
|
ddfae282e6 | ||
|
|
02663bb33a | ||
|
|
6cb1a49128 | ||
|
|
4bcddf860c | ||
|
|
d8d4593d38 | ||
|
|
f76381525f | ||
|
|
91abb2318b | ||
|
|
3a9e695336 | ||
|
|
e270c5d70a | ||
|
|
b40e88e8ec | ||
|
|
2173ed288a | ||
|
|
82669c8f23 | ||
|
|
b093a730ab | ||
|
|
8700068468 | ||
|
|
13e322944b | ||
|
|
62aecc6e9f | ||
|
|
61eca28545 | ||
|
|
c289027efc | ||
|
|
ecdae56ffa | ||
|
|
005536633c | ||
|
|
d1ec1d6936 | ||
|
|
6f68629f29 | ||
|
|
8af4bece10 | ||
|
|
18a0d6ab26 | ||
|
|
6d73bb8a12 | ||
|
|
85b0f47be1 | ||
|
|
cc3596fc8e | ||
|
|
654367dbd8 | ||
|
|
e5ccdc1a56 | ||
|
|
84389212f6 | ||
|
|
5b01c202eb | ||
|
|
153ee4fc20 | ||
|
|
03e176794e | ||
|
|
9783f4897c | ||
|
|
928f6457bc | ||
|
|
b628bd3ad1 | ||
|
|
dba0d69f63 | ||
|
|
64c203259a | ||
|
|
25ec5c8df0 | ||
|
|
22270c62f4 | ||
|
|
bb7176252c | ||
|
|
97f62b1458 | ||
|
|
5eb333ccfc | ||
|
|
bd05afb0a7 | ||
|
|
96e6780e7b | ||
|
|
5e8ed144ae | ||
|
|
588ff245ec | ||
|
|
02cef807ea | ||
|
|
041ffdfbe0 | ||
|
|
e2c362a5b3 | ||
|
|
000ff4ad45 | ||
|
|
064e16cc6f | ||
|
|
fbc6b8fa48 | ||
|
|
4e0935873d | ||
|
|
35a40a431e | ||
|
|
d24c245b4a | ||
|
|
99f59f2f1e | ||
|
|
c558eb578f | ||
|
|
97380a5410 | ||
|
|
462f40680e | ||
|
|
8a11dbe35b | ||
|
|
d2c1467a07 | ||
|
|
ff57061190 | ||
|
|
63f1034f4a | ||
|
|
87ddec9e6c | ||
|
|
b2ea52e549 | ||
|
|
92bb753cbf | ||
|
|
7428e75643 | ||
|
|
0a2ed040f0 | ||
|
|
ded0aba399 | ||
|
|
0658248535 | ||
|
|
6cf2d842ad | ||
|
|
4a6bf95ef3 | ||
|
|
af7835fc8a | ||
|
|
5e7ed2be73 | ||
|
|
11894d0ba5 | ||
|
|
8bb92bbeb3 | ||
|
|
a50af21ce0 | ||
|
|
df29ed2b78 | ||
|
|
58cba788fc | ||
|
|
760044f603 | ||
|
|
fb099a1c3e | ||
|
|
32692bc939 | ||
|
|
c514b9cb74 | ||
|
|
5d44943958 | ||
|
|
df4138e5d3 | ||
|
|
51777709f3 | ||
|
|
f7416bb0a6 | ||
|
|
43b6fdf45d | ||
|
|
818995a6ae | ||
|
|
38fa1ba9f8 | ||
|
|
b862ac113f | ||
|
|
a2526a5a99 | ||
|
|
4124e7e9f7 | ||
|
|
05d3e73b83 | ||
|
|
907f0149d8 | ||
|
|
54f4c63a51 | ||
|
|
80388adb59 | ||
|
|
ca0792a7f7 | ||
|
|
7c1e42cff9 | ||
|
|
e9904c0e74 | ||
|
|
dd9d296e62 | ||
|
|
c0005618fe | ||
|
|
feb503b448 | ||
|
|
d2c411c9d6 | ||
|
|
562e930ca0 | ||
|
|
e97b2904e8 | ||
|
|
fd07d70575 | ||
|
|
3e6c8b3f68 | ||
|
|
ad772c4a5b | ||
|
|
5396c0c9cf | ||
|
|
c6e6ae5be0 | ||
|
|
82397fec5a | ||
|
|
6301e24c02 | ||
|
|
23b3a4d9dd | ||
|
|
e6c158b2b5 | ||
|
|
5a4f2e0744 | ||
|
|
545335a74d | ||
|
|
5ab49052fe | ||
|
|
2985b7675d | ||
|
|
49cd04c5d5 | ||
|
|
2a23d1a962 | ||
|
|
13f19adc13 | ||
|
|
3e79bbf55f | ||
|
|
b400e89a3b | ||
|
|
fc8362e7e5 | ||
|
|
8b209e82d5 | ||
|
|
6fb4fdf698 | ||
|
|
9551e64b16 | ||
|
|
232f938d2c | ||
|
|
0dc24b36e6 | ||
|
|
a77eaf214f | ||
|
|
5ab08fbfcb | ||
|
|
a73c2eb32c | ||
|
|
8f9e6d5bec | ||
|
|
790e26d608 | ||
|
|
79b96bcce8 | ||
|
|
592a7f893e | ||
|
|
7408f9c204 | ||
|
|
307de42434 | ||
|
|
d1ae7f67c4 | ||
|
|
b4616885b2 | ||
|
|
7fb7f3719f | ||
|
|
d1d1d1935d | ||
|
|
5b54f66821 | ||
|
|
e99a954387 | ||
|
|
93fd9ce3cc | ||
|
|
3a3e510b11 | ||
|
|
cbaf1edf89 | ||
|
|
94b21b5fc3 | ||
|
|
f60cc4c199 | ||
|
|
81b1f120ee | ||
|
|
48a0036023 | ||
|
|
9925a6a5a5 | ||
|
|
e5ca287dcb | ||
|
|
1a5962c430 | ||
|
|
ba5a664fca | ||
|
|
f7d05c642f | ||
|
|
877810219c | ||
|
|
fed8c9a261 | ||
|
|
896d36b16b | ||
|
|
a9c6d565f4 | ||
|
|
ae58a33456 | ||
|
|
d689d7a12a | ||
|
|
f28ada8994 | ||
|
|
4d9de1d326 | ||
|
|
405b28ee91 | ||
|
|
245aeac4a9 | ||
|
|
6b8f8db991 | ||
|
|
6c06c6682a | ||
|
|
f48da38ae1 | ||
|
|
29de1dfbb2 | ||
|
|
9063bd145f | ||
|
|
436a393cb1 | ||
|
|
a7f6e264f5 | ||
|
|
777c2a36bf | ||
|
|
c005448103 | ||
|
|
54f7be2c62 | ||
|
|
1f52b91bc5 | ||
|
|
d54bafd5d1 | ||
|
|
7728e84f34 | ||
|
|
a5808eac5e | ||
|
|
5fdb54a8d9 | ||
|
|
f02a4de88d | ||
|
|
49b2999d4f | ||
|
|
60c1caa11f | ||
|
|
3eb21895d8 | ||
|
|
2d9527e94d | ||
|
|
9f26e6f75d | ||
|
|
7890698f7d | ||
|
|
e738db0b24 | ||
|
|
67524ee869 | ||
|
|
0e5c0d1509 | ||
|
|
cdff9338d9 | ||
|
|
074926ee41 | ||
|
|
b0ef6e23a3 | ||
|
|
1ab6bc5af9 | ||
|
|
04dba20871 | ||
|
|
1cd5c927f6 | ||
|
|
6513222c3b | ||
|
|
498abd5251 | ||
|
|
fc46a81eb8 | ||
|
|
c3fb6e9f32 | ||
|
|
3aba42b965 | ||
|
|
5651a43b22 | ||
|
|
929792ef6a | ||
|
|
e13cdc4523 | ||
|
|
8dc0535dac | ||
|
|
dc4541739a | ||
|
|
a4a3da7e2e | ||
|
|
21432e1416 | ||
|
|
2f568f1319 | ||
|
|
50bdd592b4 |
17
.github/instructions/instructions.md
vendored
Normal file
17
.github/instructions/instructions.md
vendored
Normal file
@@ -0,0 +1,17 @@
|
||||
---
|
||||
applyTo: '**'
|
||||
---
|
||||
|
||||
# Building and Compiling
|
||||
|
||||
If you would like to compile or build any project. Please cd into the directory and run the following command:
|
||||
|
||||
```
|
||||
npm run compile
|
||||
```
|
||||
|
||||
Ths will make sure there are no type / syntax errors.
|
||||
|
||||
# Typescript Types.
|
||||
|
||||
Please do not use "any" types. Please create proper types where required.
|
||||
126
.github/workflows/build.yml
vendored
126
.github/workflows/build.yml
vendored
@@ -19,7 +19,11 @@ jobs:
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Preinstall
|
||||
run: npm run prerun
|
||||
uses: nick-fields/retry@v3
|
||||
with:
|
||||
timeout_minutes: 10
|
||||
max_attempts: 3
|
||||
command: npm run prerun
|
||||
|
||||
# build image for accounts service
|
||||
- name: build docker image
|
||||
@@ -38,7 +42,11 @@ jobs:
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Preinstall
|
||||
run: npm run prerun
|
||||
uses: nick-fields/retry@v3
|
||||
with:
|
||||
timeout_minutes: 10
|
||||
max_attempts: 3
|
||||
command: npm run prerun
|
||||
|
||||
# build image for accounts service
|
||||
- name: build docker image
|
||||
@@ -57,7 +65,11 @@ jobs:
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Preinstall
|
||||
run: npm run prerun
|
||||
uses: nick-fields/retry@v3
|
||||
with:
|
||||
timeout_minutes: 10
|
||||
max_attempts: 3
|
||||
command: npm run prerun
|
||||
|
||||
# build image for accounts service
|
||||
- name: build docker image
|
||||
@@ -76,7 +88,11 @@ jobs:
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Preinstall
|
||||
run: npm run prerun
|
||||
uses: nick-fields/retry@v3
|
||||
with:
|
||||
timeout_minutes: 10
|
||||
max_attempts: 3
|
||||
command: npm run prerun
|
||||
|
||||
# build image for accounts service
|
||||
- name: build docker image
|
||||
@@ -95,7 +111,11 @@ jobs:
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Preinstall
|
||||
run: npm run prerun
|
||||
uses: nick-fields/retry@v3
|
||||
with:
|
||||
timeout_minutes: 10
|
||||
max_attempts: 3
|
||||
command: npm run prerun
|
||||
|
||||
# build image for accounts service
|
||||
- name: build docker image
|
||||
@@ -114,7 +134,11 @@ jobs:
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Preinstall
|
||||
run: npm run prerun
|
||||
uses: nick-fields/retry@v3
|
||||
with:
|
||||
timeout_minutes: 10
|
||||
max_attempts: 3
|
||||
command: npm run prerun
|
||||
|
||||
# build image for accounts service
|
||||
- name: build docker image
|
||||
@@ -133,7 +157,11 @@ jobs:
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Preinstall
|
||||
run: npm run prerun
|
||||
uses: nick-fields/retry@v3
|
||||
with:
|
||||
timeout_minutes: 10
|
||||
max_attempts: 3
|
||||
command: npm run prerun
|
||||
|
||||
# build image for accounts service
|
||||
- name: build docker image
|
||||
@@ -153,7 +181,11 @@ jobs:
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Preinstall
|
||||
run: npm run prerun
|
||||
uses: nick-fields/retry@v3
|
||||
with:
|
||||
timeout_minutes: 10
|
||||
max_attempts: 3
|
||||
command: npm run prerun
|
||||
|
||||
# build image for accounts service
|
||||
- name: build docker image
|
||||
@@ -172,7 +204,11 @@ jobs:
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Preinstall
|
||||
run: npm run prerun
|
||||
uses: nick-fields/retry@v3
|
||||
with:
|
||||
timeout_minutes: 10
|
||||
max_attempts: 3
|
||||
command: npm run prerun
|
||||
|
||||
|
||||
# build image for accounts service
|
||||
@@ -193,7 +229,11 @@ jobs:
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Preinstall
|
||||
run: npm run prerun
|
||||
uses: nick-fields/retry@v3
|
||||
with:
|
||||
timeout_minutes: 10
|
||||
max_attempts: 3
|
||||
command: npm run prerun
|
||||
|
||||
# build image for accounts service
|
||||
- name: build docker image
|
||||
@@ -212,7 +252,11 @@ jobs:
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Preinstall
|
||||
run: npm run prerun
|
||||
uses: nick-fields/retry@v3
|
||||
with:
|
||||
timeout_minutes: 10
|
||||
max_attempts: 3
|
||||
command: npm run prerun
|
||||
|
||||
|
||||
# build image for accounts service
|
||||
@@ -232,7 +276,11 @@ jobs:
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Preinstall
|
||||
run: npm run prerun
|
||||
uses: nick-fields/retry@v3
|
||||
with:
|
||||
timeout_minutes: 10
|
||||
max_attempts: 3
|
||||
command: npm run prerun
|
||||
|
||||
# build image for home
|
||||
- name: build docker image
|
||||
@@ -251,7 +299,11 @@ jobs:
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Preinstall
|
||||
run: npm run prerun
|
||||
uses: nick-fields/retry@v3
|
||||
with:
|
||||
timeout_minutes: 10
|
||||
max_attempts: 3
|
||||
command: npm run prerun
|
||||
|
||||
# build image for home
|
||||
- name: build docker image
|
||||
@@ -270,7 +322,11 @@ jobs:
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Preinstall
|
||||
run: npm run prerun
|
||||
uses: nick-fields/retry@v3
|
||||
with:
|
||||
timeout_minutes: 10
|
||||
max_attempts: 3
|
||||
command: npm run prerun
|
||||
|
||||
# build image probe api
|
||||
- name: build docker image
|
||||
@@ -289,7 +345,11 @@ jobs:
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Preinstall
|
||||
run: npm run prerun
|
||||
uses: nick-fields/retry@v3
|
||||
with:
|
||||
timeout_minutes: 10
|
||||
max_attempts: 3
|
||||
command: npm run prerun
|
||||
|
||||
# build image probe api
|
||||
- name: build docker image
|
||||
@@ -308,7 +368,11 @@ jobs:
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Preinstall
|
||||
run: npm run prerun
|
||||
uses: nick-fields/retry@v3
|
||||
with:
|
||||
timeout_minutes: 10
|
||||
max_attempts: 3
|
||||
command: npm run prerun
|
||||
|
||||
# build image probe api
|
||||
- name: build docker image
|
||||
@@ -327,7 +391,11 @@ jobs:
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Preinstall
|
||||
run: npm run prerun
|
||||
uses: nick-fields/retry@v3
|
||||
with:
|
||||
timeout_minutes: 10
|
||||
max_attempts: 3
|
||||
command: npm run prerun
|
||||
|
||||
# build image probe api
|
||||
- name: build docker image
|
||||
@@ -346,7 +414,11 @@ jobs:
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Preinstall
|
||||
run: npm run prerun
|
||||
uses: nick-fields/retry@v3
|
||||
with:
|
||||
timeout_minutes: 10
|
||||
max_attempts: 3
|
||||
command: npm run prerun
|
||||
|
||||
# build image probe api
|
||||
- name: build docker image
|
||||
@@ -365,7 +437,11 @@ jobs:
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Preinstall
|
||||
run: npm run prerun
|
||||
uses: nick-fields/retry@v3
|
||||
with:
|
||||
timeout_minutes: 10
|
||||
max_attempts: 3
|
||||
command: npm run prerun
|
||||
|
||||
# build image probe api
|
||||
- name: build docker image
|
||||
@@ -384,7 +460,11 @@ jobs:
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Preinstall
|
||||
run: npm run prerun
|
||||
uses: nick-fields/retry@v3
|
||||
with:
|
||||
timeout_minutes: 10
|
||||
max_attempts: 3
|
||||
command: npm run prerun
|
||||
|
||||
# build image for home
|
||||
- name: build docker image
|
||||
@@ -403,7 +483,11 @@ jobs:
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Preinstall
|
||||
run: npm run prerun
|
||||
uses: nick-fields/retry@v3
|
||||
with:
|
||||
timeout_minutes: 10
|
||||
max_attempts: 3
|
||||
command: npm run prerun
|
||||
|
||||
# build image for mail service
|
||||
- name: build docker image
|
||||
|
||||
2
.github/workflows/compile.yml
vendored
2
.github/workflows/compile.yml
vendored
@@ -422,4 +422,4 @@ jobs:
|
||||
with:
|
||||
timeout_minutes: 30
|
||||
max_attempts: 3
|
||||
command: cd MCP && npm install && npm run compile && npm run dep-check
|
||||
command: cd MCP && npm update @oneuptime/common && npm install && npm run compile && npm run dep-check
|
||||
920
.github/workflows/release.yml
vendored
920
.github/workflows/release.yml
vendored
File diff suppressed because it is too large
Load Diff
635
.github/workflows/test-release.yaml
vendored
635
.github/workflows/test-release.yaml
vendored
@@ -176,17 +176,20 @@ jobs:
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v3
|
||||
uses: nick-fields/retry@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_PASSWORD }}
|
||||
timeout_minutes: 5
|
||||
max_attempts: 3
|
||||
command: |
|
||||
echo "${{ secrets.DOCKERHUB_PASSWORD }}" | docker login --username "${{ secrets.DOCKERHUB_USERNAME }}" --password-stdin
|
||||
|
||||
- name: Login to GitHub Container Registry
|
||||
uses: docker/login-action@v3
|
||||
uses: nick-fields/retry@v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
timeout_minutes: 5
|
||||
max_attempts: 3
|
||||
command: |
|
||||
echo "${{ secrets.GITHUB_TOKEN }}" | docker login ghcr.io --username "${{ github.repository_owner }}" --password-stdin
|
||||
|
||||
- name: Build and push Docker images (test)
|
||||
uses: nick-fields/retry@v3
|
||||
@@ -268,22 +271,29 @@ jobs:
|
||||
uses: docker/setup-buildx-action@v2
|
||||
|
||||
- name: Generate Dockerfile from Dockerfile.tpl
|
||||
run: npm run prerun
|
||||
uses: nick-fields/retry@v3
|
||||
with:
|
||||
timeout_minutes: 10
|
||||
max_attempts: 3
|
||||
command: npm run prerun
|
||||
|
||||
# Build and deploy nginx.
|
||||
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v2.2.0
|
||||
- name: Login to Docker Hub
|
||||
uses: nick-fields/retry@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_PASSWORD }}
|
||||
timeout_minutes: 5
|
||||
max_attempts: 3
|
||||
command: |
|
||||
echo "${{ secrets.DOCKERHUB_PASSWORD }}" | docker login --username "${{ secrets.DOCKERHUB_USERNAME }}" --password-stdin
|
||||
|
||||
- name: Login to GitHub Container Registry
|
||||
uses: docker/login-action@v2.2.0
|
||||
uses: nick-fields/retry@v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
timeout_minutes: 5
|
||||
max_attempts: 3
|
||||
command: |
|
||||
echo "${{ secrets.GITHUB_TOKEN }}" | docker login ghcr.io --username "${{ github.repository_owner }}" --password-stdin
|
||||
|
||||
- name: Build and push
|
||||
uses: nick-fields/retry@v3
|
||||
@@ -335,22 +345,29 @@ jobs:
|
||||
uses: docker/setup-buildx-action@v2
|
||||
|
||||
- name: Generate Dockerfile from Dockerfile.tpl
|
||||
run: npm run prerun
|
||||
uses: nick-fields/retry@v3
|
||||
with:
|
||||
timeout_minutes: 10
|
||||
max_attempts: 3
|
||||
command: npm run prerun
|
||||
|
||||
# Build and deploy nginx.
|
||||
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v2.2.0
|
||||
- name: Login to Docker Hub
|
||||
uses: nick-fields/retry@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_PASSWORD }}
|
||||
timeout_minutes: 5
|
||||
max_attempts: 3
|
||||
command: |
|
||||
echo "${{ secrets.DOCKERHUB_PASSWORD }}" | docker login --username "${{ secrets.DOCKERHUB_USERNAME }}" --password-stdin
|
||||
|
||||
- name: Login to GitHub Container Registry
|
||||
uses: docker/login-action@v2.2.0
|
||||
uses: nick-fields/retry@v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
timeout_minutes: 5
|
||||
max_attempts: 3
|
||||
command: |
|
||||
echo "${{ secrets.GITHUB_TOKEN }}" | docker login ghcr.io --username "${{ github.repository_owner }}" --password-stdin
|
||||
|
||||
- name: Build and push
|
||||
uses: nick-fields/retry@v3
|
||||
@@ -402,22 +419,29 @@ jobs:
|
||||
uses: docker/setup-buildx-action@v2
|
||||
|
||||
- name: Generate Dockerfile from Dockerfile.tpl
|
||||
run: npm run prerun
|
||||
uses: nick-fields/retry@v3
|
||||
with:
|
||||
timeout_minutes: 10
|
||||
max_attempts: 3
|
||||
command: npm run prerun
|
||||
|
||||
# Build and deploy e2e.
|
||||
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v2.2.0
|
||||
- name: Login to Docker Hub
|
||||
uses: nick-fields/retry@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_PASSWORD }}
|
||||
timeout_minutes: 5
|
||||
max_attempts: 3
|
||||
command: |
|
||||
echo "${{ secrets.DOCKERHUB_PASSWORD }}" | docker login --username "${{ secrets.DOCKERHUB_USERNAME }}" --password-stdin
|
||||
|
||||
- name: Login to GitHub Container Registry
|
||||
uses: docker/login-action@v2.2.0
|
||||
uses: nick-fields/retry@v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
timeout_minutes: 5
|
||||
max_attempts: 3
|
||||
command: |
|
||||
echo "${{ secrets.GITHUB_TOKEN }}" | docker login ghcr.io --username "${{ github.repository_owner }}" --password-stdin
|
||||
|
||||
- name: Build and push
|
||||
uses: nick-fields/retry@v3
|
||||
@@ -467,22 +491,29 @@ jobs:
|
||||
uses: docker/setup-buildx-action@v2
|
||||
|
||||
- name: Generate Dockerfile from Dockerfile.tpl
|
||||
run: npm run prerun
|
||||
uses: nick-fields/retry@v3
|
||||
with:
|
||||
timeout_minutes: 10
|
||||
max_attempts: 3
|
||||
command: npm run prerun
|
||||
|
||||
# Build and deploy test-server.
|
||||
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v2.2.0
|
||||
- name: Login to Docker Hub
|
||||
uses: nick-fields/retry@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_PASSWORD }}
|
||||
timeout_minutes: 5
|
||||
max_attempts: 3
|
||||
command: |
|
||||
echo "${{ secrets.DOCKERHUB_PASSWORD }}" | docker login --username "${{ secrets.DOCKERHUB_USERNAME }}" --password-stdin
|
||||
|
||||
- name: Login to GitHub Container Registry
|
||||
uses: docker/login-action@v2.2.0
|
||||
uses: nick-fields/retry@v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
timeout_minutes: 5
|
||||
max_attempts: 3
|
||||
command: |
|
||||
echo "${{ secrets.GITHUB_TOKEN }}" | docker login ghcr.io --username "${{ github.repository_owner }}" --password-stdin
|
||||
|
||||
- name: Build and push
|
||||
uses: nick-fields/retry@v3
|
||||
@@ -533,22 +564,29 @@ jobs:
|
||||
uses: docker/setup-buildx-action@v2
|
||||
|
||||
- name: Generate Dockerfile from Dockerfile.tpl
|
||||
run: npm run prerun
|
||||
uses: nick-fields/retry@v3
|
||||
with:
|
||||
timeout_minutes: 10
|
||||
max_attempts: 3
|
||||
command: npm run prerun
|
||||
|
||||
# Build and deploy otel-collector.
|
||||
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v2.2.0
|
||||
- name: Login to Docker Hub
|
||||
uses: nick-fields/retry@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_PASSWORD }}
|
||||
timeout_minutes: 5
|
||||
max_attempts: 3
|
||||
command: |
|
||||
echo "${{ secrets.DOCKERHUB_PASSWORD }}" | docker login --username "${{ secrets.DOCKERHUB_USERNAME }}" --password-stdin
|
||||
|
||||
- name: Login to GitHub Container Registry
|
||||
uses: docker/login-action@v2.2.0
|
||||
uses: nick-fields/retry@v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
timeout_minutes: 5
|
||||
max_attempts: 3
|
||||
command: |
|
||||
echo "${{ secrets.GITHUB_TOKEN }}" | docker login ghcr.io --username "${{ github.repository_owner }}" --password-stdin
|
||||
|
||||
- name: Build and push
|
||||
uses: nick-fields/retry@v3
|
||||
@@ -599,22 +637,29 @@ jobs:
|
||||
uses: docker/setup-buildx-action@v2
|
||||
|
||||
- name: Generate Dockerfile from Dockerfile.tpl
|
||||
run: npm run prerun
|
||||
uses: nick-fields/retry@v3
|
||||
with:
|
||||
timeout_minutes: 10
|
||||
max_attempts: 3
|
||||
command: npm run prerun
|
||||
|
||||
# Build and deploy isolated-vm.
|
||||
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v2.2.0
|
||||
- name: Login to Docker Hub
|
||||
uses: nick-fields/retry@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_PASSWORD }}
|
||||
timeout_minutes: 5
|
||||
max_attempts: 3
|
||||
command: |
|
||||
echo "${{ secrets.DOCKERHUB_PASSWORD }}" | docker login --username "${{ secrets.DOCKERHUB_USERNAME }}" --password-stdin
|
||||
|
||||
- name: Login to GitHub Container Registry
|
||||
uses: docker/login-action@v2.2.0
|
||||
uses: nick-fields/retry@v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
timeout_minutes: 5
|
||||
max_attempts: 3
|
||||
command: |
|
||||
echo "${{ secrets.GITHUB_TOKEN }}" | docker login ghcr.io --username "${{ github.repository_owner }}" --password-stdin
|
||||
|
||||
- name: Build and push
|
||||
uses: nick-fields/retry@v3
|
||||
@@ -665,22 +710,29 @@ jobs:
|
||||
uses: docker/setup-buildx-action@v2
|
||||
|
||||
- name: Generate Dockerfile from Dockerfile.tpl
|
||||
run: npm run prerun
|
||||
uses: nick-fields/retry@v3
|
||||
with:
|
||||
timeout_minutes: 10
|
||||
max_attempts: 3
|
||||
command: npm run prerun
|
||||
|
||||
# Build and deploy isolated-vm.
|
||||
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v2.2.0
|
||||
- name: Login to Docker Hub
|
||||
uses: nick-fields/retry@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_PASSWORD }}
|
||||
timeout_minutes: 5
|
||||
max_attempts: 3
|
||||
command: |
|
||||
echo "${{ secrets.DOCKERHUB_PASSWORD }}" | docker login --username "${{ secrets.DOCKERHUB_USERNAME }}" --password-stdin
|
||||
|
||||
- name: Login to GitHub Container Registry
|
||||
uses: docker/login-action@v2.2.0
|
||||
uses: nick-fields/retry@v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
timeout_minutes: 5
|
||||
max_attempts: 3
|
||||
command: |
|
||||
echo "${{ secrets.GITHUB_TOKEN }}" | docker login ghcr.io --username "${{ github.repository_owner }}" --password-stdin
|
||||
|
||||
- name: Build and push
|
||||
uses: nick-fields/retry@v3
|
||||
@@ -733,22 +785,29 @@ jobs:
|
||||
uses: docker/setup-buildx-action@v2
|
||||
|
||||
- name: Generate Dockerfile from Dockerfile.tpl
|
||||
run: npm run prerun
|
||||
uses: nick-fields/retry@v3
|
||||
with:
|
||||
timeout_minutes: 10
|
||||
max_attempts: 3
|
||||
command: npm run prerun
|
||||
|
||||
# Build and deploy status-page.
|
||||
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v2.2.0
|
||||
- name: Login to Docker Hub
|
||||
uses: nick-fields/retry@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_PASSWORD }}
|
||||
timeout_minutes: 5
|
||||
max_attempts: 3
|
||||
command: |
|
||||
echo "${{ secrets.DOCKERHUB_PASSWORD }}" | docker login --username "${{ secrets.DOCKERHUB_USERNAME }}" --password-stdin
|
||||
|
||||
- name: Login to GitHub Container Registry
|
||||
uses: docker/login-action@v2.2.0
|
||||
uses: nick-fields/retry@v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
timeout_minutes: 5
|
||||
max_attempts: 3
|
||||
command: |
|
||||
echo "${{ secrets.GITHUB_TOKEN }}" | docker login ghcr.io --username "${{ github.repository_owner }}" --password-stdin
|
||||
|
||||
- name: Build and push
|
||||
uses: nick-fields/retry@v3
|
||||
@@ -801,22 +860,29 @@ jobs:
|
||||
uses: docker/setup-buildx-action@v2
|
||||
|
||||
- name: Generate Dockerfile from Dockerfile.tpl
|
||||
run: npm run prerun
|
||||
uses: nick-fields/retry@v3
|
||||
with:
|
||||
timeout_minutes: 10
|
||||
max_attempts: 3
|
||||
command: npm run prerun
|
||||
|
||||
# Build and deploy test.
|
||||
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v2.2.0
|
||||
- name: Login to Docker Hub
|
||||
uses: nick-fields/retry@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_PASSWORD }}
|
||||
timeout_minutes: 5
|
||||
max_attempts: 3
|
||||
command: |
|
||||
echo "${{ secrets.DOCKERHUB_PASSWORD }}" | docker login --username "${{ secrets.DOCKERHUB_USERNAME }}" --password-stdin
|
||||
|
||||
- name: Login to GitHub Container Registry
|
||||
uses: docker/login-action@v2.2.0
|
||||
uses: nick-fields/retry@v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
timeout_minutes: 5
|
||||
max_attempts: 3
|
||||
command: |
|
||||
echo "${{ secrets.GITHUB_TOKEN }}" | docker login ghcr.io --username "${{ github.repository_owner }}" --password-stdin
|
||||
|
||||
- name: Build and push
|
||||
uses: nick-fields/retry@v3
|
||||
@@ -867,22 +933,29 @@ jobs:
|
||||
uses: docker/setup-buildx-action@v2
|
||||
|
||||
- name: Generate Dockerfile from Dockerfile.tpl
|
||||
run: npm run prerun
|
||||
uses: nick-fields/retry@v3
|
||||
with:
|
||||
timeout_minutes: 10
|
||||
max_attempts: 3
|
||||
command: npm run prerun
|
||||
|
||||
# Build and deploy probe-ingest.
|
||||
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v2.2.0
|
||||
- name: Login to Docker Hub
|
||||
uses: nick-fields/retry@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_PASSWORD }}
|
||||
timeout_minutes: 5
|
||||
max_attempts: 3
|
||||
command: |
|
||||
echo "${{ secrets.DOCKERHUB_PASSWORD }}" | docker login --username "${{ secrets.DOCKERHUB_USERNAME }}" --password-stdin
|
||||
|
||||
- name: Login to GitHub Container Registry
|
||||
uses: docker/login-action@v2.2.0
|
||||
uses: nick-fields/retry@v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
timeout_minutes: 5
|
||||
max_attempts: 3
|
||||
command: |
|
||||
echo "${{ secrets.GITHUB_TOKEN }}" | docker login ghcr.io --username "${{ github.repository_owner }}" --password-stdin
|
||||
|
||||
- name: Build and push
|
||||
uses: nick-fields/retry@v3
|
||||
@@ -935,22 +1008,29 @@ jobs:
|
||||
uses: docker/setup-buildx-action@v2
|
||||
|
||||
- name: Generate Dockerfile from Dockerfile.tpl
|
||||
run: npm run prerun
|
||||
uses: nick-fields/retry@v3
|
||||
with:
|
||||
timeout_minutes: 10
|
||||
max_attempts: 3
|
||||
command: npm run prerun
|
||||
|
||||
# Build and deploy ServerMonitorIngest.
|
||||
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v2.2.0
|
||||
- name: Login to Docker Hub
|
||||
uses: nick-fields/retry@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_PASSWORD }}
|
||||
timeout_minutes: 5
|
||||
max_attempts: 3
|
||||
command: |
|
||||
echo "${{ secrets.DOCKERHUB_PASSWORD }}" | docker login --username "${{ secrets.DOCKERHUB_USERNAME }}" --password-stdin
|
||||
|
||||
- name: Login to GitHub Container Registry
|
||||
uses: docker/login-action@v2.2.0
|
||||
uses: nick-fields/retry@v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
timeout_minutes: 5
|
||||
max_attempts: 3
|
||||
command: |
|
||||
echo "${{ secrets.GITHUB_TOKEN }}" | docker login ghcr.io --username "${{ github.repository_owner }}" --password-stdin
|
||||
|
||||
- name: Build and push
|
||||
uses: nick-fields/retry@v3
|
||||
@@ -1004,22 +1084,29 @@ jobs:
|
||||
uses: docker/setup-buildx-action@v2
|
||||
|
||||
- name: Generate Dockerfile from Dockerfile.tpl
|
||||
run: npm run prerun
|
||||
uses: nick-fields/retry@v3
|
||||
with:
|
||||
timeout_minutes: 10
|
||||
max_attempts: 3
|
||||
command: npm run prerun
|
||||
|
||||
# Build and deploy incoming-request-ingest.
|
||||
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v2.2.0
|
||||
- name: Login to Docker Hub
|
||||
uses: nick-fields/retry@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_PASSWORD }}
|
||||
timeout_minutes: 5
|
||||
max_attempts: 3
|
||||
command: |
|
||||
echo "${{ secrets.DOCKERHUB_PASSWORD }}" | docker login --username "${{ secrets.DOCKERHUB_USERNAME }}" --password-stdin
|
||||
|
||||
- name: Login to GitHub Container Registry
|
||||
uses: docker/login-action@v2.2.0
|
||||
uses: nick-fields/retry@v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
timeout_minutes: 5
|
||||
max_attempts: 3
|
||||
command: |
|
||||
echo "${{ secrets.GITHUB_TOKEN }}" | docker login ghcr.io --username "${{ github.repository_owner }}" --password-stdin
|
||||
|
||||
- name: Build and push
|
||||
uses: nick-fields/retry@v3
|
||||
@@ -1071,22 +1158,29 @@ jobs:
|
||||
uses: docker/setup-buildx-action@v2
|
||||
|
||||
- name: Generate Dockerfile from Dockerfile.tpl
|
||||
run: npm run prerun
|
||||
uses: nick-fields/retry@v3
|
||||
with:
|
||||
timeout_minutes: 10
|
||||
max_attempts: 3
|
||||
command: npm run prerun
|
||||
|
||||
# Build and deploy incoming-request-ingest.
|
||||
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v2.2.0
|
||||
- name: Login to Docker Hub
|
||||
uses: nick-fields/retry@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_PASSWORD }}
|
||||
timeout_minutes: 5
|
||||
max_attempts: 3
|
||||
command: |
|
||||
echo "${{ secrets.DOCKERHUB_PASSWORD }}" | docker login --username "${{ secrets.DOCKERHUB_USERNAME }}" --password-stdin
|
||||
|
||||
- name: Login to GitHub Container Registry
|
||||
uses: docker/login-action@v2.2.0
|
||||
uses: nick-fields/retry@v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
timeout_minutes: 5
|
||||
max_attempts: 3
|
||||
command: |
|
||||
echo "${{ secrets.GITHUB_TOKEN }}" | docker login ghcr.io --username "${{ github.repository_owner }}" --password-stdin
|
||||
|
||||
- name: Build and push
|
||||
uses: nick-fields/retry@v3
|
||||
@@ -1137,22 +1231,29 @@ jobs:
|
||||
uses: docker/setup-buildx-action@v2
|
||||
|
||||
- name: Generate Dockerfile from Dockerfile.tpl
|
||||
run: npm run prerun
|
||||
uses: nick-fields/retry@v3
|
||||
with:
|
||||
timeout_minutes: 10
|
||||
max_attempts: 3
|
||||
command: npm run prerun
|
||||
|
||||
# Build and deploy probe-ingest.
|
||||
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v2.2.0
|
||||
- name: Login to Docker Hub
|
||||
uses: nick-fields/retry@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_PASSWORD }}
|
||||
timeout_minutes: 5
|
||||
max_attempts: 3
|
||||
command: |
|
||||
echo "${{ secrets.DOCKERHUB_PASSWORD }}" | docker login --username "${{ secrets.DOCKERHUB_USERNAME }}" --password-stdin
|
||||
|
||||
- name: Login to GitHub Container Registry
|
||||
uses: docker/login-action@v2.2.0
|
||||
uses: nick-fields/retry@v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
timeout_minutes: 5
|
||||
max_attempts: 3
|
||||
command: |
|
||||
echo "${{ secrets.GITHUB_TOKEN }}" | docker login ghcr.io --username "${{ github.repository_owner }}" --password-stdin
|
||||
|
||||
- name: Build and push
|
||||
uses: nick-fields/retry@v3
|
||||
@@ -1203,22 +1304,29 @@ jobs:
|
||||
uses: docker/setup-buildx-action@v2
|
||||
|
||||
- name: Generate Dockerfile from Dockerfile.tpl
|
||||
run: npm run prerun
|
||||
uses: nick-fields/retry@v3
|
||||
with:
|
||||
timeout_minutes: 10
|
||||
max_attempts: 3
|
||||
command: npm run prerun
|
||||
|
||||
# Build and deploy probe.
|
||||
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v2.2.0
|
||||
- name: Login to Docker Hub
|
||||
uses: nick-fields/retry@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_PASSWORD }}
|
||||
timeout_minutes: 5
|
||||
max_attempts: 3
|
||||
command: |
|
||||
echo "${{ secrets.DOCKERHUB_PASSWORD }}" | docker login --username "${{ secrets.DOCKERHUB_USERNAME }}" --password-stdin
|
||||
|
||||
- name: Login to GitHub Container Registry
|
||||
uses: docker/login-action@v2.2.0
|
||||
uses: nick-fields/retry@v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
timeout_minutes: 5
|
||||
max_attempts: 3
|
||||
command: |
|
||||
echo "${{ secrets.GITHUB_TOKEN }}" | docker login ghcr.io --username "${{ github.repository_owner }}" --password-stdin
|
||||
|
||||
- name: Build and push
|
||||
uses: nick-fields/retry@v3
|
||||
@@ -1269,22 +1377,29 @@ jobs:
|
||||
uses: docker/setup-buildx-action@v2
|
||||
|
||||
- name: Generate Dockerfile from Dockerfile.tpl
|
||||
run: npm run prerun
|
||||
uses: nick-fields/retry@v3
|
||||
with:
|
||||
timeout_minutes: 10
|
||||
max_attempts: 3
|
||||
command: npm run prerun
|
||||
|
||||
# Build and deploy dashboard.
|
||||
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v2.2.0
|
||||
- name: Login to Docker Hub
|
||||
uses: nick-fields/retry@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_PASSWORD }}
|
||||
timeout_minutes: 5
|
||||
max_attempts: 3
|
||||
command: |
|
||||
echo "${{ secrets.DOCKERHUB_PASSWORD }}" | docker login --username "${{ secrets.DOCKERHUB_USERNAME }}" --password-stdin
|
||||
|
||||
- name: Login to GitHub Container Registry
|
||||
uses: docker/login-action@v2.2.0
|
||||
uses: nick-fields/retry@v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
timeout_minutes: 5
|
||||
max_attempts: 3
|
||||
command: |
|
||||
echo "${{ secrets.GITHUB_TOKEN }}" | docker login ghcr.io --username "${{ github.repository_owner }}" --password-stdin
|
||||
|
||||
- name: Build and push
|
||||
uses: nick-fields/retry@v3
|
||||
@@ -1335,22 +1450,29 @@ jobs:
|
||||
uses: docker/setup-buildx-action@v2
|
||||
|
||||
- name: Generate Dockerfile from Dockerfile.tpl
|
||||
run: npm run prerun
|
||||
uses: nick-fields/retry@v3
|
||||
with:
|
||||
timeout_minutes: 10
|
||||
max_attempts: 3
|
||||
command: npm run prerun
|
||||
|
||||
# Build and deploy admin-dashboard.
|
||||
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v2.2.0
|
||||
- name: Login to Docker Hub
|
||||
uses: nick-fields/retry@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_PASSWORD }}
|
||||
timeout_minutes: 5
|
||||
max_attempts: 3
|
||||
command: |
|
||||
echo "${{ secrets.DOCKERHUB_PASSWORD }}" | docker login --username "${{ secrets.DOCKERHUB_USERNAME }}" --password-stdin
|
||||
|
||||
- name: Login to GitHub Container Registry
|
||||
uses: docker/login-action@v2.2.0
|
||||
uses: nick-fields/retry@v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
timeout_minutes: 5
|
||||
max_attempts: 3
|
||||
command: |
|
||||
echo "${{ secrets.GITHUB_TOKEN }}" | docker login ghcr.io --username "${{ github.repository_owner }}" --password-stdin
|
||||
|
||||
- name: Build and push
|
||||
uses: nick-fields/retry@v3
|
||||
@@ -1401,22 +1523,29 @@ jobs:
|
||||
uses: docker/setup-buildx-action@v2
|
||||
|
||||
- name: Generate Dockerfile from Dockerfile.tpl
|
||||
run: npm run prerun
|
||||
uses: nick-fields/retry@v3
|
||||
with:
|
||||
timeout_minutes: 10
|
||||
max_attempts: 3
|
||||
command: npm run prerun
|
||||
|
||||
# Build and deploy app.
|
||||
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v2.2.0
|
||||
- name: Login to Docker Hub
|
||||
uses: nick-fields/retry@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_PASSWORD }}
|
||||
timeout_minutes: 5
|
||||
max_attempts: 3
|
||||
command: |
|
||||
echo "${{ secrets.DOCKERHUB_PASSWORD }}" | docker login --username "${{ secrets.DOCKERHUB_USERNAME }}" --password-stdin
|
||||
|
||||
- name: Login to GitHub Container Registry
|
||||
uses: docker/login-action@v2.2.0
|
||||
uses: nick-fields/retry@v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
timeout_minutes: 5
|
||||
max_attempts: 3
|
||||
command: |
|
||||
echo "${{ secrets.GITHUB_TOKEN }}" | docker login ghcr.io --username "${{ github.repository_owner }}" --password-stdin
|
||||
|
||||
- name: Build and push
|
||||
uses: nick-fields/retry@v3
|
||||
@@ -1470,22 +1599,29 @@ jobs:
|
||||
uses: docker/setup-buildx-action@v2
|
||||
|
||||
- name: Generate Dockerfile from Dockerfile.tpl
|
||||
run: npm run prerun
|
||||
uses: nick-fields/retry@v3
|
||||
with:
|
||||
timeout_minutes: 10
|
||||
max_attempts: 3
|
||||
command: npm run prerun
|
||||
|
||||
# Build and deploy app.
|
||||
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v2.2.0
|
||||
- name: Login to Docker Hub
|
||||
uses: nick-fields/retry@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_PASSWORD }}
|
||||
timeout_minutes: 5
|
||||
max_attempts: 3
|
||||
command: |
|
||||
echo "${{ secrets.DOCKERHUB_PASSWORD }}" | docker login --username "${{ secrets.DOCKERHUB_USERNAME }}" --password-stdin
|
||||
|
||||
- name: Login to GitHub Container Registry
|
||||
uses: docker/login-action@v2.2.0
|
||||
uses: nick-fields/retry@v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
timeout_minutes: 5
|
||||
max_attempts: 3
|
||||
command: |
|
||||
echo "${{ secrets.GITHUB_TOKEN }}" | docker login ghcr.io --username "${{ github.repository_owner }}" --password-stdin
|
||||
|
||||
- name: Build and push
|
||||
uses: nick-fields/retry@v3
|
||||
@@ -1539,22 +1675,29 @@ jobs:
|
||||
uses: docker/setup-buildx-action@v2
|
||||
|
||||
- name: Generate Dockerfile from Dockerfile.tpl
|
||||
run: npm run prerun
|
||||
uses: nick-fields/retry@v3
|
||||
with:
|
||||
timeout_minutes: 10
|
||||
max_attempts: 3
|
||||
command: npm run prerun
|
||||
|
||||
# Build and deploy accounts.
|
||||
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v2.2.0
|
||||
- name: Login to Docker Hub
|
||||
uses: nick-fields/retry@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_PASSWORD }}
|
||||
timeout_minutes: 5
|
||||
max_attempts: 3
|
||||
command: |
|
||||
echo "${{ secrets.DOCKERHUB_PASSWORD }}" | docker login --username "${{ secrets.DOCKERHUB_USERNAME }}" --password-stdin
|
||||
|
||||
- name: Login to GitHub Container Registry
|
||||
uses: docker/login-action@v2.2.0
|
||||
uses: nick-fields/retry@v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
timeout_minutes: 5
|
||||
max_attempts: 3
|
||||
command: |
|
||||
echo "${{ secrets.GITHUB_TOKEN }}" | docker login ghcr.io --username "${{ github.repository_owner }}" --password-stdin
|
||||
|
||||
- name: Build and push
|
||||
uses: nick-fields/retry@v3
|
||||
@@ -1606,22 +1749,29 @@ jobs:
|
||||
uses: docker/setup-buildx-action@v2
|
||||
|
||||
- name: Generate Dockerfile from Dockerfile.tpl
|
||||
run: npm run prerun
|
||||
uses: nick-fields/retry@v3
|
||||
with:
|
||||
timeout_minutes: 10
|
||||
max_attempts: 3
|
||||
command: npm run prerun
|
||||
|
||||
# Build and deploy accounts.
|
||||
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v2.2.0
|
||||
- name: Login to Docker Hub
|
||||
uses: nick-fields/retry@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_PASSWORD }}
|
||||
timeout_minutes: 5
|
||||
max_attempts: 3
|
||||
command: |
|
||||
echo "${{ secrets.DOCKERHUB_PASSWORD }}" | docker login --username "${{ secrets.DOCKERHUB_USERNAME }}" --password-stdin
|
||||
|
||||
- name: Login to GitHub Container Registry
|
||||
uses: docker/login-action@v2.2.0
|
||||
uses: nick-fields/retry@v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
timeout_minutes: 5
|
||||
max_attempts: 3
|
||||
command: |
|
||||
echo "${{ secrets.GITHUB_TOKEN }}" | docker login ghcr.io --username "${{ github.repository_owner }}" --password-stdin
|
||||
|
||||
- name: Build and push
|
||||
uses: nick-fields/retry@v3
|
||||
@@ -1672,22 +1822,29 @@ jobs:
|
||||
uses: docker/setup-buildx-action@v2
|
||||
|
||||
- name: Generate Dockerfile from Dockerfile.tpl
|
||||
run: npm run prerun
|
||||
uses: nick-fields/retry@v3
|
||||
with:
|
||||
timeout_minutes: 10
|
||||
max_attempts: 3
|
||||
command: npm run prerun
|
||||
|
||||
# Build and deploy accounts.
|
||||
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v2.2.0
|
||||
- name: Login to Docker Hub
|
||||
uses: nick-fields/retry@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_PASSWORD }}
|
||||
timeout_minutes: 5
|
||||
max_attempts: 3
|
||||
command: |
|
||||
echo "${{ secrets.DOCKERHUB_PASSWORD }}" | docker login --username "${{ secrets.DOCKERHUB_USERNAME }}" --password-stdin
|
||||
|
||||
- name: Login to GitHub Container Registry
|
||||
uses: docker/login-action@v2.2.0
|
||||
uses: nick-fields/retry@v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
timeout_minutes: 5
|
||||
max_attempts: 3
|
||||
command: |
|
||||
echo "${{ secrets.GITHUB_TOKEN }}" | docker login ghcr.io --username "${{ github.repository_owner }}" --password-stdin
|
||||
|
||||
- name: Build and push
|
||||
uses: nick-fields/retry@v3
|
||||
@@ -1739,22 +1896,29 @@ jobs:
|
||||
uses: docker/setup-buildx-action@v2
|
||||
|
||||
- name: Generate Dockerfile from Dockerfile.tpl
|
||||
run: npm run prerun
|
||||
uses: nick-fields/retry@v3
|
||||
with:
|
||||
timeout_minutes: 10
|
||||
max_attempts: 3
|
||||
command: npm run prerun
|
||||
|
||||
# Build and deploy accounts.
|
||||
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v2.2.0
|
||||
- name: Login to Docker Hub
|
||||
uses: nick-fields/retry@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_PASSWORD }}
|
||||
timeout_minutes: 5
|
||||
max_attempts: 3
|
||||
command: |
|
||||
echo "${{ secrets.DOCKERHUB_PASSWORD }}" | docker login --username "${{ secrets.DOCKERHUB_USERNAME }}" --password-stdin
|
||||
|
||||
- name: Login to GitHub Container Registry
|
||||
uses: docker/login-action@v2.2.0
|
||||
uses: nick-fields/retry@v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
timeout_minutes: 5
|
||||
max_attempts: 3
|
||||
command: |
|
||||
echo "${{ secrets.GITHUB_TOKEN }}" | docker login ghcr.io --username "${{ github.repository_owner }}" --password-stdin
|
||||
|
||||
- name: Build and push
|
||||
uses: nick-fields/retry@v3
|
||||
@@ -1807,22 +1971,29 @@ jobs:
|
||||
uses: docker/setup-buildx-action@v2
|
||||
|
||||
- name: Generate Dockerfile from Dockerfile.tpl
|
||||
run: npm run prerun
|
||||
uses: nick-fields/retry@v3
|
||||
with:
|
||||
timeout_minutes: 10
|
||||
max_attempts: 3
|
||||
command: npm run prerun
|
||||
|
||||
# Build and deploy accounts.
|
||||
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v2.2.0
|
||||
- name: Login to Docker Hub
|
||||
uses: nick-fields/retry@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_PASSWORD }}
|
||||
timeout_minutes: 5
|
||||
max_attempts: 3
|
||||
command: |
|
||||
echo "${{ secrets.DOCKERHUB_PASSWORD }}" | docker login --username "${{ secrets.DOCKERHUB_USERNAME }}" --password-stdin
|
||||
|
||||
- name: Login to GitHub Container Registry
|
||||
uses: docker/login-action@v2.2.0
|
||||
uses: nick-fields/retry@v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
timeout_minutes: 5
|
||||
max_attempts: 3
|
||||
command: |
|
||||
echo "${{ secrets.GITHUB_TOKEN }}" | docker login ghcr.io --username "${{ github.repository_owner }}" --password-stdin
|
||||
|
||||
- name: Build and push
|
||||
uses: nick-fields/retry@v3
|
||||
@@ -1899,7 +2070,12 @@ jobs:
|
||||
- name: Wait for server to start
|
||||
run: bash ./Tests/Scripts/status-check.sh http://localhost
|
||||
- name: Run E2E Tests. Run docker container e2e in docker compose file
|
||||
run: export $(grep -v '^#' config.env | xargs) && docker compose -f docker-compose.dev.yml up --exit-code-from e2e --abort-on-container-exit e2e || (docker compose -f docker-compose.dev.yml logs e2e && exit 1)
|
||||
uses: nick-fields/retry@v3
|
||||
with:
|
||||
timeout_minutes: 90
|
||||
max_attempts: 3
|
||||
on_retry_command: docker compose -f docker-compose.dev.yml down -v || true
|
||||
command: export $(grep -v '^#' config.env | xargs) && docker compose -f docker-compose.dev.yml up --exit-code-from e2e --abort-on-container-exit e2e || (docker compose -f docker-compose.dev.yml logs e2e && exit 1)
|
||||
- name: Upload test results
|
||||
uses: actions/upload-artifact@v4
|
||||
# Run this on failure
|
||||
@@ -1907,7 +2083,7 @@ jobs:
|
||||
with:
|
||||
# Name of the artifact to upload.
|
||||
# Optional. Default is 'artifact'
|
||||
name: test-results
|
||||
name: test-results-${{ github.job }}-${{ github.run_attempt }}
|
||||
|
||||
# A file, directory or wildcard pattern that describes what to upload
|
||||
# Required.
|
||||
@@ -1952,7 +2128,12 @@ jobs:
|
||||
- name: Wait for server to start
|
||||
run: bash ./Tests/Scripts/status-check.sh http://localhost
|
||||
- name: Run E2E Tests. Run docker container e2e in docker compose file
|
||||
run: export $(grep -v '^#' config.env | xargs) && docker compose -f docker-compose.dev.yml up --exit-code-from e2e --abort-on-container-exit e2e || (docker compose -f docker-compose.dev.yml logs e2e && exit 1)
|
||||
uses: nick-fields/retry@v3
|
||||
with:
|
||||
timeout_minutes: 90
|
||||
max_attempts: 3
|
||||
on_retry_command: docker compose -f docker-compose.dev.yml down -v || true
|
||||
command: export $(grep -v '^#' config.env | xargs) && docker compose -f docker-compose.dev.yml up --exit-code-from e2e --abort-on-container-exit e2e || (docker compose -f docker-compose.dev.yml logs e2e && exit 1)
|
||||
- name: Upload test results
|
||||
uses: actions/upload-artifact@v4
|
||||
# Run this on failure
|
||||
@@ -1960,7 +2141,7 @@ jobs:
|
||||
with:
|
||||
# Name of the artifact to upload.
|
||||
# Optional. Default is 'artifact'
|
||||
name: test-results
|
||||
name: test-results-${{ github.job }}-${{ github.run_attempt }}
|
||||
|
||||
# A file, directory or wildcard pattern that describes what to upload
|
||||
# Required.
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
#
|
||||
|
||||
# Pull base image nodejs image.
|
||||
FROM public.ecr.aws/docker/library/node:23.8-alpine3.21
|
||||
FROM public.ecr.aws/docker/library/node:24.9-alpine3.21
|
||||
RUN mkdir /tmp/npm && chmod 2777 /tmp/npm && chown 1000:1000 /tmp/npm && npm config set cache /tmp/npm --global
|
||||
|
||||
RUN npm config set fetch-retries 5
|
||||
@@ -17,6 +17,7 @@ ARG APP_VERSION
|
||||
|
||||
ENV GIT_SHA=${GIT_SHA}
|
||||
ENV APP_VERSION=${APP_VERSION}
|
||||
ENV PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD=1
|
||||
|
||||
|
||||
# IF APP_VERSION is not set, set it to 1.0.0
|
||||
|
||||
@@ -1,10 +1,14 @@
|
||||
{
|
||||
"watch": ["./","../Common/Server", "../Common/Types", "../Common/Utils", "../Common/Models"],
|
||||
"ext": "ts,json,tsx,env,js,jsx,hbs",
|
||||
"ext": "ts,tsx",
|
||||
"ignore": [
|
||||
"./node_modules/**",
|
||||
"./public/**",
|
||||
"./bin/**",
|
||||
"./build/**",
|
||||
"greenlock.d/*"
|
||||
],
|
||||
"watchOptions": {"useFsEvents": false, "interval": 500},
|
||||
"env": {"TS_NODE_TRANSPILE_ONLY": "1", "TS_NODE_FILES": "false"},
|
||||
"exec": "node --inspect=0.0.0.0:9229 -r ts-node/register/transpile-only Index.ts"
|
||||
"exec": "node -r ts-node/register/transpile-only Index.ts"
|
||||
}
|
||||
@@ -3,7 +3,7 @@
|
||||
#
|
||||
|
||||
# Pull base image nodejs image.
|
||||
FROM public.ecr.aws/docker/library/node:23.8-alpine3.21
|
||||
FROM public.ecr.aws/docker/library/node:24.9-alpine3.21
|
||||
RUN mkdir /tmp/npm && chmod 2777 /tmp/npm && chown 1000:1000 /tmp/npm && npm config set cache /tmp/npm --global
|
||||
|
||||
RUN npm config set fetch-retries 5
|
||||
@@ -17,6 +17,7 @@ ARG APP_VERSION
|
||||
|
||||
ENV GIT_SHA=${GIT_SHA}
|
||||
ENV APP_VERSION=${APP_VERSION}
|
||||
ENV PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD=1
|
||||
|
||||
|
||||
# IF APP_VERSION is not set, set it to 1.0.0
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
{
|
||||
"watch": ["./","../Common/UI", "../Common/Types", "../Common/Utils", "../Common/Models"],
|
||||
"ext": "ts,json,tsx,env,js,jsx,hbs",
|
||||
"ext": "ts,tsx",
|
||||
"ignore": [
|
||||
"./node_modules/**",
|
||||
"./public/**",
|
||||
"./bin/**",
|
||||
"./public/**",
|
||||
"./public/dist/**",
|
||||
"./build/*",
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import {
|
||||
LOGIN_API_URL,
|
||||
VERIFY_TWO_FACTOR_AUTH_API_URL,
|
||||
VERIFY_TOTP_AUTH_API_URL,
|
||||
GENERATE_WEBAUTHN_AUTH_OPTIONS_API_URL,
|
||||
VERIFY_WEBAUTHN_AUTH_API_URL,
|
||||
} from "../Utils/ApiPaths";
|
||||
import Route from "Common/Types/API/Route";
|
||||
import URL from "Common/Types/API/URL";
|
||||
@@ -12,17 +14,20 @@ import { DASHBOARD_URL } from "Common/UI/Config";
|
||||
import OneUptimeLogo from "Common/UI/Images/logos/OneUptimeSVG/3-transparent.svg";
|
||||
import UiAnalytics from "Common/UI/Utils/Analytics";
|
||||
import LoginUtil from "Common/UI/Utils/Login";
|
||||
import UserTwoFactorAuth from "Common/Models/DatabaseModels/UserTwoFactorAuth";
|
||||
import UserTotpAuth from "Common/Models/DatabaseModels/UserTotpAuth";
|
||||
import UserWebAuthn from "Common/Models/DatabaseModels/UserWebAuthn";
|
||||
import Navigation from "Common/UI/Utils/Navigation";
|
||||
import UserUtil from "Common/UI/Utils/User";
|
||||
import User from "Common/Models/DatabaseModels/User";
|
||||
import React from "react";
|
||||
import useAsyncEffect from "use-async-effect";
|
||||
import StaticModelList from "Common/UI/Components/ModelList/StaticModelList";
|
||||
import BasicForm from "Common/UI/Components/Forms/BasicForm";
|
||||
import API from "Common/UI/Utils/API/API";
|
||||
import HTTPErrorResponse from "Common/Types/API/HTTPErrorResponse";
|
||||
import HTTPResponse from "Common/Types/API/HTTPResponse";
|
||||
import Base64 from "Common/Utils/Base64";
|
||||
import ErrorMessage from "Common/UI/Components/ErrorMessage/ErrorMessage";
|
||||
import ComponentLoader from "Common/UI/Components/ComponentLoader/ComponentLoader";
|
||||
|
||||
const LoginPage: () => JSX.Element = () => {
|
||||
const apiUrl: URL = LOGIN_API_URL;
|
||||
@@ -36,14 +41,32 @@ const LoginPage: () => JSX.Element = () => {
|
||||
const [showTwoFactorAuth, setShowTwoFactorAuth] =
|
||||
React.useState<boolean>(false);
|
||||
|
||||
const [twoFactorAuthList, setTwoFactorAuthList] = React.useState<
|
||||
UserTwoFactorAuth[]
|
||||
>([]);
|
||||
const [totpAuthList, setTotpAuthList] = React.useState<UserTotpAuth[]>([]);
|
||||
|
||||
const [selectedTwoFactorAuth, setSelectedTwoFactorAuth] = React.useState<
|
||||
UserTwoFactorAuth | undefined
|
||||
const [webAuthnList, setWebAuthnList] = React.useState<UserWebAuthn[]>([]);
|
||||
|
||||
const [selectedTotpAuth, setSelectedTotpAuth] = React.useState<
|
||||
UserTotpAuth | undefined
|
||||
>(undefined);
|
||||
|
||||
const [selectedWebAuthn, setSelectedWebAuthn] = React.useState<
|
||||
UserWebAuthn | undefined
|
||||
>(undefined);
|
||||
|
||||
type TwoFactorMethod = {
|
||||
type: "totp" | "webauthn";
|
||||
item: UserTotpAuth | UserWebAuthn;
|
||||
};
|
||||
|
||||
const twoFactorMethods: TwoFactorMethod[] = [
|
||||
...totpAuthList.map((item: UserTotpAuth) => {
|
||||
return { type: "totp" as const, item };
|
||||
}),
|
||||
...webAuthnList.map((item: UserWebAuthn) => {
|
||||
return { type: "webauthn" as const, item };
|
||||
}),
|
||||
];
|
||||
|
||||
const [isTwoFactorAuthLoading, setIsTwoFactorAuthLoading] =
|
||||
React.useState<boolean>(false);
|
||||
const [twofactorAuthError, setTwoFactorAuthError] =
|
||||
@@ -57,6 +80,96 @@ const LoginPage: () => JSX.Element = () => {
|
||||
}
|
||||
}, []);
|
||||
|
||||
useAsyncEffect(async () => {
|
||||
if (selectedWebAuthn) {
|
||||
setIsTwoFactorAuthLoading(true);
|
||||
try {
|
||||
const result: HTTPResponse<JSONObject> = await API.post({
|
||||
url: GENERATE_WEBAUTHN_AUTH_OPTIONS_API_URL,
|
||||
data: {
|
||||
data: {
|
||||
email: initialValues["email"],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (result instanceof HTTPErrorResponse) {
|
||||
throw result;
|
||||
}
|
||||
|
||||
const data: any = result.data as any;
|
||||
|
||||
// Convert base64url strings back to Uint8Array
|
||||
data.options.challenge = Base64.base64UrlToUint8Array(
|
||||
data.options.challenge,
|
||||
);
|
||||
if (data.options.allowCredentials) {
|
||||
data.options.allowCredentials.forEach((cred: any) => {
|
||||
cred.id = Base64.base64UrlToUint8Array(cred.id);
|
||||
});
|
||||
}
|
||||
|
||||
// Use WebAuthn API
|
||||
const credential: PublicKeyCredential =
|
||||
(await navigator.credentials.get({
|
||||
publicKey: data.options,
|
||||
})) as PublicKeyCredential;
|
||||
|
||||
const assertionResponse: AuthenticatorAssertionResponse =
|
||||
credential.response as AuthenticatorAssertionResponse;
|
||||
|
||||
// Verify
|
||||
const verifyResult: HTTPResponse<JSONObject> = await API.post({
|
||||
url: VERIFY_WEBAUTHN_AUTH_API_URL,
|
||||
data: {
|
||||
data: {
|
||||
...initialValues,
|
||||
challenge: data.challenge,
|
||||
credential: {
|
||||
id: credential.id,
|
||||
rawId: Base64.uint8ArrayToBase64Url(
|
||||
new Uint8Array(credential.rawId),
|
||||
),
|
||||
response: {
|
||||
authenticatorData: Base64.uint8ArrayToBase64Url(
|
||||
new Uint8Array(assertionResponse.authenticatorData),
|
||||
),
|
||||
clientDataJSON: Base64.uint8ArrayToBase64Url(
|
||||
new Uint8Array(assertionResponse.clientDataJSON),
|
||||
),
|
||||
signature: Base64.uint8ArrayToBase64Url(
|
||||
new Uint8Array(assertionResponse.signature),
|
||||
),
|
||||
userHandle: assertionResponse.userHandle
|
||||
? Base64.uint8ArrayToBase64Url(
|
||||
new Uint8Array(assertionResponse.userHandle),
|
||||
)
|
||||
: null,
|
||||
},
|
||||
type: credential.type,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (verifyResult instanceof HTTPErrorResponse) {
|
||||
throw verifyResult;
|
||||
}
|
||||
|
||||
const user: User = User.fromJSON(
|
||||
verifyResult.data as JSONObject,
|
||||
User,
|
||||
) as User;
|
||||
const miscData: JSONObject = {};
|
||||
|
||||
login(user as User, miscData);
|
||||
} catch (error) {
|
||||
setTwoFactorAuthError(API.getFriendlyErrorMessage(error as Error));
|
||||
}
|
||||
setIsTwoFactorAuthLoading(false);
|
||||
}
|
||||
}, [selectedWebAuthn]);
|
||||
|
||||
type LoginFunction = (user: User, miscData: JSONObject) => void;
|
||||
|
||||
const login: LoginFunction = (user: User, miscData: JSONObject): void => {
|
||||
@@ -156,16 +269,23 @@ const LoginPage: () => JSX.Element = () => {
|
||||
) => {
|
||||
if (
|
||||
miscData &&
|
||||
(miscData as JSONObject)["twoFactorAuth"] === true
|
||||
((((miscData as JSONObject)["totpAuthList"] as JSONArray)
|
||||
?.length || 0) > 0 ||
|
||||
(((miscData as JSONObject)["webAuthnList"] as JSONArray)
|
||||
?.length || 0) > 0)
|
||||
) {
|
||||
const twoFactorAuthList: Array<UserTwoFactorAuth> =
|
||||
UserTwoFactorAuth.fromJSONArray(
|
||||
(miscData as JSONObject)[
|
||||
"twoFactorAuthList"
|
||||
] as JSONArray,
|
||||
UserTwoFactorAuth,
|
||||
const totpAuthList: Array<UserTotpAuth> =
|
||||
UserTotpAuth.fromJSONArray(
|
||||
(miscData as JSONObject)["totpAuthList"] as JSONArray,
|
||||
UserTotpAuth,
|
||||
);
|
||||
setTwoFactorAuthList(twoFactorAuthList);
|
||||
const webAuthnList: Array<UserWebAuthn> =
|
||||
UserWebAuthn.fromJSONArray(
|
||||
(miscData as JSONObject)["webAuthnList"] as JSONArray,
|
||||
UserWebAuthn,
|
||||
);
|
||||
setTotpAuthList(totpAuthList);
|
||||
setWebAuthnList(webAuthnList);
|
||||
setShowTwoFactorAuth(true);
|
||||
return;
|
||||
}
|
||||
@@ -187,19 +307,53 @@ const LoginPage: () => JSX.Element = () => {
|
||||
/>
|
||||
)}
|
||||
|
||||
{showTwoFactorAuth && !selectedTwoFactorAuth && (
|
||||
<StaticModelList<UserTwoFactorAuth>
|
||||
titleField="name"
|
||||
descriptionField=""
|
||||
selectedItems={[]}
|
||||
list={twoFactorAuthList}
|
||||
onClick={(item: UserTwoFactorAuth) => {
|
||||
setSelectedTwoFactorAuth(item);
|
||||
}}
|
||||
/>
|
||||
{showTwoFactorAuth && !selectedTotpAuth && !selectedWebAuthn && (
|
||||
<div className="space-y-4">
|
||||
{twoFactorMethods.map(
|
||||
(method: TwoFactorMethod, index: number) => {
|
||||
return (
|
||||
<div
|
||||
key={index}
|
||||
className="cursor-pointer p-4 border border-gray-300 rounded-lg hover:bg-gray-50"
|
||||
onClick={() => {
|
||||
if (method.type === "totp") {
|
||||
setSelectedTotpAuth(method.item as UserTotpAuth);
|
||||
} else {
|
||||
setSelectedWebAuthn(method.item as UserWebAuthn);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div className="font-medium">
|
||||
{(method.item as any).name}
|
||||
</div>
|
||||
<div className="text-sm text-gray-500">
|
||||
{method.type === "totp"
|
||||
? "Authenticator App"
|
||||
: "Security Key"}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{showTwoFactorAuth && selectedTwoFactorAuth && (
|
||||
{showTwoFactorAuth && selectedWebAuthn && (
|
||||
<div className="text-center">
|
||||
<div className="text-lg font-medium mb-4">
|
||||
Authenticating with Security Key
|
||||
</div>
|
||||
<div className="text-sm text-gray-500 mb-4">
|
||||
Please follow the instructions on your security key device.
|
||||
</div>
|
||||
{isTwoFactorAuthLoading && <ComponentLoader />}
|
||||
{twofactorAuthError && (
|
||||
<ErrorMessage message={twofactorAuthError} />
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{showTwoFactorAuth && selectedTotpAuth && (
|
||||
<BasicForm
|
||||
id="two-factor-auth-form"
|
||||
name="Two Factor Auth"
|
||||
@@ -225,14 +379,17 @@ const LoginPage: () => JSX.Element = () => {
|
||||
try {
|
||||
const code: string = data["code"] as string;
|
||||
const twoFactorAuthId: string =
|
||||
selectedTwoFactorAuth.id?.toString() as string;
|
||||
selectedTotpAuth!.id?.toString() as string;
|
||||
|
||||
const result: HTTPErrorResponse | HTTPResponse<JSONObject> =
|
||||
await API.post(VERIFY_TWO_FACTOR_AUTH_API_URL, {
|
||||
await API.post({
|
||||
url: VERIFY_TOTP_AUTH_API_URL,
|
||||
data: {
|
||||
...initialValues,
|
||||
code: code,
|
||||
twoFactorAuthId: twoFactorAuthId,
|
||||
data: {
|
||||
...initialValues,
|
||||
code: code,
|
||||
twoFactorAuthId: twoFactorAuthId,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@@ -261,7 +418,7 @@ const LoginPage: () => JSX.Element = () => {
|
||||
)}
|
||||
</div>
|
||||
<div className="mt-10 text-center">
|
||||
{!selectedTwoFactorAuth && (
|
||||
{!selectedTotpAuth && !selectedWebAuthn && (
|
||||
<div className="text-muted mb-0 text-gray-500">
|
||||
Don't have an account?{" "}
|
||||
<Link
|
||||
@@ -272,11 +429,12 @@ const LoginPage: () => JSX.Element = () => {
|
||||
</Link>
|
||||
</div>
|
||||
)}
|
||||
{selectedTwoFactorAuth ? (
|
||||
{selectedTotpAuth || selectedWebAuthn ? (
|
||||
<div className="text-muted mb-0 text-gray-500">
|
||||
<Link
|
||||
onClick={() => {
|
||||
setSelectedTwoFactorAuth(undefined);
|
||||
setSelectedTotpAuth(undefined);
|
||||
setSelectedWebAuthn(undefined);
|
||||
}}
|
||||
className="text-indigo-500 hover:text-indigo-900 cursor-pointer"
|
||||
>
|
||||
|
||||
@@ -42,12 +42,12 @@ const LoginPage: () => JSX.Element = () => {
|
||||
try {
|
||||
// get sso config by email.
|
||||
const listResult: HTTPErrorResponse | HTTPResponse<JSONArray> =
|
||||
await API.get(
|
||||
URL.fromString(apiUrl.toString()).addQueryParam(
|
||||
await API.get({
|
||||
url: URL.fromString(apiUrl.toString()).addQueryParam(
|
||||
"email",
|
||||
email.toString(),
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
if (listResult instanceof HTTPErrorResponse) {
|
||||
throw listResult;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import Route from "Common/Types/API/Route";
|
||||
import URL from "Common/Types/API/URL";
|
||||
import { IDENTITY_URL } from "Common/UI/Config";
|
||||
import { IDENTITY_URL, APP_API_URL } from "Common/UI/Config";
|
||||
|
||||
export const SIGNUP_API_URL: URL = URL.fromURL(IDENTITY_URL).addRoute(
|
||||
new Route("/signup"),
|
||||
@@ -9,9 +9,17 @@ export const LOGIN_API_URL: URL = URL.fromURL(IDENTITY_URL).addRoute(
|
||||
new Route("/login"),
|
||||
);
|
||||
|
||||
export const VERIFY_TWO_FACTOR_AUTH_API_URL: URL = URL.fromURL(
|
||||
export const VERIFY_TOTP_AUTH_API_URL: URL = URL.fromURL(IDENTITY_URL).addRoute(
|
||||
new Route("/verify-totp-auth"),
|
||||
);
|
||||
|
||||
export const GENERATE_WEBAUTHN_AUTH_OPTIONS_API_URL: URL = URL.fromURL(
|
||||
APP_API_URL,
|
||||
).addRoute(new Route("/user-webauthn/generate-authentication-options"));
|
||||
|
||||
export const VERIFY_WEBAUTHN_AUTH_API_URL: URL = URL.fromURL(
|
||||
IDENTITY_URL,
|
||||
).addRoute(new Route("/verify-two-factor-auth"));
|
||||
).addRoute(new Route("/verify-webauthn-auth"));
|
||||
|
||||
export const SERVICE_PROVIDER_LOGIN_URL: URL = URL.fromURL(
|
||||
IDENTITY_URL,
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
#
|
||||
|
||||
# Pull base image nodejs image.
|
||||
FROM public.ecr.aws/docker/library/node:23.8-alpine3.21
|
||||
FROM public.ecr.aws/docker/library/node:24.9-alpine3.21
|
||||
RUN mkdir /tmp/npm && chmod 2777 /tmp/npm && chown 1000:1000 /tmp/npm && npm config set cache /tmp/npm --global
|
||||
|
||||
RUN npm config set fetch-retries 5
|
||||
@@ -17,6 +17,7 @@ ARG APP_VERSION
|
||||
|
||||
ENV GIT_SHA=${GIT_SHA}
|
||||
ENV APP_VERSION=${APP_VERSION}
|
||||
ENV PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD=1
|
||||
|
||||
|
||||
# IF APP_VERSION is not set, set it to 1.0.0
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
{
|
||||
"watch": ["./","../Common/UI", "../Common/Types", "../Common/Utils", "../Common/Models"],
|
||||
"ext": "ts,json,tsx,env,js,jsx,hbs",
|
||||
"ext": "ts,tsx",
|
||||
"ignore": [
|
||||
"./node_modules/**",
|
||||
"./public/**",
|
||||
"./bin/**",
|
||||
"./public/**",
|
||||
"./public/dist/**",
|
||||
"./build/*",
|
||||
|
||||
@@ -5,6 +5,7 @@ import Projects from "./Pages/Projects/Index";
|
||||
import SettingsAPIKey from "./Pages/Settings/APIKey/Index";
|
||||
import SettingsAuthentication from "./Pages/Settings/Authentication/Index";
|
||||
import SettingsCallSMS from "./Pages/Settings/CallSMS/Index";
|
||||
import SettingsWhatsApp from "./Pages/Settings/WhatsApp/Index";
|
||||
// Settings Pages.
|
||||
import SettingsEmail from "./Pages/Settings/Email/Index";
|
||||
import SettingsProbes from "./Pages/Settings/Probes/Index";
|
||||
@@ -105,6 +106,11 @@ const App: () => JSX.Element = () => {
|
||||
element={<SettingsCallSMS />}
|
||||
/>
|
||||
|
||||
<PageRoute
|
||||
path={RouteMap[PageMap.SETTINGS_WHATSAPP]?.toString() || ""}
|
||||
element={<SettingsWhatsApp />}
|
||||
/>
|
||||
|
||||
<PageRoute
|
||||
path={RouteMap[PageMap.SETTINGS_PROBES]?.toString() || ""}
|
||||
element={<SettingsProbes />}
|
||||
|
||||
@@ -54,9 +54,9 @@ const DashboardFooter: () => JSX.Element = () => {
|
||||
const fetchAppVersion: (appName: string) => Promise<JSONObject> = async (
|
||||
appName: string,
|
||||
): Promise<JSONObject> => {
|
||||
const response: HTTPResponse<JSONObject> = await API.get<JSONObject>(
|
||||
URL.fromString(`${HTTP_PROTOCOL}/${HOST}${appName}/version`),
|
||||
);
|
||||
const response: HTTPResponse<JSONObject> = await API.get<JSONObject>({
|
||||
url: URL.fromString(`${HTTP_PROTOCOL}/${HOST}${appName}/version`),
|
||||
});
|
||||
|
||||
if (response.data) {
|
||||
return response.data as JSONObject;
|
||||
|
||||
@@ -50,6 +50,15 @@ const DashboardSideMenu: () => JSX.Element = (): ReactElement => {
|
||||
}}
|
||||
icon={IconProp.Call}
|
||||
/>
|
||||
<SideMenuItem
|
||||
link={{
|
||||
title: "WhatsApp",
|
||||
to: RouteUtil.populateRouteParams(
|
||||
RouteMap[PageMap.SETTINGS_WHATSAPP] as Route,
|
||||
),
|
||||
}}
|
||||
icon={IconProp.WhatsApp}
|
||||
/>
|
||||
</SideMenuSection>
|
||||
|
||||
<SideMenuSection title="Monitoring">
|
||||
|
||||
454
AdminDashboard/src/Pages/Settings/WhatsApp/Index.tsx
Normal file
454
AdminDashboard/src/Pages/Settings/WhatsApp/Index.tsx
Normal file
@@ -0,0 +1,454 @@
|
||||
import PageMap from "../../../Utils/PageMap";
|
||||
import RouteMap, { RouteUtil } from "../../../Utils/RouteMap";
|
||||
import DashboardSideMenu from "../SideMenu";
|
||||
import Route from "Common/Types/API/Route";
|
||||
import ObjectID from "Common/Types/ObjectID";
|
||||
import FormFieldSchemaType from "Common/UI/Components/Forms/Types/FormFieldSchemaType";
|
||||
import CardModelDetail from "Common/UI/Components/ModelDetail/CardModelDetail";
|
||||
import Page from "Common/UI/Components/Page/Page";
|
||||
import FieldType from "Common/UI/Components/Types/FieldType";
|
||||
import GlobalConfig from "Common/Models/DatabaseModels/GlobalConfig";
|
||||
import React, { FunctionComponent, ReactElement, useState } from "react";
|
||||
import Card from "Common/UI/Components/Card/Card";
|
||||
import MarkdownViewer from "Common/UI/Components/Markdown.tsx/MarkdownViewer";
|
||||
import BasicForm from "Common/UI/Components/Forms/BasicForm";
|
||||
import Alert, { AlertType } from "Common/UI/Components/Alerts/Alert";
|
||||
import { JSONObject } from "Common/Types/JSON";
|
||||
import HTTPErrorResponse from "Common/Types/API/HTTPErrorResponse";
|
||||
import HTTPResponse from "Common/Types/API/HTTPResponse";
|
||||
import URL from "Common/Types/API/URL";
|
||||
import API from "Common/UI/Utils/API/API";
|
||||
import { APP_API_URL } from "Common/UI/Config";
|
||||
import WhatsAppTemplateMessages, {
|
||||
WhatsAppTemplateId,
|
||||
WhatsAppTemplateIds,
|
||||
WhatsAppTemplateLanguage,
|
||||
} from "Common/Types/WhatsApp/WhatsAppTemplates";
|
||||
|
||||
type ToFriendlyName = (value: string) => string;
|
||||
|
||||
const toFriendlyName: ToFriendlyName = (value: string): string => {
|
||||
return value
|
||||
.replace(/([a-z0-9])([A-Z])/g, "$1 $2")
|
||||
.replace(/_/g, " ")
|
||||
.replace(/\s+/g, " ")
|
||||
.trim();
|
||||
};
|
||||
|
||||
type ExtractTemplateVariables = (template: string) => Array<string>;
|
||||
|
||||
const extractTemplateVariables: ExtractTemplateVariables = (
|
||||
template: string,
|
||||
): Array<string> => {
|
||||
const matches: RegExpMatchArray | null = template.match(/\{\{(.*?)\}\}/g);
|
||||
|
||||
if (!matches) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const uniqueVariables: Set<string> = new Set<string>();
|
||||
|
||||
for (const match of matches) {
|
||||
const variable: string = match.replace("{{", "").replace("}}", "").trim();
|
||||
|
||||
if (variable) {
|
||||
uniqueVariables.add(variable);
|
||||
}
|
||||
}
|
||||
|
||||
return Array.from(uniqueVariables).sort((a: string, b: string) => {
|
||||
return a.localeCompare(b);
|
||||
});
|
||||
};
|
||||
|
||||
type BuildWhatsAppSetupMarkdown = () => string;
|
||||
|
||||
const buildWhatsAppSetupMarkdown: BuildWhatsAppSetupMarkdown = (): string => {
|
||||
const templateKeys: Array<keyof typeof WhatsAppTemplateIds> = Object.keys(
|
||||
WhatsAppTemplateIds,
|
||||
) as Array<keyof typeof WhatsAppTemplateIds>;
|
||||
|
||||
const description: string =
|
||||
"Follow these steps to connect Meta WhatsApp with OneUptime so notifications can be delivered via WhatsApp.";
|
||||
|
||||
const prerequisitesList: Array<string> = [
|
||||
"Meta Business Manager admin access for the WhatsApp Business Account.",
|
||||
"A WhatsApp Business phone number approved for API messaging.",
|
||||
"Admin access to OneUptime with permission to edit global notification settings.",
|
||||
];
|
||||
|
||||
const setupStepsList: Array<string> = [
|
||||
"Sign in to the [Meta Business Manager](https://business.facebook.com/) with admin access to your WhatsApp Business Account.",
|
||||
"From **Business Settings → Accounts → WhatsApp Accounts**, create or select the account that owns your sender phone number.",
|
||||
"In Buisness Portfolio, create a system user and assign it to the WhatsApp Business Account with the role of **Admin**.",
|
||||
"Generate a token for this system user and this will be your long-lived access token. Make sure to select the **whatsapp_business_management** and **whatsapp_business_messaging** permissions when generating the token.",
|
||||
"Paste the access token and phone number ID into the **Meta WhatsApp Settings** card above, then save.",
|
||||
"For the **Business Account ID**, go to **Business Settings → Business Info** (or **Business Settings → WhatsApp Accounts → Settings**) and copy the **WhatsApp Business Account ID** value.",
|
||||
"To locate the **App ID** and **App Secret**, open [Meta for Developers](https://developers.facebook.com/apps/), select your WhatsApp app, then navigate to **Settings → Basic**. The App ID is shown at the top; click **Show** next to **App Secret** to reveal and copy it.",
|
||||
"Create each template listed below in the Meta WhatsApp Manager. Make sure the template name, language, and variables match exactly. You can however change the content to your preference. Please make sure it's approved by Meta.",
|
||||
"Send a test notification from OneUptime to confirm that WhatsApp delivery succeeds.",
|
||||
];
|
||||
|
||||
const prerequisitesMarkdown: string = prerequisitesList
|
||||
.map((item: string) => {
|
||||
return `- ${item}`;
|
||||
})
|
||||
.join("\n");
|
||||
|
||||
const setupStepsMarkdown: string = setupStepsList
|
||||
.map((item: string, index: number) => {
|
||||
return `${index + 1}. ${item}`;
|
||||
})
|
||||
.join("\n");
|
||||
|
||||
const tableRows: string = templateKeys
|
||||
.map((enumKey: keyof typeof WhatsAppTemplateIds) => {
|
||||
const templateId: WhatsAppTemplateId = WhatsAppTemplateIds[enumKey];
|
||||
const friendlyName: string = toFriendlyName(enumKey.toString());
|
||||
const templateMessage: string = WhatsAppTemplateMessages[templateId];
|
||||
const language: string = WhatsAppTemplateLanguage[templateId] || "en";
|
||||
const variables: Array<string> =
|
||||
extractTemplateVariables(templateMessage);
|
||||
const variableList: string =
|
||||
variables.length > 0
|
||||
? variables
|
||||
.map((variable: string) => {
|
||||
return `\`${variable}\``;
|
||||
})
|
||||
.join(", ")
|
||||
: "_None_";
|
||||
|
||||
return `| ${friendlyName} | \`${templateId}\` | ${language} | ${variableList} |`;
|
||||
})
|
||||
.join("\n");
|
||||
|
||||
const templateBodies: string = templateKeys
|
||||
.map((enumKey: keyof typeof WhatsAppTemplateIds) => {
|
||||
const templateId: WhatsAppTemplateId = WhatsAppTemplateIds[enumKey];
|
||||
const friendlyName: string = toFriendlyName(enumKey.toString());
|
||||
const templateMessage: string = WhatsAppTemplateMessages[templateId];
|
||||
const language: string = WhatsAppTemplateLanguage[templateId] || "en";
|
||||
const variables: Array<string> =
|
||||
extractTemplateVariables(templateMessage);
|
||||
const variableMarkdown: string =
|
||||
variables.length > 0
|
||||
? variables
|
||||
.map((variable: string) => {
|
||||
return `- \`${variable}\``;
|
||||
})
|
||||
.join("\n")
|
||||
: "_None_";
|
||||
const variablesHeading: string = variables.length
|
||||
? `**Variables (${variables.length})**`
|
||||
: "**Variables**";
|
||||
|
||||
return [
|
||||
`#### ${friendlyName}`,
|
||||
"",
|
||||
`**Template Name:** \`${templateId}\``,
|
||||
`**Language:** ${language}`,
|
||||
"",
|
||||
variablesHeading,
|
||||
variableMarkdown,
|
||||
"",
|
||||
"**Body**",
|
||||
"```plaintext",
|
||||
templateMessage,
|
||||
"```",
|
||||
"",
|
||||
"---",
|
||||
].join("\n");
|
||||
})
|
||||
.join("\n\n");
|
||||
|
||||
const templateSummaryTable: string = [
|
||||
"| Friendly Name | Template Name | Language | Variables |",
|
||||
"| --- | --- | --- | --- |",
|
||||
tableRows,
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join("\n");
|
||||
|
||||
return [
|
||||
description,
|
||||
"### Prerequisites",
|
||||
prerequisitesMarkdown,
|
||||
"### Setup Steps",
|
||||
setupStepsMarkdown,
|
||||
"### Required WhatsApp Templates",
|
||||
templateSummaryTable,
|
||||
"### Template Bodies",
|
||||
"> Copy the exact template body below—including punctuation and spacing—when creating each template inside Meta. The variables list shows every placeholder that must be configured in WhatsApp Manager.",
|
||||
templateBodies,
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join("\n\n");
|
||||
};
|
||||
|
||||
const whatsappSetupMarkdown: string = buildWhatsAppSetupMarkdown();
|
||||
|
||||
const SettingsWhatsApp: FunctionComponent = (): ReactElement => {
|
||||
const [isSendingTest, setIsSendingTest] = useState<boolean>(false);
|
||||
const [testError, setTestError] = useState<string>("");
|
||||
const [testSuccess, setTestSuccess] = useState<string>("");
|
||||
|
||||
return (
|
||||
<Page
|
||||
title={"Admin Settings"}
|
||||
breadcrumbLinks={[
|
||||
{
|
||||
title: "Admin Dashboard",
|
||||
to: RouteUtil.populateRouteParams(RouteMap[PageMap.HOME] as Route),
|
||||
},
|
||||
{
|
||||
title: "Settings",
|
||||
to: RouteUtil.populateRouteParams(
|
||||
RouteMap[PageMap.SETTINGS] as Route,
|
||||
),
|
||||
},
|
||||
{
|
||||
title: "WhatsApp",
|
||||
to: RouteUtil.populateRouteParams(
|
||||
RouteMap[PageMap.SETTINGS_WHATSAPP] as Route,
|
||||
),
|
||||
},
|
||||
]}
|
||||
sideMenu={<DashboardSideMenu />}
|
||||
>
|
||||
<CardModelDetail
|
||||
name="Meta WhatsApp Settings"
|
||||
cardProps={{
|
||||
title: "Meta WhatsApp Settings",
|
||||
description:
|
||||
"Configure Meta WhatsApp credentials. These values are used to send WhatsApp notifications from OneUptime.",
|
||||
}}
|
||||
isEditable={true}
|
||||
editButtonText="Edit Meta WhatsApp Config"
|
||||
formSteps={[
|
||||
{
|
||||
title: "Credentials",
|
||||
id: "meta-credentials",
|
||||
},
|
||||
{
|
||||
title: "Meta App",
|
||||
id: "meta-app",
|
||||
},
|
||||
]}
|
||||
formFields={[
|
||||
{
|
||||
field: {
|
||||
metaWhatsAppAccessToken: true,
|
||||
},
|
||||
title: "Access Token",
|
||||
stepId: "meta-credentials",
|
||||
fieldType: FormFieldSchemaType.EncryptedText,
|
||||
required: true,
|
||||
description:
|
||||
"Long-lived access token generated in the Meta WhatsApp Business Platform.",
|
||||
placeholder: "EAAG...",
|
||||
},
|
||||
{
|
||||
field: {
|
||||
metaWhatsAppPhoneNumberId: true,
|
||||
},
|
||||
title: "Phone Number ID",
|
||||
stepId: "meta-credentials",
|
||||
fieldType: FormFieldSchemaType.Text,
|
||||
required: true,
|
||||
description:
|
||||
"The WhatsApp Business phone number ID associated with your sending number.",
|
||||
placeholder: "123456789012345",
|
||||
},
|
||||
{
|
||||
field: {
|
||||
metaWhatsAppBusinessAccountId: true,
|
||||
},
|
||||
title: "Business Account ID",
|
||||
stepId: "meta-credentials",
|
||||
fieldType: FormFieldSchemaType.Text,
|
||||
required: false,
|
||||
description:
|
||||
"Optional Business Account ID that owns the WhatsApp templates.",
|
||||
placeholder: "123456789012345",
|
||||
},
|
||||
{
|
||||
field: {
|
||||
metaWhatsAppAppId: true,
|
||||
},
|
||||
title: "App ID",
|
||||
stepId: "meta-app",
|
||||
fieldType: FormFieldSchemaType.Text,
|
||||
required: false,
|
||||
description:
|
||||
"Optional Facebook App ID tied to your WhatsApp integration.",
|
||||
placeholder: "987654321098765",
|
||||
},
|
||||
{
|
||||
field: {
|
||||
metaWhatsAppAppSecret: true,
|
||||
},
|
||||
title: "App Secret",
|
||||
stepId: "meta-app",
|
||||
fieldType: FormFieldSchemaType.EncryptedText,
|
||||
required: false,
|
||||
description:
|
||||
"Optional Facebook App Secret used for webhook signature verification.",
|
||||
placeholder: "Facebook App Secret",
|
||||
},
|
||||
]}
|
||||
modelDetailProps={{
|
||||
modelType: GlobalConfig,
|
||||
id: "model-detail-global-config-meta-whatsapp",
|
||||
fields: [
|
||||
{
|
||||
field: {
|
||||
metaWhatsAppAccessToken: true,
|
||||
},
|
||||
title: "Access Token",
|
||||
fieldType: FieldType.HiddenText,
|
||||
placeholder: "Not Configured",
|
||||
},
|
||||
{
|
||||
field: {
|
||||
metaWhatsAppPhoneNumberId: true,
|
||||
},
|
||||
title: "Phone Number ID",
|
||||
fieldType: FieldType.Text,
|
||||
placeholder: "Not Configured",
|
||||
},
|
||||
{
|
||||
field: {
|
||||
metaWhatsAppBusinessAccountId: true,
|
||||
},
|
||||
title: "Business Account ID",
|
||||
fieldType: FieldType.Text,
|
||||
placeholder: "Not Configured",
|
||||
},
|
||||
{
|
||||
field: {
|
||||
metaWhatsAppAppId: true,
|
||||
},
|
||||
title: "App ID",
|
||||
fieldType: FieldType.Text,
|
||||
placeholder: "Not Configured",
|
||||
},
|
||||
{
|
||||
field: {
|
||||
metaWhatsAppAppSecret: true,
|
||||
},
|
||||
title: "App Secret",
|
||||
fieldType: FieldType.HiddenText,
|
||||
placeholder: "Not Configured",
|
||||
},
|
||||
],
|
||||
modelId: ObjectID.getZeroObjectID(),
|
||||
}}
|
||||
/>
|
||||
|
||||
<Card
|
||||
title="Send Test WhatsApp Message"
|
||||
description="Send a test WhatsApp template message to confirm your Meta configuration."
|
||||
>
|
||||
{testSuccess ? (
|
||||
<Alert
|
||||
type={AlertType.SUCCESS}
|
||||
title={testSuccess}
|
||||
className="mb-4"
|
||||
/>
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
|
||||
<BasicForm
|
||||
id="send-test-whatsapp-form"
|
||||
name="Send Test WhatsApp Message"
|
||||
isLoading={isSendingTest}
|
||||
error={testError || ""}
|
||||
submitButtonText="Send Test Message"
|
||||
maxPrimaryButtonWidth={true}
|
||||
initialValues={{
|
||||
phoneNumber: "",
|
||||
}}
|
||||
fields={[
|
||||
{
|
||||
field: {
|
||||
phoneNumber: true,
|
||||
},
|
||||
title: "Recipient WhatsApp Number",
|
||||
description:
|
||||
"Enter the full international phone number (including country code) that should receive the test message.",
|
||||
placeholder: "+11234567890",
|
||||
required: true,
|
||||
fieldType: FormFieldSchemaType.Phone,
|
||||
disableSpellCheck: true,
|
||||
},
|
||||
]}
|
||||
onSubmit={async (
|
||||
values: JSONObject,
|
||||
onSubmitSuccessful?: () => void,
|
||||
) => {
|
||||
const toPhone: string = String(values["phoneNumber"] || "").trim();
|
||||
|
||||
if (!toPhone) {
|
||||
setTestSuccess("");
|
||||
setTestError(
|
||||
"Please enter a WhatsApp number before sending a test message.",
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
setIsSendingTest(true);
|
||||
setTestError("");
|
||||
setTestSuccess("");
|
||||
|
||||
try {
|
||||
const response: HTTPResponse<JSONObject> | HTTPErrorResponse =
|
||||
await API.post({
|
||||
url: URL.fromString(APP_API_URL.toString()).addRoute(
|
||||
"/notification/whatsapp/test",
|
||||
),
|
||||
data: {
|
||||
toPhone,
|
||||
templateKey: WhatsAppTemplateIds.TestNotification,
|
||||
templateLanguageCode:
|
||||
WhatsAppTemplateLanguage[
|
||||
WhatsAppTemplateIds.TestNotification
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
if (response instanceof HTTPErrorResponse) {
|
||||
throw response;
|
||||
}
|
||||
|
||||
if (response.isFailure()) {
|
||||
throw new Error("Failed to send test WhatsApp message.");
|
||||
}
|
||||
|
||||
setTestSuccess(
|
||||
"Test WhatsApp message sent successfully. Check the recipient device to confirm delivery.",
|
||||
);
|
||||
|
||||
if (onSubmitSuccessful) {
|
||||
onSubmitSuccessful();
|
||||
}
|
||||
} catch (err) {
|
||||
setTestError(API.getFriendlyMessage(err));
|
||||
} finally {
|
||||
setIsSendingTest(false);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</Card>
|
||||
|
||||
<Card
|
||||
title="Meta WhatsApp Setup Guide"
|
||||
description="Steps to connect Meta WhatsApp and the templates you must provision."
|
||||
>
|
||||
<MarkdownViewer text={whatsappSetupMarkdown} />
|
||||
</Card>
|
||||
</Page>
|
||||
);
|
||||
};
|
||||
|
||||
export default SettingsWhatsApp;
|
||||
@@ -15,6 +15,7 @@ enum PageMap {
|
||||
SETTINGS_HOST = "SETTINGS_HOST",
|
||||
SETTINGS_SMTP = "SETTINGS_SMTP",
|
||||
SETTINGS_CALL_AND_SMS = "SETTINGS_CALL_AND_SMS",
|
||||
SETTINGS_WHATSAPP = "SETTINGS_WHATSAPP",
|
||||
SETTINGS_PROBES = "SETTINGS_PROBES",
|
||||
SETTINGS_AUTHENTICATION = "SETTINGS_AUTHENTICATION",
|
||||
SETTINGS_API_KEY = "SETTINGS_API_KEY",
|
||||
|
||||
@@ -25,6 +25,7 @@ const RouteMap: Dictionary<Route> = {
|
||||
[PageMap.SETTINGS_HOST]: new Route(`/admin/settings/host`),
|
||||
[PageMap.SETTINGS_SMTP]: new Route(`/admin/settings/smtp`),
|
||||
[PageMap.SETTINGS_CALL_AND_SMS]: new Route(`/admin/settings/call-and-sms`),
|
||||
[PageMap.SETTINGS_WHATSAPP]: new Route(`/admin/settings/whatsapp`),
|
||||
[PageMap.SETTINGS_PROBES]: new Route(`/admin/settings/probes`),
|
||||
[PageMap.SETTINGS_AUTHENTICATION]: new Route(
|
||||
`/admin/settings/authentication`,
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
#
|
||||
|
||||
# Pull base image nodejs image.
|
||||
FROM public.ecr.aws/docker/library/node:23.8-alpine3.21
|
||||
FROM public.ecr.aws/docker/library/node:24.9-alpine3.21
|
||||
RUN mkdir /tmp/npm && chmod 2777 /tmp/npm && chown 1000:1000 /tmp/npm && npm config set cache /tmp/npm --global
|
||||
|
||||
RUN npm config set fetch-retries 5
|
||||
@@ -17,6 +17,7 @@ ARG APP_VERSION
|
||||
|
||||
ENV GIT_SHA=${GIT_SHA}
|
||||
ENV APP_VERSION=${APP_VERSION}
|
||||
ENV PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD=1
|
||||
|
||||
|
||||
# IF APP_VERSION is not set, set it to 1.0.0
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import BaseAPI from "Common/Server/API/BaseAPI";
|
||||
import BaseAnalyticsAPI from "Common/Server/API/BaseAnalyticsAPI";
|
||||
import BillingAPI from "Common/Server/API/BillingAPI";
|
||||
import BillingInvoiceAPI from "Common/Server/API/BillingInvoiceAPI";
|
||||
import BillingPaymentMethodAPI from "Common/Server/API/BillingPaymentMethodAPI";
|
||||
import CopilotCodeRepositoryAPI from "Common/Server/API/CopilotCodeRepositoryAPI";
|
||||
@@ -23,12 +24,14 @@ import WorkspaceNotificationRuleAPI from "Common/Server/API/WorkspaceNotificatio
|
||||
import StatusPageDomainAPI from "Common/Server/API/StatusPageDomainAPI";
|
||||
import StatusPageSubscriberAPI from "Common/Server/API/StatusPageSubscriberAPI";
|
||||
import UserCallAPI from "Common/Server/API/UserCallAPI";
|
||||
import UserTwoFactorAuthAPI from "Common/Server/API/UserTwoFactorAuthAPI";
|
||||
import UserTotpAuthAPI from "Common/Server/API/UserTotpAuthAPI";
|
||||
import UserWebAuthnAPI from "Common/Server/API/UserWebAuthnAPI";
|
||||
import MonitorTest from "Common/Models/DatabaseModels/MonitorTest";
|
||||
// User Notification methods.
|
||||
import UserEmailAPI from "Common/Server/API/UserEmailAPI";
|
||||
import UserNotificationLogTimelineAPI from "Common/Server/API/UserOnCallLogTimelineAPI";
|
||||
import UserSMSAPI from "Common/Server/API/UserSmsAPI";
|
||||
import UserWhatsAppAPI from "Common/Server/API/UserWhatsAppAPI";
|
||||
import UserPushAPI from "Common/Server/API/UserPushAPI";
|
||||
import ApiKeyPermissionService, {
|
||||
Service as ApiKeyPermissionServiceType,
|
||||
@@ -282,6 +285,9 @@ import ShortLinkService, {
|
||||
import SmsLogService, {
|
||||
Service as SmsLogServiceType,
|
||||
} from "Common/Server/Services/SmsLogService";
|
||||
import WhatsAppLogService, {
|
||||
Service as WhatsAppLogServiceType,
|
||||
} from "Common/Server/Services/WhatsAppLogService";
|
||||
import PushNotificationLogService, {
|
||||
Service as PushNotificationLogServiceType,
|
||||
} from "Common/Server/Services/PushNotificationLogService";
|
||||
@@ -327,6 +333,9 @@ import TeamMemberService, {
|
||||
import TeamPermissionService, {
|
||||
Service as TeamPermissionServiceType,
|
||||
} from "Common/Server/Services/TeamPermissionService";
|
||||
import TeamComplianceSettingService, {
|
||||
TeamComplianceSettingService as TeamComplianceSettingServiceType,
|
||||
} from "Common/Server/Services/TeamComplianceSettingService";
|
||||
import TeamService, {
|
||||
Service as TeamServiceType,
|
||||
} from "Common/Server/Services/TeamService";
|
||||
@@ -453,6 +462,7 @@ import ServiceCatalogOwnerUser from "Common/Models/DatabaseModels/ServiceCatalog
|
||||
import ServiceCopilotCodeRepository from "Common/Models/DatabaseModels/ServiceCopilotCodeRepository";
|
||||
import ShortLink from "Common/Models/DatabaseModels/ShortLink";
|
||||
import SmsLog from "Common/Models/DatabaseModels/SmsLog";
|
||||
import WhatsAppLog from "Common/Models/DatabaseModels/WhatsAppLog";
|
||||
import StatusPageAnnouncement from "Common/Models/DatabaseModels/StatusPageAnnouncement";
|
||||
// Custom Fields API
|
||||
import StatusPageCustomField from "Common/Models/DatabaseModels/StatusPageCustomField";
|
||||
@@ -469,6 +479,7 @@ import StatusPageSSO from "Common/Models/DatabaseModels/StatusPageSso";
|
||||
import Team from "Common/Models/DatabaseModels/Team";
|
||||
import TeamMember from "Common/Models/DatabaseModels/TeamMember";
|
||||
import TeamPermission from "Common/Models/DatabaseModels/TeamPermission";
|
||||
import TeamComplianceSetting from "Common/Models/DatabaseModels/TeamComplianceSetting";
|
||||
import TelemetryService from "Common/Models/DatabaseModels/TelemetryService";
|
||||
import TelemetryUsageBilling from "Common/Models/DatabaseModels/TelemetryUsageBilling";
|
||||
import User from "Common/Models/DatabaseModels/User";
|
||||
@@ -521,6 +532,7 @@ import ScheduledMaintenanceFeedService, {
|
||||
} from "Common/Server/Services/ScheduledMaintenanceFeedService";
|
||||
|
||||
import SlackAPI from "Common/Server/API/SlackAPI";
|
||||
import MicrosoftTeamsAPI from "Common/Server/API/MicrosoftTeamsAPI";
|
||||
|
||||
import WorkspaceProjectAuthToken from "Common/Models/DatabaseModels/WorkspaceProjectAuthToken";
|
||||
import WorkspaceProjectAuthTokenService, {
|
||||
@@ -550,10 +562,7 @@ import MetricTypeService, {
|
||||
import MetricType from "Common/Models/DatabaseModels/MetricType";
|
||||
|
||||
import OnCallDutyPolicyAPI from "Common/Server/API/OnCallDutyPolicyAPI";
|
||||
|
||||
// OnCallDutyPolicyOwnerTeam
|
||||
// OnCallDutyPolicyOwnerUser
|
||||
// OnCallDutyPolicyFeed
|
||||
import TeamComplianceAPI from "Common/Server/API/TeamComplianceAPI";
|
||||
|
||||
import OnCallDutyPolicyFeed from "Common/Models/DatabaseModels/OnCallDutyPolicyFeed";
|
||||
import OnCallDutyPolicyFeedService, {
|
||||
@@ -1167,6 +1176,14 @@ const BaseAPIFeatureSet: FeatureSet = {
|
||||
).getRouter(),
|
||||
);
|
||||
|
||||
app.use(
|
||||
`/${APP_NAME.toLocaleLowerCase()}`,
|
||||
new BaseAPI<TeamComplianceSetting, TeamComplianceSettingServiceType>(
|
||||
TeamComplianceSetting,
|
||||
TeamComplianceSettingService,
|
||||
).getRouter(),
|
||||
);
|
||||
|
||||
app.use(
|
||||
`/${APP_NAME.toLocaleLowerCase()}`,
|
||||
new BaseAPI<MonitorStatus, MonitorStatusServiceType>(
|
||||
@@ -1526,6 +1543,14 @@ const BaseAPIFeatureSet: FeatureSet = {
|
||||
new BaseAPI<SmsLog, SmsLogServiceType>(SmsLog, SmsLogService).getRouter(),
|
||||
);
|
||||
|
||||
app.use(
|
||||
`/${APP_NAME.toLocaleLowerCase()}`,
|
||||
new BaseAPI<WhatsAppLog, WhatsAppLogServiceType>(
|
||||
WhatsAppLog,
|
||||
WhatsAppLogService,
|
||||
).getRouter(),
|
||||
);
|
||||
|
||||
app.use(
|
||||
`/${APP_NAME.toLocaleLowerCase()}`,
|
||||
new BaseAPI<PushNotificationLog, PushNotificationLogServiceType>(
|
||||
@@ -1581,7 +1606,6 @@ const BaseAPIFeatureSet: FeatureSet = {
|
||||
MonitorTimelineStatusService,
|
||||
).getRouter(),
|
||||
);
|
||||
|
||||
app.use(`/${APP_NAME.toLocaleLowerCase()}`, new ShortLinkAPI().getRouter());
|
||||
app.use(`/${APP_NAME.toLocaleLowerCase()}`, new MonitorAPI().getRouter());
|
||||
app.use(
|
||||
@@ -1595,6 +1619,12 @@ const BaseAPIFeatureSet: FeatureSet = {
|
||||
new OnCallDutyPolicyAPI().getRouter(),
|
||||
);
|
||||
|
||||
// TeamComplianceAPI
|
||||
app.use(
|
||||
`/${APP_NAME.toLocaleLowerCase()}`,
|
||||
new TeamComplianceAPI().getRouter(),
|
||||
);
|
||||
|
||||
app.use(
|
||||
`/${APP_NAME.toLocaleLowerCase()}`,
|
||||
new WorkspaceNotificationRuleAPI().getRouter(),
|
||||
@@ -1619,6 +1649,10 @@ const BaseAPIFeatureSet: FeatureSet = {
|
||||
new ResellerPlanAPI().getRouter(),
|
||||
);
|
||||
app.use(`/${APP_NAME.toLocaleLowerCase()}`, new SlackAPI().getRouter());
|
||||
app.use(
|
||||
`/${APP_NAME.toLocaleLowerCase()}`,
|
||||
new MicrosoftTeamsAPI().getRouter(),
|
||||
);
|
||||
app.use(
|
||||
`/${APP_NAME.toLocaleLowerCase()}`,
|
||||
new GlobalConfigAPI().getRouter(),
|
||||
@@ -1646,10 +1680,18 @@ const BaseAPIFeatureSet: FeatureSet = {
|
||||
app.use(`/${APP_NAME.toLocaleLowerCase()}`, new UserCallAPI().getRouter());
|
||||
app.use(
|
||||
`/${APP_NAME.toLocaleLowerCase()}`,
|
||||
new UserTwoFactorAuthAPI().getRouter(),
|
||||
new UserTotpAuthAPI().getRouter(),
|
||||
);
|
||||
app.use(
|
||||
`/${APP_NAME.toLocaleLowerCase()}`,
|
||||
new UserWebAuthnAPI().getRouter(),
|
||||
);
|
||||
app.use(`/${APP_NAME.toLocaleLowerCase()}`, new UserEmailAPI().getRouter());
|
||||
app.use(`/${APP_NAME.toLocaleLowerCase()}`, new UserSMSAPI().getRouter());
|
||||
app.use(
|
||||
`/${APP_NAME.toLocaleLowerCase()}`,
|
||||
new UserWhatsAppAPI().getRouter(),
|
||||
);
|
||||
app.use(`/${APP_NAME.toLocaleLowerCase()}`, new UserPushAPI().getRouter());
|
||||
app.use(`/${APP_NAME.toLocaleLowerCase()}`, new ProbeAPI().getRouter());
|
||||
|
||||
@@ -1670,6 +1712,8 @@ const BaseAPIFeatureSet: FeatureSet = {
|
||||
new BillingInvoiceAPI().getRouter(),
|
||||
);
|
||||
|
||||
app.use(`/${APP_NAME.toLocaleLowerCase()}`, new BillingAPI().getRouter());
|
||||
|
||||
app.use(
|
||||
`/${APP_NAME.toLocaleLowerCase()}`,
|
||||
new BaseAPI<
|
||||
|
||||
@@ -24,7 +24,7 @@ import AccessTokenService from "Common/Server/Services/AccessTokenService";
|
||||
import EmailVerificationTokenService from "Common/Server/Services/EmailVerificationTokenService";
|
||||
import MailService from "Common/Server/Services/MailService";
|
||||
import UserService from "Common/Server/Services/UserService";
|
||||
import UserTwoFactorAuthService from "Common/Server/Services/UserTwoFactorAuthService";
|
||||
import UserTotpAuthService from "Common/Server/Services/UserTotpAuthService";
|
||||
import CookieUtil from "Common/Server/Utils/Cookie";
|
||||
import Express, {
|
||||
ExpressRequest,
|
||||
@@ -34,10 +34,12 @@ import Express, {
|
||||
} from "Common/Server/Utils/Express";
|
||||
import logger from "Common/Server/Utils/Logger";
|
||||
import Response from "Common/Server/Utils/Response";
|
||||
import TwoFactorAuth from "Common/Server/Utils/TwoFactorAuth";
|
||||
import TotpAuth from "Common/Server/Utils/TotpAuth";
|
||||
import EmailVerificationToken from "Common/Models/DatabaseModels/EmailVerificationToken";
|
||||
import User from "Common/Models/DatabaseModels/User";
|
||||
import UserTwoFactorAuth from "Common/Models/DatabaseModels/UserTwoFactorAuth";
|
||||
import UserTotpAuth from "Common/Models/DatabaseModels/UserTotpAuth";
|
||||
import UserWebAuthn from "Common/Models/DatabaseModels/UserWebAuthn";
|
||||
import UserWebAuthnService from "Common/Server/Services/UserWebAuthnService";
|
||||
|
||||
const router: ExpressRouter = Express.getRouter();
|
||||
|
||||
@@ -503,7 +505,7 @@ router.post(
|
||||
);
|
||||
|
||||
router.post(
|
||||
"/verify-two-factor-auth",
|
||||
"/verify-totp-auth",
|
||||
async (
|
||||
req: ExpressRequest,
|
||||
res: ExpressResponse,
|
||||
@@ -513,7 +515,25 @@ router.post(
|
||||
req: req,
|
||||
res: res,
|
||||
next: next,
|
||||
verifyTwoFactorAuth: true,
|
||||
verifyTotpAuth: true,
|
||||
verifyWebAuthn: false,
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
router.post(
|
||||
"/verify-webauthn-auth",
|
||||
async (
|
||||
req: ExpressRequest,
|
||||
res: ExpressResponse,
|
||||
next: NextFunction,
|
||||
): Promise<void> => {
|
||||
return login({
|
||||
req: req,
|
||||
res: res,
|
||||
next: next,
|
||||
verifyTotpAuth: false,
|
||||
verifyWebAuthn: true,
|
||||
});
|
||||
},
|
||||
);
|
||||
@@ -529,62 +549,99 @@ router.post(
|
||||
req: req,
|
||||
res: res,
|
||||
next: next,
|
||||
verifyTwoFactorAuth: false,
|
||||
verifyTotpAuth: false,
|
||||
verifyWebAuthn: false,
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
type FetchTwoFactorAuthListFunction = (
|
||||
userId: ObjectID,
|
||||
) => Promise<Array<UserTwoFactorAuth>>;
|
||||
type FetchTotpAuthListFunction = (userId: ObjectID) => Promise<{
|
||||
totpAuthList: Array<UserTotpAuth>;
|
||||
webAuthnList: Array<UserWebAuthn>;
|
||||
}>;
|
||||
|
||||
const fetchTwoFactorAuthList: FetchTwoFactorAuthListFunction = async (
|
||||
const fetchTotpAuthList: FetchTotpAuthListFunction = async (
|
||||
userId: ObjectID,
|
||||
): Promise<Array<UserTwoFactorAuth>> => {
|
||||
const twoFactorAuthList: Array<UserTwoFactorAuth> =
|
||||
await UserTwoFactorAuthService.findBy({
|
||||
query: {
|
||||
userId: userId,
|
||||
isVerified: true,
|
||||
},
|
||||
select: {
|
||||
_id: true,
|
||||
userId: true,
|
||||
name: true,
|
||||
},
|
||||
limit: LIMIT_PER_PROJECT,
|
||||
skip: 0,
|
||||
props: {
|
||||
isRoot: true,
|
||||
},
|
||||
});
|
||||
): Promise<{
|
||||
totpAuthList: Array<UserTotpAuth>;
|
||||
webAuthnList: Array<UserWebAuthn>;
|
||||
}> => {
|
||||
const totpAuthList: Array<UserTotpAuth> = await UserTotpAuthService.findBy({
|
||||
query: {
|
||||
userId: userId,
|
||||
isVerified: true,
|
||||
},
|
||||
select: {
|
||||
_id: true,
|
||||
userId: true,
|
||||
name: true,
|
||||
},
|
||||
limit: LIMIT_PER_PROJECT,
|
||||
skip: 0,
|
||||
props: {
|
||||
isRoot: true,
|
||||
},
|
||||
});
|
||||
|
||||
return twoFactorAuthList;
|
||||
const webAuthnList: Array<UserWebAuthn> = await UserWebAuthnService.findBy({
|
||||
query: {
|
||||
userId: userId,
|
||||
isVerified: true,
|
||||
},
|
||||
select: {
|
||||
_id: true,
|
||||
userId: true,
|
||||
name: true,
|
||||
},
|
||||
limit: LIMIT_PER_PROJECT,
|
||||
skip: 0,
|
||||
props: {
|
||||
isRoot: true,
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
totpAuthList: totpAuthList || [],
|
||||
webAuthnList: webAuthnList || [],
|
||||
};
|
||||
};
|
||||
|
||||
type LoginFunction = (options: {
|
||||
req: ExpressRequest;
|
||||
res: ExpressResponse;
|
||||
next: NextFunction;
|
||||
verifyTwoFactorAuth: boolean;
|
||||
verifyTotpAuth: boolean;
|
||||
verifyWebAuthn: boolean;
|
||||
}) => Promise<void>;
|
||||
|
||||
const login: LoginFunction = async (options: {
|
||||
req: ExpressRequest;
|
||||
res: ExpressResponse;
|
||||
next: NextFunction;
|
||||
verifyTwoFactorAuth: boolean;
|
||||
verifyTotpAuth: boolean;
|
||||
verifyWebAuthn: boolean;
|
||||
}): Promise<void> => {
|
||||
const req: ExpressRequest = options.req;
|
||||
const res: ExpressResponse = options.res;
|
||||
const next: NextFunction = options.next;
|
||||
const verifyTwoFactorAuth: boolean = options.verifyTwoFactorAuth;
|
||||
const verifyTotpAuth: boolean = options.verifyTotpAuth;
|
||||
const verifyWebAuthn: boolean = options.verifyWebAuthn;
|
||||
|
||||
try {
|
||||
const data: JSONObject = req.body["data"];
|
||||
|
||||
logger.debug("Login request data: " + JSON.stringify(req.body, null, 2));
|
||||
|
||||
const user: User = BaseModel.fromJSON(data as JSONObject, User) as User;
|
||||
|
||||
if (!user.email || !user.password) {
|
||||
return Response.sendErrorResponse(
|
||||
req,
|
||||
res,
|
||||
new BadDataException("Email and password are required."),
|
||||
);
|
||||
}
|
||||
|
||||
await user.password?.hashValue(EncryptionSecret);
|
||||
|
||||
const alreadySavedUser: User | null = await UserService.findOneBy({
|
||||
@@ -638,13 +695,21 @@ const login: LoginFunction = async (options: {
|
||||
);
|
||||
}
|
||||
|
||||
if (alreadySavedUser.enableTwoFactorAuth && !verifyTwoFactorAuth) {
|
||||
if (
|
||||
alreadySavedUser.enableTwoFactorAuth &&
|
||||
!verifyTotpAuth &&
|
||||
!verifyWebAuthn
|
||||
) {
|
||||
// If two factor auth is enabled then we will send the user to the two factor auth page.
|
||||
|
||||
const twoFactorAuthList: Array<UserTwoFactorAuth> =
|
||||
await fetchTwoFactorAuthList(alreadySavedUser.id!);
|
||||
const { totpAuthList, webAuthnList } = await fetchTotpAuthList(
|
||||
alreadySavedUser.id!,
|
||||
);
|
||||
|
||||
if (!twoFactorAuthList || twoFactorAuthList.length === 0) {
|
||||
if (
|
||||
(!totpAuthList || totpAuthList.length === 0) &&
|
||||
(!webAuthnList || webAuthnList.length === 0)
|
||||
) {
|
||||
const errorMessage: string = IsBillingEnabled
|
||||
? "Two Factor Authentication is enabled but no two factor auth is setup. Please contact OneUptime support for help."
|
||||
: "Two Factor Authentication is enabled but no two factor auth is setup. Please contact your server admin to disable two factor auth for this account.";
|
||||
@@ -656,62 +721,68 @@ const login: LoginFunction = async (options: {
|
||||
);
|
||||
}
|
||||
|
||||
return Response.sendEntityResponse(req, res, user, User, {
|
||||
return Response.sendEntityResponse(req, res, alreadySavedUser, User, {
|
||||
miscData: {
|
||||
twoFactorAuthList: UserTwoFactorAuth.toJSONArray(
|
||||
twoFactorAuthList,
|
||||
UserTwoFactorAuth,
|
||||
),
|
||||
twoFactorAuth: true,
|
||||
totpAuthList: UserTotpAuth.toJSONArray(totpAuthList, UserTotpAuth),
|
||||
webAuthnList: UserWebAuthn.toJSONArray(webAuthnList, UserWebAuthn),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
if (verifyTwoFactorAuth) {
|
||||
// code from req
|
||||
const code: string = data["code"] as string;
|
||||
const twoFactorAuthId: string = data["twoFactorAuthId"] as string;
|
||||
if (verifyTotpAuth || verifyWebAuthn) {
|
||||
if (verifyTotpAuth) {
|
||||
// code from req
|
||||
const code: string = data["code"] as string;
|
||||
const twoFactorAuthId: string = data["twoFactorAuthId"] as string;
|
||||
|
||||
const twoFactorAuth: UserTwoFactorAuth | null =
|
||||
await UserTwoFactorAuthService.findOneBy({
|
||||
query: {
|
||||
_id: twoFactorAuthId,
|
||||
userId: alreadySavedUser.id!,
|
||||
isVerified: true,
|
||||
},
|
||||
select: {
|
||||
_id: true,
|
||||
twoFactorSecret: true,
|
||||
},
|
||||
props: {
|
||||
isRoot: true,
|
||||
},
|
||||
const totpAuth: UserTotpAuth | null =
|
||||
await UserTotpAuthService.findOneBy({
|
||||
query: {
|
||||
_id: twoFactorAuthId,
|
||||
userId: alreadySavedUser.id!,
|
||||
isVerified: true,
|
||||
},
|
||||
select: {
|
||||
_id: true,
|
||||
twoFactorSecret: true,
|
||||
},
|
||||
props: {
|
||||
isRoot: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!totpAuth) {
|
||||
return Response.sendErrorResponse(
|
||||
req,
|
||||
res,
|
||||
new BadDataException("Invalid two factor auth id."),
|
||||
);
|
||||
}
|
||||
|
||||
const isVerified: boolean = TotpAuth.verifyToken({
|
||||
token: code,
|
||||
secret: totpAuth.twoFactorSecret!,
|
||||
email: alreadySavedUser.email!,
|
||||
});
|
||||
|
||||
if (!twoFactorAuth) {
|
||||
return Response.sendErrorResponse(
|
||||
req,
|
||||
res,
|
||||
new BadDataException("Invalid two factor auth id."),
|
||||
);
|
||||
if (!isVerified) {
|
||||
return Response.sendErrorResponse(
|
||||
req,
|
||||
res,
|
||||
new BadDataException("Invalid code."),
|
||||
);
|
||||
}
|
||||
} else if (verifyWebAuthn) {
|
||||
const expectedChallenge: string = data["challenge"] as string;
|
||||
const credential: any = data["credential"];
|
||||
|
||||
await UserWebAuthnService.verifyAuthentication({
|
||||
userId: alreadySavedUser.id!.toString(),
|
||||
challenge: expectedChallenge,
|
||||
credential: credential,
|
||||
});
|
||||
}
|
||||
|
||||
const isVerified: boolean = TwoFactorAuth.verifyToken({
|
||||
token: code,
|
||||
secret: twoFactorAuth.twoFactorSecret!,
|
||||
email: alreadySavedUser.email!,
|
||||
});
|
||||
|
||||
if (!isVerified) {
|
||||
return Response.sendErrorResponse(
|
||||
req,
|
||||
res,
|
||||
new BadDataException("Invalid code."),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Refresh Permissions for this user here.
|
||||
} // Refresh Permissions for this user here.
|
||||
await AccessTokenService.refreshUserAllPermissions(alreadySavedUser.id!);
|
||||
|
||||
if (alreadySavedUser.password.toString() === user.password!.toString()) {
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -40,12 +40,18 @@ import Name from "Common/Types/Name";
|
||||
|
||||
const router: ExpressRouter = Express.getRouter();
|
||||
|
||||
// This route is used to get the SSO config for the user.
|
||||
// when the user logs in from OneUptime and not from the IDP.
|
||||
/*
|
||||
* This route is used to get the SSO config for the user.
|
||||
* when the user logs in from OneUptime and not from the IDP.
|
||||
*/
|
||||
|
||||
router.get(
|
||||
"/service-provider-login",
|
||||
async (req: ExpressRequest, res: ExpressResponse): Promise<void> => {
|
||||
async (
|
||||
req: ExpressRequest,
|
||||
res: ExpressResponse,
|
||||
next: NextFunction,
|
||||
): Promise<void> => {
|
||||
try {
|
||||
if (!req.query["email"]) {
|
||||
return Response.sendErrorResponse(
|
||||
@@ -150,7 +156,11 @@ router.get(
|
||||
} catch (err) {
|
||||
logger.error(err);
|
||||
|
||||
Response.sendErrorResponse(req, res, err as Exception);
|
||||
if (err instanceof Exception) {
|
||||
return next(err);
|
||||
}
|
||||
|
||||
return next(new ServerException());
|
||||
}
|
||||
},
|
||||
);
|
||||
@@ -160,7 +170,7 @@ router.get(
|
||||
async (
|
||||
req: ExpressRequest,
|
||||
res: ExpressResponse,
|
||||
_next: NextFunction,
|
||||
next: NextFunction,
|
||||
): Promise<void> => {
|
||||
try {
|
||||
if (!req.params["projectId"]) {
|
||||
@@ -236,22 +246,42 @@ router.get(
|
||||
} catch (err) {
|
||||
logger.error(err);
|
||||
|
||||
Response.sendErrorResponse(req, res, err as Exception);
|
||||
if (err instanceof Exception) {
|
||||
return next(err);
|
||||
}
|
||||
|
||||
return next(new ServerException());
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
router.get(
|
||||
"/idp-login/:projectId/:projectSsoId",
|
||||
async (req: ExpressRequest, res: ExpressResponse): Promise<void> => {
|
||||
return await loginUserWithSso(req, res);
|
||||
async (
|
||||
req: ExpressRequest,
|
||||
res: ExpressResponse,
|
||||
next: NextFunction,
|
||||
): Promise<void> => {
|
||||
try {
|
||||
await loginUserWithSso(req, res);
|
||||
} catch (err) {
|
||||
return next(err);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
router.post(
|
||||
"/idp-login/:projectId/:projectSsoId",
|
||||
async (req: ExpressRequest, res: ExpressResponse): Promise<void> => {
|
||||
return await loginUserWithSso(req, res);
|
||||
async (
|
||||
req: ExpressRequest,
|
||||
res: ExpressResponse,
|
||||
next: NextFunction,
|
||||
): Promise<void> => {
|
||||
try {
|
||||
await loginUserWithSso(req, res);
|
||||
} catch (err) {
|
||||
return next(err);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
@@ -434,8 +464,10 @@ const loginUserWithSso: LoginUserWithSsoFunction = async (
|
||||
isNewUser = true;
|
||||
}
|
||||
|
||||
// If he does not then add him to teams that he should belong and log in.
|
||||
// This should never happen because email is verified before he logs in with SSO.
|
||||
/*
|
||||
* If he does not then add him to teams that he should belong and log in.
|
||||
* This should never happen because email is verified before he logs in with SSO.
|
||||
*/
|
||||
if (!alreadySavedUser.isEmailVerified && !isNewUser) {
|
||||
await AuthenticationEmail.sendVerificationEmail(alreadySavedUser!);
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ import Express, {
|
||||
ExpressRequest,
|
||||
ExpressResponse,
|
||||
ExpressRouter,
|
||||
NextFunction,
|
||||
OneUptimeRequest,
|
||||
} from "Common/Server/Utils/Express";
|
||||
import Response from "Common/Server/Utils/Response";
|
||||
@@ -29,7 +30,11 @@ const router: ExpressRouter = Express.getRouter();
|
||||
router.get(
|
||||
"/status-page-scim/v2/:statusPageScimId/ServiceProviderConfig",
|
||||
SCIMMiddleware.isAuthorizedSCIMRequest,
|
||||
async (req: ExpressRequest, res: ExpressResponse): Promise<void> => {
|
||||
async (
|
||||
req: ExpressRequest,
|
||||
res: ExpressResponse,
|
||||
next: NextFunction,
|
||||
): Promise<void> => {
|
||||
try {
|
||||
logger.debug(
|
||||
`Status Page SCIM ServiceProviderConfig - scimId: ${req.params["statusPageScimId"]!}`,
|
||||
@@ -44,7 +49,7 @@ router.get(
|
||||
return Response.sendJsonObjectResponse(req, res, serviceProviderConfig);
|
||||
} catch (err) {
|
||||
logger.error(err);
|
||||
return Response.sendErrorResponse(req, res, err as BadRequestException);
|
||||
return next(err);
|
||||
}
|
||||
},
|
||||
);
|
||||
@@ -53,7 +58,11 @@ router.get(
|
||||
router.get(
|
||||
"/status-page-scim/v2/:statusPageScimId/Users",
|
||||
SCIMMiddleware.isAuthorizedSCIMRequest,
|
||||
async (req: ExpressRequest, res: ExpressResponse): Promise<void> => {
|
||||
async (
|
||||
req: ExpressRequest,
|
||||
res: ExpressResponse,
|
||||
next: NextFunction,
|
||||
): Promise<void> => {
|
||||
try {
|
||||
logger.debug(
|
||||
`Status Page SCIM Users list request for statusPageScimId: ${req.params["statusPageScimId"]}`,
|
||||
@@ -164,7 +173,7 @@ router.get(
|
||||
});
|
||||
} catch (err) {
|
||||
logger.error(err);
|
||||
return Response.sendErrorResponse(req, res, err as BadRequestException);
|
||||
return next(err);
|
||||
}
|
||||
},
|
||||
);
|
||||
@@ -173,7 +182,11 @@ router.get(
|
||||
router.get(
|
||||
"/status-page-scim/v2/:statusPageScimId/Users/:userId",
|
||||
SCIMMiddleware.isAuthorizedSCIMRequest,
|
||||
async (req: ExpressRequest, res: ExpressResponse): Promise<void> => {
|
||||
async (
|
||||
req: ExpressRequest,
|
||||
res: ExpressResponse,
|
||||
next: NextFunction,
|
||||
): Promise<void> => {
|
||||
try {
|
||||
logger.debug(
|
||||
`Status Page SCIM Get individual user request for userId: ${req.params["userId"]}, statusPageScimId: ${req.params["statusPageScimId"]}`,
|
||||
@@ -231,7 +244,7 @@ router.get(
|
||||
return Response.sendJsonObjectResponse(req, res, user);
|
||||
} catch (err) {
|
||||
logger.error(err);
|
||||
return Response.sendErrorResponse(req, res, err as BadRequestException);
|
||||
return next(err);
|
||||
}
|
||||
},
|
||||
);
|
||||
@@ -240,7 +253,11 @@ router.get(
|
||||
router.post(
|
||||
"/status-page-scim/v2/:statusPageScimId/Users",
|
||||
SCIMMiddleware.isAuthorizedSCIMRequest,
|
||||
async (req: ExpressRequest, res: ExpressResponse): Promise<void> => {
|
||||
async (
|
||||
req: ExpressRequest,
|
||||
res: ExpressResponse,
|
||||
next: NextFunction,
|
||||
): Promise<void> => {
|
||||
try {
|
||||
logger.debug(
|
||||
`Status Page SCIM Create user request for statusPageScimId: ${req.params["statusPageScimId"]}`,
|
||||
@@ -333,7 +350,7 @@ router.post(
|
||||
return Response.sendJsonObjectResponse(req, res, createdUser);
|
||||
} catch (err) {
|
||||
logger.error(err);
|
||||
return Response.sendErrorResponse(req, res, err as BadRequestException);
|
||||
return next(err);
|
||||
}
|
||||
},
|
||||
);
|
||||
@@ -342,7 +359,11 @@ router.post(
|
||||
router.put(
|
||||
"/status-page-scim/v2/:statusPageScimId/Users/:userId",
|
||||
SCIMMiddleware.isAuthorizedSCIMRequest,
|
||||
async (req: ExpressRequest, res: ExpressResponse): Promise<void> => {
|
||||
async (
|
||||
req: ExpressRequest,
|
||||
res: ExpressResponse,
|
||||
next: NextFunction,
|
||||
): Promise<void> => {
|
||||
try {
|
||||
logger.debug(
|
||||
`Status Page SCIM Update user request for userId: ${req.params["userId"]}, statusPageScimId: ${req.params["statusPageScimId"]}`,
|
||||
@@ -489,7 +510,7 @@ router.put(
|
||||
return Response.sendJsonObjectResponse(req, res, user);
|
||||
} catch (err) {
|
||||
logger.error(err);
|
||||
return Response.sendErrorResponse(req, res, err as BadRequestException);
|
||||
return next(err);
|
||||
}
|
||||
},
|
||||
);
|
||||
@@ -498,7 +519,11 @@ router.put(
|
||||
router.delete(
|
||||
"/status-page-scim/v2/:statusPageScimId/Users/:userId",
|
||||
SCIMMiddleware.isAuthorizedSCIMRequest,
|
||||
async (req: ExpressRequest, res: ExpressResponse): Promise<void> => {
|
||||
async (
|
||||
req: ExpressRequest,
|
||||
res: ExpressResponse,
|
||||
next: NextFunction,
|
||||
): Promise<void> => {
|
||||
try {
|
||||
logger.debug(
|
||||
`Status Page SCIM Delete user request for userId: ${req.params["userId"]}, statusPageScimId: ${req.params["statusPageScimId"]}`,
|
||||
@@ -562,7 +587,7 @@ router.delete(
|
||||
return Response.sendEmptySuccessResponse(req, res);
|
||||
} catch (err) {
|
||||
logger.error(err);
|
||||
return Response.sendErrorResponse(req, res, err as BadRequestException);
|
||||
return next(err);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
@@ -115,7 +115,11 @@ router.get(
|
||||
|
||||
router.post(
|
||||
"/status-page-idp-login/:statusPageId/:statusPageSsoId",
|
||||
async (req: ExpressRequest, res: ExpressResponse): Promise<void> => {
|
||||
async (
|
||||
req: ExpressRequest,
|
||||
res: ExpressResponse,
|
||||
next: NextFunction,
|
||||
): Promise<void> => {
|
||||
try {
|
||||
const samlResponseBase64: string = req.body.SAMLResponse;
|
||||
|
||||
@@ -312,7 +316,11 @@ router.post(
|
||||
);
|
||||
} catch (err) {
|
||||
logger.error(err);
|
||||
Response.sendErrorResponse(req, res, new ServerException());
|
||||
if (err instanceof Exception) {
|
||||
return next(err);
|
||||
}
|
||||
|
||||
return next(new ServerException());
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
@@ -35,6 +35,8 @@ export default class AuthenticationEmail {
|
||||
const host: Hostname = await DatabaseConfig.getHost();
|
||||
const httpProtocol: Protocol = await DatabaseConfig.getHttpProtocol();
|
||||
|
||||
logger.debug("Sending verification email");
|
||||
|
||||
MailService.sendMail({
|
||||
toEmail: user.email!,
|
||||
subject: "Please verify email.",
|
||||
@@ -50,8 +52,13 @@ export default class AuthenticationEmail {
|
||||
).toString(),
|
||||
homeUrl: new URL(httpProtocol, host).toString(),
|
||||
},
|
||||
}).catch((err: Error) => {
|
||||
logger.error(err);
|
||||
});
|
||||
})
|
||||
.then(() => {
|
||||
logger.debug("Verification email sent");
|
||||
})
|
||||
.catch((err: Error) => {
|
||||
logger.debug("Error sending verification email");
|
||||
logger.error(err);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -227,6 +227,27 @@ export const generateUsersListResponse: (
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Generate SCIM ListResponse for groups
|
||||
*/
|
||||
export const generateGroupsListResponse: (
|
||||
groups: JSONObject[],
|
||||
startIndex: number,
|
||||
totalResults: number,
|
||||
) => JSONObject = (
|
||||
groups: JSONObject[],
|
||||
startIndex: number,
|
||||
totalResults: number,
|
||||
): JSONObject => {
|
||||
return {
|
||||
schemas: ["urn:ietf:params:scim:api:messages:2.0:ListResponse"],
|
||||
totalResults: totalResults,
|
||||
startIndex: startIndex,
|
||||
itemsPerPage: groups.length,
|
||||
Resources: groups,
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Parse query parameters for SCIM list requests
|
||||
*/
|
||||
|
||||
@@ -184,15 +184,17 @@ export default class SSOUtil {
|
||||
return null;
|
||||
}
|
||||
|
||||
// get displayName attribute.
|
||||
// {
|
||||
// "$": {
|
||||
// "Name": "http://schemas.microsoft.com/identity/claims/displayname"
|
||||
// },
|
||||
// "AttributeValue": [
|
||||
// "Nawaz Dhandala"
|
||||
// ]
|
||||
// },
|
||||
/*
|
||||
* get displayName attribute.
|
||||
* {
|
||||
* "$": {
|
||||
* "Name": "http://schemas.microsoft.com/identity/claims/displayname"
|
||||
* },
|
||||
* "AttributeValue": [
|
||||
* "Nawaz Dhandala"
|
||||
* ]
|
||||
* },
|
||||
*/
|
||||
|
||||
for (let i: number = 0; i < samlAttribute.length; i++) {
|
||||
const attribute: JSONObject = samlAttribute[i] as JSONObject;
|
||||
|
||||
@@ -12,6 +12,7 @@ import Express, {
|
||||
ExpressRequest,
|
||||
ExpressResponse,
|
||||
ExpressRouter,
|
||||
NextFunction,
|
||||
} from "Common/Server/Utils/Express";
|
||||
import logger from "Common/Server/Utils/Logger";
|
||||
import Response from "Common/Server/Utils/Response";
|
||||
@@ -22,139 +23,146 @@ const router: ExpressRouter = Express.getRouter();
|
||||
router.post(
|
||||
"/make-call",
|
||||
ClusterKeyAuthorization.isAuthorizedServiceMiddleware,
|
||||
async (req: ExpressRequest, res: ExpressResponse) => {
|
||||
const body: JSONObject = JSONFunctions.deserialize(req.body);
|
||||
async (req: ExpressRequest, res: ExpressResponse, next: NextFunction) => {
|
||||
try {
|
||||
const body: JSONObject = JSONFunctions.deserialize(req.body);
|
||||
|
||||
await CallService.makeCall(body["callRequest"] as CallRequest, {
|
||||
projectId: body["projectId"] as ObjectID,
|
||||
isSensitive: (body["isSensitive"] as boolean) || false,
|
||||
userOnCallLogTimelineId:
|
||||
(body["userOnCallLogTimelineId"] as ObjectID) || undefined,
|
||||
customTwilioConfig: body["customTwilioConfig"] as any,
|
||||
incidentId: (body["incidentId"] as ObjectID) || undefined,
|
||||
alertId: (body["alertId"] as ObjectID) || undefined,
|
||||
scheduledMaintenanceId:
|
||||
(body["scheduledMaintenanceId"] as ObjectID) || undefined,
|
||||
statusPageId: (body["statusPageId"] as ObjectID) || undefined,
|
||||
statusPageAnnouncementId:
|
||||
(body["statusPageAnnouncementId"] as ObjectID) || undefined,
|
||||
userId: (body["userId"] as ObjectID) || undefined,
|
||||
onCallPolicyId: (body["onCallPolicyId"] as ObjectID) || undefined,
|
||||
onCallPolicyEscalationRuleId:
|
||||
(body["onCallPolicyEscalationRuleId"] as ObjectID) || undefined,
|
||||
onCallDutyPolicyExecutionLogTimelineId:
|
||||
(body["onCallDutyPolicyExecutionLogTimelineId"] as ObjectID) ||
|
||||
undefined,
|
||||
onCallScheduleId: (body["onCallScheduleId"] as ObjectID) || undefined,
|
||||
teamId: (body["teamId"] as ObjectID) || undefined,
|
||||
});
|
||||
await CallService.makeCall(body["callRequest"] as CallRequest, {
|
||||
projectId: body["projectId"] as ObjectID,
|
||||
isSensitive: (body["isSensitive"] as boolean) || false,
|
||||
userOnCallLogTimelineId:
|
||||
(body["userOnCallLogTimelineId"] as ObjectID) || undefined,
|
||||
customTwilioConfig: body["customTwilioConfig"] as any,
|
||||
incidentId: (body["incidentId"] as ObjectID) || undefined,
|
||||
alertId: (body["alertId"] as ObjectID) || undefined,
|
||||
scheduledMaintenanceId:
|
||||
(body["scheduledMaintenanceId"] as ObjectID) || undefined,
|
||||
statusPageId: (body["statusPageId"] as ObjectID) || undefined,
|
||||
statusPageAnnouncementId:
|
||||
(body["statusPageAnnouncementId"] as ObjectID) || undefined,
|
||||
userId: (body["userId"] as ObjectID) || undefined,
|
||||
onCallPolicyId: (body["onCallPolicyId"] as ObjectID) || undefined,
|
||||
onCallPolicyEscalationRuleId:
|
||||
(body["onCallPolicyEscalationRuleId"] as ObjectID) || undefined,
|
||||
onCallDutyPolicyExecutionLogTimelineId:
|
||||
(body["onCallDutyPolicyExecutionLogTimelineId"] as ObjectID) ||
|
||||
undefined,
|
||||
onCallScheduleId: (body["onCallScheduleId"] as ObjectID) || undefined,
|
||||
teamId: (body["teamId"] as ObjectID) || undefined,
|
||||
});
|
||||
|
||||
return Response.sendEmptySuccessResponse(req, res);
|
||||
return Response.sendEmptySuccessResponse(req, res);
|
||||
} catch (err) {
|
||||
return next(err);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
router.post("/test", async (req: ExpressRequest, res: ExpressResponse) => {
|
||||
const body: JSONObject = req.body;
|
||||
router.post(
|
||||
"/test",
|
||||
async (req: ExpressRequest, res: ExpressResponse, next: NextFunction) => {
|
||||
try {
|
||||
const body: JSONObject = req.body;
|
||||
|
||||
const callSMSConfigId: ObjectID = new ObjectID(
|
||||
body["callSMSConfigId"] as string,
|
||||
);
|
||||
const callSMSConfigId: ObjectID = new ObjectID(
|
||||
body["callSMSConfigId"] as string,
|
||||
);
|
||||
|
||||
const config: ProjectCallSMSConfig | null =
|
||||
await ProjectCallSMSConfigService.findOneById({
|
||||
id: callSMSConfigId,
|
||||
props: {
|
||||
isRoot: true,
|
||||
},
|
||||
select: {
|
||||
_id: true,
|
||||
twilioAccountSID: true,
|
||||
twilioAuthToken: true,
|
||||
twilioPrimaryPhoneNumber: true,
|
||||
twilioSecondaryPhoneNumbers: true,
|
||||
projectId: true,
|
||||
},
|
||||
});
|
||||
const config: ProjectCallSMSConfig | null =
|
||||
await ProjectCallSMSConfigService.findOneById({
|
||||
id: callSMSConfigId,
|
||||
props: {
|
||||
isRoot: true,
|
||||
},
|
||||
select: {
|
||||
_id: true,
|
||||
twilioAccountSID: true,
|
||||
twilioAuthToken: true,
|
||||
twilioPrimaryPhoneNumber: true,
|
||||
twilioSecondaryPhoneNumbers: true,
|
||||
projectId: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!config) {
|
||||
return Response.sendErrorResponse(
|
||||
req,
|
||||
res,
|
||||
new BadDataException(
|
||||
"call and sms config not found for id" + callSMSConfigId.toString(),
|
||||
),
|
||||
);
|
||||
}
|
||||
if (!config) {
|
||||
return Response.sendErrorResponse(
|
||||
req,
|
||||
res,
|
||||
new BadDataException(
|
||||
"call and sms config not found for id" + callSMSConfigId.toString(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
const toPhone: Phone = new Phone(body["toPhone"] as string);
|
||||
const toPhone: Phone = new Phone(body["toPhone"] as string);
|
||||
|
||||
if (!toPhone) {
|
||||
return Response.sendErrorResponse(
|
||||
req,
|
||||
res,
|
||||
new BadDataException("toPhone is required"),
|
||||
);
|
||||
}
|
||||
if (!toPhone) {
|
||||
return Response.sendErrorResponse(
|
||||
req,
|
||||
res,
|
||||
new BadDataException("toPhone is required"),
|
||||
);
|
||||
}
|
||||
|
||||
// if any of the twilio config is missing, we will not send make the call
|
||||
// if any of the twilio config is missing, we will not send make the call
|
||||
|
||||
if (!config.twilioAccountSID) {
|
||||
return Response.sendErrorResponse(
|
||||
req,
|
||||
res,
|
||||
new BadDataException("twilioAccountSID is required"),
|
||||
);
|
||||
}
|
||||
if (!config.twilioAccountSID) {
|
||||
return Response.sendErrorResponse(
|
||||
req,
|
||||
res,
|
||||
new BadDataException("twilioAccountSID is required"),
|
||||
);
|
||||
}
|
||||
|
||||
if (!config.twilioAuthToken) {
|
||||
return Response.sendErrorResponse(
|
||||
req,
|
||||
res,
|
||||
new BadDataException("twilioAuthToken is required"),
|
||||
);
|
||||
}
|
||||
if (!config.twilioAuthToken) {
|
||||
return Response.sendErrorResponse(
|
||||
req,
|
||||
res,
|
||||
new BadDataException("twilioAuthToken is required"),
|
||||
);
|
||||
}
|
||||
|
||||
if (!config.twilioPrimaryPhoneNumber) {
|
||||
return Response.sendErrorResponse(
|
||||
req,
|
||||
res,
|
||||
new BadDataException("twilioPrimaryPhoneNumber is required"),
|
||||
);
|
||||
}
|
||||
if (!config.twilioPrimaryPhoneNumber) {
|
||||
return Response.sendErrorResponse(
|
||||
req,
|
||||
res,
|
||||
new BadDataException("twilioPrimaryPhoneNumber is required"),
|
||||
);
|
||||
}
|
||||
|
||||
const twilioConfig: TwilioConfig | undefined =
|
||||
ProjectCallSMSConfigService.toTwilioConfig(config);
|
||||
const twilioConfig: TwilioConfig | undefined =
|
||||
ProjectCallSMSConfigService.toTwilioConfig(config);
|
||||
|
||||
try {
|
||||
if (!twilioConfig) {
|
||||
throw new BadDataException("twilioConfig is undefined");
|
||||
try {
|
||||
if (!twilioConfig) {
|
||||
throw new BadDataException("twilioConfig is undefined");
|
||||
}
|
||||
|
||||
const testCallRequest: CallRequest = {
|
||||
data: [
|
||||
{
|
||||
sayMessage: "This is a test call from OneUptime.",
|
||||
},
|
||||
],
|
||||
to: toPhone,
|
||||
};
|
||||
|
||||
await CallService.makeCall(testCallRequest, {
|
||||
projectId: config.projectId,
|
||||
customTwilioConfig: twilioConfig,
|
||||
});
|
||||
} catch (err) {
|
||||
logger.error(err);
|
||||
throw new BadDataException(
|
||||
"Error making test call. Please check the twilio logs for more details",
|
||||
);
|
||||
}
|
||||
|
||||
return Response.sendEmptySuccessResponse(req, res);
|
||||
} catch (err) {
|
||||
return next(err);
|
||||
}
|
||||
|
||||
const testCallRequest: CallRequest = {
|
||||
data: [
|
||||
{
|
||||
sayMessage: "This is a test call from OneUptime.",
|
||||
},
|
||||
],
|
||||
to: toPhone,
|
||||
};
|
||||
|
||||
await CallService.makeCall(testCallRequest, {
|
||||
projectId: config.projectId,
|
||||
customTwilioConfig: twilioConfig,
|
||||
});
|
||||
} catch (err) {
|
||||
logger.error(err);
|
||||
return Response.sendErrorResponse(
|
||||
req,
|
||||
res,
|
||||
new BadDataException(
|
||||
"Error making test call. Please check the twilio logs for more details",
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return Response.sendEmptySuccessResponse(req, res);
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
export default router;
|
||||
|
||||
@@ -11,6 +11,7 @@ import Express, {
|
||||
ExpressRequest,
|
||||
ExpressResponse,
|
||||
ExpressRouter,
|
||||
NextFunction,
|
||||
} from "Common/Server/Utils/Express";
|
||||
import Response from "Common/Server/Utils/Response";
|
||||
|
||||
@@ -19,70 +20,74 @@ const router: ExpressRouter = Express.getRouter();
|
||||
router.post(
|
||||
"/send",
|
||||
ClusterKeyAuthorization.isAuthorizedServiceMiddleware,
|
||||
async (req: ExpressRequest, res: ExpressResponse) => {
|
||||
const body: JSONObject = req.body;
|
||||
async (req: ExpressRequest, res: ExpressResponse, next: NextFunction) => {
|
||||
try {
|
||||
const body: JSONObject = req.body;
|
||||
|
||||
const mail: EmailMessage = {
|
||||
templateType: body["templateType"] as EmailTemplateType,
|
||||
toEmail: new Email(body["toEmail"] as string),
|
||||
subject: body["subject"] as string,
|
||||
vars: body["vars"] as Dictionary<string>,
|
||||
body: (body["body"] as string) || "",
|
||||
};
|
||||
const mail: EmailMessage = {
|
||||
templateType: body["templateType"] as EmailTemplateType,
|
||||
toEmail: new Email(body["toEmail"] as string),
|
||||
subject: body["subject"] as string,
|
||||
vars: body["vars"] as Dictionary<string>,
|
||||
body: (body["body"] as string) || "",
|
||||
};
|
||||
|
||||
let mailServer: EmailServer | undefined = undefined;
|
||||
let mailServer: EmailServer | undefined = undefined;
|
||||
|
||||
if (hasMailServerSettingsInBody(body)) {
|
||||
mailServer = MailService.getEmailServer(req.body);
|
||||
if (hasMailServerSettingsInBody(body)) {
|
||||
mailServer = MailService.getEmailServer(req.body);
|
||||
}
|
||||
|
||||
await MailService.send(mail, {
|
||||
projectId: body["projectId"]
|
||||
? new ObjectID(body["projectId"] as string)
|
||||
: undefined,
|
||||
emailServer: mailServer,
|
||||
userOnCallLogTimelineId:
|
||||
(body["userOnCallLogTimelineId"] as ObjectID) || undefined,
|
||||
incidentId: body["incidentId"]
|
||||
? new ObjectID(body["incidentId"].toString())
|
||||
: undefined,
|
||||
alertId: body["alertId"]
|
||||
? new ObjectID(body["alertId"].toString())
|
||||
: undefined,
|
||||
scheduledMaintenanceId: body["scheduledMaintenanceId"]
|
||||
? new ObjectID(body["scheduledMaintenanceId"].toString())
|
||||
: undefined,
|
||||
statusPageId: body["statusPageId"]
|
||||
? new ObjectID(body["statusPageId"].toString())
|
||||
: undefined,
|
||||
statusPageAnnouncementId: body["statusPageAnnouncementId"]
|
||||
? new ObjectID(body["statusPageAnnouncementId"].toString())
|
||||
: undefined,
|
||||
userId: body["userId"]
|
||||
? new ObjectID(body["userId"].toString())
|
||||
: undefined,
|
||||
onCallPolicyId: body["onCallPolicyId"]
|
||||
? new ObjectID(body["onCallPolicyId"].toString())
|
||||
: undefined,
|
||||
onCallPolicyEscalationRuleId: body["onCallPolicyEscalationRuleId"]
|
||||
? new ObjectID(body["onCallPolicyEscalationRuleId"].toString())
|
||||
: undefined,
|
||||
onCallDutyPolicyExecutionLogTimelineId: body[
|
||||
"onCallDutyPolicyExecutionLogTimelineId"
|
||||
]
|
||||
? new ObjectID(
|
||||
body["onCallDutyPolicyExecutionLogTimelineId"].toString(),
|
||||
)
|
||||
: undefined,
|
||||
onCallScheduleId: body["onCallScheduleId"]
|
||||
? new ObjectID(body["onCallScheduleId"].toString())
|
||||
: undefined,
|
||||
teamId: body["teamId"]
|
||||
? new ObjectID(body["teamId"].toString())
|
||||
: undefined,
|
||||
});
|
||||
|
||||
return Response.sendEmptySuccessResponse(req, res);
|
||||
} catch (err) {
|
||||
return next(err);
|
||||
}
|
||||
|
||||
await MailService.send(mail, {
|
||||
projectId: body["projectId"]
|
||||
? new ObjectID(body["projectId"] as string)
|
||||
: undefined,
|
||||
emailServer: mailServer,
|
||||
userOnCallLogTimelineId:
|
||||
(body["userOnCallLogTimelineId"] as ObjectID) || undefined,
|
||||
incidentId: body["incidentId"]
|
||||
? new ObjectID(body["incidentId"].toString())
|
||||
: undefined,
|
||||
alertId: body["alertId"]
|
||||
? new ObjectID(body["alertId"].toString())
|
||||
: undefined,
|
||||
scheduledMaintenanceId: body["scheduledMaintenanceId"]
|
||||
? new ObjectID(body["scheduledMaintenanceId"].toString())
|
||||
: undefined,
|
||||
statusPageId: body["statusPageId"]
|
||||
? new ObjectID(body["statusPageId"].toString())
|
||||
: undefined,
|
||||
statusPageAnnouncementId: body["statusPageAnnouncementId"]
|
||||
? new ObjectID(body["statusPageAnnouncementId"].toString())
|
||||
: undefined,
|
||||
userId: body["userId"]
|
||||
? new ObjectID(body["userId"].toString())
|
||||
: undefined,
|
||||
onCallPolicyId: body["onCallPolicyId"]
|
||||
? new ObjectID(body["onCallPolicyId"].toString())
|
||||
: undefined,
|
||||
onCallPolicyEscalationRuleId: body["onCallPolicyEscalationRuleId"]
|
||||
? new ObjectID(body["onCallPolicyEscalationRuleId"].toString())
|
||||
: undefined,
|
||||
onCallDutyPolicyExecutionLogTimelineId: body[
|
||||
"onCallDutyPolicyExecutionLogTimelineId"
|
||||
]
|
||||
? new ObjectID(
|
||||
body["onCallDutyPolicyExecutionLogTimelineId"].toString(),
|
||||
)
|
||||
: undefined,
|
||||
onCallScheduleId: body["onCallScheduleId"]
|
||||
? new ObjectID(body["onCallScheduleId"].toString())
|
||||
: undefined,
|
||||
teamId: body["teamId"]
|
||||
? new ObjectID(body["teamId"].toString())
|
||||
: undefined,
|
||||
});
|
||||
|
||||
return Response.sendEmptySuccessResponse(req, res);
|
||||
},
|
||||
);
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ import Express, {
|
||||
ExpressRequest,
|
||||
ExpressResponse,
|
||||
ExpressRouter,
|
||||
NextFunction,
|
||||
} from "Common/Server/Utils/Express";
|
||||
import Response from "Common/Server/Utils/Response";
|
||||
import { JSONObject } from "Common/Types/JSON";
|
||||
@@ -15,50 +16,54 @@ const router: ExpressRouter = Express.getRouter();
|
||||
router.post(
|
||||
"/send",
|
||||
ClusterKeyAuthorization.isAuthorizedServiceMiddleware,
|
||||
async (req: ExpressRequest, res: ExpressResponse) => {
|
||||
const body: JSONObject = JSONFunctions.deserialize(req.body);
|
||||
async (req: ExpressRequest, res: ExpressResponse, next: NextFunction) => {
|
||||
try {
|
||||
const body: JSONObject = JSONFunctions.deserialize(req.body);
|
||||
|
||||
// Support both new devices format and legacy deviceTokens/deviceNames format
|
||||
let devices: Array<{ token: string; name?: string }> = [];
|
||||
// Support both new devices format and legacy deviceTokens/deviceNames format
|
||||
let devices: Array<{ token: string; name?: string }> = [];
|
||||
|
||||
if (body["devices"]) {
|
||||
// New format: devices as array of objects
|
||||
devices = body["devices"] as Array<{ token: string; name?: string }>;
|
||||
} else {
|
||||
throw new Error("Invalid request format: 'devices' array is required.");
|
||||
if (body["devices"]) {
|
||||
// New format: devices as array of objects
|
||||
devices = body["devices"] as Array<{ token: string; name?: string }>;
|
||||
} else {
|
||||
throw new Error("Invalid request format: 'devices' array is required.");
|
||||
}
|
||||
|
||||
await PushService.send(
|
||||
{
|
||||
devices,
|
||||
deviceType: (body["deviceType"] as any) || "web",
|
||||
message: body["message"] as any,
|
||||
},
|
||||
{
|
||||
projectId: (body["projectId"] as ObjectID) || undefined,
|
||||
isSensitive: (body["isSensitive"] as boolean) || false,
|
||||
userOnCallLogTimelineId:
|
||||
(body["userOnCallLogTimelineId"] as ObjectID) || undefined,
|
||||
incidentId: (body["incidentId"] as ObjectID) || undefined,
|
||||
alertId: (body["alertId"] as ObjectID) || undefined,
|
||||
scheduledMaintenanceId:
|
||||
(body["scheduledMaintenanceId"] as ObjectID) || undefined,
|
||||
statusPageId: (body["statusPageId"] as ObjectID) || undefined,
|
||||
statusPageAnnouncementId:
|
||||
(body["statusPageAnnouncementId"] as ObjectID) || undefined,
|
||||
userId: (body["userId"] as ObjectID) || undefined,
|
||||
onCallPolicyId: (body["onCallPolicyId"] as ObjectID) || undefined,
|
||||
onCallPolicyEscalationRuleId:
|
||||
(body["onCallPolicyEscalationRuleId"] as ObjectID) || undefined,
|
||||
onCallDutyPolicyExecutionLogTimelineId:
|
||||
(body["onCallDutyPolicyExecutionLogTimelineId"] as ObjectID) ||
|
||||
undefined,
|
||||
onCallScheduleId: (body["onCallScheduleId"] as ObjectID) || undefined,
|
||||
teamId: (body["teamId"] as ObjectID) || undefined,
|
||||
},
|
||||
);
|
||||
|
||||
return Response.sendEmptySuccessResponse(req, res);
|
||||
} catch (err) {
|
||||
return next(err);
|
||||
}
|
||||
|
||||
await PushService.send(
|
||||
{
|
||||
devices,
|
||||
deviceType: (body["deviceType"] as any) || "web",
|
||||
message: body["message"] as any,
|
||||
},
|
||||
{
|
||||
projectId: (body["projectId"] as ObjectID) || undefined,
|
||||
isSensitive: (body["isSensitive"] as boolean) || false,
|
||||
userOnCallLogTimelineId:
|
||||
(body["userOnCallLogTimelineId"] as ObjectID) || undefined,
|
||||
incidentId: (body["incidentId"] as ObjectID) || undefined,
|
||||
alertId: (body["alertId"] as ObjectID) || undefined,
|
||||
scheduledMaintenanceId:
|
||||
(body["scheduledMaintenanceId"] as ObjectID) || undefined,
|
||||
statusPageId: (body["statusPageId"] as ObjectID) || undefined,
|
||||
statusPageAnnouncementId:
|
||||
(body["statusPageAnnouncementId"] as ObjectID) || undefined,
|
||||
userId: (body["userId"] as ObjectID) || undefined,
|
||||
onCallPolicyId: (body["onCallPolicyId"] as ObjectID) || undefined,
|
||||
onCallPolicyEscalationRuleId:
|
||||
(body["onCallPolicyEscalationRuleId"] as ObjectID) || undefined,
|
||||
onCallDutyPolicyExecutionLogTimelineId:
|
||||
(body["onCallDutyPolicyExecutionLogTimelineId"] as ObjectID) ||
|
||||
undefined,
|
||||
onCallScheduleId: (body["onCallScheduleId"] as ObjectID) || undefined,
|
||||
teamId: (body["teamId"] as ObjectID) || undefined,
|
||||
},
|
||||
);
|
||||
|
||||
return Response.sendEmptySuccessResponse(req, res);
|
||||
},
|
||||
);
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@ import Express, {
|
||||
ExpressRequest,
|
||||
ExpressResponse,
|
||||
ExpressRouter,
|
||||
NextFunction,
|
||||
} from "Common/Server/Utils/Express";
|
||||
import logger from "Common/Server/Utils/Logger";
|
||||
import Response from "Common/Server/Utils/Response";
|
||||
@@ -21,130 +22,141 @@ const router: ExpressRouter = Express.getRouter();
|
||||
router.post(
|
||||
"/send",
|
||||
ClusterKeyAuthorization.isAuthorizedServiceMiddleware,
|
||||
async (req: ExpressRequest, res: ExpressResponse) => {
|
||||
const body: JSONObject = JSONFunctions.deserialize(req.body);
|
||||
async (req: ExpressRequest, res: ExpressResponse, next: NextFunction) => {
|
||||
try {
|
||||
const body: JSONObject = JSONFunctions.deserialize(req.body);
|
||||
|
||||
await SmsService.sendSms(body["to"] as Phone, body["message"] as string, {
|
||||
projectId: body["projectId"] as ObjectID,
|
||||
isSensitive: (body["isSensitive"] as boolean) || false,
|
||||
userOnCallLogTimelineId:
|
||||
(body["userOnCallLogTimelineId"] as ObjectID) || undefined,
|
||||
customTwilioConfig: body["customTwilioConfig"] as any,
|
||||
incidentId: (body["incidentId"] as ObjectID) || undefined,
|
||||
alertId: (body["alertId"] as ObjectID) || undefined,
|
||||
scheduledMaintenanceId:
|
||||
(body["scheduledMaintenanceId"] as ObjectID) || undefined,
|
||||
statusPageId: (body["statusPageId"] as ObjectID) || undefined,
|
||||
statusPageAnnouncementId:
|
||||
(body["statusPageAnnouncementId"] as ObjectID) || undefined,
|
||||
userId: (body["userId"] as ObjectID) || undefined,
|
||||
onCallPolicyId: (body["onCallPolicyId"] as ObjectID) || undefined,
|
||||
onCallPolicyEscalationRuleId:
|
||||
(body["onCallPolicyEscalationRuleId"] as ObjectID) || undefined,
|
||||
onCallDutyPolicyExecutionLogTimelineId:
|
||||
(body["onCallDutyPolicyExecutionLogTimelineId"] as ObjectID) ||
|
||||
undefined,
|
||||
onCallScheduleId: (body["onCallScheduleId"] as ObjectID) || undefined,
|
||||
teamId: (body["teamId"] as ObjectID) || undefined,
|
||||
});
|
||||
await SmsService.sendSms(body["to"] as Phone, body["message"] as string, {
|
||||
projectId: body["projectId"] as ObjectID,
|
||||
isSensitive: (body["isSensitive"] as boolean) || false,
|
||||
userOnCallLogTimelineId:
|
||||
(body["userOnCallLogTimelineId"] as ObjectID) || undefined,
|
||||
customTwilioConfig: body["customTwilioConfig"] as any,
|
||||
incidentId: (body["incidentId"] as ObjectID) || undefined,
|
||||
alertId: (body["alertId"] as ObjectID) || undefined,
|
||||
scheduledMaintenanceId:
|
||||
(body["scheduledMaintenanceId"] as ObjectID) || undefined,
|
||||
statusPageId: (body["statusPageId"] as ObjectID) || undefined,
|
||||
statusPageAnnouncementId:
|
||||
(body["statusPageAnnouncementId"] as ObjectID) || undefined,
|
||||
userId: (body["userId"] as ObjectID) || undefined,
|
||||
onCallPolicyId: (body["onCallPolicyId"] as ObjectID) || undefined,
|
||||
onCallPolicyEscalationRuleId:
|
||||
(body["onCallPolicyEscalationRuleId"] as ObjectID) || undefined,
|
||||
onCallDutyPolicyExecutionLogTimelineId:
|
||||
(body["onCallDutyPolicyExecutionLogTimelineId"] as ObjectID) ||
|
||||
undefined,
|
||||
onCallScheduleId: (body["onCallScheduleId"] as ObjectID) || undefined,
|
||||
teamId: (body["teamId"] as ObjectID) || undefined,
|
||||
});
|
||||
|
||||
return Response.sendEmptySuccessResponse(req, res);
|
||||
return Response.sendEmptySuccessResponse(req, res);
|
||||
} catch (err) {
|
||||
return next(err);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
router.post("/test", async (req: ExpressRequest, res: ExpressResponse) => {
|
||||
const body: JSONObject = req.body;
|
||||
router.post(
|
||||
"/test",
|
||||
async (req: ExpressRequest, res: ExpressResponse, next: NextFunction) => {
|
||||
try {
|
||||
const body: JSONObject = req.body;
|
||||
|
||||
const callSMSConfigId: ObjectID = new ObjectID(
|
||||
body["callSMSConfigId"] as string,
|
||||
);
|
||||
const callSMSConfigId: ObjectID = new ObjectID(
|
||||
body["callSMSConfigId"] as string,
|
||||
);
|
||||
|
||||
const config: ProjectCallSMSConfig | null =
|
||||
await ProjectCallSMSConfigService.findOneById({
|
||||
id: callSMSConfigId,
|
||||
props: {
|
||||
isRoot: true,
|
||||
},
|
||||
select: {
|
||||
_id: true,
|
||||
twilioAccountSID: true,
|
||||
twilioAuthToken: true,
|
||||
twilioPrimaryPhoneNumber: true,
|
||||
twilioSecondaryPhoneNumbers: true,
|
||||
projectId: true,
|
||||
},
|
||||
});
|
||||
const config: ProjectCallSMSConfig | null =
|
||||
await ProjectCallSMSConfigService.findOneById({
|
||||
id: callSMSConfigId,
|
||||
props: {
|
||||
isRoot: true,
|
||||
},
|
||||
select: {
|
||||
_id: true,
|
||||
twilioAccountSID: true,
|
||||
twilioAuthToken: true,
|
||||
twilioPrimaryPhoneNumber: true,
|
||||
twilioSecondaryPhoneNumbers: true,
|
||||
projectId: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!config) {
|
||||
return Response.sendErrorResponse(
|
||||
req,
|
||||
res,
|
||||
new BadDataException(
|
||||
"call and sms config not found for id" + callSMSConfigId.toString(),
|
||||
),
|
||||
);
|
||||
}
|
||||
if (!config) {
|
||||
return Response.sendErrorResponse(
|
||||
req,
|
||||
res,
|
||||
new BadDataException(
|
||||
"call and sms config not found for id" + callSMSConfigId.toString(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
const toPhone: Phone = new Phone(body["toPhone"] as string);
|
||||
const toPhone: Phone = new Phone(body["toPhone"] as string);
|
||||
|
||||
if (!toPhone) {
|
||||
return Response.sendErrorResponse(
|
||||
req,
|
||||
res,
|
||||
new BadDataException("toPhone is required"),
|
||||
);
|
||||
}
|
||||
if (!toPhone) {
|
||||
return Response.sendErrorResponse(
|
||||
req,
|
||||
res,
|
||||
new BadDataException("toPhone is required"),
|
||||
);
|
||||
}
|
||||
|
||||
// if any of the twilio config is missing, we will not send make the call
|
||||
// if any of the twilio config is missing, we will not send make the call
|
||||
|
||||
if (!config.twilioAccountSID) {
|
||||
return Response.sendErrorResponse(
|
||||
req,
|
||||
res,
|
||||
new BadDataException("twilioAccountSID is required"),
|
||||
);
|
||||
}
|
||||
if (!config.twilioAccountSID) {
|
||||
return Response.sendErrorResponse(
|
||||
req,
|
||||
res,
|
||||
new BadDataException("twilioAccountSID is required"),
|
||||
);
|
||||
}
|
||||
|
||||
if (!config.twilioAuthToken) {
|
||||
return Response.sendErrorResponse(
|
||||
req,
|
||||
res,
|
||||
new BadDataException("twilioAuthToken is required"),
|
||||
);
|
||||
}
|
||||
if (!config.twilioAuthToken) {
|
||||
return Response.sendErrorResponse(
|
||||
req,
|
||||
res,
|
||||
new BadDataException("twilioAuthToken is required"),
|
||||
);
|
||||
}
|
||||
|
||||
if (!config.twilioPrimaryPhoneNumber) {
|
||||
return Response.sendErrorResponse(
|
||||
req,
|
||||
res,
|
||||
new BadDataException("twilioPrimaryPhoneNumber is required"),
|
||||
);
|
||||
}
|
||||
if (!config.twilioPrimaryPhoneNumber) {
|
||||
return Response.sendErrorResponse(
|
||||
req,
|
||||
res,
|
||||
new BadDataException("twilioPrimaryPhoneNumber is required"),
|
||||
);
|
||||
}
|
||||
|
||||
const twilioConfig: TwilioConfig | undefined =
|
||||
ProjectCallSMSConfigService.toTwilioConfig(config);
|
||||
const twilioConfig: TwilioConfig | undefined =
|
||||
ProjectCallSMSConfigService.toTwilioConfig(config);
|
||||
|
||||
try {
|
||||
if (!twilioConfig) {
|
||||
throw new BadDataException("twilioConfig is undefined");
|
||||
try {
|
||||
if (!twilioConfig) {
|
||||
throw new BadDataException("twilioConfig is undefined");
|
||||
}
|
||||
|
||||
await SmsService.sendSms(
|
||||
toPhone,
|
||||
"This is a test SMS from OneUptime.",
|
||||
{
|
||||
projectId: config.projectId,
|
||||
customTwilioConfig: twilioConfig,
|
||||
},
|
||||
);
|
||||
} catch (err) {
|
||||
logger.error(err);
|
||||
throw new BadDataException(
|
||||
"Failed to send test SMS. Please check the twilio logs for more details.",
|
||||
);
|
||||
}
|
||||
|
||||
return Response.sendEmptySuccessResponse(req, res);
|
||||
} catch (err) {
|
||||
return next(err);
|
||||
}
|
||||
|
||||
await SmsService.sendSms(toPhone, "This is a test SMS from OneUptime.", {
|
||||
projectId: config.projectId,
|
||||
customTwilioConfig: twilioConfig,
|
||||
});
|
||||
} catch (err) {
|
||||
logger.error(err);
|
||||
return Response.sendErrorResponse(
|
||||
req,
|
||||
res,
|
||||
new BadDataException(
|
||||
"Failed to send test SMS. Please check the twilio logs for more details.",
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return Response.sendEmptySuccessResponse(req, res);
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
export default router;
|
||||
|
||||
@@ -11,6 +11,7 @@ import Express, {
|
||||
ExpressRequest,
|
||||
ExpressResponse,
|
||||
ExpressRouter,
|
||||
NextFunction,
|
||||
} from "Common/Server/Utils/Express";
|
||||
import logger from "Common/Server/Utils/Logger";
|
||||
import Response from "Common/Server/Utils/Response";
|
||||
@@ -18,87 +19,92 @@ import ProjectSmtpConfig from "Common/Models/DatabaseModels/ProjectSmtpConfig";
|
||||
|
||||
const router: ExpressRouter = Express.getRouter();
|
||||
|
||||
router.post("/test", async (req: ExpressRequest, res: ExpressResponse) => {
|
||||
const body: JSONObject = req.body;
|
||||
router.post(
|
||||
"/test",
|
||||
async (req: ExpressRequest, res: ExpressResponse, next: NextFunction) => {
|
||||
try {
|
||||
const body: JSONObject = req.body;
|
||||
|
||||
const smtpConfigId: ObjectID = new ObjectID(body["smtpConfigId"] as string);
|
||||
const smtpConfigId: ObjectID = new ObjectID(
|
||||
body["smtpConfigId"] as string,
|
||||
);
|
||||
|
||||
const config: ProjectSmtpConfig | null =
|
||||
await ProjectSMTPConfigService.findOneById({
|
||||
id: smtpConfigId,
|
||||
props: {
|
||||
isRoot: true,
|
||||
},
|
||||
select: {
|
||||
_id: true,
|
||||
hostname: true,
|
||||
port: true,
|
||||
username: true,
|
||||
password: true,
|
||||
fromEmail: true,
|
||||
fromName: true,
|
||||
secure: true,
|
||||
projectId: true,
|
||||
},
|
||||
});
|
||||
const config: ProjectSmtpConfig | null =
|
||||
await ProjectSMTPConfigService.findOneById({
|
||||
id: smtpConfigId,
|
||||
props: {
|
||||
isRoot: true,
|
||||
},
|
||||
select: {
|
||||
_id: true,
|
||||
hostname: true,
|
||||
port: true,
|
||||
username: true,
|
||||
password: true,
|
||||
fromEmail: true,
|
||||
fromName: true,
|
||||
secure: true,
|
||||
projectId: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!config) {
|
||||
return Response.sendErrorResponse(
|
||||
req,
|
||||
res,
|
||||
new BadDataException(
|
||||
"smtp-config not found for id" + smtpConfigId.toString(),
|
||||
),
|
||||
);
|
||||
}
|
||||
if (!config) {
|
||||
return Response.sendErrorResponse(
|
||||
req,
|
||||
res,
|
||||
new BadDataException(
|
||||
"smtp-config not found for id" + smtpConfigId.toString(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
const toEmail: Email = new Email(body["toEmail"] as string);
|
||||
const toEmail: Email = new Email(body["toEmail"] as string);
|
||||
|
||||
if (!toEmail) {
|
||||
return Response.sendErrorResponse(
|
||||
req,
|
||||
res,
|
||||
new BadDataException("toEmail is required"),
|
||||
);
|
||||
}
|
||||
if (!toEmail) {
|
||||
return Response.sendErrorResponse(
|
||||
req,
|
||||
res,
|
||||
new BadDataException("toEmail is required"),
|
||||
);
|
||||
}
|
||||
|
||||
const mail: EmailMessage = {
|
||||
templateType: EmailTemplateType.SMTPTest,
|
||||
toEmail: new Email(body["toEmail"] as string),
|
||||
subject: "Test Email from OneUptime",
|
||||
vars: {},
|
||||
body: "",
|
||||
};
|
||||
const mail: EmailMessage = {
|
||||
templateType: EmailTemplateType.SMTPTest,
|
||||
toEmail: new Email(body["toEmail"] as string),
|
||||
subject: "Test Email from OneUptime",
|
||||
vars: {},
|
||||
body: "",
|
||||
};
|
||||
|
||||
const mailServer: EmailServer = {
|
||||
id: config.id!,
|
||||
host: config.hostname!,
|
||||
port: config.port!,
|
||||
username: config.username!,
|
||||
password: config.password!,
|
||||
fromEmail: config.fromEmail!,
|
||||
fromName: config.fromName!,
|
||||
secure: Boolean(config.secure),
|
||||
};
|
||||
const mailServer: EmailServer = {
|
||||
id: config.id!,
|
||||
host: config.hostname!,
|
||||
port: config.port!,
|
||||
username: config.username!,
|
||||
password: config.password!,
|
||||
fromEmail: config.fromEmail!,
|
||||
fromName: config.fromName!,
|
||||
secure: Boolean(config.secure),
|
||||
};
|
||||
|
||||
try {
|
||||
await MailService.send(mail, {
|
||||
emailServer: mailServer,
|
||||
projectId: config.projectId!,
|
||||
timeout: 4000,
|
||||
});
|
||||
} catch (err) {
|
||||
logger.error(err);
|
||||
return Response.sendErrorResponse(
|
||||
req,
|
||||
res,
|
||||
new BadDataException(
|
||||
"Cannot send email. Please check your SMTP config. If you are using Google or Gmail, please dont since it does not support machine access to their mail servers. If you are still having issues, please uncheck SSL/TLS toggle and try again. We recommend using SendGrid or Mailgun or any large volume mail provider for SMTP.",
|
||||
),
|
||||
);
|
||||
}
|
||||
try {
|
||||
await MailService.send(mail, {
|
||||
emailServer: mailServer,
|
||||
projectId: config.projectId!,
|
||||
timeout: 4000,
|
||||
});
|
||||
} catch (err) {
|
||||
logger.error(err);
|
||||
throw new BadDataException(
|
||||
"Cannot send email. Please check your SMTP config. If you are using Google or Gmail, please dont since it does not support machine access to their mail servers. If you are still having issues, please uncheck SSL/TLS toggle and try again. We recommend using SendGrid or Mailgun or any large volume mail provider for SMTP.",
|
||||
);
|
||||
}
|
||||
|
||||
return Response.sendEmptySuccessResponse(req, res);
|
||||
});
|
||||
return Response.sendEmptySuccessResponse(req, res);
|
||||
} catch (err) {
|
||||
return next(err);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
export default router;
|
||||
|
||||
171
App/FeatureSet/Notification/API/WhatsApp.ts
Normal file
171
App/FeatureSet/Notification/API/WhatsApp.ts
Normal file
@@ -0,0 +1,171 @@
|
||||
import WhatsAppService from "../Services/WhatsAppService";
|
||||
import BadDataException from "Common/Types/Exception/BadDataException";
|
||||
import { JSONObject } from "Common/Types/JSON";
|
||||
import ObjectID from "Common/Types/ObjectID";
|
||||
import Phone from "Common/Types/Phone";
|
||||
import WhatsAppMessage from "Common/Types/WhatsApp/WhatsAppMessage";
|
||||
import {
|
||||
WhatsAppTemplateId,
|
||||
WhatsAppTemplateIds,
|
||||
WhatsAppTemplateLanguage,
|
||||
} from "Common/Types/WhatsApp/WhatsAppTemplates";
|
||||
import ClusterKeyAuthorization from "Common/Server/Middleware/ClusterKeyAuthorization";
|
||||
import Express, {
|
||||
ExpressRequest,
|
||||
ExpressResponse,
|
||||
ExpressRouter,
|
||||
NextFunction,
|
||||
} from "Common/Server/Utils/Express";
|
||||
import Response from "Common/Server/Utils/Response";
|
||||
|
||||
const router: ExpressRouter = Express.getRouter();
|
||||
|
||||
const toTemplateVariables: (
|
||||
rawVariables: JSONObject | undefined,
|
||||
) => Record<string, string> | undefined = (
|
||||
rawVariables: JSONObject | undefined,
|
||||
): Record<string, string> | undefined => {
|
||||
if (!rawVariables) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const result: Record<string, string> = {};
|
||||
|
||||
for (const key of Object.keys(rawVariables)) {
|
||||
const value: unknown = rawVariables[key];
|
||||
if (value !== null && value !== undefined) {
|
||||
result[key] = String(value);
|
||||
}
|
||||
}
|
||||
|
||||
return Object.keys(result).length > 0 ? result : undefined;
|
||||
};
|
||||
|
||||
router.post(
|
||||
"/send",
|
||||
ClusterKeyAuthorization.isAuthorizedServiceMiddleware,
|
||||
async (req: ExpressRequest, res: ExpressResponse, next: NextFunction) => {
|
||||
const body: JSONObject = req.body as JSONObject;
|
||||
|
||||
if (!body["to"]) {
|
||||
throw new BadDataException("`to` phone number is required");
|
||||
}
|
||||
|
||||
const toPhone: Phone = new Phone(body["to"] as string);
|
||||
|
||||
const message: WhatsAppMessage = {
|
||||
to: toPhone,
|
||||
body: (body["body"] as string) || "",
|
||||
templateKey: (body["templateKey"] as string) || undefined,
|
||||
templateVariables: toTemplateVariables(
|
||||
body["templateVariables"] as JSONObject | undefined,
|
||||
),
|
||||
templateLanguageCode:
|
||||
(body["templateLanguageCode"] as string) || undefined,
|
||||
};
|
||||
|
||||
try {
|
||||
await WhatsAppService.sendWhatsApp(message, {
|
||||
projectId: body["projectId"]
|
||||
? new ObjectID(body["projectId"] as string)
|
||||
: undefined,
|
||||
isSensitive: (body["isSensitive"] as boolean) || false,
|
||||
userOnCallLogTimelineId: body["userOnCallLogTimelineId"]
|
||||
? new ObjectID(body["userOnCallLogTimelineId"] as string)
|
||||
: undefined,
|
||||
incidentId: body["incidentId"]
|
||||
? new ObjectID(body["incidentId"] as string)
|
||||
: undefined,
|
||||
alertId: body["alertId"]
|
||||
? new ObjectID(body["alertId"] as string)
|
||||
: undefined,
|
||||
scheduledMaintenanceId: body["scheduledMaintenanceId"]
|
||||
? new ObjectID(body["scheduledMaintenanceId"] as string)
|
||||
: undefined,
|
||||
statusPageId: body["statusPageId"]
|
||||
? new ObjectID(body["statusPageId"] as string)
|
||||
: undefined,
|
||||
statusPageAnnouncementId: body["statusPageAnnouncementId"]
|
||||
? new ObjectID(body["statusPageAnnouncementId"] as string)
|
||||
: undefined,
|
||||
userId: body["userId"]
|
||||
? new ObjectID(body["userId"] as string)
|
||||
: undefined,
|
||||
onCallPolicyId: body["onCallPolicyId"]
|
||||
? new ObjectID(body["onCallPolicyId"] as string)
|
||||
: undefined,
|
||||
onCallPolicyEscalationRuleId: body["onCallPolicyEscalationRuleId"]
|
||||
? new ObjectID(body["onCallPolicyEscalationRuleId"] as string)
|
||||
: undefined,
|
||||
onCallDutyPolicyExecutionLogTimelineId: body[
|
||||
"onCallDutyPolicyExecutionLogTimelineId"
|
||||
]
|
||||
? new ObjectID(
|
||||
body["onCallDutyPolicyExecutionLogTimelineId"] as string,
|
||||
)
|
||||
: undefined,
|
||||
onCallScheduleId: body["onCallScheduleId"]
|
||||
? new ObjectID(body["onCallScheduleId"] as string)
|
||||
: undefined,
|
||||
teamId: body["teamId"]
|
||||
? new ObjectID(body["teamId"] as string)
|
||||
: undefined,
|
||||
});
|
||||
|
||||
return Response.sendEmptySuccessResponse(req, res);
|
||||
} catch (err) {
|
||||
return next(err);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
router.post(
|
||||
"/test",
|
||||
async (req: ExpressRequest, res: ExpressResponse, next: NextFunction) => {
|
||||
try {
|
||||
const body: JSONObject = req.body as JSONObject;
|
||||
|
||||
if (!body["toPhone"]) {
|
||||
throw new BadDataException("toPhone is required");
|
||||
}
|
||||
|
||||
const toPhone: Phone = new Phone(body["toPhone"] as string);
|
||||
|
||||
const templateKey: WhatsAppTemplateId =
|
||||
WhatsAppTemplateIds.TestNotification;
|
||||
|
||||
const templateLanguageCode: string =
|
||||
WhatsAppTemplateLanguage[templateKey] || "en";
|
||||
|
||||
const message: WhatsAppMessage = {
|
||||
to: toPhone,
|
||||
body: "",
|
||||
templateKey,
|
||||
templateVariables: undefined,
|
||||
templateLanguageCode,
|
||||
};
|
||||
|
||||
try {
|
||||
await WhatsAppService.sendWhatsApp(message, {
|
||||
projectId: body["projectId"]
|
||||
? new ObjectID(body["projectId"] as string)
|
||||
: undefined,
|
||||
isSensitive: false,
|
||||
});
|
||||
} catch (err) {
|
||||
const errorMsg: string =
|
||||
err instanceof Error && err.message
|
||||
? err.message
|
||||
: "Failed to send test WhatsApp message.";
|
||||
|
||||
throw new BadDataException(errorMsg);
|
||||
}
|
||||
|
||||
return Response.sendEmptySuccessResponse(req, res);
|
||||
} catch (err) {
|
||||
return next(err);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
export default router;
|
||||
@@ -12,6 +12,8 @@ import Phone from "Common/Types/Phone";
|
||||
|
||||
type GetGlobalSMTPConfig = () => Promise<EmailServer | null>;
|
||||
|
||||
export const DEFAULT_META_WHATSAPP_API_VERSION: string = "v23.0";
|
||||
|
||||
export const getGlobalSMTPConfig: GetGlobalSMTPConfig =
|
||||
async (): Promise<EmailServer | null> => {
|
||||
const globalConfig: GlobalConfig | null =
|
||||
@@ -222,6 +224,83 @@ export const SMSHighRiskCostInCents: number = process.env[
|
||||
? parseInt(process.env["SMS_HIGH_RISK_COST_IN_CENTS"])
|
||||
: 0;
|
||||
|
||||
export interface MetaWhatsAppConfig {
|
||||
accessToken: string;
|
||||
phoneNumberId: string;
|
||||
businessAccountId?: string | undefined;
|
||||
appId?: string | undefined;
|
||||
appSecret?: string | undefined;
|
||||
apiVersion?: string | undefined;
|
||||
}
|
||||
|
||||
type GetMetaWhatsAppConfigFunction = () => Promise<MetaWhatsAppConfig>;
|
||||
|
||||
export const getMetaWhatsAppConfig: GetMetaWhatsAppConfigFunction =
|
||||
async (): Promise<MetaWhatsAppConfig> => {
|
||||
const globalConfig: GlobalConfig | null =
|
||||
await GlobalConfigService.findOneBy({
|
||||
query: {
|
||||
_id: ObjectID.getZeroObjectID().toString(),
|
||||
},
|
||||
props: {
|
||||
isRoot: true,
|
||||
},
|
||||
select: {
|
||||
metaWhatsAppAccessToken: true,
|
||||
metaWhatsAppPhoneNumberId: true,
|
||||
metaWhatsAppBusinessAccountId: true,
|
||||
metaWhatsAppAppId: true,
|
||||
metaWhatsAppAppSecret: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!globalConfig) {
|
||||
throw new BadDataException("Global Config not found");
|
||||
}
|
||||
|
||||
const accessToken: string | undefined =
|
||||
globalConfig.metaWhatsAppAccessToken?.trim();
|
||||
const phoneNumberId: string | undefined =
|
||||
globalConfig.metaWhatsAppPhoneNumberId?.trim();
|
||||
|
||||
if (!accessToken) {
|
||||
throw new BadDataException(
|
||||
"Meta WhatsApp access token not configured. Please set it in the Admin Dashboard: " +
|
||||
AdminDashboardClientURL.toString(),
|
||||
);
|
||||
}
|
||||
|
||||
if (!phoneNumberId) {
|
||||
throw new BadDataException(
|
||||
"Meta WhatsApp phone number ID not configured. Please set it in the Admin Dashboard: " +
|
||||
AdminDashboardClientURL.toString(),
|
||||
);
|
||||
}
|
||||
|
||||
const businessAccountId: string | undefined =
|
||||
globalConfig.metaWhatsAppBusinessAccountId?.trim() || undefined;
|
||||
const appId: string | undefined =
|
||||
globalConfig.metaWhatsAppAppId?.trim() || undefined;
|
||||
const appSecret: string | undefined =
|
||||
globalConfig.metaWhatsAppAppSecret?.trim() || undefined;
|
||||
const apiVersion: string = DEFAULT_META_WHATSAPP_API_VERSION;
|
||||
|
||||
return {
|
||||
accessToken,
|
||||
phoneNumberId,
|
||||
businessAccountId,
|
||||
appId,
|
||||
appSecret,
|
||||
apiVersion,
|
||||
};
|
||||
};
|
||||
|
||||
export const WhatsAppTextDefaultCostInCents: number = process.env[
|
||||
"WHATSAPP_TEXT_DEFAULT_COST_IN_CENTS"
|
||||
]
|
||||
? parseInt(process.env["WHATSAPP_TEXT_DEFAULT_COST_IN_CENTS"])
|
||||
: 0;
|
||||
|
||||
export const CallHighRiskCostInCentsPerMinute: number = process.env[
|
||||
"CALL_HIGH_RISK_COST_IN_CENTS_PER_MINUTE"
|
||||
]
|
||||
|
||||
@@ -2,6 +2,7 @@ import CallAPI from "./API/Call";
|
||||
// API
|
||||
import MailAPI from "./API/Mail";
|
||||
import SmsAPI from "./API/SMS";
|
||||
import WhatsAppAPI from "./API/WhatsApp";
|
||||
import PushNotificationAPI from "./API/PushNotification";
|
||||
import SMTPConfigAPI from "./API/SMTPConfig";
|
||||
import "./Utils/Handlebars";
|
||||
@@ -16,6 +17,7 @@ const NotificationFeatureSet: FeatureSet = {
|
||||
|
||||
app.use([`/${APP_NAME}/email`, "/email"], MailAPI);
|
||||
app.use([`/${APP_NAME}/sms`, "/sms"], SmsAPI);
|
||||
app.use([`/${APP_NAME}/whatsapp`, "/whatsapp"], WhatsAppAPI);
|
||||
app.use([`/${APP_NAME}/push`, "/push"], PushNotificationAPI);
|
||||
app.use([`/${APP_NAME}/call`, "/call"], CallAPI);
|
||||
app.use([`/${APP_NAME}/smtp-config`, "/smtp-config"], SMTPConfigAPI);
|
||||
|
||||
@@ -47,9 +47,11 @@ function extractSayMessagesFromCallRequest(callRequest: CallRequest): string {
|
||||
if ((item as GatherInput).introMessage) {
|
||||
sayMessages.push((item as GatherInput).introMessage);
|
||||
}
|
||||
// NOTE: Excluding noInputMessage and onInputCallRequest messages from summary
|
||||
// as they contain system responses like "Good bye", "Invalid input", "You have acknowledged"
|
||||
// which should not be included in the call summary according to user requirements
|
||||
/*
|
||||
* NOTE: Excluding noInputMessage and onInputCallRequest messages from summary
|
||||
* as they contain system responses like "Good bye", "Invalid input", "You have acknowledged"
|
||||
* which should not be included in the call summary according to user requirements
|
||||
*/
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -37,11 +37,49 @@ class TransporterPool {
|
||||
private static semaphore: Map<string, number> = new Map();
|
||||
private static readonly MAX_CONCURRENT_CONNECTIONS = 100;
|
||||
|
||||
private static resolveConnectionSettings(emailServer: EmailServer): {
|
||||
portNumber: number;
|
||||
wantsSecureConnection: boolean;
|
||||
secureConnection: boolean;
|
||||
requireTLS: boolean;
|
||||
mode: "implicit-tls" | "starttls" | "plain";
|
||||
} {
|
||||
const portNumber: number = emailServer.port.toNumber();
|
||||
const wantsSecureConnection: boolean = emailServer.secure;
|
||||
const isImplicitTLSPort: boolean = portNumber === 465;
|
||||
|
||||
const secureConnection: boolean = isImplicitTLSPort;
|
||||
const requireTLS: boolean = wantsSecureConnection && !isImplicitTLSPort;
|
||||
|
||||
let mode: "implicit-tls" | "starttls" | "plain" = "plain";
|
||||
|
||||
if (secureConnection) {
|
||||
mode = "implicit-tls";
|
||||
} else if (requireTLS) {
|
||||
mode = "starttls";
|
||||
}
|
||||
|
||||
return {
|
||||
portNumber,
|
||||
wantsSecureConnection,
|
||||
secureConnection,
|
||||
requireTLS,
|
||||
mode,
|
||||
};
|
||||
}
|
||||
|
||||
private static getPoolKey(emailServer: EmailServer): string {
|
||||
const { portNumber, mode } = this.resolveConnectionSettings(emailServer);
|
||||
const username: string = emailServer.username || "noauth";
|
||||
|
||||
return `${emailServer.host.toString()}:${portNumber}:${username}:${mode}`;
|
||||
}
|
||||
|
||||
public static getTransporter(
|
||||
emailServer: EmailServer,
|
||||
options: { timeout?: number | undefined },
|
||||
): Transporter {
|
||||
const key: string = `${emailServer.host.toString()}:${emailServer.port.toNumber()}:${emailServer.username || "noauth"}`;
|
||||
const key: string = this.getPoolKey(emailServer);
|
||||
|
||||
if (!this.pools.has(key)) {
|
||||
const transporter: Transporter = this.createTransporter(
|
||||
@@ -59,9 +97,12 @@ class TransporterPool {
|
||||
emailServer: EmailServer,
|
||||
options: { timeout?: number | undefined },
|
||||
): Transporter {
|
||||
const { portNumber, wantsSecureConnection, secureConnection, requireTLS } =
|
||||
this.resolveConnectionSettings(emailServer);
|
||||
|
||||
let tlsOptions: tls.ConnectionOptions | undefined = undefined;
|
||||
|
||||
if (!emailServer.secure) {
|
||||
if (!wantsSecureConnection) {
|
||||
tlsOptions = {
|
||||
rejectUnauthorized: false,
|
||||
};
|
||||
@@ -69,8 +110,9 @@ class TransporterPool {
|
||||
|
||||
return nodemailer.createTransport({
|
||||
host: emailServer.host.toString(),
|
||||
port: emailServer.port.toNumber(),
|
||||
secure: emailServer.secure,
|
||||
port: portNumber,
|
||||
secure: secureConnection,
|
||||
requireTLS,
|
||||
tls: tlsOptions,
|
||||
auth:
|
||||
emailServer.username && emailServer.password
|
||||
@@ -88,7 +130,7 @@ class TransporterPool {
|
||||
public static async acquireConnection(
|
||||
emailServer: EmailServer,
|
||||
): Promise<void> {
|
||||
const key: string = `${emailServer.host.toString()}:${emailServer.port.toNumber()}:${emailServer.username || "noauth"}`;
|
||||
const key: string = this.getPoolKey(emailServer);
|
||||
|
||||
while ((this.semaphore.get(key) || 0) >= this.MAX_CONCURRENT_CONNECTIONS) {
|
||||
await new Promise<void>((resolve: () => void) => {
|
||||
@@ -100,7 +142,7 @@ class TransporterPool {
|
||||
}
|
||||
|
||||
public static releaseConnection(emailServer: EmailServer): void {
|
||||
const key: string = `${emailServer.host.toString()}:${emailServer.port.toNumber()}:${emailServer.username || "noauth"}`;
|
||||
const key: string = this.getPoolKey(emailServer);
|
||||
const current: number = this.semaphore.get(key) || 0;
|
||||
this.semaphore.set(key, Math.max(0, current - 1));
|
||||
}
|
||||
|
||||
489
App/FeatureSet/Notification/Services/WhatsAppService.ts
Normal file
489
App/FeatureSet/Notification/Services/WhatsAppService.ts
Normal file
@@ -0,0 +1,489 @@
|
||||
import {
|
||||
WhatsAppTextDefaultCostInCents,
|
||||
getMetaWhatsAppConfig,
|
||||
MetaWhatsAppConfig,
|
||||
DEFAULT_META_WHATSAPP_API_VERSION,
|
||||
} from "../Config";
|
||||
import BadDataException from "Common/Types/Exception/BadDataException";
|
||||
import ObjectID from "Common/Types/ObjectID";
|
||||
import UserNotificationStatus from "Common/Types/UserNotification/UserNotificationStatus";
|
||||
import WhatsAppMessage from "Common/Types/WhatsApp/WhatsAppMessage";
|
||||
import WhatsAppStatus from "Common/Types/WhatsAppStatus";
|
||||
import {
|
||||
AuthenticationTemplates,
|
||||
WhatsAppTemplateId,
|
||||
} from "Common/Types/WhatsApp/WhatsAppTemplates";
|
||||
import { JSONArray, JSONObject } from "Common/Types/JSON";
|
||||
import { IsBillingEnabled } from "Common/Server/EnvironmentConfig";
|
||||
import NotificationService from "Common/Server/Services/NotificationService";
|
||||
import ProjectService from "Common/Server/Services/ProjectService";
|
||||
import UserOnCallLogTimelineService from "Common/Server/Services/UserOnCallLogTimelineService";
|
||||
import WhatsAppLogService from "Common/Server/Services/WhatsAppLogService";
|
||||
import logger from "Common/Server/Utils/Logger";
|
||||
import Project from "Common/Models/DatabaseModels/Project";
|
||||
import WhatsAppLog from "Common/Models/DatabaseModels/WhatsAppLog";
|
||||
import API from "Common/Utils/API";
|
||||
import HTTPErrorResponse from "Common/Types/API/HTTPErrorResponse";
|
||||
import HTTPResponse from "Common/Types/API/HTTPResponse";
|
||||
import Protocol from "Common/Types/API/Protocol";
|
||||
import Route from "Common/Types/API/Route";
|
||||
import URL from "Common/Types/API/URL";
|
||||
|
||||
const SENSITIVE_MESSAGE_PLACEHOLDER: string =
|
||||
"This message is sensitive and is not logged";
|
||||
|
||||
export default class WhatsAppService {
|
||||
public static async sendWhatsApp(
|
||||
message: WhatsAppMessage,
|
||||
options: {
|
||||
projectId?: ObjectID | undefined;
|
||||
isSensitive?: boolean | undefined;
|
||||
userOnCallLogTimelineId?: ObjectID | undefined;
|
||||
incidentId?: ObjectID | undefined;
|
||||
alertId?: ObjectID | undefined;
|
||||
scheduledMaintenanceId?: ObjectID | undefined;
|
||||
statusPageId?: ObjectID | undefined;
|
||||
statusPageAnnouncementId?: ObjectID | undefined;
|
||||
userId?: ObjectID | undefined;
|
||||
onCallPolicyId?: ObjectID | undefined;
|
||||
onCallPolicyEscalationRuleId?: ObjectID | undefined;
|
||||
onCallDutyPolicyExecutionLogTimelineId?: ObjectID | undefined;
|
||||
onCallScheduleId?: ObjectID | undefined;
|
||||
teamId?: ObjectID | undefined;
|
||||
} = {},
|
||||
): Promise<void> {
|
||||
let sendError: Error | null = null;
|
||||
const whatsAppLog: WhatsAppLog = new WhatsAppLog();
|
||||
|
||||
try {
|
||||
if (!message.to) {
|
||||
throw new BadDataException(
|
||||
"WhatsApp recipient phone number is required",
|
||||
);
|
||||
}
|
||||
|
||||
if (!message.body && !message.templateKey) {
|
||||
throw new BadDataException(
|
||||
"Either WhatsApp message body or template key must be provided",
|
||||
);
|
||||
}
|
||||
|
||||
const config: MetaWhatsAppConfig = await getMetaWhatsAppConfig();
|
||||
|
||||
const isSensitiveMessage: boolean = Boolean(options.isSensitive);
|
||||
const messageSummary: string = isSensitiveMessage
|
||||
? SENSITIVE_MESSAGE_PLACEHOLDER
|
||||
: message.body ||
|
||||
(message.templateKey
|
||||
? `Template: ${message.templateKey}${
|
||||
message.templateVariables
|
||||
? " Variables: " + JSON.stringify(message.templateVariables)
|
||||
: ""
|
||||
}`
|
||||
: "");
|
||||
|
||||
whatsAppLog.toNumber = message.to;
|
||||
whatsAppLog.messageText = messageSummary;
|
||||
whatsAppLog.whatsAppCostInUSDCents = 0;
|
||||
|
||||
if (options.projectId) {
|
||||
whatsAppLog.projectId = options.projectId;
|
||||
}
|
||||
|
||||
if (options.incidentId) {
|
||||
whatsAppLog.incidentId = options.incidentId;
|
||||
}
|
||||
|
||||
if (options.alertId) {
|
||||
whatsAppLog.alertId = options.alertId;
|
||||
}
|
||||
|
||||
if (options.scheduledMaintenanceId) {
|
||||
whatsAppLog.scheduledMaintenanceId = options.scheduledMaintenanceId;
|
||||
}
|
||||
|
||||
if (options.statusPageId) {
|
||||
whatsAppLog.statusPageId = options.statusPageId;
|
||||
}
|
||||
|
||||
if (options.statusPageAnnouncementId) {
|
||||
whatsAppLog.statusPageAnnouncementId = options.statusPageAnnouncementId;
|
||||
}
|
||||
|
||||
if (options.userId) {
|
||||
whatsAppLog.userId = options.userId;
|
||||
}
|
||||
|
||||
if (options.teamId) {
|
||||
whatsAppLog.teamId = options.teamId;
|
||||
}
|
||||
|
||||
if (options.onCallPolicyId) {
|
||||
whatsAppLog.onCallDutyPolicyId = options.onCallPolicyId;
|
||||
}
|
||||
|
||||
if (options.onCallPolicyEscalationRuleId) {
|
||||
whatsAppLog.onCallDutyPolicyEscalationRuleId =
|
||||
options.onCallPolicyEscalationRuleId;
|
||||
}
|
||||
|
||||
if (options.onCallScheduleId) {
|
||||
whatsAppLog.onCallDutyPolicyScheduleId = options.onCallScheduleId;
|
||||
}
|
||||
|
||||
let messageCost: number = 0;
|
||||
const shouldChargeForMessage: boolean = IsBillingEnabled;
|
||||
|
||||
if (shouldChargeForMessage) {
|
||||
messageCost = WhatsAppTextDefaultCostInCents / 100;
|
||||
}
|
||||
|
||||
let project: Project | null = null;
|
||||
|
||||
if (options.projectId) {
|
||||
project = await ProjectService.findOneById({
|
||||
id: options.projectId,
|
||||
select: {
|
||||
smsOrCallCurrentBalanceInUSDCents: true,
|
||||
lowCallAndSMSBalanceNotificationSentToOwners: true,
|
||||
name: true,
|
||||
notEnabledSmsOrCallNotificationSentToOwners: true,
|
||||
},
|
||||
props: {
|
||||
isRoot: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!project) {
|
||||
whatsAppLog.status = WhatsAppStatus.Error;
|
||||
whatsAppLog.statusMessage = `Project ${options.projectId.toString()} not found.`;
|
||||
logger.error(whatsAppLog.statusMessage);
|
||||
await WhatsAppLogService.create({
|
||||
data: whatsAppLog,
|
||||
props: {
|
||||
isRoot: true,
|
||||
},
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (shouldChargeForMessage) {
|
||||
let updatedBalance: number =
|
||||
project.smsOrCallCurrentBalanceInUSDCents || 0;
|
||||
|
||||
try {
|
||||
updatedBalance = await NotificationService.rechargeIfBalanceIsLow(
|
||||
project.id!,
|
||||
);
|
||||
} catch (err) {
|
||||
logger.error(err);
|
||||
}
|
||||
|
||||
project.smsOrCallCurrentBalanceInUSDCents = updatedBalance;
|
||||
|
||||
if (!project.smsOrCallCurrentBalanceInUSDCents) {
|
||||
whatsAppLog.status = WhatsAppStatus.LowBalance;
|
||||
whatsAppLog.statusMessage = `Project ${options.projectId.toString()} does not have enough balance for WhatsApp messages.`;
|
||||
logger.error(whatsAppLog.statusMessage);
|
||||
|
||||
await WhatsAppLogService.create({
|
||||
data: whatsAppLog,
|
||||
props: {
|
||||
isRoot: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!project.lowCallAndSMSBalanceNotificationSentToOwners) {
|
||||
await ProjectService.updateOneById({
|
||||
id: project.id!,
|
||||
data: {
|
||||
lowCallAndSMSBalanceNotificationSentToOwners: true,
|
||||
},
|
||||
props: {
|
||||
isRoot: true,
|
||||
},
|
||||
});
|
||||
|
||||
await ProjectService.sendEmailToProjectOwners(
|
||||
project.id!,
|
||||
`Low WhatsApp message balance for ${project.name || ""}`,
|
||||
`We tried to send a WhatsApp message to ${message.to.toString()} with message:<br/><br/>${messageSummary}<br/><br/>The message was not sent because your project does not have enough balance for WhatsApp messages. Current balance is ${
|
||||
(project.smsOrCallCurrentBalanceInUSDCents || 0) / 100
|
||||
} USD. Required balance for this message is ${messageCost} USD. Please enable auto recharge or recharge manually.`,
|
||||
);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (project.smsOrCallCurrentBalanceInUSDCents < messageCost * 100) {
|
||||
whatsAppLog.status = WhatsAppStatus.LowBalance;
|
||||
whatsAppLog.statusMessage = `Project does not have enough balance to send WhatsApp message. Current balance is ${
|
||||
project.smsOrCallCurrentBalanceInUSDCents / 100
|
||||
} USD. Required balance is ${messageCost} USD.`;
|
||||
logger.error(whatsAppLog.statusMessage);
|
||||
|
||||
await WhatsAppLogService.create({
|
||||
data: whatsAppLog,
|
||||
props: {
|
||||
isRoot: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!project.lowCallAndSMSBalanceNotificationSentToOwners) {
|
||||
await ProjectService.updateOneById({
|
||||
id: project.id!,
|
||||
data: {
|
||||
lowCallAndSMSBalanceNotificationSentToOwners: true,
|
||||
},
|
||||
props: {
|
||||
isRoot: true,
|
||||
},
|
||||
});
|
||||
|
||||
await ProjectService.sendEmailToProjectOwners(
|
||||
project.id!,
|
||||
`Low WhatsApp message balance for ${project.name || ""}`,
|
||||
`We tried to send a WhatsApp message to ${message.to.toString()} with message:<br/><br/>${messageSummary}<br/><br/>The message was not sent because your project does not have enough balance for WhatsApp messages. Current balance is ${
|
||||
project.smsOrCallCurrentBalanceInUSDCents / 100
|
||||
} USD. Required balance is ${messageCost} USD. Please enable auto recharge or recharge manually.`,
|
||||
);
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const payload: JSONObject = {
|
||||
messaging_product: "whatsapp",
|
||||
recipient_type: "individual",
|
||||
to: message.to.toString(),
|
||||
} as JSONObject;
|
||||
|
||||
if (!message.templateKey) {
|
||||
throw new BadDataException("WhatsApp message template key is required");
|
||||
}
|
||||
|
||||
if (message.templateKey) {
|
||||
const template: JSONObject = {
|
||||
name: message.templateKey,
|
||||
language: {
|
||||
code: message.templateLanguageCode || "en",
|
||||
},
|
||||
} as JSONObject;
|
||||
|
||||
const components: JSONArray = [];
|
||||
|
||||
if (
|
||||
message.templateVariables &&
|
||||
Object.keys(message.templateVariables).length > 0
|
||||
) {
|
||||
const parameters: JSONArray = [];
|
||||
|
||||
for (const [key, value] of Object.entries(
|
||||
message.templateVariables,
|
||||
)) {
|
||||
parameters.push({
|
||||
type: "text",
|
||||
parameter_name: key,
|
||||
text: value,
|
||||
} as JSONObject);
|
||||
}
|
||||
|
||||
if (parameters.length > 0) {
|
||||
components.push({
|
||||
type: "body",
|
||||
parameters,
|
||||
} as JSONObject);
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Check if this is an authentication template
|
||||
* Authentication templates may have special requirements including button components
|
||||
*/
|
||||
const isAuthTemplate: boolean = AuthenticationTemplates.has(
|
||||
message.templateKey as WhatsAppTemplateId,
|
||||
);
|
||||
|
||||
if (isAuthTemplate) {
|
||||
logger.info(
|
||||
`Sending authentication template: ${message.templateKey}`,
|
||||
);
|
||||
|
||||
/*
|
||||
* Authentication templates in WhatsApp may have a button component for "Copy Code"
|
||||
* If the template was created with a button, we need to provide button parameters
|
||||
*/
|
||||
if (message.templateVariables) {
|
||||
const otpCode: string | undefined =
|
||||
message.templateVariables["1"] ||
|
||||
message.templateVariables["otp"] ||
|
||||
message.templateVariables["code"];
|
||||
|
||||
if (otpCode) {
|
||||
/*
|
||||
* Add button component - the index should match the button position in the template
|
||||
* Usually authentication templates have the button as the first (and only) button
|
||||
*/
|
||||
components.push({
|
||||
type: "button",
|
||||
sub_type: "url",
|
||||
index: 0,
|
||||
parameters: [
|
||||
{
|
||||
type: "text",
|
||||
text: otpCode,
|
||||
},
|
||||
],
|
||||
} as JSONObject);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (components.length > 0) {
|
||||
template["components"] = components;
|
||||
}
|
||||
|
||||
payload["type"] = "template";
|
||||
payload["template"] = template;
|
||||
} else {
|
||||
payload["type"] = "text";
|
||||
payload["text"] = {
|
||||
body: message.body || "",
|
||||
} as JSONObject;
|
||||
}
|
||||
|
||||
const apiVersion: string =
|
||||
config.apiVersion?.trim() || DEFAULT_META_WHATSAPP_API_VERSION;
|
||||
|
||||
const url: URL = new URL(
|
||||
Protocol.HTTPS,
|
||||
"graph.facebook.com",
|
||||
new Route(`${apiVersion}/${config.phoneNumberId}/messages`),
|
||||
);
|
||||
|
||||
logger.debug(`WhatsApp API request: ${JSON.stringify(payload)}`);
|
||||
|
||||
const response: HTTPResponse<JSONObject> | HTTPErrorResponse =
|
||||
await API.post<JSONObject>({
|
||||
url,
|
||||
data: payload,
|
||||
headers: {
|
||||
Authorization: `Bearer ${config.accessToken}`,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
});
|
||||
|
||||
if (response instanceof HTTPErrorResponse) {
|
||||
logger.error("Failed to send WhatsApp message.");
|
||||
logger.error(response);
|
||||
const responseDataAsJSONObject: JSONObject = response.data;
|
||||
const responseJsonAsJSONObject: JSONObject | undefined =
|
||||
(response.jsonData as JSONObject | undefined) || undefined;
|
||||
|
||||
// Log full error details for debugging
|
||||
const errorObject: JSONObject | undefined =
|
||||
(responseDataAsJSONObject["error"] as JSONObject | undefined) ||
|
||||
(responseJsonAsJSONObject?.["error"] as JSONObject | undefined);
|
||||
|
||||
if (errorObject) {
|
||||
logger.error("WhatsApp API Error Details:");
|
||||
logger.error(JSON.stringify(errorObject, null, 2));
|
||||
}
|
||||
|
||||
const detailedErrorMessage: string | undefined =
|
||||
((responseDataAsJSONObject["error"] as JSONObject | undefined)?.[
|
||||
"message"
|
||||
] as string | undefined) ||
|
||||
((responseJsonAsJSONObject?.["error"] as JSONObject | undefined)?.[
|
||||
"message"
|
||||
] as string | undefined);
|
||||
|
||||
throw new BadDataException(
|
||||
detailedErrorMessage || "Failed to send WhatsApp message.",
|
||||
);
|
||||
}
|
||||
|
||||
const responseData: JSONObject = (response.jsonData || {}) as JSONObject;
|
||||
|
||||
let messageId: string | undefined = undefined;
|
||||
const messagesArray: JSONArray | undefined =
|
||||
(responseData["messages"] as JSONArray) || undefined;
|
||||
|
||||
if (Array.isArray(messagesArray) && messagesArray.length > 0) {
|
||||
const firstMessage: JSONObject = messagesArray[0] as JSONObject;
|
||||
if (firstMessage["id"]) {
|
||||
messageId = firstMessage["id"] as string;
|
||||
}
|
||||
}
|
||||
|
||||
whatsAppLog.status = WhatsAppStatus.Success;
|
||||
whatsAppLog.statusMessage = messageId
|
||||
? `Message ID: ${messageId}`
|
||||
: "WhatsApp message sent successfully";
|
||||
|
||||
if (shouldChargeForMessage && project) {
|
||||
const deduction: number = Math.floor(messageCost * 100);
|
||||
whatsAppLog.whatsAppCostInUSDCents = deduction;
|
||||
|
||||
project.smsOrCallCurrentBalanceInUSDCents = Math.max(
|
||||
0,
|
||||
Math.floor(
|
||||
(project.smsOrCallCurrentBalanceInUSDCents || 0) - deduction,
|
||||
),
|
||||
);
|
||||
|
||||
await ProjectService.updateOneById({
|
||||
id: project.id!,
|
||||
data: {
|
||||
smsOrCallCurrentBalanceInUSDCents:
|
||||
project.smsOrCallCurrentBalanceInUSDCents,
|
||||
notEnabledSmsOrCallNotificationSentToOwners: false,
|
||||
},
|
||||
props: {
|
||||
isRoot: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
} catch (error: any) {
|
||||
logger.error("Failed to send WhatsApp message.");
|
||||
logger.error(error);
|
||||
whatsAppLog.whatsAppCostInUSDCents = 0;
|
||||
whatsAppLog.status = WhatsAppStatus.Error;
|
||||
const errorMessage: string =
|
||||
error && error.message ? error.message.toString() : `${error}`;
|
||||
whatsAppLog.statusMessage = errorMessage;
|
||||
|
||||
sendError = error;
|
||||
}
|
||||
|
||||
if (options.projectId) {
|
||||
await WhatsAppLogService.create({
|
||||
data: whatsAppLog,
|
||||
props: {
|
||||
isRoot: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
if (options.userOnCallLogTimelineId) {
|
||||
await UserOnCallLogTimelineService.updateOneById({
|
||||
id: options.userOnCallLogTimelineId,
|
||||
data: {
|
||||
status:
|
||||
whatsAppLog.status === WhatsAppStatus.Success
|
||||
? UserNotificationStatus.Sent
|
||||
: UserNotificationStatus.Error,
|
||||
statusMessage: whatsAppLog.statusMessage,
|
||||
},
|
||||
props: {
|
||||
isRoot: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
if (sendError) {
|
||||
throw sendError;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -42,10 +42,10 @@ loadPartials().catch((err: Error) => {
|
||||
|
||||
Handlebars.registerHelper("ifCond", function (v1, v2, options) {
|
||||
if (v1 === v2) {
|
||||
//@ts-ignore
|
||||
//@ts-expect-error - Handlebars uses dynamic this context for template helpers
|
||||
return options.fn(this);
|
||||
}
|
||||
//@ts-ignore
|
||||
//@ts-expect-error - Handlebars uses dynamic this context for template helpers
|
||||
return options.inverse(this);
|
||||
});
|
||||
|
||||
@@ -56,9 +56,9 @@ Handlebars.registerHelper("concat", (v1: any, v2: any) => {
|
||||
|
||||
Handlebars.registerHelper("ifNotCond", function (v1, v2, options) {
|
||||
if (v1 !== v2) {
|
||||
//@ts-ignore
|
||||
//@ts-expect-error - Handlebars uses dynamic this context for template helpers
|
||||
return options.fn(this);
|
||||
}
|
||||
//@ts-ignore
|
||||
//@ts-expect-error - Handlebars uses dynamic this context for template helpers
|
||||
return options.inverse(this);
|
||||
});
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
{
|
||||
"watch": ["./","../Common/Server", "../Common/Types", "../Common/Utils", "../Common/Models"],
|
||||
"ext": "ts,json,tsx,env,js,jsx,hbs",
|
||||
"ext": "ts,tsx",
|
||||
"ignore": [
|
||||
"./node_modules/**",
|
||||
"./public/**",
|
||||
"./bin/**",
|
||||
"./build/**",
|
||||
"greenlock.d/*"
|
||||
],
|
||||
"watchOptions": {
|
||||
@@ -12,5 +16,5 @@
|
||||
"TS_NODE_TRANSPILE_ONLY": "1",
|
||||
"TS_NODE_FILES": "false"
|
||||
},
|
||||
"exec": "node --inspect=0.0.0.0:9229 -r ts-node/register/transpile-only Index.ts"
|
||||
"exec": "node -r ts-node/register/transpile-only Index.ts"
|
||||
}
|
||||
@@ -262,6 +262,96 @@ export default class GlobalConfig extends GlobalConfigModel {
|
||||
})
|
||||
public twilioSecondaryPhoneNumbers?: string = undefined; // phone numbers separated by comma
|
||||
|
||||
@ColumnAccessControl({
|
||||
create: [],
|
||||
read: [],
|
||||
update: [],
|
||||
})
|
||||
@TableColumn({
|
||||
type: TableColumnType.VeryLongText,
|
||||
title: "Meta WhatsApp Access Token",
|
||||
description:
|
||||
"Access token generated from Meta for sending WhatsApp messages.",
|
||||
})
|
||||
@Column({
|
||||
type: ColumnType.VeryLongText,
|
||||
nullable: true,
|
||||
unique: true,
|
||||
})
|
||||
public metaWhatsAppAccessToken?: string = undefined;
|
||||
|
||||
@ColumnAccessControl({
|
||||
create: [],
|
||||
read: [],
|
||||
update: [],
|
||||
})
|
||||
@TableColumn({
|
||||
type: TableColumnType.ShortText,
|
||||
title: "Meta WhatsApp Phone Number ID",
|
||||
description: "The WhatsApp Business phone number ID from Meta.",
|
||||
})
|
||||
@Column({
|
||||
type: ColumnType.ShortText,
|
||||
length: ColumnLength.ShortText,
|
||||
nullable: true,
|
||||
unique: true,
|
||||
})
|
||||
public metaWhatsAppPhoneNumberId?: string = undefined;
|
||||
|
||||
@ColumnAccessControl({
|
||||
create: [],
|
||||
read: [],
|
||||
update: [],
|
||||
})
|
||||
@TableColumn({
|
||||
type: TableColumnType.ShortText,
|
||||
title: "Meta WhatsApp Business Account ID",
|
||||
description: "Business account ID associated with your WhatsApp setup.",
|
||||
})
|
||||
@Column({
|
||||
type: ColumnType.ShortText,
|
||||
length: ColumnLength.ShortText,
|
||||
nullable: true,
|
||||
unique: true,
|
||||
})
|
||||
public metaWhatsAppBusinessAccountId?: string = undefined;
|
||||
|
||||
@ColumnAccessControl({
|
||||
create: [],
|
||||
read: [],
|
||||
update: [],
|
||||
})
|
||||
@TableColumn({
|
||||
type: TableColumnType.ShortText,
|
||||
title: "Meta WhatsApp App ID",
|
||||
description:
|
||||
"Facebook App ID used for the WhatsApp Business Platform integration.",
|
||||
})
|
||||
@Column({
|
||||
type: ColumnType.ShortText,
|
||||
length: ColumnLength.ShortText,
|
||||
nullable: true,
|
||||
unique: true,
|
||||
})
|
||||
public metaWhatsAppAppId?: string = undefined;
|
||||
|
||||
@ColumnAccessControl({
|
||||
create: [],
|
||||
read: [],
|
||||
update: [],
|
||||
})
|
||||
@TableColumn({
|
||||
type: TableColumnType.VeryLongText,
|
||||
title: "Meta WhatsApp App Secret",
|
||||
description: "Facebook App Secret for the WhatsApp Business Platform.",
|
||||
})
|
||||
@Column({
|
||||
type: ColumnType.VeryLongText,
|
||||
nullable: true,
|
||||
unique: true,
|
||||
})
|
||||
public metaWhatsAppAppSecret?: string = undefined;
|
||||
|
||||
@ColumnAccessControl({
|
||||
create: [],
|
||||
read: [],
|
||||
|
||||
@@ -100,6 +100,7 @@ import ServiceCopilotCodeRepository from "./ServiceCopilotCodeRepository";
|
||||
import ShortLink from "./ShortLink";
|
||||
// SMS
|
||||
import SmsLog from "./SmsLog";
|
||||
import WhatsAppLog from "./WhatsAppLog";
|
||||
import PushNotificationLog from "./PushNotificationLog";
|
||||
import WorkspaceNotificationLog from "./WorkspaceNotificationLog";
|
||||
// Status Page
|
||||
@@ -123,6 +124,7 @@ import StatusPageSubscriber from "./StatusPageSubscriber";
|
||||
import Team from "./Team";
|
||||
import TeamMember from "./TeamMember";
|
||||
import TeamPermission from "./TeamPermission";
|
||||
import TeamComplianceSetting from "./TeamComplianceSetting";
|
||||
import TelemetryService from "./TelemetryService";
|
||||
import UsageBilling from "./TelemetryUsageBilling";
|
||||
import User from "./User";
|
||||
@@ -130,6 +132,7 @@ import UserCall from "./UserCall";
|
||||
// Notification Methods
|
||||
import UserEmail from "./UserEmail";
|
||||
import UserPush from "./UserPush";
|
||||
import UserWhatsApp from "./UserWhatsApp";
|
||||
// User Notification Rules
|
||||
import UserNotificationRule from "./UserNotificationRule";
|
||||
import UserNotificationSetting from "./UserNotificationSetting";
|
||||
@@ -145,7 +148,8 @@ import ServiceCatalogDependency from "./ServiceCatalogDependency";
|
||||
import ServiceCatalogMonitor from "./ServiceCatalogMonitor";
|
||||
import ServiceCatalogTelemetryService from "./ServiceCatalogTelemetryService";
|
||||
|
||||
import UserTwoFactorAuth from "./UserTwoFactorAuth";
|
||||
import UserTotpAuth from "./UserTotpAuth";
|
||||
import UserWebAuthn from "./UserWebAuthn";
|
||||
|
||||
import TelemetryIngestionKey from "./TelemetryIngestionKey";
|
||||
|
||||
@@ -196,6 +200,7 @@ const AllModelTypes: Array<{
|
||||
Team,
|
||||
TeamMember,
|
||||
TeamPermission,
|
||||
TeamComplianceSetting,
|
||||
ApiKey,
|
||||
Label,
|
||||
ApiKeyPermission,
|
||||
@@ -294,6 +299,7 @@ const AllModelTypes: Array<{
|
||||
StatusPageOwnerUser,
|
||||
|
||||
SmsLog,
|
||||
WhatsAppLog,
|
||||
PushNotificationLog,
|
||||
WorkspaceNotificationLog,
|
||||
CallLog,
|
||||
@@ -303,6 +309,7 @@ const AllModelTypes: Array<{
|
||||
UserSms,
|
||||
UserCall,
|
||||
UserPush,
|
||||
UserWhatsApp,
|
||||
|
||||
UserNotificationRule,
|
||||
UserOnCallLog,
|
||||
@@ -364,7 +371,8 @@ const AllModelTypes: Array<{
|
||||
ProbeOwnerTeam,
|
||||
ProbeOwnerUser,
|
||||
|
||||
UserTwoFactorAuth,
|
||||
UserTotpAuth,
|
||||
UserWebAuthn,
|
||||
|
||||
TelemetryIngestionKey,
|
||||
|
||||
|
||||
@@ -29,10 +29,17 @@ import {
|
||||
ManyToOne,
|
||||
} from "typeorm";
|
||||
import NotificationRuleWorkspaceChannel from "../../Types/Workspace/NotificationRules/NotificationRuleWorkspaceChannel";
|
||||
import EnableWorkflow from "../../Types/Database/EnableWorkflow";
|
||||
|
||||
@EnableDocumentation()
|
||||
@AccessControlColumn("labels")
|
||||
@TenantColumn("projectId")
|
||||
@EnableWorkflow({
|
||||
create: true,
|
||||
delete: true,
|
||||
update: true,
|
||||
read: true,
|
||||
})
|
||||
@TableAccessControl({
|
||||
create: [
|
||||
Permission.ProjectOwner,
|
||||
|
||||
@@ -25,6 +25,7 @@ import Permission from "../../Types/Permission";
|
||||
import UserNotificationEventType from "../../Types/UserNotification/UserNotificationEventType";
|
||||
import { Column, Entity, Index, JoinColumn, ManyToOne } from "typeorm";
|
||||
import Alert from "./Alert";
|
||||
import EnableWorkflow from "../../Types/Database/EnableWorkflow";
|
||||
|
||||
@TableBillingAccessControl({
|
||||
create: PlanType.Growth,
|
||||
@@ -32,6 +33,12 @@ import Alert from "./Alert";
|
||||
update: PlanType.Growth,
|
||||
delete: PlanType.Growth,
|
||||
})
|
||||
@EnableWorkflow({
|
||||
create: true,
|
||||
delete: true,
|
||||
update: true,
|
||||
read: true,
|
||||
})
|
||||
@EnableDocumentation()
|
||||
@TenantColumn("projectId")
|
||||
@TableAccessControl({
|
||||
|
||||
@@ -29,6 +29,7 @@ import {
|
||||
ManyToMany,
|
||||
ManyToOne,
|
||||
} from "typeorm";
|
||||
import EnableWorkflow from "../../Types/Database/EnableWorkflow";
|
||||
|
||||
@EnableDocumentation()
|
||||
@TableBillingAccessControl({
|
||||
@@ -37,6 +38,12 @@ import {
|
||||
update: PlanType.Growth,
|
||||
delete: PlanType.Growth,
|
||||
})
|
||||
@EnableWorkflow({
|
||||
create: true,
|
||||
delete: true,
|
||||
update: true,
|
||||
read: true,
|
||||
})
|
||||
@AccessControlColumn("labels")
|
||||
@TenantColumn("projectId")
|
||||
@TableAccessControl({
|
||||
|
||||
@@ -17,6 +17,7 @@ import Permission from "../../Types/Permission";
|
||||
import { Column, Entity, Index, JoinColumn, ManyToOne } from "typeorm";
|
||||
import TableBillingAccessControl from "../../Types/Database/AccessControl/TableBillingAccessControl";
|
||||
import { PlanType } from "../../Types/Billing/SubscriptionPlan";
|
||||
import EnableWorkflow from "../../Types/Database/EnableWorkflow";
|
||||
|
||||
@EnableDocumentation()
|
||||
@TableBillingAccessControl({
|
||||
@@ -26,6 +27,12 @@ import { PlanType } from "../../Types/Billing/SubscriptionPlan";
|
||||
delete: PlanType.Growth,
|
||||
})
|
||||
@TenantColumn("projectId")
|
||||
@EnableWorkflow({
|
||||
create: true,
|
||||
delete: true,
|
||||
update: true,
|
||||
read: true,
|
||||
})
|
||||
@TableAccessControl({
|
||||
create: [
|
||||
Permission.ProjectOwner,
|
||||
|
||||
@@ -132,9 +132,11 @@ export default class OnCallDutyPolicyUserOverride extends BaseModel {
|
||||
})
|
||||
public projectId?: ObjectID = undefined;
|
||||
|
||||
// If this is null then it's a global override
|
||||
// If this is set then it's a policy specific override
|
||||
// Policy specifc override will take precedence over global override
|
||||
/*
|
||||
* If this is null then it's a global override
|
||||
* If this is set then it's a policy specific override
|
||||
* Policy specifc override will take precedence over global override
|
||||
*/
|
||||
|
||||
@ColumnAccessControl({
|
||||
create: [
|
||||
|
||||
@@ -798,6 +798,33 @@ export default class Project extends TenantModel {
|
||||
})
|
||||
public enableSmsNotifications?: boolean = undefined;
|
||||
|
||||
@ColumnAccessControl({
|
||||
create: [],
|
||||
read: [
|
||||
Permission.ProjectOwner,
|
||||
Permission.ProjectAdmin,
|
||||
Permission.ProjectMember,
|
||||
Permission.ReadProject,
|
||||
Permission.UnAuthorizedSsoUser,
|
||||
Permission.ProjectUser,
|
||||
],
|
||||
update: [Permission.ProjectOwner, Permission.ManageProjectBilling],
|
||||
})
|
||||
@TableColumn({
|
||||
required: true,
|
||||
isDefaultValueColumn: true,
|
||||
type: TableColumnType.Boolean,
|
||||
title: "Enable WhatsApp Notifications",
|
||||
description: "Enable WhatsApp notifications for this project.",
|
||||
defaultValue: false,
|
||||
})
|
||||
@Column({
|
||||
nullable: false,
|
||||
default: false,
|
||||
type: ColumnType.Boolean,
|
||||
})
|
||||
public enableWhatsAppNotifications?: boolean = undefined;
|
||||
|
||||
@ColumnAccessControl({
|
||||
create: [],
|
||||
read: [
|
||||
@@ -1292,6 +1319,32 @@ export default class Project extends TenantModel {
|
||||
})
|
||||
public letCustomerSupportAccessProject?: boolean = undefined;
|
||||
|
||||
/*
|
||||
* This is an internal field. This is used for internal analytics for example: Metabase.
|
||||
* Values can be between 0 and 100.
|
||||
*/
|
||||
@ColumnAccessControl({
|
||||
create: [],
|
||||
read: [],
|
||||
update: [],
|
||||
})
|
||||
@TableColumn({
|
||||
required: true,
|
||||
type: TableColumnType.Number,
|
||||
isDefaultValueColumn: true,
|
||||
hideColumnInDocumentation: true,
|
||||
title: "Discount Percent",
|
||||
description: "Discount percentage applied to the project billing",
|
||||
defaultValue: 0,
|
||||
})
|
||||
@Column({
|
||||
type: ColumnType.Number,
|
||||
nullable: false,
|
||||
unique: false,
|
||||
default: 0,
|
||||
})
|
||||
public discountPercent?: number = undefined;
|
||||
|
||||
@ColumnAccessControl({
|
||||
create: [],
|
||||
read: [
|
||||
|
||||
@@ -334,6 +334,37 @@ export default class ProjectSCIM extends BaseModel {
|
||||
})
|
||||
public autoDeprovisionUsers?: boolean = undefined;
|
||||
|
||||
@ColumnAccessControl({
|
||||
create: [
|
||||
Permission.ProjectOwner,
|
||||
Permission.ProjectAdmin,
|
||||
Permission.CreateProjectSSO,
|
||||
],
|
||||
read: [
|
||||
Permission.ProjectOwner,
|
||||
Permission.ProjectAdmin,
|
||||
Permission.ProjectMember,
|
||||
Permission.ReadProjectSSO,
|
||||
],
|
||||
update: [
|
||||
Permission.ProjectOwner,
|
||||
Permission.ProjectAdmin,
|
||||
Permission.EditProjectSSO,
|
||||
],
|
||||
})
|
||||
@TableColumn({
|
||||
isDefaultValueColumn: true,
|
||||
type: TableColumnType.Boolean,
|
||||
title: "Enable Push Groups",
|
||||
description: "Enable push groups provisioning instead of default teams",
|
||||
defaultValue: false,
|
||||
})
|
||||
@Column({
|
||||
type: ColumnType.Boolean,
|
||||
default: false,
|
||||
})
|
||||
public enablePushGroups?: boolean = undefined;
|
||||
|
||||
@ColumnAccessControl({
|
||||
create: [],
|
||||
read: [
|
||||
|
||||
@@ -336,8 +336,10 @@ export default class ProjectSmtpConfig extends BaseModel {
|
||||
})
|
||||
public deletedByUserId?: ObjectID = undefined;
|
||||
|
||||
// This is not required because some SMTP servers do not require authentication.
|
||||
// eg: https://learn.microsoft.com/en-us/exchange/mail-flow-best-practices/how-to-set-up-a-multifunction-device-or-application-to-send-email-using-microsoft-365-or-office-365#option-2-send-mail-directly-from-your-printer-or-application-to-microsoft-365-or-office-365-direct-send
|
||||
/*
|
||||
* This is not required because some SMTP servers do not require authentication.
|
||||
* eg: https://learn.microsoft.com/en-us/exchange/mail-flow-best-practices/how-to-set-up-a-multifunction-device-or-application-to-send-email-using-microsoft-365-or-office-365#option-2-send-mail-directly-from-your-printer-or-application-to-microsoft-365-or-office-365-direct-send
|
||||
*/
|
||||
@ColumnAccessControl({
|
||||
create: [
|
||||
Permission.ProjectOwner,
|
||||
|
||||
@@ -420,10 +420,12 @@ export default class StatusPageDomain extends BaseModel {
|
||||
@JoinColumn({ name: "deletedByUserId" })
|
||||
public deletedByUser?: User = undefined;
|
||||
|
||||
// This token is used by the Worker.
|
||||
// worker pings the status page of customers - eg: status.company.com/verify-token/:id
|
||||
// and the end point on Status Page project returns 200.
|
||||
// when that happens the isCnameVerified is set to True and the certificate is added to Greenlock.
|
||||
/*
|
||||
* This token is used by the Worker.
|
||||
* worker pings the status page of customers - eg: status.company.com/verify-token/:id
|
||||
* and the end point on Status Page project returns 200.
|
||||
* when that happens the isCnameVerified is set to True and the certificate is added to Greenlock.
|
||||
*/
|
||||
@ColumnAccessControl({
|
||||
create: [
|
||||
Permission.ProjectOwner,
|
||||
|
||||
403
Common/Models/DatabaseModels/TeamComplianceSetting.ts
Normal file
403
Common/Models/DatabaseModels/TeamComplianceSetting.ts
Normal file
@@ -0,0 +1,403 @@
|
||||
import Project from "./Project";
|
||||
import Team from "./Team";
|
||||
import User from "./User";
|
||||
import BaseModel from "./DatabaseBaseModel/DatabaseBaseModel";
|
||||
import Route from "../../Types/API/Route";
|
||||
import { PlanType } from "../../Types/Billing/SubscriptionPlan";
|
||||
import ColumnAccessControl from "../../Types/Database/AccessControl/ColumnAccessControl";
|
||||
import TableAccessControl from "../../Types/Database/AccessControl/TableAccessControl";
|
||||
import TableBillingAccessControl from "../../Types/Database/AccessControl/TableBillingAccessControl";
|
||||
import ColumnLength from "../../Types/Database/ColumnLength";
|
||||
import ColumnType from "../../Types/Database/ColumnType";
|
||||
import CrudApiEndpoint from "../../Types/Database/CrudApiEndpoint";
|
||||
import EnableDocumentation from "../../Types/Database/EnableDocumentation";
|
||||
import EnableWorkflow from "../../Types/Database/EnableWorkflow";
|
||||
import TableColumn from "../../Types/Database/TableColumn";
|
||||
import TableColumnType from "../../Types/Database/TableColumnType";
|
||||
import TableMetadata from "../../Types/Database/TableMetadata";
|
||||
import TenantColumn from "../../Types/Database/TenantColumn";
|
||||
import IconProp from "../../Types/Icon/IconProp";
|
||||
import { JSONObject } from "../../Types/JSON";
|
||||
import ObjectID from "../../Types/ObjectID";
|
||||
import Permission from "../../Types/Permission";
|
||||
import ComplianceRuleType from "../../Types/Team/ComplianceRuleType";
|
||||
import { Column, Entity, Index, JoinColumn, ManyToOne } from "typeorm";
|
||||
|
||||
@TableBillingAccessControl({
|
||||
create: PlanType.Scale,
|
||||
read: PlanType.Free,
|
||||
update: PlanType.Scale,
|
||||
delete: PlanType.Free,
|
||||
})
|
||||
@EnableDocumentation()
|
||||
@TableAccessControl({
|
||||
create: [
|
||||
Permission.ProjectOwner,
|
||||
Permission.ProjectAdmin,
|
||||
Permission.EditProjectTeam,
|
||||
],
|
||||
read: [
|
||||
Permission.ProjectOwner,
|
||||
Permission.ProjectAdmin,
|
||||
Permission.ProjectMember,
|
||||
Permission.ReadProjectTeam,
|
||||
],
|
||||
delete: [
|
||||
Permission.ProjectOwner,
|
||||
Permission.ProjectAdmin,
|
||||
Permission.EditProjectTeam,
|
||||
],
|
||||
update: [
|
||||
Permission.ProjectOwner,
|
||||
Permission.ProjectAdmin,
|
||||
Permission.EditProjectTeam,
|
||||
],
|
||||
})
|
||||
@TenantColumn("projectId")
|
||||
@CrudApiEndpoint(new Route("/team-compliance-setting"))
|
||||
@Index(["teamId", "ruleType"], { unique: true })
|
||||
@Entity({
|
||||
name: "TeamComplianceSetting",
|
||||
})
|
||||
@EnableWorkflow({
|
||||
create: false,
|
||||
delete: false,
|
||||
update: false,
|
||||
read: false,
|
||||
})
|
||||
@TableMetadata({
|
||||
tableName: "TeamComplianceSetting",
|
||||
singularName: "Team Compliance Setting",
|
||||
pluralName: "Team Compliance Settings",
|
||||
icon: IconProp.CheckCircle,
|
||||
tableDescription: "Compliance settings for your OneUptime team",
|
||||
})
|
||||
export default class TeamComplianceSetting extends BaseModel {
|
||||
@ColumnAccessControl({
|
||||
create: [
|
||||
Permission.ProjectOwner,
|
||||
Permission.ProjectAdmin,
|
||||
Permission.EditProjectTeam,
|
||||
],
|
||||
read: [
|
||||
Permission.ProjectOwner,
|
||||
Permission.ProjectAdmin,
|
||||
Permission.ProjectMember,
|
||||
Permission.ReadProjectTeam,
|
||||
],
|
||||
update: [],
|
||||
})
|
||||
@TableColumn({
|
||||
manyToOneRelationColumn: "projectId",
|
||||
type: TableColumnType.Entity,
|
||||
modelType: Project,
|
||||
title: "Project",
|
||||
description: "Relation to Project Resource in which this object belongs",
|
||||
})
|
||||
@ManyToOne(
|
||||
() => {
|
||||
return Project;
|
||||
},
|
||||
{
|
||||
eager: false,
|
||||
nullable: true,
|
||||
onDelete: "CASCADE",
|
||||
orphanedRowAction: "nullify",
|
||||
},
|
||||
)
|
||||
@JoinColumn({ name: "projectId" })
|
||||
public project?: Project = undefined;
|
||||
|
||||
@ColumnAccessControl({
|
||||
create: [
|
||||
Permission.ProjectOwner,
|
||||
Permission.ProjectAdmin,
|
||||
Permission.EditProjectTeam,
|
||||
],
|
||||
read: [
|
||||
Permission.ProjectOwner,
|
||||
Permission.ProjectAdmin,
|
||||
Permission.ProjectMember,
|
||||
Permission.ReadProjectTeam,
|
||||
],
|
||||
update: [],
|
||||
})
|
||||
@Index()
|
||||
@TableColumn({
|
||||
type: TableColumnType.ObjectID,
|
||||
required: true,
|
||||
canReadOnRelationQuery: true,
|
||||
title: "Project ID",
|
||||
description: "ID of your OneUptime Project in which this object belongs",
|
||||
})
|
||||
@Column({
|
||||
type: ColumnType.ObjectID,
|
||||
nullable: false,
|
||||
transformer: ObjectID.getDatabaseTransformer(),
|
||||
})
|
||||
public projectId?: ObjectID = undefined;
|
||||
|
||||
@ColumnAccessControl({
|
||||
create: [
|
||||
Permission.ProjectOwner,
|
||||
Permission.ProjectAdmin,
|
||||
Permission.EditProjectTeam,
|
||||
],
|
||||
read: [
|
||||
Permission.ProjectOwner,
|
||||
Permission.ProjectAdmin,
|
||||
Permission.ProjectMember,
|
||||
Permission.ReadProjectTeam,
|
||||
],
|
||||
update: [],
|
||||
})
|
||||
@TableColumn({
|
||||
manyToOneRelationColumn: "teamId",
|
||||
type: TableColumnType.Entity,
|
||||
modelType: Team,
|
||||
title: "Team",
|
||||
description: "Team this compliance setting belongs to.",
|
||||
})
|
||||
@ManyToOne(
|
||||
() => {
|
||||
return Team;
|
||||
},
|
||||
{
|
||||
eager: false,
|
||||
nullable: true,
|
||||
onDelete: "CASCADE",
|
||||
orphanedRowAction: "nullify",
|
||||
},
|
||||
)
|
||||
@JoinColumn({ name: "teamId" })
|
||||
public team?: Team = undefined;
|
||||
|
||||
@ColumnAccessControl({
|
||||
create: [
|
||||
Permission.ProjectOwner,
|
||||
Permission.ProjectAdmin,
|
||||
Permission.EditProjectTeam,
|
||||
],
|
||||
read: [
|
||||
Permission.ProjectOwner,
|
||||
Permission.ProjectAdmin,
|
||||
Permission.ProjectMember,
|
||||
Permission.ReadProjectTeam,
|
||||
],
|
||||
update: [],
|
||||
})
|
||||
@Index()
|
||||
@TableColumn({
|
||||
type: TableColumnType.ObjectID,
|
||||
required: true,
|
||||
title: "Team ID",
|
||||
description: "ID of Team this compliance setting belongs to.",
|
||||
})
|
||||
@Column({
|
||||
type: ColumnType.ObjectID,
|
||||
nullable: false,
|
||||
transformer: ObjectID.getDatabaseTransformer(),
|
||||
})
|
||||
public teamId?: ObjectID = undefined;
|
||||
|
||||
@ColumnAccessControl({
|
||||
create: [
|
||||
Permission.ProjectOwner,
|
||||
Permission.ProjectAdmin,
|
||||
Permission.EditProjectTeam,
|
||||
],
|
||||
read: [
|
||||
Permission.ProjectOwner,
|
||||
Permission.ProjectAdmin,
|
||||
Permission.ProjectMember,
|
||||
Permission.ReadProjectTeam,
|
||||
],
|
||||
update: [],
|
||||
})
|
||||
@TableColumn({
|
||||
manyToOneRelationColumn: "createdByUserId",
|
||||
type: TableColumnType.Entity,
|
||||
modelType: User,
|
||||
title: "Created by User",
|
||||
description:
|
||||
"Relation to User who created this object (if this object was created by a User)",
|
||||
})
|
||||
@ManyToOne(
|
||||
() => {
|
||||
return User;
|
||||
},
|
||||
{
|
||||
eager: false,
|
||||
nullable: true,
|
||||
onDelete: "SET NULL",
|
||||
orphanedRowAction: "nullify",
|
||||
},
|
||||
)
|
||||
@JoinColumn({ name: "createdByUserId" })
|
||||
public createdByUser?: User = undefined;
|
||||
|
||||
@ColumnAccessControl({
|
||||
create: [
|
||||
Permission.ProjectOwner,
|
||||
Permission.ProjectAdmin,
|
||||
Permission.EditProjectTeam,
|
||||
],
|
||||
read: [
|
||||
Permission.ProjectOwner,
|
||||
Permission.ProjectAdmin,
|
||||
Permission.ProjectMember,
|
||||
Permission.ReadProjectTeam,
|
||||
],
|
||||
update: [],
|
||||
})
|
||||
@TableColumn({
|
||||
type: TableColumnType.ObjectID,
|
||||
title: "Created by User ID",
|
||||
description:
|
||||
"User ID who created this object (if this object was created by a User)",
|
||||
})
|
||||
@Column({
|
||||
type: ColumnType.ObjectID,
|
||||
nullable: true,
|
||||
transformer: ObjectID.getDatabaseTransformer(),
|
||||
})
|
||||
public createdByUserId?: ObjectID = undefined;
|
||||
|
||||
@ColumnAccessControl({
|
||||
create: [],
|
||||
read: [],
|
||||
update: [],
|
||||
})
|
||||
@TableColumn({
|
||||
manyToOneRelationColumn: "deletedByUserId",
|
||||
type: TableColumnType.Entity,
|
||||
title: "Deleted by User",
|
||||
modelType: User,
|
||||
description:
|
||||
"Relation to User who deleted this object (if this object was deleted by a User)",
|
||||
})
|
||||
@ManyToOne(
|
||||
() => {
|
||||
return User;
|
||||
},
|
||||
{
|
||||
cascade: false,
|
||||
eager: false,
|
||||
nullable: true,
|
||||
onDelete: "SET NULL",
|
||||
orphanedRowAction: "nullify",
|
||||
},
|
||||
)
|
||||
@JoinColumn({ name: "deletedByUserId" })
|
||||
public deletedByUser?: User = undefined;
|
||||
|
||||
@ColumnAccessControl({
|
||||
create: [],
|
||||
read: [],
|
||||
update: [],
|
||||
})
|
||||
@TableColumn({
|
||||
type: TableColumnType.ObjectID,
|
||||
title: "Deleted by User ID",
|
||||
description:
|
||||
"User ID who deleted this object (if this object was deleted by a User)",
|
||||
})
|
||||
@Column({
|
||||
type: ColumnType.ObjectID,
|
||||
nullable: true,
|
||||
transformer: ObjectID.getDatabaseTransformer(),
|
||||
})
|
||||
public deletedByUserId?: ObjectID = undefined;
|
||||
|
||||
@ColumnAccessControl({
|
||||
create: [
|
||||
Permission.ProjectOwner,
|
||||
Permission.ProjectAdmin,
|
||||
Permission.EditProjectTeam,
|
||||
],
|
||||
read: [
|
||||
Permission.ProjectOwner,
|
||||
Permission.ProjectAdmin,
|
||||
Permission.ProjectMember,
|
||||
Permission.ReadProjectTeam,
|
||||
],
|
||||
update: [
|
||||
Permission.ProjectOwner,
|
||||
Permission.ProjectAdmin,
|
||||
Permission.EditProjectTeam,
|
||||
],
|
||||
})
|
||||
@TableColumn({
|
||||
required: true,
|
||||
type: TableColumnType.LongText,
|
||||
title: "Rule Type",
|
||||
description: "Type of compliance rule.",
|
||||
})
|
||||
@Column({
|
||||
nullable: false,
|
||||
type: ColumnType.LongText,
|
||||
length: ColumnLength.LongText,
|
||||
})
|
||||
public ruleType?: ComplianceRuleType = undefined;
|
||||
|
||||
@ColumnAccessControl({
|
||||
create: [
|
||||
Permission.ProjectOwner,
|
||||
Permission.ProjectAdmin,
|
||||
Permission.EditProjectTeam,
|
||||
],
|
||||
read: [
|
||||
Permission.ProjectOwner,
|
||||
Permission.ProjectAdmin,
|
||||
Permission.ProjectMember,
|
||||
Permission.ReadProjectTeam,
|
||||
],
|
||||
update: [
|
||||
Permission.ProjectOwner,
|
||||
Permission.ProjectAdmin,
|
||||
Permission.EditProjectTeam,
|
||||
],
|
||||
})
|
||||
@TableColumn({
|
||||
isDefaultValueColumn: true,
|
||||
type: TableColumnType.Boolean,
|
||||
title: "Enabled",
|
||||
description: "Whether this compliance rule is enabled.",
|
||||
defaultValue: false,
|
||||
})
|
||||
@Column({
|
||||
type: ColumnType.Boolean,
|
||||
default: false,
|
||||
})
|
||||
public enabled?: boolean = undefined;
|
||||
|
||||
@ColumnAccessControl({
|
||||
create: [
|
||||
Permission.ProjectOwner,
|
||||
Permission.ProjectAdmin,
|
||||
Permission.EditProjectTeam,
|
||||
],
|
||||
read: [
|
||||
Permission.ProjectOwner,
|
||||
Permission.ProjectAdmin,
|
||||
Permission.ProjectMember,
|
||||
Permission.ReadProjectTeam,
|
||||
],
|
||||
update: [
|
||||
Permission.ProjectOwner,
|
||||
Permission.ProjectAdmin,
|
||||
Permission.EditProjectTeam,
|
||||
],
|
||||
})
|
||||
@TableColumn({
|
||||
required: false,
|
||||
type: TableColumnType.JSON,
|
||||
title: "Options",
|
||||
description: "Additional options for this compliance rule.",
|
||||
})
|
||||
@Column({
|
||||
type: ColumnType.JSON,
|
||||
nullable: true,
|
||||
})
|
||||
public options?: JSONObject = undefined;
|
||||
}
|
||||
@@ -1,7 +1,6 @@
|
||||
import File from "./File";
|
||||
import UserModel from "../../Models/DatabaseModels/DatabaseBaseModel/UserModel";
|
||||
import Route from "../../Types/API/Route";
|
||||
import URL from "../../Types/API/URL";
|
||||
import CompanySize from "../../Types/Company/CompanySize";
|
||||
import JobRole from "../../Types/Company/JobRole";
|
||||
import AllowAccessIfSubscriptionIsUnpaid from "../../Types/Database/AccessControl/AllowAccessIfSubscriptionIsUnpaid";
|
||||
@@ -301,51 +300,6 @@ class User extends UserModel {
|
||||
})
|
||||
public twoFactorAuthEnabled?: boolean = undefined;
|
||||
|
||||
@ColumnAccessControl({
|
||||
create: [],
|
||||
read: [],
|
||||
|
||||
update: [],
|
||||
})
|
||||
@TableColumn({ type: TableColumnType.ShortText })
|
||||
@Column({
|
||||
type: ColumnType.ShortText,
|
||||
length: ColumnLength.ShortText,
|
||||
nullable: true,
|
||||
unique: false,
|
||||
})
|
||||
public twoFactorSecretCode?: string = undefined;
|
||||
|
||||
@ColumnAccessControl({
|
||||
create: [],
|
||||
read: [],
|
||||
|
||||
update: [],
|
||||
})
|
||||
@TableColumn({ type: TableColumnType.ShortURL })
|
||||
@Column({
|
||||
type: ColumnType.ShortURL,
|
||||
length: ColumnLength.ShortURL,
|
||||
nullable: true,
|
||||
unique: false,
|
||||
transformer: URL.getDatabaseTransformer(),
|
||||
})
|
||||
public twoFactorAuthUrl?: URL = undefined;
|
||||
|
||||
@ColumnAccessControl({
|
||||
create: [],
|
||||
read: [Permission.CurrentUser],
|
||||
|
||||
update: [],
|
||||
})
|
||||
@TableColumn({ type: TableColumnType.Array })
|
||||
@Column({
|
||||
type: ColumnType.Array,
|
||||
nullable: true,
|
||||
unique: false,
|
||||
})
|
||||
public backupCodes?: Array<string> = undefined;
|
||||
|
||||
@ColumnAccessControl({
|
||||
create: [],
|
||||
read: [],
|
||||
|
||||
@@ -6,6 +6,7 @@ import UserCall from "./UserCall";
|
||||
import UserEmail from "./UserEmail";
|
||||
import UserPush from "./UserPush";
|
||||
import UserSMS from "./UserSMS";
|
||||
import UserWhatsApp from "./UserWhatsApp";
|
||||
import BaseModel from "./DatabaseBaseModel/DatabaseBaseModel";
|
||||
import Route from "../../Types/API/Route";
|
||||
import ColumnAccessControl from "../../Types/Database/AccessControl/ColumnAccessControl";
|
||||
@@ -383,6 +384,53 @@ class UserNotificationRule extends BaseModel {
|
||||
})
|
||||
public userSmsId?: ObjectID = undefined;
|
||||
|
||||
@ColumnAccessControl({
|
||||
create: [Permission.CurrentUser],
|
||||
read: [Permission.CurrentUser],
|
||||
update: [],
|
||||
})
|
||||
@TableColumn({
|
||||
manyToOneRelationColumn: "userWhatsAppId",
|
||||
type: TableColumnType.Entity,
|
||||
modelType: UserWhatsApp,
|
||||
title: "User WhatsApp",
|
||||
description:
|
||||
"Relation to User WhatsApp Resource in which this object belongs",
|
||||
})
|
||||
@ManyToOne(
|
||||
() => {
|
||||
return UserWhatsApp;
|
||||
},
|
||||
{
|
||||
eager: false,
|
||||
nullable: true,
|
||||
onDelete: "CASCADE",
|
||||
orphanedRowAction: "nullify",
|
||||
},
|
||||
)
|
||||
@JoinColumn({ name: "userWhatsAppId" })
|
||||
public userWhatsApp?: UserWhatsApp = undefined;
|
||||
|
||||
@ColumnAccessControl({
|
||||
create: [Permission.CurrentUser],
|
||||
read: [Permission.CurrentUser],
|
||||
update: [],
|
||||
})
|
||||
@Index()
|
||||
@TableColumn({
|
||||
type: TableColumnType.ObjectID,
|
||||
required: false,
|
||||
canReadOnRelationQuery: true,
|
||||
title: "User WhatsApp ID",
|
||||
description: "ID of User WhatsApp in which this object belongs",
|
||||
})
|
||||
@Column({
|
||||
type: ColumnType.ObjectID,
|
||||
nullable: true,
|
||||
transformer: ObjectID.getDatabaseTransformer(),
|
||||
})
|
||||
public userWhatsAppId?: ObjectID = undefined;
|
||||
|
||||
@ColumnAccessControl({
|
||||
create: [Permission.CurrentUser],
|
||||
read: [Permission.CurrentUser],
|
||||
|
||||
@@ -273,6 +273,22 @@ class UserNotificationSetting extends BaseModel {
|
||||
})
|
||||
public alertBySMS?: boolean = undefined;
|
||||
|
||||
@ColumnAccessControl({
|
||||
create: [Permission.CurrentUser],
|
||||
read: [Permission.CurrentUser],
|
||||
update: [Permission.CurrentUser],
|
||||
})
|
||||
@TableColumn({
|
||||
isDefaultValueColumn: true,
|
||||
type: TableColumnType.Boolean,
|
||||
defaultValue: false,
|
||||
})
|
||||
@Column({
|
||||
type: ColumnType.Boolean,
|
||||
default: false,
|
||||
})
|
||||
public alertByWhatsApp?: boolean = undefined;
|
||||
|
||||
@ColumnAccessControl({
|
||||
create: [Permission.CurrentUser],
|
||||
read: [Permission.CurrentUser],
|
||||
|
||||
@@ -13,6 +13,7 @@ import UserNotificationRule from "./UserNotificationRule";
|
||||
import UserPush from "./UserPush";
|
||||
import UserOnCallLog from "./UserOnCallLog";
|
||||
import UserSMS from "./UserSMS";
|
||||
import UserWhatsApp from "./UserWhatsApp";
|
||||
import BaseModel from "./DatabaseBaseModel/DatabaseBaseModel";
|
||||
import Route from "../../Types/API/Route";
|
||||
import { PlanType } from "../../Types/Billing/SubscriptionPlan";
|
||||
@@ -834,6 +835,53 @@ export default class UserOnCallLogTimeline extends BaseModel {
|
||||
})
|
||||
public userSmsId?: ObjectID = undefined;
|
||||
|
||||
@ColumnAccessControl({
|
||||
create: [],
|
||||
read: [Permission.CurrentUser],
|
||||
update: [],
|
||||
})
|
||||
@TableColumn({
|
||||
manyToOneRelationColumn: "userWhatsAppId",
|
||||
type: TableColumnType.Entity,
|
||||
modelType: UserWhatsApp,
|
||||
title: "User WhatsApp",
|
||||
description:
|
||||
"Relation to User WhatsApp Resource in which this object belongs",
|
||||
})
|
||||
@ManyToOne(
|
||||
() => {
|
||||
return UserWhatsApp;
|
||||
},
|
||||
{
|
||||
eager: false,
|
||||
nullable: true,
|
||||
onDelete: "CASCADE",
|
||||
orphanedRowAction: "nullify",
|
||||
},
|
||||
)
|
||||
@JoinColumn({ name: "userWhatsAppId" })
|
||||
public userWhatsApp?: UserWhatsApp = undefined;
|
||||
|
||||
@ColumnAccessControl({
|
||||
create: [],
|
||||
read: [Permission.CurrentUser],
|
||||
update: [],
|
||||
})
|
||||
@Index()
|
||||
@TableColumn({
|
||||
type: TableColumnType.ObjectID,
|
||||
required: false,
|
||||
canReadOnRelationQuery: true,
|
||||
title: "User WhatsApp ID",
|
||||
description: "ID of User WhatsApp in which this object belongs",
|
||||
})
|
||||
@Column({
|
||||
type: ColumnType.ObjectID,
|
||||
nullable: true,
|
||||
transformer: ObjectID.getDatabaseTransformer(),
|
||||
})
|
||||
public userWhatsAppId?: ObjectID = undefined;
|
||||
|
||||
@ColumnAccessControl({
|
||||
create: [],
|
||||
read: [Permission.CurrentUser],
|
||||
|
||||
@@ -27,19 +27,19 @@ import { Column, Entity, JoinColumn, ManyToOne } from "typeorm";
|
||||
delete: [Permission.CurrentUser],
|
||||
update: [Permission.CurrentUser],
|
||||
})
|
||||
@CrudApiEndpoint(new Route("/user-two-factor-auth"))
|
||||
@CrudApiEndpoint(new Route("/user-totp-auth"))
|
||||
@Entity({
|
||||
name: "UserTwoFactorAuth",
|
||||
name: "UserTotpAuth",
|
||||
})
|
||||
@TableMetadata({
|
||||
tableName: "UserTwoFactorAuth",
|
||||
singularName: "Two Factor Auth",
|
||||
pluralName: "Two Factor Auth",
|
||||
tableName: "UserTotpAuth",
|
||||
singularName: "TOTP Auth",
|
||||
pluralName: "TOTP Auth",
|
||||
icon: IconProp.ShieldCheck,
|
||||
tableDescription: "Two Factor Authentication for users",
|
||||
tableDescription: "TOTP Authentication for users",
|
||||
})
|
||||
@CurrentUserCanAccessRecordBy("userId")
|
||||
class UserTwoFactorAuth extends BaseModel {
|
||||
class UserTotpAuth extends BaseModel {
|
||||
@ColumnAccessControl({
|
||||
create: [Permission.CurrentUser],
|
||||
read: [Permission.CurrentUser],
|
||||
@@ -48,8 +48,8 @@ class UserTwoFactorAuth extends BaseModel {
|
||||
@TableColumn({
|
||||
type: TableColumnType.ShortText,
|
||||
canReadOnRelationQuery: true,
|
||||
title: "Two Factor Auth Name",
|
||||
description: "Name of the two factor authentication",
|
||||
title: "TOTP Auth Name",
|
||||
description: "Name of the TOTP authentication",
|
||||
})
|
||||
@Column({
|
||||
type: ColumnType.ShortText,
|
||||
@@ -67,8 +67,8 @@ class UserTwoFactorAuth extends BaseModel {
|
||||
@TableColumn({
|
||||
type: TableColumnType.VeryLongText,
|
||||
canReadOnRelationQuery: false,
|
||||
title: "Two Factor Auth Secret",
|
||||
description: "Secret of the two factor authentication",
|
||||
title: "TOTP Auth Secret",
|
||||
description: "Secret of the TOTP authentication",
|
||||
})
|
||||
@Column({
|
||||
type: ColumnType.VeryLongText,
|
||||
@@ -85,8 +85,8 @@ class UserTwoFactorAuth extends BaseModel {
|
||||
@TableColumn({
|
||||
type: TableColumnType.VeryLongText,
|
||||
canReadOnRelationQuery: false,
|
||||
title: "Two Factor Auth OTP URL",
|
||||
description: "OTP URL of the two factor authentication",
|
||||
title: "TOTP Auth OTP URL",
|
||||
description: "OTP URL of the TOTP authentication",
|
||||
})
|
||||
@Column({
|
||||
type: ColumnType.VeryLongText,
|
||||
@@ -106,7 +106,7 @@ class UserTwoFactorAuth extends BaseModel {
|
||||
title: "Is Verified",
|
||||
isDefaultValueColumn: true,
|
||||
description:
|
||||
"Is this two factor authentication verified and validated (has user entered the tokent to verify it)",
|
||||
"Is this TOTP authentication verified and validated (has user entered the token to verify it)",
|
||||
defaultValue: false,
|
||||
})
|
||||
@Column({
|
||||
@@ -171,7 +171,7 @@ class UserTwoFactorAuth extends BaseModel {
|
||||
manyToOneRelationColumn: "userId",
|
||||
type: TableColumnType.Entity,
|
||||
title: "User",
|
||||
description: "Relation to User who owns this two factor authentication",
|
||||
description: "Relation to User who owns this TOTP authentication",
|
||||
})
|
||||
@ManyToOne(
|
||||
() => {
|
||||
@@ -207,4 +207,4 @@ class UserTwoFactorAuth extends BaseModel {
|
||||
public userId?: ObjectID = undefined;
|
||||
}
|
||||
|
||||
export default UserTwoFactorAuth;
|
||||
export default UserTotpAuth;
|
||||
244
Common/Models/DatabaseModels/UserWebAuthn.ts
Normal file
244
Common/Models/DatabaseModels/UserWebAuthn.ts
Normal file
@@ -0,0 +1,244 @@
|
||||
import BaseModel from "./DatabaseBaseModel/DatabaseBaseModel";
|
||||
import User from "./User";
|
||||
import Route from "../../Types/API/Route";
|
||||
import AllowAccessIfSubscriptionIsUnpaid from "../../Types/Database/AccessControl/AllowAccessIfSubscriptionIsUnpaid";
|
||||
import ColumnAccessControl from "../../Types/Database/AccessControl/ColumnAccessControl";
|
||||
import TableAccessControl from "../../Types/Database/AccessControl/TableAccessControl";
|
||||
import ColumnLength from "../../Types/Database/ColumnLength";
|
||||
import ColumnType from "../../Types/Database/ColumnType";
|
||||
import CrudApiEndpoint from "../../Types/Database/CrudApiEndpoint";
|
||||
import CurrentUserCanAccessRecordBy from "../../Types/Database/CurrentUserCanAccessRecordBy";
|
||||
import EnableDocumentation from "../../Types/Database/EnableDocumentation";
|
||||
import TableColumn from "../../Types/Database/TableColumn";
|
||||
import TableColumnType from "../../Types/Database/TableColumnType";
|
||||
import TableMetadata from "../../Types/Database/TableMetadata";
|
||||
import IconProp from "../../Types/Icon/IconProp";
|
||||
import ObjectID from "../../Types/ObjectID";
|
||||
import Permission from "../../Types/Permission";
|
||||
import { Column, Entity, JoinColumn, ManyToOne } from "typeorm";
|
||||
|
||||
@EnableDocumentation({
|
||||
isMasterAdminApiDocs: true,
|
||||
})
|
||||
@AllowAccessIfSubscriptionIsUnpaid()
|
||||
@TableAccessControl({
|
||||
create: [Permission.CurrentUser],
|
||||
read: [Permission.CurrentUser],
|
||||
delete: [Permission.CurrentUser],
|
||||
update: [Permission.CurrentUser],
|
||||
})
|
||||
@CrudApiEndpoint(new Route("/user-webauthn"))
|
||||
@Entity({
|
||||
name: "UserWebAuthn",
|
||||
})
|
||||
@TableMetadata({
|
||||
tableName: "UserWebAuthn",
|
||||
singularName: "WebAuthn Credential",
|
||||
pluralName: "WebAuthn Credentials",
|
||||
icon: IconProp.ShieldCheck,
|
||||
tableDescription: "WebAuthn credentials for users (security keys)",
|
||||
})
|
||||
@CurrentUserCanAccessRecordBy("userId")
|
||||
class UserWebAuthn extends BaseModel {
|
||||
@ColumnAccessControl({
|
||||
create: [Permission.CurrentUser],
|
||||
read: [Permission.CurrentUser],
|
||||
update: [Permission.CurrentUser],
|
||||
})
|
||||
@TableColumn({
|
||||
type: TableColumnType.ShortText,
|
||||
canReadOnRelationQuery: true,
|
||||
title: "Credential Name",
|
||||
description: "Name of the WebAuthn credential",
|
||||
})
|
||||
@Column({
|
||||
type: ColumnType.ShortText,
|
||||
length: ColumnLength.ShortText,
|
||||
nullable: false,
|
||||
unique: false,
|
||||
})
|
||||
public name?: string = undefined;
|
||||
|
||||
@ColumnAccessControl({
|
||||
create: [Permission.CurrentUser],
|
||||
read: [],
|
||||
update: [],
|
||||
})
|
||||
@TableColumn({
|
||||
type: TableColumnType.VeryLongText,
|
||||
canReadOnRelationQuery: false,
|
||||
title: "Credential ID",
|
||||
description: "Unique identifier for the WebAuthn credential",
|
||||
})
|
||||
@Column({
|
||||
type: ColumnType.VeryLongText,
|
||||
nullable: false,
|
||||
unique: true,
|
||||
})
|
||||
public credentialId?: string = undefined;
|
||||
|
||||
@ColumnAccessControl({
|
||||
create: [Permission.CurrentUser],
|
||||
read: [],
|
||||
update: [],
|
||||
})
|
||||
@TableColumn({
|
||||
type: TableColumnType.VeryLongText,
|
||||
canReadOnRelationQuery: false,
|
||||
title: "Public Key",
|
||||
description: "Public key of the WebAuthn credential",
|
||||
})
|
||||
@Column({
|
||||
type: ColumnType.VeryLongText,
|
||||
nullable: false,
|
||||
unique: false,
|
||||
})
|
||||
public publicKey?: string = undefined;
|
||||
|
||||
@ColumnAccessControl({
|
||||
create: [Permission.CurrentUser],
|
||||
read: [],
|
||||
update: [],
|
||||
})
|
||||
@TableColumn({
|
||||
type: TableColumnType.VeryLongText,
|
||||
canReadOnRelationQuery: false,
|
||||
title: "Counter",
|
||||
description: "Counter for the WebAuthn credential",
|
||||
})
|
||||
@Column({
|
||||
type: ColumnType.VeryLongText,
|
||||
nullable: false,
|
||||
unique: false,
|
||||
})
|
||||
public counter?: string = undefined;
|
||||
|
||||
@ColumnAccessControl({
|
||||
create: [Permission.CurrentUser],
|
||||
read: [],
|
||||
update: [],
|
||||
})
|
||||
@TableColumn({
|
||||
type: TableColumnType.VeryLongText,
|
||||
canReadOnRelationQuery: false,
|
||||
title: "Transports",
|
||||
description: "Transports supported by the WebAuthn credential",
|
||||
})
|
||||
@Column({
|
||||
type: ColumnType.VeryLongText,
|
||||
nullable: true,
|
||||
unique: false,
|
||||
})
|
||||
public transports?: string = undefined;
|
||||
|
||||
@ColumnAccessControl({
|
||||
create: [Permission.CurrentUser],
|
||||
read: [Permission.CurrentUser],
|
||||
update: [],
|
||||
})
|
||||
@TableColumn({
|
||||
type: TableColumnType.Boolean,
|
||||
canReadOnRelationQuery: true,
|
||||
title: "Is Verified",
|
||||
isDefaultValueColumn: true,
|
||||
description: "Is this WebAuthn credential verified and validated",
|
||||
defaultValue: false,
|
||||
})
|
||||
@Column({
|
||||
type: ColumnType.Boolean,
|
||||
nullable: false,
|
||||
default: false,
|
||||
})
|
||||
public isVerified?: boolean = undefined;
|
||||
|
||||
@ColumnAccessControl({
|
||||
create: [],
|
||||
read: [],
|
||||
update: [],
|
||||
})
|
||||
@TableColumn({
|
||||
manyToOneRelationColumn: "deletedByUserId",
|
||||
type: TableColumnType.Entity,
|
||||
title: "Deleted by User",
|
||||
modelType: User,
|
||||
description:
|
||||
"Relation to User who deleted this object (if this object was deleted by a User)",
|
||||
})
|
||||
@ManyToOne(
|
||||
() => {
|
||||
return User;
|
||||
},
|
||||
{
|
||||
cascade: false,
|
||||
eager: false,
|
||||
nullable: true,
|
||||
onDelete: "SET NULL",
|
||||
orphanedRowAction: "nullify",
|
||||
},
|
||||
)
|
||||
@JoinColumn({ name: "deletedByUserId" })
|
||||
public deletedByUser?: User = undefined;
|
||||
|
||||
@ColumnAccessControl({
|
||||
create: [],
|
||||
read: [],
|
||||
update: [],
|
||||
})
|
||||
@TableColumn({
|
||||
type: TableColumnType.ObjectID,
|
||||
title: "Deleted by User ID",
|
||||
description:
|
||||
"User ID who deleted this object (if this object was deleted by a User)",
|
||||
})
|
||||
@Column({
|
||||
type: ColumnType.ObjectID,
|
||||
nullable: true,
|
||||
transformer: ObjectID.getDatabaseTransformer(),
|
||||
})
|
||||
public deletedByUserId?: ObjectID = undefined;
|
||||
|
||||
@ColumnAccessControl({
|
||||
create: [Permission.CurrentUser],
|
||||
read: [Permission.CurrentUser],
|
||||
update: [Permission.CurrentUser],
|
||||
})
|
||||
@TableColumn({
|
||||
manyToOneRelationColumn: "userId",
|
||||
type: TableColumnType.Entity,
|
||||
title: "User",
|
||||
description: "Relation to User who owns this WebAuthn credential",
|
||||
})
|
||||
@ManyToOne(
|
||||
() => {
|
||||
return User;
|
||||
},
|
||||
{
|
||||
cascade: false,
|
||||
eager: false,
|
||||
nullable: true,
|
||||
onDelete: "CASCADE",
|
||||
orphanedRowAction: "nullify",
|
||||
},
|
||||
)
|
||||
@JoinColumn({ name: "userId" })
|
||||
public user?: User = undefined;
|
||||
|
||||
@ColumnAccessControl({
|
||||
create: [Permission.CurrentUser],
|
||||
read: [Permission.CurrentUser],
|
||||
update: [Permission.CurrentUser],
|
||||
})
|
||||
@TableColumn({
|
||||
type: TableColumnType.ObjectID,
|
||||
title: "User ID",
|
||||
description: "User ID who owns this WebAuthn credential",
|
||||
})
|
||||
@Column({
|
||||
type: ColumnType.ObjectID,
|
||||
nullable: true,
|
||||
transformer: ObjectID.getDatabaseTransformer(),
|
||||
})
|
||||
public userId?: ObjectID = undefined;
|
||||
}
|
||||
|
||||
export default UserWebAuthn;
|
||||
288
Common/Models/DatabaseModels/UserWhatsApp.ts
Normal file
288
Common/Models/DatabaseModels/UserWhatsApp.ts
Normal file
@@ -0,0 +1,288 @@
|
||||
import Project from "./Project";
|
||||
import User from "./User";
|
||||
import BaseModel from "./DatabaseBaseModel/DatabaseBaseModel";
|
||||
import Route from "../../Types/API/Route";
|
||||
import AllowAccessIfSubscriptionIsUnpaid from "../../Types/Database/AccessControl/AllowAccessIfSubscriptionIsUnpaid";
|
||||
import ColumnAccessControl from "../../Types/Database/AccessControl/ColumnAccessControl";
|
||||
import TableAccessControl from "../../Types/Database/AccessControl/TableAccessControl";
|
||||
import ColumnLength from "../../Types/Database/ColumnLength";
|
||||
import ColumnType from "../../Types/Database/ColumnType";
|
||||
import CrudApiEndpoint from "../../Types/Database/CrudApiEndpoint";
|
||||
import CurrentUserCanAccessRecordBy from "../../Types/Database/CurrentUserCanAccessRecordBy";
|
||||
import TableColumn from "../../Types/Database/TableColumn";
|
||||
import TableColumnType from "../../Types/Database/TableColumnType";
|
||||
import TableMetadata from "../../Types/Database/TableMetadata";
|
||||
import TenantColumn from "../../Types/Database/TenantColumn";
|
||||
import IconProp from "../../Types/Icon/IconProp";
|
||||
import ObjectID from "../../Types/ObjectID";
|
||||
import Permission from "../../Types/Permission";
|
||||
import Phone from "../../Types/Phone";
|
||||
import Text from "../../Types/Text";
|
||||
import { Column, Entity, Index, JoinColumn, ManyToOne } from "typeorm";
|
||||
|
||||
@TenantColumn("projectId")
|
||||
@AllowAccessIfSubscriptionIsUnpaid()
|
||||
@TableAccessControl({
|
||||
create: [Permission.CurrentUser],
|
||||
read: [Permission.CurrentUser],
|
||||
delete: [Permission.CurrentUser],
|
||||
update: [Permission.CurrentUser],
|
||||
})
|
||||
@CrudApiEndpoint(new Route("/user-whatsapp"))
|
||||
@Entity({
|
||||
name: "UserWhatsApp",
|
||||
})
|
||||
@TableMetadata({
|
||||
tableName: "UserWhatsApp",
|
||||
singularName: "WhatsApp Number",
|
||||
pluralName: "WhatsApp Numbers",
|
||||
icon: IconProp.WhatsApp,
|
||||
tableDescription: "WhatsApp numbers used for WhatsApp notifications.",
|
||||
})
|
||||
@CurrentUserCanAccessRecordBy("userId")
|
||||
class UserWhatsApp extends BaseModel {
|
||||
@ColumnAccessControl({
|
||||
create: [Permission.CurrentUser],
|
||||
read: [Permission.CurrentUser],
|
||||
update: [],
|
||||
})
|
||||
@TableColumn({
|
||||
manyToOneRelationColumn: "projectId",
|
||||
type: TableColumnType.Entity,
|
||||
modelType: Project,
|
||||
title: "Project",
|
||||
description: "Relation to Project Resource in which this object belongs",
|
||||
})
|
||||
@ManyToOne(
|
||||
() => {
|
||||
return Project;
|
||||
},
|
||||
{
|
||||
eager: false,
|
||||
nullable: true,
|
||||
onDelete: "CASCADE",
|
||||
orphanedRowAction: "nullify",
|
||||
},
|
||||
)
|
||||
@JoinColumn({ name: "projectId" })
|
||||
public project?: Project = undefined;
|
||||
|
||||
@ColumnAccessControl({
|
||||
create: [Permission.CurrentUser],
|
||||
read: [Permission.CurrentUser],
|
||||
update: [],
|
||||
})
|
||||
@Index()
|
||||
@TableColumn({
|
||||
type: TableColumnType.ObjectID,
|
||||
required: true,
|
||||
canReadOnRelationQuery: true,
|
||||
title: "Project ID",
|
||||
description: "ID of your OneUptime Project in which this object belongs",
|
||||
})
|
||||
@Column({
|
||||
type: ColumnType.ObjectID,
|
||||
nullable: false,
|
||||
transformer: ObjectID.getDatabaseTransformer(),
|
||||
})
|
||||
public projectId?: ObjectID = undefined;
|
||||
|
||||
@ColumnAccessControl({
|
||||
create: [Permission.CurrentUser],
|
||||
read: [Permission.CurrentUser],
|
||||
update: [],
|
||||
})
|
||||
@TableColumn({
|
||||
title: "WhatsApp Number",
|
||||
required: true,
|
||||
unique: false,
|
||||
type: TableColumnType.Phone,
|
||||
canReadOnRelationQuery: true,
|
||||
})
|
||||
@Column({
|
||||
type: ColumnType.Phone,
|
||||
length: ColumnLength.Phone,
|
||||
unique: false,
|
||||
nullable: false,
|
||||
transformer: Phone.getDatabaseTransformer(),
|
||||
})
|
||||
public phone?: Phone = undefined;
|
||||
|
||||
@ColumnAccessControl({
|
||||
create: [Permission.CurrentUser],
|
||||
read: [Permission.CurrentUser],
|
||||
update: [],
|
||||
})
|
||||
@TableColumn({
|
||||
manyToOneRelationColumn: "user",
|
||||
type: TableColumnType.Entity,
|
||||
modelType: User,
|
||||
title: "User",
|
||||
description: "Relation to User who this WhatsApp number belongs to",
|
||||
})
|
||||
@ManyToOne(
|
||||
() => {
|
||||
return User;
|
||||
},
|
||||
{
|
||||
eager: false,
|
||||
nullable: true,
|
||||
onDelete: "CASCADE",
|
||||
orphanedRowAction: "nullify",
|
||||
},
|
||||
)
|
||||
@JoinColumn({ name: "userId" })
|
||||
public user?: User = undefined;
|
||||
|
||||
@ColumnAccessControl({
|
||||
create: [Permission.CurrentUser],
|
||||
read: [Permission.CurrentUser],
|
||||
update: [],
|
||||
})
|
||||
@TableColumn({
|
||||
type: TableColumnType.ObjectID,
|
||||
title: "User ID",
|
||||
description: "User ID who this WhatsApp number belongs to",
|
||||
})
|
||||
@Column({
|
||||
type: ColumnType.ObjectID,
|
||||
nullable: true,
|
||||
transformer: ObjectID.getDatabaseTransformer(),
|
||||
})
|
||||
@Index()
|
||||
public userId?: ObjectID = undefined;
|
||||
|
||||
@ColumnAccessControl({
|
||||
create: [Permission.CurrentUser],
|
||||
read: [Permission.CurrentUser],
|
||||
update: [],
|
||||
})
|
||||
@TableColumn({
|
||||
manyToOneRelationColumn: "createdByUserId",
|
||||
type: TableColumnType.Entity,
|
||||
modelType: User,
|
||||
title: "Created by User",
|
||||
description:
|
||||
"Relation to User who created this object (if this object was created by a User)",
|
||||
})
|
||||
@ManyToOne(
|
||||
() => {
|
||||
return User;
|
||||
},
|
||||
{
|
||||
eager: false,
|
||||
nullable: true,
|
||||
onDelete: "SET NULL",
|
||||
orphanedRowAction: "nullify",
|
||||
},
|
||||
)
|
||||
@JoinColumn({ name: "createdByUserId" })
|
||||
public createdByUser?: User = undefined;
|
||||
|
||||
@ColumnAccessControl({
|
||||
create: [Permission.CurrentUser],
|
||||
read: [Permission.CurrentUser],
|
||||
update: [],
|
||||
})
|
||||
@TableColumn({
|
||||
type: TableColumnType.ObjectID,
|
||||
title: "Created by User ID",
|
||||
description:
|
||||
"User ID who created this object (if this object was created by a User)",
|
||||
})
|
||||
@Column({
|
||||
type: ColumnType.ObjectID,
|
||||
nullable: true,
|
||||
transformer: ObjectID.getDatabaseTransformer(),
|
||||
})
|
||||
public createdByUserId?: ObjectID = undefined;
|
||||
|
||||
@ColumnAccessControl({
|
||||
create: [],
|
||||
read: [],
|
||||
update: [],
|
||||
})
|
||||
@TableColumn({
|
||||
manyToOneRelationColumn: "deletedByUserId",
|
||||
type: TableColumnType.Entity,
|
||||
title: "Deleted by User",
|
||||
modelType: User,
|
||||
description:
|
||||
"Relation to User who deleted this object (if this object was deleted by a User)",
|
||||
})
|
||||
@ManyToOne(
|
||||
() => {
|
||||
return User;
|
||||
},
|
||||
{
|
||||
cascade: false,
|
||||
eager: false,
|
||||
nullable: true,
|
||||
onDelete: "SET NULL",
|
||||
orphanedRowAction: "nullify",
|
||||
},
|
||||
)
|
||||
@JoinColumn({ name: "deletedByUserId" })
|
||||
public deletedByUser?: User = undefined;
|
||||
|
||||
@ColumnAccessControl({
|
||||
create: [],
|
||||
read: [],
|
||||
update: [],
|
||||
})
|
||||
@TableColumn({
|
||||
type: TableColumnType.ObjectID,
|
||||
title: "Deleted by User ID",
|
||||
description:
|
||||
"User ID who deleted this object (if this object was deleted by a User)",
|
||||
})
|
||||
@Column({
|
||||
type: ColumnType.ObjectID,
|
||||
nullable: true,
|
||||
transformer: ObjectID.getDatabaseTransformer(),
|
||||
})
|
||||
public deletedByUserId?: ObjectID = undefined;
|
||||
|
||||
@ColumnAccessControl({
|
||||
create: [],
|
||||
read: [Permission.CurrentUser],
|
||||
update: [],
|
||||
})
|
||||
@TableColumn({
|
||||
title: "Is Verified",
|
||||
description: "Is this WhatsApp number verified?",
|
||||
isDefaultValueColumn: true,
|
||||
type: TableColumnType.Boolean,
|
||||
defaultValue: false,
|
||||
})
|
||||
@Column({
|
||||
type: ColumnType.Boolean,
|
||||
default: false,
|
||||
})
|
||||
public isVerified?: boolean = undefined;
|
||||
|
||||
@ColumnAccessControl({
|
||||
create: [],
|
||||
read: [],
|
||||
update: [],
|
||||
})
|
||||
@TableColumn({
|
||||
title: "Verification Code",
|
||||
description: "Temporary Verification Code",
|
||||
isDefaultValueColumn: true,
|
||||
computed: true,
|
||||
required: true,
|
||||
type: TableColumnType.ShortText,
|
||||
forceGetDefaultValueOnCreate: () => {
|
||||
return Text.generateRandomNumber(6);
|
||||
},
|
||||
})
|
||||
@Column({
|
||||
type: ColumnType.ShortText,
|
||||
nullable: false,
|
||||
length: ColumnLength.ShortText,
|
||||
})
|
||||
public verificationCode?: string = undefined;
|
||||
}
|
||||
|
||||
export default UserWhatsApp;
|
||||
884
Common/Models/DatabaseModels/WhatsAppLog.ts
Normal file
884
Common/Models/DatabaseModels/WhatsAppLog.ts
Normal file
@@ -0,0 +1,884 @@
|
||||
import Project from "./Project";
|
||||
import Incident from "./Incident";
|
||||
import Alert from "./Alert";
|
||||
import ScheduledMaintenance from "./ScheduledMaintenance";
|
||||
import StatusPage from "./StatusPage";
|
||||
import StatusPageAnnouncement from "./StatusPageAnnouncement";
|
||||
import User from "./User";
|
||||
import OnCallDutyPolicy from "./OnCallDutyPolicy";
|
||||
import OnCallDutyPolicyEscalationRule from "./OnCallDutyPolicyEscalationRule";
|
||||
import OnCallDutyPolicySchedule from "./OnCallDutyPolicySchedule";
|
||||
import Team from "./Team";
|
||||
import BaseModel from "./DatabaseBaseModel/DatabaseBaseModel";
|
||||
import Route from "../../Types/API/Route";
|
||||
import ColumnAccessControl from "../../Types/Database/AccessControl/ColumnAccessControl";
|
||||
import TableAccessControl from "../../Types/Database/AccessControl/TableAccessControl";
|
||||
import ColumnLength from "../../Types/Database/ColumnLength";
|
||||
import ColumnType from "../../Types/Database/ColumnType";
|
||||
import CrudApiEndpoint from "../../Types/Database/CrudApiEndpoint";
|
||||
import EnableDocumentation from "../../Types/Database/EnableDocumentation";
|
||||
import EnableWorkflow from "../../Types/Database/EnableWorkflow";
|
||||
import TableColumn from "../../Types/Database/TableColumn";
|
||||
import TableColumnType from "../../Types/Database/TableColumnType";
|
||||
import TableMetadata from "../../Types/Database/TableMetadata";
|
||||
import TenantColumn from "../../Types/Database/TenantColumn";
|
||||
import IconProp from "../../Types/Icon/IconProp";
|
||||
import ObjectID from "../../Types/ObjectID";
|
||||
import Permission from "../../Types/Permission";
|
||||
import Phone from "../../Types/Phone";
|
||||
import WhatsAppStatus from "../../Types/WhatsAppStatus";
|
||||
import { Column, Entity, Index, JoinColumn, ManyToOne } from "typeorm";
|
||||
|
||||
@EnableDocumentation()
|
||||
@TenantColumn("projectId")
|
||||
@TableAccessControl({
|
||||
create: [],
|
||||
read: [
|
||||
Permission.ProjectOwner,
|
||||
Permission.ProjectAdmin,
|
||||
Permission.ProjectMember,
|
||||
Permission.ReadSmsLog,
|
||||
],
|
||||
delete: [],
|
||||
update: [],
|
||||
})
|
||||
@CrudApiEndpoint(new Route("/whatsapp-log"))
|
||||
@Entity({
|
||||
name: "WhatsAppLog",
|
||||
})
|
||||
@EnableWorkflow({
|
||||
create: true,
|
||||
delete: false,
|
||||
update: false,
|
||||
})
|
||||
@TableMetadata({
|
||||
tableName: "WhatsAppLog",
|
||||
singularName: "WhatsApp Log",
|
||||
pluralName: "WhatsApp Logs",
|
||||
icon: IconProp.WhatsApp,
|
||||
tableDescription:
|
||||
"Logs of all the WhatsApp messages sent out to all users and subscribers for this project.",
|
||||
})
|
||||
export default class WhatsAppLog extends BaseModel {
|
||||
@ColumnAccessControl({
|
||||
create: [],
|
||||
read: [
|
||||
Permission.ProjectOwner,
|
||||
Permission.ProjectAdmin,
|
||||
Permission.ProjectMember,
|
||||
Permission.ReadSmsLog,
|
||||
],
|
||||
update: [],
|
||||
})
|
||||
@TableColumn({
|
||||
manyToOneRelationColumn: "projectId",
|
||||
type: TableColumnType.Entity,
|
||||
modelType: Project,
|
||||
title: "Project",
|
||||
description: "Relation to Project Resource in which this object belongs",
|
||||
})
|
||||
@ManyToOne(
|
||||
() => {
|
||||
return Project;
|
||||
},
|
||||
{
|
||||
eager: false,
|
||||
nullable: true,
|
||||
onDelete: "CASCADE",
|
||||
orphanedRowAction: "nullify",
|
||||
},
|
||||
)
|
||||
@JoinColumn({ name: "projectId" })
|
||||
public project?: Project = undefined;
|
||||
|
||||
@ColumnAccessControl({
|
||||
create: [],
|
||||
read: [
|
||||
Permission.ProjectOwner,
|
||||
Permission.ProjectAdmin,
|
||||
Permission.ProjectMember,
|
||||
Permission.ReadSmsLog,
|
||||
],
|
||||
update: [],
|
||||
})
|
||||
@Index()
|
||||
@TableColumn({
|
||||
type: TableColumnType.ObjectID,
|
||||
required: true,
|
||||
canReadOnRelationQuery: true,
|
||||
title: "Project ID",
|
||||
description: "ID of your OneUptime Project in which this object belongs",
|
||||
})
|
||||
@Column({
|
||||
type: ColumnType.ObjectID,
|
||||
nullable: false,
|
||||
transformer: ObjectID.getDatabaseTransformer(),
|
||||
})
|
||||
public projectId?: ObjectID = undefined;
|
||||
|
||||
@ColumnAccessControl({
|
||||
create: [],
|
||||
read: [
|
||||
Permission.ProjectOwner,
|
||||
Permission.ProjectAdmin,
|
||||
Permission.ProjectMember,
|
||||
Permission.ReadSmsLog,
|
||||
],
|
||||
update: [],
|
||||
})
|
||||
@Index()
|
||||
@TableColumn({
|
||||
required: true,
|
||||
type: TableColumnType.Phone,
|
||||
title: "To Number",
|
||||
description: "Phone Number WhatsApp message was sent to",
|
||||
canReadOnRelationQuery: false,
|
||||
})
|
||||
@Column({
|
||||
nullable: false,
|
||||
type: ColumnType.Phone,
|
||||
length: ColumnLength.Phone,
|
||||
transformer: Phone.getDatabaseTransformer(),
|
||||
})
|
||||
public toNumber?: Phone = undefined;
|
||||
|
||||
@ColumnAccessControl({
|
||||
create: [],
|
||||
read: [
|
||||
Permission.ProjectOwner,
|
||||
Permission.ProjectAdmin,
|
||||
Permission.ProjectMember,
|
||||
Permission.ReadSmsLog,
|
||||
],
|
||||
update: [],
|
||||
})
|
||||
@Index()
|
||||
@TableColumn({
|
||||
required: false,
|
||||
type: TableColumnType.Phone,
|
||||
title: "From Number",
|
||||
description:
|
||||
"Phone Number WhatsApp message was sent from (Business Number ID)",
|
||||
canReadOnRelationQuery: false,
|
||||
})
|
||||
@Column({
|
||||
nullable: true,
|
||||
type: ColumnType.Phone,
|
||||
length: ColumnLength.Phone,
|
||||
transformer: Phone.getDatabaseTransformer(),
|
||||
})
|
||||
public fromNumber?: Phone = undefined;
|
||||
|
||||
@ColumnAccessControl({
|
||||
create: [],
|
||||
read: [
|
||||
Permission.ProjectOwner,
|
||||
Permission.ProjectAdmin,
|
||||
Permission.ProjectMember,
|
||||
Permission.ReadSmsLog,
|
||||
],
|
||||
update: [],
|
||||
})
|
||||
@TableColumn({
|
||||
required: false,
|
||||
type: TableColumnType.VeryLongText,
|
||||
title: "Message Text",
|
||||
description: "Text content of the WhatsApp message",
|
||||
canReadOnRelationQuery: false,
|
||||
})
|
||||
@Column({
|
||||
nullable: true,
|
||||
type: ColumnType.VeryLongText,
|
||||
})
|
||||
public messageText?: string = undefined;
|
||||
|
||||
@ColumnAccessControl({
|
||||
create: [],
|
||||
read: [
|
||||
Permission.ProjectOwner,
|
||||
Permission.ProjectAdmin,
|
||||
Permission.ProjectMember,
|
||||
Permission.ReadSmsLog,
|
||||
],
|
||||
update: [],
|
||||
})
|
||||
@TableColumn({
|
||||
required: false,
|
||||
type: TableColumnType.LongText,
|
||||
title: "Status Message",
|
||||
description: "Status Message (if any)",
|
||||
canReadOnRelationQuery: false,
|
||||
})
|
||||
@Column({
|
||||
nullable: true,
|
||||
type: ColumnType.LongText,
|
||||
length: ColumnLength.LongText,
|
||||
})
|
||||
public statusMessage?: string = undefined;
|
||||
|
||||
@ColumnAccessControl({
|
||||
create: [],
|
||||
read: [
|
||||
Permission.ProjectOwner,
|
||||
Permission.ProjectAdmin,
|
||||
Permission.ProjectMember,
|
||||
Permission.ReadSmsLog,
|
||||
],
|
||||
update: [],
|
||||
})
|
||||
@TableColumn({
|
||||
required: true,
|
||||
type: TableColumnType.ShortText,
|
||||
title: "Status of the WhatsApp Message",
|
||||
description: "Status of the WhatsApp message sent",
|
||||
canReadOnRelationQuery: false,
|
||||
})
|
||||
@Column({
|
||||
nullable: false,
|
||||
type: ColumnType.ShortText,
|
||||
length: ColumnLength.ShortText,
|
||||
})
|
||||
public status?: WhatsAppStatus = undefined;
|
||||
|
||||
@ColumnAccessControl({
|
||||
create: [],
|
||||
read: [
|
||||
Permission.ProjectOwner,
|
||||
Permission.ProjectAdmin,
|
||||
Permission.ProjectMember,
|
||||
Permission.ReadSmsLog,
|
||||
],
|
||||
update: [],
|
||||
})
|
||||
@Index()
|
||||
@TableColumn({
|
||||
required: true,
|
||||
type: TableColumnType.Number,
|
||||
title: "WhatsApp Cost",
|
||||
description: "WhatsApp Message Cost in USD Cents",
|
||||
canReadOnRelationQuery: false,
|
||||
isDefaultValueColumn: true,
|
||||
defaultValue: 0,
|
||||
})
|
||||
@Column({
|
||||
nullable: false,
|
||||
default: 0,
|
||||
type: ColumnType.Number,
|
||||
})
|
||||
public whatsAppCostInUSDCents?: number = undefined;
|
||||
|
||||
@ColumnAccessControl({
|
||||
create: [],
|
||||
read: [
|
||||
Permission.ProjectOwner,
|
||||
Permission.ProjectAdmin,
|
||||
Permission.ProjectMember,
|
||||
Permission.ReadSmsLog,
|
||||
],
|
||||
update: [],
|
||||
})
|
||||
@TableColumn({
|
||||
manyToOneRelationColumn: "incidentId",
|
||||
type: TableColumnType.Entity,
|
||||
modelType: Incident,
|
||||
title: "Incident",
|
||||
description: "Incident associated with this message (if any)",
|
||||
})
|
||||
@ManyToOne(
|
||||
() => {
|
||||
return Incident;
|
||||
},
|
||||
{
|
||||
eager: false,
|
||||
nullable: true,
|
||||
onDelete: "CASCADE",
|
||||
orphanedRowAction: "nullify",
|
||||
},
|
||||
)
|
||||
@JoinColumn({ name: "incidentId" })
|
||||
public incident?: Incident = undefined;
|
||||
|
||||
@ColumnAccessControl({
|
||||
create: [],
|
||||
read: [
|
||||
Permission.ProjectOwner,
|
||||
Permission.ProjectAdmin,
|
||||
Permission.ProjectMember,
|
||||
Permission.ReadSmsLog,
|
||||
],
|
||||
update: [],
|
||||
})
|
||||
@Index()
|
||||
@TableColumn({
|
||||
type: TableColumnType.ObjectID,
|
||||
required: false,
|
||||
canReadOnRelationQuery: true,
|
||||
title: "Incident ID",
|
||||
description: "ID of Incident associated with this message (if any)",
|
||||
})
|
||||
@Column({
|
||||
type: ColumnType.ObjectID,
|
||||
nullable: true,
|
||||
transformer: ObjectID.getDatabaseTransformer(),
|
||||
})
|
||||
public incidentId?: ObjectID = undefined;
|
||||
|
||||
@ColumnAccessControl({
|
||||
create: [],
|
||||
read: [
|
||||
Permission.ProjectOwner,
|
||||
Permission.ProjectAdmin,
|
||||
Permission.ProjectMember,
|
||||
Permission.ReadSmsLog,
|
||||
],
|
||||
update: [],
|
||||
})
|
||||
@TableColumn({
|
||||
manyToOneRelationColumn: "userId",
|
||||
type: TableColumnType.Entity,
|
||||
modelType: User,
|
||||
title: "User",
|
||||
description: "User who initiated this message (if any)",
|
||||
})
|
||||
@ManyToOne(
|
||||
() => {
|
||||
return User;
|
||||
},
|
||||
{
|
||||
eager: false,
|
||||
nullable: true,
|
||||
onDelete: "CASCADE",
|
||||
orphanedRowAction: "nullify",
|
||||
},
|
||||
)
|
||||
@JoinColumn({ name: "userId" })
|
||||
public user?: User = undefined;
|
||||
|
||||
@ColumnAccessControl({
|
||||
create: [],
|
||||
read: [
|
||||
Permission.ProjectOwner,
|
||||
Permission.ProjectAdmin,
|
||||
Permission.ProjectMember,
|
||||
Permission.ReadSmsLog,
|
||||
],
|
||||
update: [],
|
||||
})
|
||||
@Index()
|
||||
@TableColumn({
|
||||
type: TableColumnType.ObjectID,
|
||||
required: false,
|
||||
canReadOnRelationQuery: true,
|
||||
title: "User ID",
|
||||
description: "ID of User who initiated this message (if any)",
|
||||
})
|
||||
@Column({
|
||||
type: ColumnType.ObjectID,
|
||||
nullable: true,
|
||||
transformer: ObjectID.getDatabaseTransformer(),
|
||||
})
|
||||
public userId?: ObjectID = undefined;
|
||||
|
||||
@ColumnAccessControl({
|
||||
create: [],
|
||||
read: [
|
||||
Permission.ProjectOwner,
|
||||
Permission.ProjectAdmin,
|
||||
Permission.ProjectMember,
|
||||
Permission.ReadSmsLog,
|
||||
],
|
||||
update: [],
|
||||
})
|
||||
@TableColumn({
|
||||
manyToOneRelationColumn: "alertId",
|
||||
type: TableColumnType.Entity,
|
||||
modelType: Alert,
|
||||
title: "Alert",
|
||||
description: "Alert associated with this message (if any)",
|
||||
})
|
||||
@ManyToOne(
|
||||
() => {
|
||||
return Alert;
|
||||
},
|
||||
{
|
||||
eager: false,
|
||||
nullable: true,
|
||||
onDelete: "CASCADE",
|
||||
orphanedRowAction: "nullify",
|
||||
},
|
||||
)
|
||||
@JoinColumn({ name: "alertId" })
|
||||
public alert?: Alert = undefined;
|
||||
|
||||
@ColumnAccessControl({
|
||||
create: [],
|
||||
read: [
|
||||
Permission.ProjectOwner,
|
||||
Permission.ProjectAdmin,
|
||||
Permission.ProjectMember,
|
||||
Permission.ReadSmsLog,
|
||||
],
|
||||
update: [],
|
||||
})
|
||||
@Index()
|
||||
@TableColumn({
|
||||
type: TableColumnType.ObjectID,
|
||||
required: false,
|
||||
canReadOnRelationQuery: true,
|
||||
title: "Alert ID",
|
||||
description: "ID of Alert associated with this message (if any)",
|
||||
})
|
||||
@Column({
|
||||
type: ColumnType.ObjectID,
|
||||
nullable: true,
|
||||
transformer: ObjectID.getDatabaseTransformer(),
|
||||
})
|
||||
public alertId?: ObjectID = undefined;
|
||||
|
||||
@ColumnAccessControl({
|
||||
create: [],
|
||||
read: [
|
||||
Permission.ProjectOwner,
|
||||
Permission.ProjectAdmin,
|
||||
Permission.ProjectMember,
|
||||
Permission.ReadSmsLog,
|
||||
],
|
||||
update: [],
|
||||
})
|
||||
@TableColumn({
|
||||
manyToOneRelationColumn: "scheduledMaintenanceId",
|
||||
type: TableColumnType.Entity,
|
||||
modelType: ScheduledMaintenance,
|
||||
title: "Scheduled Maintenance",
|
||||
description: "Scheduled Maintenance associated with this message (if any)",
|
||||
})
|
||||
@ManyToOne(
|
||||
() => {
|
||||
return ScheduledMaintenance;
|
||||
},
|
||||
{
|
||||
eager: false,
|
||||
nullable: true,
|
||||
onDelete: "CASCADE",
|
||||
orphanedRowAction: "nullify",
|
||||
},
|
||||
)
|
||||
@JoinColumn({ name: "scheduledMaintenanceId" })
|
||||
public scheduledMaintenance?: ScheduledMaintenance = undefined;
|
||||
|
||||
@ColumnAccessControl({
|
||||
create: [],
|
||||
read: [
|
||||
Permission.ProjectOwner,
|
||||
Permission.ProjectAdmin,
|
||||
Permission.ProjectMember,
|
||||
Permission.ReadSmsLog,
|
||||
],
|
||||
update: [],
|
||||
})
|
||||
@Index()
|
||||
@TableColumn({
|
||||
type: TableColumnType.ObjectID,
|
||||
required: false,
|
||||
canReadOnRelationQuery: true,
|
||||
title: "Scheduled Maintenance ID",
|
||||
description:
|
||||
"ID of Scheduled Maintenance associated with this message (if any)",
|
||||
})
|
||||
@Column({
|
||||
type: ColumnType.ObjectID,
|
||||
nullable: true,
|
||||
transformer: ObjectID.getDatabaseTransformer(),
|
||||
})
|
||||
public scheduledMaintenanceId?: ObjectID = undefined;
|
||||
|
||||
@ColumnAccessControl({
|
||||
create: [],
|
||||
read: [
|
||||
Permission.ProjectOwner,
|
||||
Permission.ProjectAdmin,
|
||||
Permission.ProjectMember,
|
||||
Permission.ReadSmsLog,
|
||||
],
|
||||
update: [],
|
||||
})
|
||||
@TableColumn({
|
||||
manyToOneRelationColumn: "statusPageId",
|
||||
type: TableColumnType.Entity,
|
||||
modelType: StatusPage,
|
||||
title: "Status Page",
|
||||
description: "Status Page associated with this message (if any)",
|
||||
})
|
||||
@ManyToOne(
|
||||
() => {
|
||||
return StatusPage;
|
||||
},
|
||||
{
|
||||
eager: false,
|
||||
nullable: true,
|
||||
onDelete: "CASCADE",
|
||||
orphanedRowAction: "nullify",
|
||||
},
|
||||
)
|
||||
@JoinColumn({ name: "statusPageId" })
|
||||
public statusPage?: StatusPage = undefined;
|
||||
|
||||
@ColumnAccessControl({
|
||||
create: [],
|
||||
read: [
|
||||
Permission.ProjectOwner,
|
||||
Permission.ProjectAdmin,
|
||||
Permission.ProjectMember,
|
||||
Permission.ReadSmsLog,
|
||||
],
|
||||
update: [],
|
||||
})
|
||||
@Index()
|
||||
@TableColumn({
|
||||
type: TableColumnType.ObjectID,
|
||||
required: false,
|
||||
canReadOnRelationQuery: true,
|
||||
title: "Status Page ID",
|
||||
description: "ID of Status Page associated with this message (if any)",
|
||||
})
|
||||
@Column({
|
||||
type: ColumnType.ObjectID,
|
||||
nullable: true,
|
||||
transformer: ObjectID.getDatabaseTransformer(),
|
||||
})
|
||||
public statusPageId?: ObjectID = undefined;
|
||||
|
||||
@ColumnAccessControl({
|
||||
create: [],
|
||||
read: [
|
||||
Permission.ProjectOwner,
|
||||
Permission.ProjectAdmin,
|
||||
Permission.ProjectMember,
|
||||
Permission.ReadSmsLog,
|
||||
],
|
||||
update: [],
|
||||
})
|
||||
@TableColumn({
|
||||
manyToOneRelationColumn: "statusPageAnnouncementId",
|
||||
type: TableColumnType.Entity,
|
||||
modelType: StatusPageAnnouncement,
|
||||
title: "Status Page Announcement",
|
||||
description:
|
||||
"Status Page Announcement associated with this message (if any)",
|
||||
})
|
||||
@ManyToOne(
|
||||
() => {
|
||||
return StatusPageAnnouncement;
|
||||
},
|
||||
{
|
||||
eager: false,
|
||||
nullable: true,
|
||||
onDelete: "CASCADE",
|
||||
orphanedRowAction: "nullify",
|
||||
},
|
||||
)
|
||||
@JoinColumn({ name: "statusPageAnnouncementId" })
|
||||
public statusPageAnnouncement?: StatusPageAnnouncement = undefined;
|
||||
|
||||
@ColumnAccessControl({
|
||||
create: [],
|
||||
read: [
|
||||
Permission.ProjectOwner,
|
||||
Permission.ProjectAdmin,
|
||||
Permission.ProjectMember,
|
||||
Permission.ReadSmsLog,
|
||||
],
|
||||
update: [],
|
||||
})
|
||||
@Index()
|
||||
@TableColumn({
|
||||
type: TableColumnType.ObjectID,
|
||||
required: false,
|
||||
canReadOnRelationQuery: true,
|
||||
title: "Status Page Announcement ID",
|
||||
description:
|
||||
"ID of Status Page Announcement associated with this message (if any)",
|
||||
})
|
||||
@Column({
|
||||
type: ColumnType.ObjectID,
|
||||
nullable: true,
|
||||
transformer: ObjectID.getDatabaseTransformer(),
|
||||
})
|
||||
public statusPageAnnouncementId?: ObjectID = undefined;
|
||||
|
||||
@ColumnAccessControl({
|
||||
create: [],
|
||||
read: [
|
||||
Permission.ProjectOwner,
|
||||
Permission.ProjectAdmin,
|
||||
Permission.ProjectMember,
|
||||
Permission.ReadSmsLog,
|
||||
],
|
||||
update: [],
|
||||
})
|
||||
@TableColumn({
|
||||
manyToOneRelationColumn: "teamId",
|
||||
type: TableColumnType.Entity,
|
||||
modelType: Team,
|
||||
title: "Team",
|
||||
description: "Team associated with this message (if any)",
|
||||
})
|
||||
@ManyToOne(
|
||||
() => {
|
||||
return Team;
|
||||
},
|
||||
{
|
||||
eager: false,
|
||||
nullable: true,
|
||||
onDelete: "CASCADE",
|
||||
orphanedRowAction: "nullify",
|
||||
},
|
||||
)
|
||||
@JoinColumn({ name: "teamId" })
|
||||
public team?: Team = undefined;
|
||||
|
||||
@ColumnAccessControl({
|
||||
create: [],
|
||||
read: [
|
||||
Permission.ProjectOwner,
|
||||
Permission.ProjectAdmin,
|
||||
Permission.ProjectMember,
|
||||
Permission.ReadSmsLog,
|
||||
],
|
||||
update: [],
|
||||
})
|
||||
@Index()
|
||||
@TableColumn({
|
||||
type: TableColumnType.ObjectID,
|
||||
required: false,
|
||||
canReadOnRelationQuery: true,
|
||||
title: "Team ID",
|
||||
description: "ID of Team associated with this message (if any)",
|
||||
})
|
||||
@Column({
|
||||
type: ColumnType.ObjectID,
|
||||
nullable: true,
|
||||
transformer: ObjectID.getDatabaseTransformer(),
|
||||
})
|
||||
public teamId?: ObjectID = undefined;
|
||||
|
||||
@ColumnAccessControl({
|
||||
create: [],
|
||||
read: [
|
||||
Permission.ProjectOwner,
|
||||
Permission.ProjectAdmin,
|
||||
Permission.ProjectMember,
|
||||
Permission.ReadSmsLog,
|
||||
],
|
||||
update: [],
|
||||
})
|
||||
@TableColumn({
|
||||
manyToOneRelationColumn: "onCallDutyPolicyId",
|
||||
type: TableColumnType.Entity,
|
||||
modelType: OnCallDutyPolicy,
|
||||
title: "On-Call Duty Policy",
|
||||
description: "On-Call Duty Policy associated with this message (if any)",
|
||||
})
|
||||
@ManyToOne(
|
||||
() => {
|
||||
return OnCallDutyPolicy;
|
||||
},
|
||||
{
|
||||
eager: false,
|
||||
nullable: true,
|
||||
onDelete: "CASCADE",
|
||||
orphanedRowAction: "nullify",
|
||||
},
|
||||
)
|
||||
@JoinColumn({ name: "onCallDutyPolicyId" })
|
||||
public onCallDutyPolicy?: OnCallDutyPolicy = undefined;
|
||||
|
||||
@ColumnAccessControl({
|
||||
create: [],
|
||||
read: [
|
||||
Permission.ProjectOwner,
|
||||
Permission.ProjectAdmin,
|
||||
Permission.ProjectMember,
|
||||
Permission.ReadSmsLog,
|
||||
],
|
||||
update: [],
|
||||
})
|
||||
@Index()
|
||||
@TableColumn({
|
||||
type: TableColumnType.ObjectID,
|
||||
required: false,
|
||||
canReadOnRelationQuery: true,
|
||||
title: "On-Call Duty Policy ID",
|
||||
description:
|
||||
"ID of On-Call Duty Policy associated with this message (if any)",
|
||||
})
|
||||
@Column({
|
||||
type: ColumnType.ObjectID,
|
||||
nullable: true,
|
||||
transformer: ObjectID.getDatabaseTransformer(),
|
||||
})
|
||||
public onCallDutyPolicyId?: ObjectID = undefined;
|
||||
|
||||
@ColumnAccessControl({
|
||||
create: [],
|
||||
read: [
|
||||
Permission.ProjectOwner,
|
||||
Permission.ProjectAdmin,
|
||||
Permission.ProjectMember,
|
||||
Permission.ReadSmsLog,
|
||||
],
|
||||
update: [],
|
||||
})
|
||||
@TableColumn({
|
||||
manyToOneRelationColumn: "onCallDutyPolicyEscalationRuleId",
|
||||
type: TableColumnType.Entity,
|
||||
modelType: OnCallDutyPolicyEscalationRule,
|
||||
title: "On-Call Duty Policy Escalation Rule",
|
||||
description:
|
||||
"On-Call Duty Policy Escalation Rule associated with this message (if any)",
|
||||
})
|
||||
@ManyToOne(
|
||||
() => {
|
||||
return OnCallDutyPolicyEscalationRule;
|
||||
},
|
||||
{
|
||||
eager: false,
|
||||
nullable: true,
|
||||
onDelete: "CASCADE",
|
||||
orphanedRowAction: "nullify",
|
||||
},
|
||||
)
|
||||
@JoinColumn({ name: "onCallDutyPolicyEscalationRuleId" })
|
||||
public onCallDutyPolicyEscalationRule?: OnCallDutyPolicyEscalationRule =
|
||||
undefined;
|
||||
|
||||
@ColumnAccessControl({
|
||||
create: [],
|
||||
read: [
|
||||
Permission.ProjectOwner,
|
||||
Permission.ProjectAdmin,
|
||||
Permission.ProjectMember,
|
||||
Permission.ReadSmsLog,
|
||||
],
|
||||
update: [],
|
||||
})
|
||||
@Index()
|
||||
@TableColumn({
|
||||
type: TableColumnType.ObjectID,
|
||||
required: false,
|
||||
canReadOnRelationQuery: true,
|
||||
title: "On-Call Duty Policy Escalation Rule ID",
|
||||
description:
|
||||
"ID of On-Call Duty Policy Escalation Rule associated with this message (if any)",
|
||||
})
|
||||
@Column({
|
||||
type: ColumnType.ObjectID,
|
||||
nullable: true,
|
||||
transformer: ObjectID.getDatabaseTransformer(),
|
||||
})
|
||||
public onCallDutyPolicyEscalationRuleId?: ObjectID = undefined;
|
||||
|
||||
@ColumnAccessControl({
|
||||
create: [],
|
||||
read: [
|
||||
Permission.ProjectOwner,
|
||||
Permission.ProjectAdmin,
|
||||
Permission.ProjectMember,
|
||||
Permission.ReadSmsLog,
|
||||
],
|
||||
update: [],
|
||||
})
|
||||
@TableColumn({
|
||||
manyToOneRelationColumn: "onCallDutyPolicyScheduleId",
|
||||
type: TableColumnType.Entity,
|
||||
modelType: OnCallDutyPolicySchedule,
|
||||
title: "On-Call Duty Policy Schedule",
|
||||
description:
|
||||
"On-Call Duty Policy Schedule associated with this message (if any)",
|
||||
})
|
||||
@ManyToOne(
|
||||
() => {
|
||||
return OnCallDutyPolicySchedule;
|
||||
},
|
||||
{
|
||||
eager: false,
|
||||
nullable: true,
|
||||
onDelete: "CASCADE",
|
||||
orphanedRowAction: "nullify",
|
||||
},
|
||||
)
|
||||
@JoinColumn({ name: "onCallDutyPolicyScheduleId" })
|
||||
public onCallDutyPolicySchedule?: OnCallDutyPolicySchedule = undefined;
|
||||
|
||||
@ColumnAccessControl({
|
||||
create: [],
|
||||
read: [
|
||||
Permission.ProjectOwner,
|
||||
Permission.ProjectAdmin,
|
||||
Permission.ProjectMember,
|
||||
Permission.ReadSmsLog,
|
||||
],
|
||||
update: [],
|
||||
})
|
||||
@Index()
|
||||
@TableColumn({
|
||||
type: TableColumnType.ObjectID,
|
||||
required: false,
|
||||
canReadOnRelationQuery: true,
|
||||
title: "On-Call Duty Policy Schedule ID",
|
||||
description:
|
||||
"ID of On-Call Duty Policy Schedule associated with this message (if any)",
|
||||
})
|
||||
@Column({
|
||||
type: ColumnType.ObjectID,
|
||||
nullable: true,
|
||||
transformer: ObjectID.getDatabaseTransformer(),
|
||||
})
|
||||
public onCallDutyPolicyScheduleId?: ObjectID = undefined;
|
||||
|
||||
@ColumnAccessControl({
|
||||
create: [],
|
||||
read: [],
|
||||
update: [],
|
||||
})
|
||||
@TableColumn({
|
||||
manyToOneRelationColumn: "deletedByUserId",
|
||||
type: TableColumnType.Entity,
|
||||
title: "Deleted by User",
|
||||
modelType: User,
|
||||
description:
|
||||
"Relation to User who deleted this object (if this object was deleted by a User)",
|
||||
})
|
||||
@ManyToOne(
|
||||
() => {
|
||||
return User;
|
||||
},
|
||||
{
|
||||
cascade: false,
|
||||
eager: false,
|
||||
nullable: true,
|
||||
onDelete: "SET NULL",
|
||||
orphanedRowAction: "nullify",
|
||||
},
|
||||
)
|
||||
@JoinColumn({ name: "deletedByUserId" })
|
||||
public deletedByUser?: User = undefined;
|
||||
|
||||
@ColumnAccessControl({
|
||||
create: [],
|
||||
read: [],
|
||||
update: [],
|
||||
})
|
||||
@TableColumn({
|
||||
type: TableColumnType.ObjectID,
|
||||
title: "Deleted by User ID",
|
||||
description:
|
||||
"User ID who deleted this object (if this object was deleted by a User)",
|
||||
})
|
||||
@Column({
|
||||
type: ColumnType.ObjectID,
|
||||
nullable: true,
|
||||
transformer: ObjectID.getDatabaseTransformer(),
|
||||
})
|
||||
public deletedByUserId?: ObjectID = undefined;
|
||||
}
|
||||
@@ -21,6 +21,11 @@ export interface MiscData {
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
export interface MicrosoftTeamsTeam {
|
||||
id: string;
|
||||
name: string;
|
||||
}
|
||||
|
||||
export interface SlackMiscData extends MiscData {
|
||||
teamId: string;
|
||||
teamName: string;
|
||||
@@ -34,6 +39,22 @@ export interface SlackMiscData extends MiscData {
|
||||
};
|
||||
}
|
||||
|
||||
export interface MicrosoftTeamsMiscData extends MiscData {
|
||||
tenantId: string;
|
||||
teamId: string;
|
||||
teamName: string;
|
||||
botId: string;
|
||||
appAccessToken?: string;
|
||||
adminConsentGranted?: boolean;
|
||||
lastAppTokenIssuedAt?: string;
|
||||
adminConsentGrantedAt?: string;
|
||||
adminConsentGrantedBy?: string;
|
||||
availableTeams?: Record<string, MicrosoftTeamsTeam>;
|
||||
appAccessTokenExpiresAt?: string;
|
||||
}
|
||||
|
||||
export type WorkspaceMiscData = SlackMiscData | MicrosoftTeamsMiscData;
|
||||
|
||||
@TenantColumn("projectId")
|
||||
@TableAccessControl({
|
||||
create: [
|
||||
|
||||
@@ -235,8 +235,10 @@ export default class BaseAPI<
|
||||
): Promise<void> {
|
||||
await this.onBeforeList(req, res);
|
||||
|
||||
// Extract pagination parameters from query or body (for POST requests)
|
||||
// Support both 'skip' and 'offset' parameters (offset is alias for skip)
|
||||
/*
|
||||
* Extract pagination parameters from query or body (for POST requests)
|
||||
* Support both 'skip' and 'offset' parameters (offset is alias for skip)
|
||||
*/
|
||||
let skipValue: number = 0;
|
||||
let limitValue: number = DEFAULT_LIMIT;
|
||||
|
||||
|
||||
123
Common/Server/API/BillingAPI.ts
Normal file
123
Common/Server/API/BillingAPI.ts
Normal file
@@ -0,0 +1,123 @@
|
||||
import { IsBillingEnabled } from "../EnvironmentConfig";
|
||||
import UserMiddleware from "../Middleware/UserAuthorization";
|
||||
import BillingService from "../Services/BillingService";
|
||||
import ProjectService from "../Services/ProjectService";
|
||||
import Express, {
|
||||
ExpressRequest,
|
||||
ExpressResponse,
|
||||
ExpressRouter,
|
||||
NextFunction,
|
||||
OneUptimeRequest,
|
||||
} from "../Utils/Express";
|
||||
import Response from "../Utils/Response";
|
||||
import BadDataException from "../../Types/Exception/BadDataException";
|
||||
import Permission, { UserPermission } from "../../Types/Permission";
|
||||
import Project from "../../Models/DatabaseModels/Project";
|
||||
import CommonAPI from "./CommonAPI";
|
||||
import ObjectID from "../../Types/ObjectID";
|
||||
import DatabaseCommonInteractionProps from "../../Types/BaseDatabase/DatabaseCommonInteractionProps";
|
||||
|
||||
export default class BillingAPI {
|
||||
public router: ExpressRouter;
|
||||
|
||||
public constructor() {
|
||||
this.router = Express.getRouter();
|
||||
|
||||
this.router.get(
|
||||
`/billing/customer-balance`,
|
||||
UserMiddleware.getUserMiddleware,
|
||||
async (req: ExpressRequest, res: ExpressResponse, next: NextFunction) => {
|
||||
try {
|
||||
if (!IsBillingEnabled) {
|
||||
throw new BadDataException(
|
||||
"Billing is not enabled for this server",
|
||||
);
|
||||
}
|
||||
|
||||
const userPermissions: Array<UserPermission> = (
|
||||
await this.getPermissionsForTenant(req)
|
||||
).filter((permission: UserPermission) => {
|
||||
return (
|
||||
permission.permission.toString() ===
|
||||
Permission.ProjectOwner.toString() ||
|
||||
permission.permission.toString() ===
|
||||
Permission.ManageProjectBilling.toString()
|
||||
);
|
||||
});
|
||||
|
||||
if (
|
||||
userPermissions.length === 0 &&
|
||||
!(req as OneUptimeRequest).userAuthorization?.isMasterAdmin
|
||||
) {
|
||||
throw new BadDataException(
|
||||
`You need ${Permission.ProjectOwner} or ${Permission.ManageProjectBilling} permission to view billing balance.`,
|
||||
);
|
||||
}
|
||||
|
||||
const project: Project | null = await ProjectService.findOneById({
|
||||
id: this.getTenantId(req)!,
|
||||
props: {
|
||||
isRoot: true,
|
||||
},
|
||||
select: {
|
||||
_id: true,
|
||||
paymentProviderCustomerId: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!project) {
|
||||
throw new BadDataException("Project not found");
|
||||
}
|
||||
|
||||
if (!project.paymentProviderCustomerId) {
|
||||
throw new BadDataException("Payment Provider customer not found");
|
||||
}
|
||||
|
||||
const balance: number = await BillingService.getCustomerBalance(
|
||||
project.paymentProviderCustomerId,
|
||||
);
|
||||
|
||||
return Response.sendJsonObjectResponse(req, res, {
|
||||
balance: balance,
|
||||
});
|
||||
} catch (err) {
|
||||
next(err);
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
public async getPermissionsForTenant(
|
||||
req: ExpressRequest,
|
||||
): Promise<Array<UserPermission>> {
|
||||
const permissions: Array<UserPermission> = [];
|
||||
|
||||
const props: DatabaseCommonInteractionProps =
|
||||
await CommonAPI.getDatabaseCommonInteractionProps(req);
|
||||
|
||||
if (
|
||||
props &&
|
||||
props.userTenantAccessPermission &&
|
||||
props.userTenantAccessPermission[props.tenantId?.toString() || ""]
|
||||
) {
|
||||
return (
|
||||
props.userTenantAccessPermission[props.tenantId?.toString() || ""]
|
||||
?.permissions || []
|
||||
);
|
||||
}
|
||||
|
||||
return permissions;
|
||||
}
|
||||
|
||||
public getTenantId(req: ExpressRequest): ObjectID | null {
|
||||
if ((req as OneUptimeRequest).tenantId) {
|
||||
return (req as OneUptimeRequest).tenantId as ObjectID;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public getRouter(): ExpressRouter {
|
||||
return this.router;
|
||||
}
|
||||
}
|
||||
@@ -21,20 +21,24 @@ export default class GlobalConfigAPI extends BaseAPI<
|
||||
`${new this.entityType().getCrudApiPath()?.toString()}/vars`,
|
||||
async (req: ExpressRequest, res: ExpressResponse, next: NextFunction) => {
|
||||
try {
|
||||
// const globalConfig: GlobalConfig | null =
|
||||
// await GlobalConfigService.findOneById({
|
||||
// id: ObjectID.getZeroObjectID(),
|
||||
// select: {
|
||||
// useHttps: true,
|
||||
// },
|
||||
// props: {
|
||||
// isRoot: true,
|
||||
// },
|
||||
// });
|
||||
/*
|
||||
* const globalConfig: GlobalConfig | null =
|
||||
* await GlobalConfigService.findOneById({
|
||||
* id: ObjectID.getZeroObjectID(),
|
||||
* select: {
|
||||
* useHttps: true,
|
||||
* },
|
||||
* props: {
|
||||
* isRoot: true,
|
||||
* },
|
||||
* });
|
||||
*/
|
||||
|
||||
return Response.sendJsonObjectResponse(req, res, {
|
||||
// USE_HTTPS:
|
||||
// globalConfig?.useHttps?.toString() || 'false',
|
||||
/*
|
||||
* USE_HTTPS:
|
||||
* globalConfig?.useHttps?.toString() || 'false',
|
||||
*/
|
||||
});
|
||||
} catch (err) {
|
||||
next(err);
|
||||
|
||||
1248
Common/Server/API/MicrosoftTeamsAPI.ts
Normal file
1248
Common/Server/API/MicrosoftTeamsAPI.ts
Normal file
File diff suppressed because it is too large
Load Diff
@@ -4,11 +4,11 @@ import Express, {
|
||||
ExpressRequest,
|
||||
ExpressResponse,
|
||||
ExpressRouter,
|
||||
NextFunction,
|
||||
OneUptimeRequest,
|
||||
} from "../Utils/Express";
|
||||
import Response from "../Utils/Response";
|
||||
import BadDataException from "../../Types/Exception/BadDataException";
|
||||
import Exception from "../../Types/Exception/Exception";
|
||||
import JSONFunctions from "../../Types/JSONFunctions";
|
||||
import ObjectID from "../../Types/ObjectID";
|
||||
import Permission, { UserPermission } from "../../Types/Permission";
|
||||
@@ -19,7 +19,7 @@ const router: ExpressRouter = Express.getRouter();
|
||||
router.post(
|
||||
"/notification/recharge",
|
||||
UserMiddleware.getUserMiddleware,
|
||||
async (req: ExpressRequest, res: ExpressResponse) => {
|
||||
async (req: ExpressRequest, res: ExpressResponse, next: NextFunction) => {
|
||||
try {
|
||||
let amount: number | PositiveNumber = JSONFunctions.deserializeValue(
|
||||
req.body.amount,
|
||||
@@ -106,8 +106,8 @@ router.post(
|
||||
),
|
||||
);
|
||||
}
|
||||
} catch (err: any) {
|
||||
return Response.sendErrorResponse(req, res, err as Exception);
|
||||
} catch (err) {
|
||||
return next(err);
|
||||
}
|
||||
|
||||
return Response.sendEmptySuccessResponse(req, res);
|
||||
|
||||
@@ -25,8 +25,10 @@ export default class ProjectAPI extends BaseAPI<Project, ProjectServiceType> {
|
||||
public constructor() {
|
||||
super(Project, ProjectService);
|
||||
|
||||
/// This API lists all the projects where user is its team member.
|
||||
/// This API is usually used to show project selector dropdown in the UI
|
||||
/*
|
||||
* This API lists all the projects where user is its team member.
|
||||
* This API is usually used to show project selector dropdown in the UI
|
||||
*/
|
||||
this.router.post(
|
||||
`${new this.entityType()
|
||||
.getCrudApiPath()
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
import ProjectSsoService, {
|
||||
Service as ProjectSsoServiceType,
|
||||
} from "../Services/ProjectSsoService";
|
||||
import { ExpressRequest, ExpressResponse } from "../Utils/Express";
|
||||
import {
|
||||
ExpressRequest,
|
||||
ExpressResponse,
|
||||
NextFunction,
|
||||
} from "../Utils/Express";
|
||||
import Response from "../Utils/Response";
|
||||
import BaseAPI from "./BaseAPI";
|
||||
import { LIMIT_PER_PROJECT } from "../../Types/Database/LimitMax";
|
||||
@@ -22,43 +26,47 @@ export default class ProjectSsoAPI extends BaseAPI<
|
||||
`${new this.entityType()
|
||||
.getCrudApiPath()
|
||||
?.toString()}/:projectId/sso-list`,
|
||||
async (req: ExpressRequest, res: ExpressResponse) => {
|
||||
const projectId: ObjectID = new ObjectID(
|
||||
req.params["projectId"] as string,
|
||||
);
|
||||
async (req: ExpressRequest, res: ExpressResponse, next: NextFunction) => {
|
||||
try {
|
||||
const projectId: ObjectID = new ObjectID(
|
||||
req.params["projectId"] as string,
|
||||
);
|
||||
|
||||
if (!projectId) {
|
||||
return Response.sendErrorResponse(
|
||||
if (!projectId) {
|
||||
return Response.sendErrorResponse(
|
||||
req,
|
||||
res,
|
||||
new BadDataException("Invalid project id."),
|
||||
);
|
||||
}
|
||||
|
||||
const sso: Array<ProjectSSO> = await this.service.findBy({
|
||||
query: {
|
||||
projectId: projectId,
|
||||
isEnabled: true,
|
||||
},
|
||||
limit: LIMIT_PER_PROJECT,
|
||||
skip: 0,
|
||||
select: {
|
||||
name: true,
|
||||
description: true,
|
||||
_id: true,
|
||||
},
|
||||
props: {
|
||||
isRoot: true,
|
||||
},
|
||||
});
|
||||
|
||||
return Response.sendEntityArrayResponse(
|
||||
req,
|
||||
res,
|
||||
new BadDataException("Invalid project id."),
|
||||
sso,
|
||||
new PositiveNumber(sso.length),
|
||||
ProjectSSO,
|
||||
);
|
||||
} catch (err) {
|
||||
return next(err);
|
||||
}
|
||||
|
||||
const sso: Array<ProjectSSO> = await this.service.findBy({
|
||||
query: {
|
||||
projectId: projectId,
|
||||
isEnabled: true,
|
||||
},
|
||||
limit: LIMIT_PER_PROJECT,
|
||||
skip: 0,
|
||||
select: {
|
||||
name: true,
|
||||
description: true,
|
||||
_id: true,
|
||||
},
|
||||
props: {
|
||||
isRoot: true,
|
||||
},
|
||||
});
|
||||
|
||||
return Response.sendEntityArrayResponse(
|
||||
req,
|
||||
res,
|
||||
sso,
|
||||
new PositiveNumber(sso.length),
|
||||
ProjectSSO,
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@@ -30,8 +30,10 @@ export default class ResellerPlanAPI extends BaseAPI<
|
||||
public constructor() {
|
||||
super(ResellerPlan, ResellerPlanService);
|
||||
|
||||
// Reseller Plan Action API
|
||||
// TODO: Refactor this API and make it partner specific.
|
||||
/*
|
||||
* Reseller Plan Action API
|
||||
* TODO: Refactor this API and make it partner specific.
|
||||
*/
|
||||
this.router.post(
|
||||
`${new this.entityType()
|
||||
.getCrudApiPath()
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
import ShortLinkService, {
|
||||
Service as ShortLinkServiceType,
|
||||
} from "../Services/ShortLinkService";
|
||||
import { ExpressRequest, ExpressResponse } from "../Utils/Express";
|
||||
import {
|
||||
ExpressRequest,
|
||||
ExpressResponse,
|
||||
NextFunction,
|
||||
} from "../Utils/Express";
|
||||
import Response from "../Utils/Response";
|
||||
import BaseAPI from "./BaseAPI";
|
||||
import BadDataException from "../../Types/Exception/BadDataException";
|
||||
@@ -18,34 +22,38 @@ export default class ShortLinkAPI extends BaseAPI<
|
||||
`${new this.entityType()
|
||||
.getCrudApiPath()
|
||||
?.toString()}/redirect-to-shortlink/:id`,
|
||||
async (req: ExpressRequest, res: ExpressResponse) => {
|
||||
if (!req.params["id"]) {
|
||||
return Response.sendErrorResponse(
|
||||
req,
|
||||
res,
|
||||
new BadDataException("id is required"),
|
||||
async (req: ExpressRequest, res: ExpressResponse, next: NextFunction) => {
|
||||
try {
|
||||
if (!req.params["id"]) {
|
||||
return Response.sendErrorResponse(
|
||||
req,
|
||||
res,
|
||||
new BadDataException("id is required"),
|
||||
);
|
||||
}
|
||||
|
||||
if (req.params["id"] === "status") {
|
||||
return Response.sendJsonObjectResponse(req, res, {
|
||||
status: "ok",
|
||||
});
|
||||
}
|
||||
|
||||
const link: ShortLink | null = await ShortLinkService.getShortLinkFor(
|
||||
req.params["id"],
|
||||
);
|
||||
|
||||
if (!link || !link.link) {
|
||||
return Response.sendErrorResponse(
|
||||
req,
|
||||
res,
|
||||
new BadDataException("This URL is invalid or expired"),
|
||||
);
|
||||
}
|
||||
|
||||
return Response.redirect(req, res, link.link);
|
||||
} catch (err) {
|
||||
return next(err);
|
||||
}
|
||||
|
||||
if (req.params["id"] === "status") {
|
||||
return Response.sendJsonObjectResponse(req, res, {
|
||||
status: "ok",
|
||||
});
|
||||
}
|
||||
|
||||
const link: ShortLink | null = await ShortLinkService.getShortLinkFor(
|
||||
req.params["id"],
|
||||
);
|
||||
|
||||
if (!link || !link.link) {
|
||||
return Response.sendErrorResponse(
|
||||
req,
|
||||
res,
|
||||
new BadDataException("This URL is invalid or expired"),
|
||||
);
|
||||
}
|
||||
|
||||
return Response.redirect(req, res, link.link);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@@ -38,6 +38,10 @@ import SlackOnCallDutyActions from "../Utils/Workspace/Slack/Actions/OnCallDutyP
|
||||
import WorkspaceProjectAuthToken, {
|
||||
SlackMiscData,
|
||||
} from "../../Models/DatabaseModels/WorkspaceProjectAuthToken";
|
||||
import UserMiddleware from "../Middleware/UserAuthorization";
|
||||
import CommonAPI from "./CommonAPI";
|
||||
import SlackUtil from "../Utils/Workspace/Slack/Slack";
|
||||
import DatabaseCommonInteractionProps from "../../Types/BaseDatabase/DatabaseCommonInteractionProps";
|
||||
|
||||
export default class SlackAPI {
|
||||
public getRouter(): ExpressRouter {
|
||||
@@ -153,13 +157,13 @@ export default class SlackAPI {
|
||||
// send the request to slack api to get the access token https://slack.com/api/oauth.v2.access
|
||||
|
||||
const response: HTTPErrorResponse | HTTPResponse<JSONObject> =
|
||||
await API.post(
|
||||
URL.fromString("https://slack.com/api/oauth.v2.access"),
|
||||
requestBody,
|
||||
{
|
||||
await API.post({
|
||||
url: URL.fromString("https://slack.com/api/oauth.v2.access"),
|
||||
data: requestBody,
|
||||
headers: {
|
||||
"Content-Type": "application/x-www-form-urlencoded",
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
if (response instanceof HTTPErrorResponse) {
|
||||
throw response;
|
||||
@@ -177,29 +181,31 @@ export default class SlackAPI {
|
||||
let botUserId: string | undefined = undefined;
|
||||
let slackUserAccessToken: string | undefined = undefined;
|
||||
|
||||
// ReponseBody is in this format.
|
||||
// {
|
||||
// "ok": true,
|
||||
// "access_token": "sample-token",
|
||||
// "token_type": "bot",
|
||||
// "scope": "commands,incoming-webhook",
|
||||
// "bot_user_id": "U0KRQLJ9H",
|
||||
// "app_id": "A0KRD7HC3",
|
||||
// "team": {
|
||||
// "name": "Slack Pickleball Team",
|
||||
// "id": "T9TK3CUKW"
|
||||
// },
|
||||
// "enterprise": {
|
||||
// "name": "slack-pickleball",
|
||||
// "id": "E12345678"
|
||||
// },
|
||||
// "authed_user": {
|
||||
// "id": "U1234",
|
||||
// "scope": "chat:write",
|
||||
// "access_token": "sample-token",
|
||||
// "token_type": "user"
|
||||
// }
|
||||
// }
|
||||
/*
|
||||
* ReponseBody is in this format.
|
||||
* {
|
||||
* "ok": true,
|
||||
* "access_token": "sample-token",
|
||||
* "token_type": "bot",
|
||||
* "scope": "commands,incoming-webhook",
|
||||
* "bot_user_id": "U0KRQLJ9H",
|
||||
* "app_id": "A0KRD7HC3",
|
||||
* "team": {
|
||||
* "name": "Slack Pickleball Team",
|
||||
* "id": "T9TK3CUKW"
|
||||
* },
|
||||
* "enterprise": {
|
||||
* "name": "slack-pickleball",
|
||||
* "id": "E12345678"
|
||||
* },
|
||||
* "authed_user": {
|
||||
* "id": "U1234",
|
||||
* "scope": "chat:write",
|
||||
* "access_token": "sample-token",
|
||||
* "token_type": "user"
|
||||
* }
|
||||
* }
|
||||
*/
|
||||
|
||||
if (responseBody["ok"] !== true) {
|
||||
return Response.sendErrorResponse(
|
||||
@@ -365,13 +371,13 @@ export default class SlackAPI {
|
||||
// send the request to slack api to get the access token https://slack.com/api/oauth.v2.access
|
||||
|
||||
const response: HTTPErrorResponse | HTTPResponse<JSONObject> =
|
||||
await API.post(
|
||||
URL.fromString("https://slack.com/api/openid.connect.token"),
|
||||
requestBody,
|
||||
{
|
||||
await API.post({
|
||||
url: URL.fromString("https://slack.com/api/openid.connect.token"),
|
||||
data: requestBody,
|
||||
headers: {
|
||||
"Content-Type": "application/x-www-form-urlencoded",
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
if (response instanceof HTTPErrorResponse) {
|
||||
throw response;
|
||||
@@ -403,31 +409,35 @@ export default class SlackAPI {
|
||||
"id_token"
|
||||
] as JSONObject;
|
||||
|
||||
// Example of Response Body
|
||||
// {
|
||||
// "iss": "https://slack.com",
|
||||
// "sub": "U123ABC456",
|
||||
// "aud": "25259531569.1115258246291",
|
||||
// "exp": 1626874955,
|
||||
// "iat": 1626874655,
|
||||
// "auth_time": 1626874655,
|
||||
// "nonce": "abcd",
|
||||
// "at_hash": "abc...123",
|
||||
// "https://slack.com/team_id": "T0123ABC456",
|
||||
// "https://slack.com/user_id": "U123ABC456",
|
||||
// "email": "alice@example.com",
|
||||
// "email_verified": true,
|
||||
// "date_email_verified": 1622128723,
|
||||
// "locale": "en-US",
|
||||
// "name": "Alice",
|
||||
// "given_name": "",
|
||||
// "family_name": "",
|
||||
// "https://slack.com/team_image_230": "https://secure.gravatar.com/avatar/bc.png",
|
||||
// "https://slack.com/team_image_default": true
|
||||
// }
|
||||
/*
|
||||
* Example of Response Body
|
||||
* {
|
||||
* "iss": "https://slack.com",
|
||||
* "sub": "U123ABC456",
|
||||
* "aud": "25259531569.1115258246291",
|
||||
* "exp": 1626874955,
|
||||
* "iat": 1626874655,
|
||||
* "auth_time": 1626874655,
|
||||
* "nonce": "abcd",
|
||||
* "at_hash": "abc...123",
|
||||
* "https://slack.com/team_id": "T0123ABC456",
|
||||
* "https://slack.com/user_id": "U123ABC456",
|
||||
* "email": "alice@example.com",
|
||||
* "email_verified": true,
|
||||
* "date_email_verified": 1622128723,
|
||||
* "locale": "en-US",
|
||||
* "name": "Alice",
|
||||
* "given_name": "",
|
||||
* "family_name": "",
|
||||
* "https://slack.com/team_image_230": "https://secure.gravatar.com/avatar/bc.png",
|
||||
* "https://slack.com/team_image_default": true
|
||||
* }
|
||||
*/
|
||||
|
||||
// check if the team id matches the project id.
|
||||
// get project auth.
|
||||
/*
|
||||
* check if the team id matches the project id.
|
||||
* get project auth.
|
||||
*/
|
||||
|
||||
const projectAuth: WorkspaceProjectAuthToken | null =
|
||||
await WorkspaceProjectAuthTokenService.findOneBy({
|
||||
@@ -656,6 +666,71 @@ export default class SlackAPI {
|
||||
},
|
||||
);
|
||||
|
||||
// Fetch and cache all Slack channels for current tenant's project
|
||||
router.get(
|
||||
"/slack/get-all-channels",
|
||||
UserMiddleware.getUserMiddleware,
|
||||
async (req: ExpressRequest, res: ExpressResponse) => {
|
||||
const props: DatabaseCommonInteractionProps =
|
||||
await CommonAPI.getDatabaseCommonInteractionProps(req);
|
||||
|
||||
if (!props.tenantId) {
|
||||
return Response.sendErrorResponse(
|
||||
req,
|
||||
res,
|
||||
new BadRequestException("ProjectId (tenant) not found in request"),
|
||||
);
|
||||
}
|
||||
|
||||
// Get Slack project auth
|
||||
const projectAuth: WorkspaceProjectAuthToken | null =
|
||||
await WorkspaceProjectAuthTokenService.getProjectAuth({
|
||||
projectId: props.tenantId,
|
||||
workspaceType: WorkspaceType.Slack,
|
||||
});
|
||||
|
||||
if (!projectAuth || !projectAuth.authToken) {
|
||||
return Response.sendErrorResponse(
|
||||
req,
|
||||
res,
|
||||
new BadRequestException(
|
||||
"Slack is not connected for this project. Please connect Slack first.",
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// Fetch all channels (also updates cache under miscData.channelCache)
|
||||
|
||||
let updatedProjectAuth: WorkspaceProjectAuthToken | null = projectAuth;
|
||||
|
||||
if (!(projectAuth.miscData as SlackMiscData)?.channelCache) {
|
||||
await SlackUtil.getAllWorkspaceChannels({
|
||||
authToken: projectAuth.authToken,
|
||||
projectId: props.tenantId,
|
||||
});
|
||||
|
||||
// Re-fetch to return the latest cached object
|
||||
updatedProjectAuth =
|
||||
await WorkspaceProjectAuthTokenService.getProjectAuth({
|
||||
projectId: props.tenantId,
|
||||
workspaceType: WorkspaceType.Slack,
|
||||
});
|
||||
}
|
||||
|
||||
const channelCache: {
|
||||
[channelName: string]: {
|
||||
id: string;
|
||||
name: string;
|
||||
lastUpdated: string;
|
||||
};
|
||||
} =
|
||||
((updatedProjectAuth?.miscData as SlackMiscData | undefined) || {})
|
||||
?.channelCache || {};
|
||||
|
||||
return Response.sendJsonObjectResponse(req, res, channelCache as any);
|
||||
},
|
||||
);
|
||||
|
||||
// options load endpoint.
|
||||
|
||||
router.post(
|
||||
|
||||
@@ -181,6 +181,7 @@ export default class StatusPageAPI extends BaseAPI<
|
||||
return Response.sendJsonObjectResponse(req, res, {
|
||||
title: statusPage.pageTitle || statusPage.name,
|
||||
description: statusPage.pageDescription,
|
||||
_id: statusPage._id?.toString(),
|
||||
});
|
||||
},
|
||||
);
|
||||
@@ -734,9 +735,11 @@ export default class StatusPageAPI extends BaseAPI<
|
||||
req: req,
|
||||
});
|
||||
|
||||
// get start and end date from request body.
|
||||
// if no end date is provided then it will be current date.
|
||||
// if no start date is provided then it will be 14 days ago from end date.
|
||||
/*
|
||||
* get start and end date from request body.
|
||||
* if no end date is provided then it will be current date.
|
||||
* if no start date is provided then it will be 14 days ago from end date.
|
||||
*/
|
||||
|
||||
let startDate: Date = OneUptimeDate.getSomeDaysAgo(14);
|
||||
let endDate: Date = OneUptimeDate.getCurrentDate();
|
||||
@@ -1100,7 +1103,7 @@ export default class StatusPageAPI extends BaseAPI<
|
||||
},
|
||||
select: select,
|
||||
sort: {
|
||||
createdAt: SortOrder.Ascending,
|
||||
createdAt: SortOrder.Descending,
|
||||
},
|
||||
|
||||
skip: 0,
|
||||
|
||||
106
Common/Server/API/TeamComplianceAPI.ts
Normal file
106
Common/Server/API/TeamComplianceAPI.ts
Normal file
@@ -0,0 +1,106 @@
|
||||
import UserMiddleware from "../Middleware/UserAuthorization";
|
||||
import TeamComplianceService, {
|
||||
TeamComplianceStatus,
|
||||
UserComplianceStatus,
|
||||
} from "../Services/TeamComplianceService";
|
||||
import {
|
||||
ExpressRequest,
|
||||
ExpressResponse,
|
||||
NextFunction,
|
||||
} from "../Utils/Express";
|
||||
import Response from "../Utils/Response";
|
||||
import BaseAPI from "./BaseAPI";
|
||||
import CommonAPI from "./CommonAPI";
|
||||
import DatabaseCommonInteractionProps from "../../Types/BaseDatabase/DatabaseCommonInteractionProps";
|
||||
import BadDataException from "../../Types/Exception/BadDataException";
|
||||
import ObjectID from "../../Types/ObjectID";
|
||||
import Team from "../../Models/DatabaseModels/Team";
|
||||
import TeamService, {
|
||||
Service as TeamServiceType,
|
||||
} from "../Services/TeamService";
|
||||
import ComplianceRuleType from "../../Types/Team/ComplianceRuleType";
|
||||
|
||||
export default class TeamComplianceAPI extends BaseAPI<Team, TeamServiceType> {
|
||||
public constructor() {
|
||||
super(Team, TeamService);
|
||||
|
||||
// Get team compliance status
|
||||
this.router.get(
|
||||
`${new this.entityType().getCrudApiPath()?.toString()}/compliance-status/:teamId`,
|
||||
UserMiddleware.getUserMiddleware,
|
||||
async (req: ExpressRequest, res: ExpressResponse, next: NextFunction) => {
|
||||
try {
|
||||
const databaseProps: DatabaseCommonInteractionProps =
|
||||
await CommonAPI.getDatabaseCommonInteractionProps(req);
|
||||
|
||||
const projectId: ObjectID = databaseProps.tenantId as ObjectID;
|
||||
|
||||
const teamId: ObjectID = new ObjectID(req.params["teamId"] as string);
|
||||
|
||||
if (!projectId) {
|
||||
return Response.sendErrorResponse(
|
||||
req,
|
||||
res,
|
||||
new BadDataException("Invalid projectId."),
|
||||
);
|
||||
}
|
||||
|
||||
if (!teamId) {
|
||||
return Response.sendErrorResponse(
|
||||
req,
|
||||
res,
|
||||
new BadDataException("Invalid teamId."),
|
||||
);
|
||||
}
|
||||
|
||||
const complianceStatus: TeamComplianceStatus =
|
||||
await TeamComplianceService.getTeamComplianceStatus(
|
||||
teamId,
|
||||
projectId,
|
||||
);
|
||||
|
||||
// Convert ObjectIDs to strings for JSON response
|
||||
const responseData: {
|
||||
teamId: string;
|
||||
teamName: string;
|
||||
complianceSettings: Array<{
|
||||
ruleType: ComplianceRuleType;
|
||||
enabled: boolean;
|
||||
}>;
|
||||
userComplianceStatuses: Array<{
|
||||
userId: string;
|
||||
userName: string;
|
||||
userEmail: string;
|
||||
userProfilePictureId: string | undefined;
|
||||
isCompliant: boolean;
|
||||
nonCompliantRules: Array<{
|
||||
ruleType: ComplianceRuleType;
|
||||
reason: string;
|
||||
}>;
|
||||
}>;
|
||||
} = {
|
||||
teamId: complianceStatus.teamId.toString(),
|
||||
teamName: complianceStatus.teamName,
|
||||
complianceSettings: complianceStatus.complianceSettings,
|
||||
userComplianceStatuses: complianceStatus.userComplianceStatuses.map(
|
||||
(user: UserComplianceStatus) => {
|
||||
return {
|
||||
userId: user.userId.toString(),
|
||||
userName: user.userName,
|
||||
userEmail: user.userEmail,
|
||||
userProfilePictureId: user.userProfilePictureId?.toString(),
|
||||
isCompliant: user.isCompliant,
|
||||
nonCompliantRules: user.nonCompliantRules,
|
||||
};
|
||||
},
|
||||
),
|
||||
};
|
||||
|
||||
return Response.sendJsonObjectResponse(req, res, responseData);
|
||||
} catch (e) {
|
||||
next(e);
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,7 @@ import UserCallService, {
|
||||
import {
|
||||
ExpressRequest,
|
||||
ExpressResponse,
|
||||
NextFunction,
|
||||
OneUptimeRequest,
|
||||
} from "../Utils/Express";
|
||||
import Response from "../Utils/Response";
|
||||
@@ -23,97 +24,105 @@ export default class UserCallAPI extends BaseAPI<
|
||||
this.router.post(
|
||||
`/user-call/verify`,
|
||||
UserMiddleware.getUserMiddleware,
|
||||
async (req: ExpressRequest, res: ExpressResponse) => {
|
||||
req = req as OneUptimeRequest;
|
||||
async (req: ExpressRequest, res: ExpressResponse, next: NextFunction) => {
|
||||
try {
|
||||
req = req as OneUptimeRequest;
|
||||
|
||||
if (!req.body.itemId) {
|
||||
return Response.sendErrorResponse(
|
||||
req,
|
||||
res,
|
||||
new BadDataException("Invalid item ID"),
|
||||
);
|
||||
if (!req.body.itemId) {
|
||||
return Response.sendErrorResponse(
|
||||
req,
|
||||
res,
|
||||
new BadDataException("Invalid item ID"),
|
||||
);
|
||||
}
|
||||
|
||||
if (!req.body.code) {
|
||||
return Response.sendErrorResponse(
|
||||
req,
|
||||
res,
|
||||
new BadDataException("Invalid code"),
|
||||
);
|
||||
}
|
||||
|
||||
// Check if the code matches and verify the phone number.
|
||||
const item: UserSMS | null = await this.service.findOneById({
|
||||
id: req.body["itemId"],
|
||||
props: {
|
||||
isRoot: true,
|
||||
},
|
||||
select: {
|
||||
userId: true,
|
||||
verificationCode: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!item) {
|
||||
return Response.sendErrorResponse(
|
||||
req,
|
||||
res,
|
||||
new BadDataException("Item not found"),
|
||||
);
|
||||
}
|
||||
|
||||
//check user id
|
||||
|
||||
if (
|
||||
item.userId?.toString() !==
|
||||
(req as OneUptimeRequest)?.userAuthorization?.userId?.toString()
|
||||
) {
|
||||
return Response.sendErrorResponse(
|
||||
req,
|
||||
res,
|
||||
new BadDataException("Invalid user ID"),
|
||||
);
|
||||
}
|
||||
|
||||
if (item.verificationCode !== req.body["code"]) {
|
||||
return Response.sendErrorResponse(
|
||||
req,
|
||||
res,
|
||||
new BadDataException("Invalid code"),
|
||||
);
|
||||
}
|
||||
|
||||
await this.service.updateOneById({
|
||||
id: item.id!,
|
||||
props: {
|
||||
isRoot: true,
|
||||
},
|
||||
data: {
|
||||
isVerified: true,
|
||||
},
|
||||
});
|
||||
|
||||
return Response.sendEmptySuccessResponse(req, res);
|
||||
} catch (err) {
|
||||
return next(err);
|
||||
}
|
||||
|
||||
if (!req.body.code) {
|
||||
return Response.sendErrorResponse(
|
||||
req,
|
||||
res,
|
||||
new BadDataException("Invalid code"),
|
||||
);
|
||||
}
|
||||
|
||||
// Check if the code matches and verify the phone number.
|
||||
const item: UserSMS | null = await this.service.findOneById({
|
||||
id: req.body["itemId"],
|
||||
props: {
|
||||
isRoot: true,
|
||||
},
|
||||
select: {
|
||||
userId: true,
|
||||
verificationCode: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!item) {
|
||||
return Response.sendErrorResponse(
|
||||
req,
|
||||
res,
|
||||
new BadDataException("Item not found"),
|
||||
);
|
||||
}
|
||||
|
||||
//check user id
|
||||
|
||||
if (
|
||||
item.userId?.toString() !==
|
||||
(req as OneUptimeRequest)?.userAuthorization?.userId?.toString()
|
||||
) {
|
||||
return Response.sendErrorResponse(
|
||||
req,
|
||||
res,
|
||||
new BadDataException("Invalid user ID"),
|
||||
);
|
||||
}
|
||||
|
||||
if (item.verificationCode !== req.body["code"]) {
|
||||
return Response.sendErrorResponse(
|
||||
req,
|
||||
res,
|
||||
new BadDataException("Invalid code"),
|
||||
);
|
||||
}
|
||||
|
||||
await this.service.updateOneById({
|
||||
id: item.id!,
|
||||
props: {
|
||||
isRoot: true,
|
||||
},
|
||||
data: {
|
||||
isVerified: true,
|
||||
},
|
||||
});
|
||||
|
||||
return Response.sendEmptySuccessResponse(req, res);
|
||||
},
|
||||
);
|
||||
|
||||
this.router.post(
|
||||
`/user-call/resend-verification-code`,
|
||||
UserMiddleware.getUserMiddleware,
|
||||
async (req: ExpressRequest, res: ExpressResponse) => {
|
||||
req = req as OneUptimeRequest;
|
||||
async (req: ExpressRequest, res: ExpressResponse, next: NextFunction) => {
|
||||
try {
|
||||
req = req as OneUptimeRequest;
|
||||
|
||||
if (!req.body.itemId) {
|
||||
return Response.sendErrorResponse(
|
||||
req,
|
||||
res,
|
||||
new BadDataException("Invalid item ID"),
|
||||
);
|
||||
if (!req.body.itemId) {
|
||||
return Response.sendErrorResponse(
|
||||
req,
|
||||
res,
|
||||
new BadDataException("Invalid item ID"),
|
||||
);
|
||||
}
|
||||
|
||||
await this.service.resendVerificationCode(req.body.itemId);
|
||||
|
||||
return Response.sendEmptySuccessResponse(req, res);
|
||||
} catch (err) {
|
||||
return next(err);
|
||||
}
|
||||
|
||||
await this.service.resendVerificationCode(req.body.itemId);
|
||||
|
||||
return Response.sendEmptySuccessResponse(req, res);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import UserEmailService, {
|
||||
import {
|
||||
ExpressRequest,
|
||||
ExpressResponse,
|
||||
NextFunction,
|
||||
OneUptimeRequest,
|
||||
} from "../Utils/Express";
|
||||
import Response from "../Utils/Response";
|
||||
@@ -22,77 +23,81 @@ export default class UserEmailAPI extends BaseAPI<
|
||||
this.router.post(
|
||||
`${new this.entityType().getCrudApiPath()?.toString()}/verify`,
|
||||
UserMiddleware.getUserMiddleware,
|
||||
async (req: ExpressRequest, res: ExpressResponse) => {
|
||||
req = req as OneUptimeRequest;
|
||||
async (req: ExpressRequest, res: ExpressResponse, next: NextFunction) => {
|
||||
try {
|
||||
req = req as OneUptimeRequest;
|
||||
|
||||
if (!req.body.itemId) {
|
||||
return Response.sendErrorResponse(
|
||||
req,
|
||||
res,
|
||||
new BadDataException("Invalid item ID"),
|
||||
);
|
||||
if (!req.body.itemId) {
|
||||
return Response.sendErrorResponse(
|
||||
req,
|
||||
res,
|
||||
new BadDataException("Invalid item ID"),
|
||||
);
|
||||
}
|
||||
|
||||
if (!req.body.code) {
|
||||
return Response.sendErrorResponse(
|
||||
req,
|
||||
res,
|
||||
new BadDataException("Invalid code"),
|
||||
);
|
||||
}
|
||||
|
||||
// Check if the code matches and verify the email.
|
||||
const item: UserEmail | null = await this.service.findOneById({
|
||||
id: req.body["itemId"],
|
||||
props: {
|
||||
isRoot: true,
|
||||
},
|
||||
select: {
|
||||
userId: true,
|
||||
verificationCode: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!item) {
|
||||
return Response.sendErrorResponse(
|
||||
req,
|
||||
res,
|
||||
new BadDataException("Item not found"),
|
||||
);
|
||||
}
|
||||
|
||||
//check user id
|
||||
|
||||
if (
|
||||
item.userId?.toString() !==
|
||||
(req as OneUptimeRequest)?.userAuthorization?.userId?.toString()
|
||||
) {
|
||||
return Response.sendErrorResponse(
|
||||
req,
|
||||
res,
|
||||
new BadDataException("Invalid user ID"),
|
||||
);
|
||||
}
|
||||
|
||||
if (item.verificationCode !== req.body["code"]) {
|
||||
return Response.sendErrorResponse(
|
||||
req,
|
||||
res,
|
||||
new BadDataException("Invalid code"),
|
||||
);
|
||||
}
|
||||
|
||||
await this.service.updateOneById({
|
||||
id: item.id!,
|
||||
props: {
|
||||
isRoot: true,
|
||||
},
|
||||
data: {
|
||||
isVerified: true,
|
||||
},
|
||||
});
|
||||
|
||||
return Response.sendEmptySuccessResponse(req, res);
|
||||
} catch (err) {
|
||||
return next(err);
|
||||
}
|
||||
|
||||
if (!req.body.code) {
|
||||
return Response.sendErrorResponse(
|
||||
req,
|
||||
res,
|
||||
new BadDataException("Invalid code"),
|
||||
);
|
||||
}
|
||||
|
||||
// Check if the code matches and verify the email.
|
||||
const item: UserEmail | null = await this.service.findOneById({
|
||||
id: req.body["itemId"],
|
||||
props: {
|
||||
isRoot: true,
|
||||
},
|
||||
select: {
|
||||
userId: true,
|
||||
verificationCode: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!item) {
|
||||
return Response.sendErrorResponse(
|
||||
req,
|
||||
res,
|
||||
new BadDataException("Item not found"),
|
||||
);
|
||||
}
|
||||
|
||||
//check user id
|
||||
|
||||
if (
|
||||
item.userId?.toString() !==
|
||||
(req as OneUptimeRequest)?.userAuthorization?.userId?.toString()
|
||||
) {
|
||||
return Response.sendErrorResponse(
|
||||
req,
|
||||
res,
|
||||
new BadDataException("Invalid user ID"),
|
||||
);
|
||||
}
|
||||
|
||||
if (item.verificationCode !== req.body["code"]) {
|
||||
return Response.sendErrorResponse(
|
||||
req,
|
||||
res,
|
||||
new BadDataException("Invalid code"),
|
||||
);
|
||||
}
|
||||
|
||||
await this.service.updateOneById({
|
||||
id: item.id!,
|
||||
props: {
|
||||
isRoot: true,
|
||||
},
|
||||
data: {
|
||||
isVerified: true,
|
||||
},
|
||||
});
|
||||
|
||||
return Response.sendEmptySuccessResponse(req, res);
|
||||
},
|
||||
);
|
||||
|
||||
@@ -101,20 +106,24 @@ export default class UserEmailAPI extends BaseAPI<
|
||||
.getCrudApiPath()
|
||||
?.toString()}/resend-verification-code`,
|
||||
UserMiddleware.getUserMiddleware,
|
||||
async (req: ExpressRequest, res: ExpressResponse) => {
|
||||
req = req as OneUptimeRequest;
|
||||
async (req: ExpressRequest, res: ExpressResponse, next: NextFunction) => {
|
||||
try {
|
||||
req = req as OneUptimeRequest;
|
||||
|
||||
if (!req.body.itemId) {
|
||||
return Response.sendErrorResponse(
|
||||
req,
|
||||
res,
|
||||
new BadDataException("Invalid item ID"),
|
||||
);
|
||||
if (!req.body.itemId) {
|
||||
return Response.sendErrorResponse(
|
||||
req,
|
||||
res,
|
||||
new BadDataException("Invalid item ID"),
|
||||
);
|
||||
}
|
||||
|
||||
await this.service.resendVerificationCode(req.body.itemId);
|
||||
|
||||
return Response.sendEmptySuccessResponse(req, res);
|
||||
} catch (err) {
|
||||
return next(err);
|
||||
}
|
||||
|
||||
await this.service.resendVerificationCode(req.body.itemId);
|
||||
|
||||
return Response.sendEmptySuccessResponse(req, res);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import UserOnCallLogTimelineService, {
|
||||
import {
|
||||
ExpressRequest,
|
||||
ExpressResponse,
|
||||
NextFunction,
|
||||
OneUptimeRequest,
|
||||
} from "../Utils/Express";
|
||||
import Response from "../Utils/Response";
|
||||
@@ -34,47 +35,228 @@ export default class UserNotificationLogTimelineAPI extends BaseAPI<
|
||||
.getCrudApiPath()
|
||||
?.toString()}/call/gather-input/:itemId`,
|
||||
NotificationMiddleware.isValidCallNotificationRequest,
|
||||
async (req: ExpressRequest, res: ExpressResponse) => {
|
||||
req = req as OneUptimeRequest;
|
||||
async (req: ExpressRequest, res: ExpressResponse, next: NextFunction) => {
|
||||
try {
|
||||
req = req as OneUptimeRequest;
|
||||
|
||||
if (!req.params["itemId"]) {
|
||||
return Response.sendErrorResponse(
|
||||
if (!req.params["itemId"]) {
|
||||
return Response.sendErrorResponse(
|
||||
req,
|
||||
res,
|
||||
new BadDataException("Invalid item ID"),
|
||||
);
|
||||
}
|
||||
|
||||
const token: JSONObject = (req as any).callTokenData;
|
||||
|
||||
const itemId: ObjectID = new ObjectID(req.params["itemId"]);
|
||||
|
||||
const timelineItem: UserOnCallLogTimeline | null =
|
||||
await this.service.findOneById({
|
||||
id: itemId,
|
||||
select: {
|
||||
_id: true,
|
||||
projectId: true,
|
||||
triggeredByIncidentId: true,
|
||||
triggeredByAlertId: true,
|
||||
},
|
||||
props: {
|
||||
isRoot: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!timelineItem) {
|
||||
return Response.sendErrorResponse(
|
||||
req,
|
||||
res,
|
||||
new BadDataException("Invalid item Id"),
|
||||
);
|
||||
}
|
||||
|
||||
// check digits.
|
||||
|
||||
if (req.body["Digits"] === "1") {
|
||||
// then ack incident
|
||||
await this.service.updateOneById({
|
||||
id: itemId,
|
||||
data: {
|
||||
acknowledgedAt: OneUptimeDate.getCurrentDate(),
|
||||
isAcknowledged: true,
|
||||
status: UserNotificationStatus.Acknowledged,
|
||||
statusMessage: "Notification Acknowledged",
|
||||
},
|
||||
props: {
|
||||
isRoot: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
return NotificationMiddleware.sendResponse(req, res, token as any);
|
||||
} catch (error) {
|
||||
return next(error);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
/*
|
||||
* We have this ack page to show the user a confirmation page before acknowledging the notification.
|
||||
* this is because email clients automatically make a get request to the url in the email and ack the notification automatically which is not what we want.
|
||||
* so we need to create this page for the user to confirm that they want to acknowledge the notification.
|
||||
*/
|
||||
this.router.get(
|
||||
`${new this.entityType()
|
||||
.getCrudApiPath()
|
||||
?.toString()}/acknowledge-page/:itemId`,
|
||||
async (req: ExpressRequest, res: ExpressResponse, next: NextFunction) => {
|
||||
try {
|
||||
req = req as OneUptimeRequest;
|
||||
|
||||
if (!req.params["itemId"]) {
|
||||
return Response.sendErrorResponse(
|
||||
req,
|
||||
res,
|
||||
new BadDataException("Item ID is required"),
|
||||
);
|
||||
}
|
||||
|
||||
const itemId: ObjectID = new ObjectID(req.params["itemId"]);
|
||||
|
||||
const timelineItem: UserOnCallLogTimeline | null =
|
||||
await this.service.findOneById({
|
||||
id: itemId,
|
||||
select: {
|
||||
_id: true,
|
||||
projectId: true,
|
||||
triggeredByIncidentId: true,
|
||||
triggeredByIncident: {
|
||||
title: true,
|
||||
description: true,
|
||||
},
|
||||
triggeredByAlertId: true,
|
||||
triggeredByAlert: {
|
||||
title: true,
|
||||
description: true,
|
||||
},
|
||||
},
|
||||
props: {
|
||||
isRoot: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!timelineItem) {
|
||||
return Response.sendErrorResponse(
|
||||
req,
|
||||
res,
|
||||
new BadDataException("Invalid item Id"),
|
||||
);
|
||||
}
|
||||
|
||||
const notificationType: string = timelineItem.triggeredByIncidentId
|
||||
? "Incident"
|
||||
: "Alert";
|
||||
|
||||
const host: Hostname = await DatabaseConfig.getHost();
|
||||
const httpProtocol: Protocol = await DatabaseConfig.getHttpProtocol();
|
||||
|
||||
return Response.render(
|
||||
req,
|
||||
res,
|
||||
new BadDataException("Invalid item ID"),
|
||||
);
|
||||
}
|
||||
|
||||
const token: JSONObject = (req as any).callTokenData;
|
||||
|
||||
const itemId: ObjectID = new ObjectID(req.params["itemId"]);
|
||||
|
||||
const timelineItem: UserOnCallLogTimeline | null =
|
||||
await this.service.findOneById({
|
||||
id: itemId,
|
||||
select: {
|
||||
_id: true,
|
||||
projectId: true,
|
||||
triggeredByIncidentId: true,
|
||||
triggeredByAlertId: true,
|
||||
"/usr/src/Common/Server/Views/AcknowledgeUserOnCallNotification.ejs",
|
||||
{
|
||||
title: `Acknowledge ${notificationType} - ${timelineItem.triggeredByIncident?.title || timelineItem.triggeredByAlert?.title}`,
|
||||
message: `Do you want to acknowledge this ${notificationType}?`,
|
||||
acknowledgeText: `Acknowledge ${notificationType}`,
|
||||
acknowledgeUrl: new URL(
|
||||
httpProtocol,
|
||||
host,
|
||||
new Route(AppApiRoute.toString())
|
||||
.addRoute(new UserOnCallLogTimeline().crudApiPath!)
|
||||
.addRoute("/acknowledge/" + itemId.toString()),
|
||||
).toString(),
|
||||
},
|
||||
props: {
|
||||
isRoot: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!timelineItem) {
|
||||
return Response.sendErrorResponse(
|
||||
req,
|
||||
res,
|
||||
new BadDataException("Invalid item Id"),
|
||||
);
|
||||
} catch (error) {
|
||||
return next(error);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
// check digits.
|
||||
// This is the link that actually acknowledges the notification.
|
||||
this.router.get(
|
||||
`${new this.entityType()
|
||||
.getCrudApiPath()
|
||||
?.toString()}/acknowledge/:itemId`,
|
||||
async (req: ExpressRequest, res: ExpressResponse, next: NextFunction) => {
|
||||
try {
|
||||
req = req as OneUptimeRequest;
|
||||
|
||||
if (!req.params["itemId"]) {
|
||||
return Response.sendErrorResponse(
|
||||
req,
|
||||
res,
|
||||
new BadDataException("Item ID is required"),
|
||||
);
|
||||
}
|
||||
|
||||
const itemId: ObjectID = new ObjectID(req.params["itemId"]);
|
||||
|
||||
const timelineItem: UserOnCallLogTimeline | null =
|
||||
await this.service.findOneById({
|
||||
id: itemId,
|
||||
select: {
|
||||
_id: true,
|
||||
projectId: true,
|
||||
triggeredByIncidentId: true,
|
||||
triggeredByAlertId: true,
|
||||
triggeredByAlert: {
|
||||
title: true,
|
||||
},
|
||||
triggeredByIncident: {
|
||||
title: true,
|
||||
},
|
||||
acknowledgedAt: true,
|
||||
isAcknowledged: true,
|
||||
},
|
||||
props: {
|
||||
isRoot: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!timelineItem) {
|
||||
return Response.sendErrorResponse(
|
||||
req,
|
||||
res,
|
||||
new BadDataException("Invalid item Id"),
|
||||
);
|
||||
}
|
||||
|
||||
const host: Hostname = await DatabaseConfig.getHost();
|
||||
const httpProtocol: Protocol = await DatabaseConfig.getHttpProtocol();
|
||||
|
||||
if (timelineItem.isAcknowledged) {
|
||||
// already acknowledged. Then show already acknowledged page with view details button.
|
||||
|
||||
const viewDetailsUrl: URL = new URL(
|
||||
httpProtocol,
|
||||
host,
|
||||
DashboardRoute.addRoute(
|
||||
`/${timelineItem.projectId?.toString()}/${timelineItem.triggeredByIncidentId ? "incidents" : "alerts"}/${timelineItem.triggeredByIncidentId ? timelineItem.triggeredByIncidentId!.toString() : timelineItem.triggeredByAlertId!.toString()}`,
|
||||
),
|
||||
);
|
||||
|
||||
return Response.render(
|
||||
req,
|
||||
res,
|
||||
"/usr/src/Common/Server/Views/ViewMessage.ejs",
|
||||
{
|
||||
title: `Notification Already Acknowledged - ${timelineItem.triggeredByIncident?.title || timelineItem.triggeredByAlert?.title}`,
|
||||
message: `This notification has already been acknowledged.`,
|
||||
viewDetailsText: `View ${timelineItem.triggeredByIncidentId ? "Incident" : "Alert"}`,
|
||||
viewDetailsUrl: viewDetailsUrl.toString(),
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
if (req.body["Digits"] === "1") {
|
||||
// then ack incident
|
||||
await this.service.updateOneById({
|
||||
id: itemId,
|
||||
data: {
|
||||
@@ -87,212 +269,45 @@ export default class UserNotificationLogTimelineAPI extends BaseAPI<
|
||||
isRoot: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
return NotificationMiddleware.sendResponse(req, res, token as any);
|
||||
},
|
||||
);
|
||||
// redirect to dashboard to incidents page.
|
||||
|
||||
// We have this ack page to show the user a confirmation page before acknowledging the notification.
|
||||
// this is because email clients automatically make a get request to the url in the email and ack the notification automatically which is not what we want.
|
||||
// so we need to create this page for the user to confirm that they want to acknowledge the notification.
|
||||
this.router.get(
|
||||
`${new this.entityType()
|
||||
.getCrudApiPath()
|
||||
?.toString()}/acknowledge-page/:itemId`,
|
||||
async (req: ExpressRequest, res: ExpressResponse) => {
|
||||
req = req as OneUptimeRequest;
|
||||
if (timelineItem.triggeredByIncidentId) {
|
||||
return Response.redirect(
|
||||
req,
|
||||
res,
|
||||
new URL(
|
||||
httpProtocol,
|
||||
host,
|
||||
DashboardRoute.addRoute(
|
||||
`/${timelineItem.projectId?.toString()}/incidents/${timelineItem.triggeredByIncidentId!.toString()}`,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if (!req.params["itemId"]) {
|
||||
return Response.sendErrorResponse(
|
||||
req,
|
||||
res,
|
||||
new BadDataException("Item ID is required"),
|
||||
);
|
||||
}
|
||||
if (timelineItem.triggeredByAlertId) {
|
||||
return Response.redirect(
|
||||
req,
|
||||
res,
|
||||
new URL(
|
||||
httpProtocol,
|
||||
host,
|
||||
DashboardRoute.addRoute(
|
||||
`/${timelineItem.projectId?.toString()}/alerts/${timelineItem.triggeredByAlertId!.toString()}`,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
const itemId: ObjectID = new ObjectID(req.params["itemId"]);
|
||||
|
||||
const timelineItem: UserOnCallLogTimeline | null =
|
||||
await this.service.findOneById({
|
||||
id: itemId,
|
||||
select: {
|
||||
_id: true,
|
||||
projectId: true,
|
||||
triggeredByIncidentId: true,
|
||||
triggeredByIncident: {
|
||||
title: true,
|
||||
description: true,
|
||||
},
|
||||
triggeredByAlertId: true,
|
||||
triggeredByAlert: {
|
||||
title: true,
|
||||
description: true,
|
||||
},
|
||||
},
|
||||
props: {
|
||||
isRoot: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!timelineItem) {
|
||||
return Response.sendErrorResponse(
|
||||
req,
|
||||
res,
|
||||
new BadDataException("Invalid item Id"),
|
||||
);
|
||||
} catch (error) {
|
||||
return next(error);
|
||||
}
|
||||
|
||||
const notificationType: string = timelineItem.triggeredByIncidentId
|
||||
? "Incident"
|
||||
: "Alert";
|
||||
|
||||
const host: Hostname = await DatabaseConfig.getHost();
|
||||
const httpProtocol: Protocol = await DatabaseConfig.getHttpProtocol();
|
||||
|
||||
return Response.render(
|
||||
req,
|
||||
res,
|
||||
"/usr/src/Common/Server/Views/AcknowledgeUserOnCallNotification.ejs",
|
||||
{
|
||||
title: `Acknowledge ${notificationType} - ${timelineItem.triggeredByIncident?.title || timelineItem.triggeredByAlert?.title}`,
|
||||
message: `Do you want to acknowledge this ${notificationType}?`,
|
||||
acknowledgeText: `Acknowledge ${notificationType}`,
|
||||
acknowledgeUrl: new URL(
|
||||
httpProtocol,
|
||||
host,
|
||||
new Route(AppApiRoute.toString())
|
||||
.addRoute(new UserOnCallLogTimeline().crudApiPath!)
|
||||
.addRoute("/acknowledge/" + itemId.toString()),
|
||||
).toString(),
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
// This is the link that actually acknowledges the notification.
|
||||
this.router.get(
|
||||
`${new this.entityType()
|
||||
.getCrudApiPath()
|
||||
?.toString()}/acknowledge/:itemId`,
|
||||
async (req: ExpressRequest, res: ExpressResponse) => {
|
||||
req = req as OneUptimeRequest;
|
||||
|
||||
if (!req.params["itemId"]) {
|
||||
return Response.sendErrorResponse(
|
||||
req,
|
||||
res,
|
||||
new BadDataException("Item ID is required"),
|
||||
);
|
||||
}
|
||||
|
||||
const itemId: ObjectID = new ObjectID(req.params["itemId"]);
|
||||
|
||||
const timelineItem: UserOnCallLogTimeline | null =
|
||||
await this.service.findOneById({
|
||||
id: itemId,
|
||||
select: {
|
||||
_id: true,
|
||||
projectId: true,
|
||||
triggeredByIncidentId: true,
|
||||
triggeredByAlertId: true,
|
||||
triggeredByAlert: {
|
||||
title: true,
|
||||
},
|
||||
triggeredByIncident: {
|
||||
title: true,
|
||||
},
|
||||
acknowledgedAt: true,
|
||||
isAcknowledged: true,
|
||||
},
|
||||
props: {
|
||||
isRoot: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!timelineItem) {
|
||||
return Response.sendErrorResponse(
|
||||
req,
|
||||
res,
|
||||
new BadDataException("Invalid item Id"),
|
||||
);
|
||||
}
|
||||
|
||||
const host: Hostname = await DatabaseConfig.getHost();
|
||||
const httpProtocol: Protocol = await DatabaseConfig.getHttpProtocol();
|
||||
|
||||
if (timelineItem.isAcknowledged) {
|
||||
// already acknowledged. Then show already acknowledged page with view details button.
|
||||
|
||||
const viewDetailsUrl: URL = new URL(
|
||||
httpProtocol,
|
||||
host,
|
||||
DashboardRoute.addRoute(
|
||||
`/${timelineItem.projectId?.toString()}/${timelineItem.triggeredByIncidentId ? "incidents" : "alerts"}/${timelineItem.triggeredByIncidentId ? timelineItem.triggeredByIncidentId!.toString() : timelineItem.triggeredByAlertId!.toString()}`,
|
||||
),
|
||||
);
|
||||
|
||||
return Response.render(
|
||||
req,
|
||||
res,
|
||||
"/usr/src/Common/Server/Views/ViewMessage.ejs",
|
||||
{
|
||||
title: `Notification Already Acknowledged - ${timelineItem.triggeredByIncident?.title || timelineItem.triggeredByAlert?.title}`,
|
||||
message: `This notification has already been acknowledged.`,
|
||||
viewDetailsText: `View ${timelineItem.triggeredByIncidentId ? "Incident" : "Alert"}`,
|
||||
viewDetailsUrl: viewDetailsUrl.toString(),
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
await this.service.updateOneById({
|
||||
id: itemId,
|
||||
data: {
|
||||
acknowledgedAt: OneUptimeDate.getCurrentDate(),
|
||||
isAcknowledged: true,
|
||||
status: UserNotificationStatus.Acknowledged,
|
||||
statusMessage: "Notification Acknowledged",
|
||||
},
|
||||
props: {
|
||||
isRoot: true,
|
||||
},
|
||||
});
|
||||
|
||||
// redirect to dashboard to incidents page.
|
||||
|
||||
if (timelineItem.triggeredByIncidentId) {
|
||||
return Response.redirect(
|
||||
req,
|
||||
res,
|
||||
new URL(
|
||||
httpProtocol,
|
||||
host,
|
||||
DashboardRoute.addRoute(
|
||||
`/${timelineItem.projectId?.toString()}/incidents/${timelineItem.triggeredByIncidentId!.toString()}`,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if (timelineItem.triggeredByAlertId) {
|
||||
return Response.redirect(
|
||||
req,
|
||||
res,
|
||||
new URL(
|
||||
httpProtocol,
|
||||
host,
|
||||
DashboardRoute.addRoute(
|
||||
`/${timelineItem.projectId?.toString()}/alerts/${timelineItem.triggeredByAlertId!.toString()}`,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return Response.sendErrorResponse(
|
||||
req,
|
||||
res,
|
||||
new BadDataException("Invalid item Id"),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@@ -108,104 +108,104 @@ export default class UserPushAPI extends BaseAPI<
|
||||
this.router.post(
|
||||
`/user-push/:deviceId/test-notification`,
|
||||
UserMiddleware.getUserMiddleware,
|
||||
async (req: ExpressRequest, res: ExpressResponse) => {
|
||||
req = req as OneUptimeRequest;
|
||||
|
||||
if (!req.params["deviceId"]) {
|
||||
return Response.sendErrorResponse(
|
||||
req,
|
||||
res,
|
||||
new BadDataException("Device ID is required"),
|
||||
);
|
||||
}
|
||||
|
||||
// Get the device
|
||||
const device: UserPush | null = await this.service.findOneById({
|
||||
id: new ObjectID(req.params["deviceId"]),
|
||||
props: {
|
||||
isRoot: true,
|
||||
},
|
||||
select: {
|
||||
userId: true,
|
||||
deviceName: true,
|
||||
deviceToken: true,
|
||||
deviceType: true,
|
||||
isVerified: true,
|
||||
projectId: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!device) {
|
||||
return Response.sendErrorResponse(
|
||||
req,
|
||||
res,
|
||||
new BadDataException("Device not found"),
|
||||
);
|
||||
}
|
||||
|
||||
// Check if the device belongs to the current user
|
||||
if (
|
||||
device.userId?.toString() !==
|
||||
(req as OneUptimeRequest).userAuthorization!.userId!.toString()
|
||||
) {
|
||||
return Response.sendErrorResponse(
|
||||
req,
|
||||
res,
|
||||
new BadDataException("Unauthorized access to device"),
|
||||
);
|
||||
}
|
||||
|
||||
if (!device.isVerified) {
|
||||
return Response.sendErrorResponse(
|
||||
req,
|
||||
res,
|
||||
new BadDataException("Device is not verified"),
|
||||
);
|
||||
}
|
||||
|
||||
async (req: ExpressRequest, res: ExpressResponse, next: NextFunction) => {
|
||||
try {
|
||||
// Send test notification
|
||||
const testMessage: PushNotificationMessage =
|
||||
PushNotificationUtil.createGenericNotification({
|
||||
title: "Test Notification from OneUptime",
|
||||
body: "This is a test notification to verify your device is working correctly.",
|
||||
clickAction: "/dashboard",
|
||||
tag: "test-notification",
|
||||
requireInteraction: false,
|
||||
});
|
||||
req = req as OneUptimeRequest;
|
||||
|
||||
await PushNotificationService.sendPushNotification(
|
||||
{
|
||||
devices: [
|
||||
{
|
||||
token: device.deviceToken!,
|
||||
...(device.deviceName && {
|
||||
name: device.deviceName,
|
||||
}),
|
||||
},
|
||||
],
|
||||
message: testMessage,
|
||||
deviceType: device.deviceType!,
|
||||
if (!req.params["deviceId"]) {
|
||||
return Response.sendErrorResponse(
|
||||
req,
|
||||
res,
|
||||
new BadDataException("Device ID is required"),
|
||||
);
|
||||
}
|
||||
|
||||
// Get the device
|
||||
const device: UserPush | null = await this.service.findOneById({
|
||||
id: new ObjectID(req.params["deviceId"]),
|
||||
props: {
|
||||
isRoot: true,
|
||||
},
|
||||
{
|
||||
isSensitive: false,
|
||||
projectId: device.projectId!,
|
||||
userId: device.userId!,
|
||||
select: {
|
||||
userId: true,
|
||||
deviceName: true,
|
||||
deviceToken: true,
|
||||
deviceType: true,
|
||||
isVerified: true,
|
||||
projectId: true,
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
if (!device) {
|
||||
return Response.sendErrorResponse(
|
||||
req,
|
||||
res,
|
||||
new BadDataException("Device not found"),
|
||||
);
|
||||
}
|
||||
|
||||
// Check if the device belongs to the current user
|
||||
if (
|
||||
device.userId?.toString() !==
|
||||
(req as OneUptimeRequest).userAuthorization!.userId!.toString()
|
||||
) {
|
||||
return Response.sendErrorResponse(
|
||||
req,
|
||||
res,
|
||||
new BadDataException("Unauthorized access to device"),
|
||||
);
|
||||
}
|
||||
|
||||
if (!device.isVerified) {
|
||||
return Response.sendErrorResponse(
|
||||
req,
|
||||
res,
|
||||
new BadDataException("Device is not verified"),
|
||||
);
|
||||
}
|
||||
|
||||
try {
|
||||
// Send test notification
|
||||
const testMessage: PushNotificationMessage =
|
||||
PushNotificationUtil.createGenericNotification({
|
||||
title: "Test Notification from OneUptime",
|
||||
body: "This is a test notification to verify your device is working correctly.",
|
||||
clickAction: "/dashboard",
|
||||
tag: "test-notification",
|
||||
requireInteraction: false,
|
||||
});
|
||||
|
||||
await PushNotificationService.sendPushNotification(
|
||||
{
|
||||
devices: [
|
||||
{
|
||||
token: device.deviceToken!,
|
||||
...(device.deviceName && {
|
||||
name: device.deviceName,
|
||||
}),
|
||||
},
|
||||
],
|
||||
message: testMessage,
|
||||
deviceType: device.deviceType!,
|
||||
},
|
||||
{
|
||||
isSensitive: false,
|
||||
projectId: device.projectId!,
|
||||
userId: device.userId!,
|
||||
},
|
||||
);
|
||||
} catch (error: any) {
|
||||
throw new BadDataException(
|
||||
`Failed to send test notification: ${error.message}`,
|
||||
);
|
||||
}
|
||||
|
||||
return Response.sendJsonObjectResponse(req, res, {
|
||||
success: true,
|
||||
message: "Test notification sent successfully",
|
||||
});
|
||||
} catch (error: any) {
|
||||
return Response.sendErrorResponse(
|
||||
req,
|
||||
res,
|
||||
new BadDataException(
|
||||
`Failed to send test notification: ${error.message}`,
|
||||
),
|
||||
);
|
||||
} catch (error) {
|
||||
return next(error);
|
||||
}
|
||||
},
|
||||
);
|
||||
@@ -213,100 +213,108 @@ export default class UserPushAPI extends BaseAPI<
|
||||
this.router.post(
|
||||
`/user-push/:deviceId/verify`,
|
||||
UserMiddleware.getUserMiddleware,
|
||||
async (req: ExpressRequest, res: ExpressResponse) => {
|
||||
req = req as OneUptimeRequest;
|
||||
async (req: ExpressRequest, res: ExpressResponse, next: NextFunction) => {
|
||||
try {
|
||||
req = req as OneUptimeRequest;
|
||||
|
||||
if (!req.params["deviceId"]) {
|
||||
return Response.sendErrorResponse(
|
||||
req,
|
||||
res,
|
||||
new BadDataException("Device ID is required"),
|
||||
);
|
||||
if (!req.params["deviceId"]) {
|
||||
return Response.sendErrorResponse(
|
||||
req,
|
||||
res,
|
||||
new BadDataException("Device ID is required"),
|
||||
);
|
||||
}
|
||||
|
||||
const device: UserPush | null = await this.service.findOneById({
|
||||
id: new ObjectID(req.params["deviceId"]),
|
||||
props: {
|
||||
isRoot: true,
|
||||
},
|
||||
select: {
|
||||
userId: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!device) {
|
||||
return Response.sendErrorResponse(
|
||||
req,
|
||||
res,
|
||||
new BadDataException("Device not found"),
|
||||
);
|
||||
}
|
||||
|
||||
// Check if the device belongs to the current user
|
||||
if (
|
||||
device.userId?.toString() !==
|
||||
(req as OneUptimeRequest).userAuthorization!.userId!.toString()
|
||||
) {
|
||||
return Response.sendErrorResponse(
|
||||
req,
|
||||
res,
|
||||
new BadDataException("Unauthorized access to device"),
|
||||
);
|
||||
}
|
||||
|
||||
await this.service.verifyDevice(device._id!.toString());
|
||||
|
||||
return Response.sendEmptySuccessResponse(req, res);
|
||||
} catch (error) {
|
||||
return next(error);
|
||||
}
|
||||
|
||||
const device: UserPush | null = await this.service.findOneById({
|
||||
id: new ObjectID(req.params["deviceId"]),
|
||||
props: {
|
||||
isRoot: true,
|
||||
},
|
||||
select: {
|
||||
userId: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!device) {
|
||||
return Response.sendErrorResponse(
|
||||
req,
|
||||
res,
|
||||
new BadDataException("Device not found"),
|
||||
);
|
||||
}
|
||||
|
||||
// Check if the device belongs to the current user
|
||||
if (
|
||||
device.userId?.toString() !==
|
||||
(req as OneUptimeRequest).userAuthorization!.userId!.toString()
|
||||
) {
|
||||
return Response.sendErrorResponse(
|
||||
req,
|
||||
res,
|
||||
new BadDataException("Unauthorized access to device"),
|
||||
);
|
||||
}
|
||||
|
||||
await this.service.verifyDevice(device._id!.toString());
|
||||
|
||||
return Response.sendEmptySuccessResponse(req, res);
|
||||
},
|
||||
);
|
||||
|
||||
this.router.post(
|
||||
`/user-push/:deviceId/unverify`,
|
||||
UserMiddleware.getUserMiddleware,
|
||||
async (req: ExpressRequest, res: ExpressResponse) => {
|
||||
req = req as OneUptimeRequest;
|
||||
async (req: ExpressRequest, res: ExpressResponse, next: NextFunction) => {
|
||||
try {
|
||||
req = req as OneUptimeRequest;
|
||||
|
||||
if (!req.params["deviceId"]) {
|
||||
return Response.sendErrorResponse(
|
||||
req,
|
||||
res,
|
||||
new BadDataException("Device ID is required"),
|
||||
);
|
||||
if (!req.params["deviceId"]) {
|
||||
return Response.sendErrorResponse(
|
||||
req,
|
||||
res,
|
||||
new BadDataException("Device ID is required"),
|
||||
);
|
||||
}
|
||||
|
||||
const device: UserPush | null = await this.service.findOneById({
|
||||
id: new ObjectID(req.params["deviceId"]),
|
||||
props: {
|
||||
isRoot: true,
|
||||
},
|
||||
select: {
|
||||
userId: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!device) {
|
||||
return Response.sendErrorResponse(
|
||||
req,
|
||||
res,
|
||||
new BadDataException("Device not found"),
|
||||
);
|
||||
}
|
||||
|
||||
// Check if the device belongs to the current user
|
||||
if (
|
||||
device.userId?.toString() !==
|
||||
(req as OneUptimeRequest).userAuthorization!.userId!.toString()
|
||||
) {
|
||||
return Response.sendErrorResponse(
|
||||
req,
|
||||
res,
|
||||
new BadDataException("Unauthorized access to device"),
|
||||
);
|
||||
}
|
||||
|
||||
await this.service.unverifyDevice(device._id!.toString());
|
||||
|
||||
return Response.sendEmptySuccessResponse(req, res);
|
||||
} catch (error) {
|
||||
return next(error);
|
||||
}
|
||||
|
||||
const device: UserPush | null = await this.service.findOneById({
|
||||
id: new ObjectID(req.params["deviceId"]),
|
||||
props: {
|
||||
isRoot: true,
|
||||
},
|
||||
select: {
|
||||
userId: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!device) {
|
||||
return Response.sendErrorResponse(
|
||||
req,
|
||||
res,
|
||||
new BadDataException("Device not found"),
|
||||
);
|
||||
}
|
||||
|
||||
// Check if the device belongs to the current user
|
||||
if (
|
||||
device.userId?.toString() !==
|
||||
(req as OneUptimeRequest).userAuthorization!.userId!.toString()
|
||||
) {
|
||||
return Response.sendErrorResponse(
|
||||
req,
|
||||
res,
|
||||
new BadDataException("Unauthorized access to device"),
|
||||
);
|
||||
}
|
||||
|
||||
await this.service.unverifyDevice(device._id!.toString());
|
||||
|
||||
return Response.sendEmptySuccessResponse(req, res);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import UserSMSService, {
|
||||
import {
|
||||
ExpressRequest,
|
||||
ExpressResponse,
|
||||
NextFunction,
|
||||
OneUptimeRequest,
|
||||
} from "../Utils/Express";
|
||||
import Response from "../Utils/Response";
|
||||
@@ -19,97 +20,105 @@ export default class UserSMSAPI extends BaseAPI<UserSMS, UserSMSServiceType> {
|
||||
this.router.post(
|
||||
`/user-sms/verify`,
|
||||
UserMiddleware.getUserMiddleware,
|
||||
async (req: ExpressRequest, res: ExpressResponse) => {
|
||||
req = req as OneUptimeRequest;
|
||||
async (req: ExpressRequest, res: ExpressResponse, next: NextFunction) => {
|
||||
try {
|
||||
req = req as OneUptimeRequest;
|
||||
|
||||
if (!req.body.itemId) {
|
||||
return Response.sendErrorResponse(
|
||||
req,
|
||||
res,
|
||||
new BadDataException("Invalid item ID"),
|
||||
);
|
||||
if (!req.body.itemId) {
|
||||
return Response.sendErrorResponse(
|
||||
req,
|
||||
res,
|
||||
new BadDataException("Invalid item ID"),
|
||||
);
|
||||
}
|
||||
|
||||
if (!req.body.code) {
|
||||
return Response.sendErrorResponse(
|
||||
req,
|
||||
res,
|
||||
new BadDataException("Invalid code"),
|
||||
);
|
||||
}
|
||||
|
||||
// Check if the code matches and verify the phone number.
|
||||
const item: UserSMS | null = await this.service.findOneById({
|
||||
id: req.body["itemId"],
|
||||
props: {
|
||||
isRoot: true,
|
||||
},
|
||||
select: {
|
||||
userId: true,
|
||||
verificationCode: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!item) {
|
||||
return Response.sendErrorResponse(
|
||||
req,
|
||||
res,
|
||||
new BadDataException("Item not found"),
|
||||
);
|
||||
}
|
||||
|
||||
//check user id
|
||||
|
||||
if (
|
||||
item.userId?.toString() !==
|
||||
(req as OneUptimeRequest)?.userAuthorization?.userId?.toString()
|
||||
) {
|
||||
return Response.sendErrorResponse(
|
||||
req,
|
||||
res,
|
||||
new BadDataException("Invalid user ID"),
|
||||
);
|
||||
}
|
||||
|
||||
if (item.verificationCode !== req.body["code"]) {
|
||||
return Response.sendErrorResponse(
|
||||
req,
|
||||
res,
|
||||
new BadDataException("Invalid code"),
|
||||
);
|
||||
}
|
||||
|
||||
await this.service.updateOneById({
|
||||
id: item.id!,
|
||||
props: {
|
||||
isRoot: true,
|
||||
},
|
||||
data: {
|
||||
isVerified: true,
|
||||
},
|
||||
});
|
||||
|
||||
return Response.sendEmptySuccessResponse(req, res);
|
||||
} catch (err) {
|
||||
return next(err);
|
||||
}
|
||||
|
||||
if (!req.body.code) {
|
||||
return Response.sendErrorResponse(
|
||||
req,
|
||||
res,
|
||||
new BadDataException("Invalid code"),
|
||||
);
|
||||
}
|
||||
|
||||
// Check if the code matches and verify the phone number.
|
||||
const item: UserSMS | null = await this.service.findOneById({
|
||||
id: req.body["itemId"],
|
||||
props: {
|
||||
isRoot: true,
|
||||
},
|
||||
select: {
|
||||
userId: true,
|
||||
verificationCode: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!item) {
|
||||
return Response.sendErrorResponse(
|
||||
req,
|
||||
res,
|
||||
new BadDataException("Item not found"),
|
||||
);
|
||||
}
|
||||
|
||||
//check user id
|
||||
|
||||
if (
|
||||
item.userId?.toString() !==
|
||||
(req as OneUptimeRequest)?.userAuthorization?.userId?.toString()
|
||||
) {
|
||||
return Response.sendErrorResponse(
|
||||
req,
|
||||
res,
|
||||
new BadDataException("Invalid user ID"),
|
||||
);
|
||||
}
|
||||
|
||||
if (item.verificationCode !== req.body["code"]) {
|
||||
return Response.sendErrorResponse(
|
||||
req,
|
||||
res,
|
||||
new BadDataException("Invalid code"),
|
||||
);
|
||||
}
|
||||
|
||||
await this.service.updateOneById({
|
||||
id: item.id!,
|
||||
props: {
|
||||
isRoot: true,
|
||||
},
|
||||
data: {
|
||||
isVerified: true,
|
||||
},
|
||||
});
|
||||
|
||||
return Response.sendEmptySuccessResponse(req, res);
|
||||
},
|
||||
);
|
||||
|
||||
this.router.post(
|
||||
`/user-sms/resend-verification-code`,
|
||||
UserMiddleware.getUserMiddleware,
|
||||
async (req: ExpressRequest, res: ExpressResponse) => {
|
||||
req = req as OneUptimeRequest;
|
||||
async (req: ExpressRequest, res: ExpressResponse, next: NextFunction) => {
|
||||
try {
|
||||
req = req as OneUptimeRequest;
|
||||
|
||||
if (!req.body.itemId) {
|
||||
return Response.sendErrorResponse(
|
||||
req,
|
||||
res,
|
||||
new BadDataException("Invalid item ID"),
|
||||
);
|
||||
if (!req.body.itemId) {
|
||||
return Response.sendErrorResponse(
|
||||
req,
|
||||
res,
|
||||
new BadDataException("Invalid item ID"),
|
||||
);
|
||||
}
|
||||
|
||||
await this.service.resendVerificationCode(req.body.itemId);
|
||||
|
||||
return Response.sendEmptySuccessResponse(req, res);
|
||||
} catch (err) {
|
||||
return next(err);
|
||||
}
|
||||
|
||||
await this.service.resendVerificationCode(req.body.itemId);
|
||||
|
||||
return Response.sendEmptySuccessResponse(req, res);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import ObjectID from "../../Types/ObjectID";
|
||||
import UserMiddleware from "../Middleware/UserAuthorization";
|
||||
import UserTwoFactorAuthService, {
|
||||
Service as UserTwoFactorAuthServiceType,
|
||||
} from "../Services/UserTwoFactorAuthService";
|
||||
import UserTotpAuthService, {
|
||||
Service as UserTotpAuthServiceType,
|
||||
} from "../Services/UserTotpAuthService";
|
||||
import {
|
||||
ExpressRequest,
|
||||
ExpressResponse,
|
||||
@@ -10,27 +10,27 @@ import {
|
||||
OneUptimeRequest,
|
||||
} from "../Utils/Express";
|
||||
import BaseAPI from "./BaseAPI";
|
||||
import UserTwoFactorAuth from "../../Models/DatabaseModels/UserTwoFactorAuth";
|
||||
import UserTotpAuth from "../../Models/DatabaseModels/UserTotpAuth";
|
||||
import BadDataException from "../../Types/Exception/BadDataException";
|
||||
import TwoFactorAuth from "../Utils/TwoFactorAuth";
|
||||
import TotpAuth from "../Utils/TotpAuth";
|
||||
import Response from "../Utils/Response";
|
||||
import User from "../../Models/DatabaseModels/User";
|
||||
import UserService from "../Services/UserService";
|
||||
|
||||
export default class UserTwoFactorAuthAPI extends BaseAPI<
|
||||
UserTwoFactorAuth,
|
||||
UserTwoFactorAuthServiceType
|
||||
export default class UserTotpAuthAPI extends BaseAPI<
|
||||
UserTotpAuth,
|
||||
UserTotpAuthServiceType
|
||||
> {
|
||||
public constructor() {
|
||||
super(UserTwoFactorAuth, UserTwoFactorAuthService);
|
||||
super(UserTotpAuth, UserTotpAuthService);
|
||||
|
||||
this.router.post(
|
||||
`${new this.entityType().getCrudApiPath()?.toString()}/validate`,
|
||||
UserMiddleware.getUserMiddleware,
|
||||
async (req: ExpressRequest, res: ExpressResponse, next: NextFunction) => {
|
||||
try {
|
||||
const userTwoFactorAuth: UserTwoFactorAuth | null =
|
||||
await UserTwoFactorAuthService.findOneById({
|
||||
const userTotpAuth: UserTotpAuth | null =
|
||||
await UserTotpAuthService.findOneById({
|
||||
id: new ObjectID(req.body["id"]),
|
||||
select: {
|
||||
twoFactorSecret: true,
|
||||
@@ -41,24 +41,24 @@ export default class UserTwoFactorAuthAPI extends BaseAPI<
|
||||
},
|
||||
});
|
||||
|
||||
if (!userTwoFactorAuth) {
|
||||
throw new BadDataException("Two factor auth not found");
|
||||
if (!userTotpAuth) {
|
||||
throw new BadDataException("TOTP auth not found");
|
||||
}
|
||||
|
||||
if (
|
||||
userTwoFactorAuth.userId?.toString() !==
|
||||
userTotpAuth.userId?.toString() !==
|
||||
(req as OneUptimeRequest).userAuthorization?.userId.toString()
|
||||
) {
|
||||
throw new BadDataException("Two factor auth not found");
|
||||
}
|
||||
|
||||
if (!userTwoFactorAuth.userId) {
|
||||
if (!userTotpAuth.userId) {
|
||||
throw new BadDataException("User not found");
|
||||
}
|
||||
|
||||
// get user email.
|
||||
const user: User | null = await UserService.findOneById({
|
||||
id: userTwoFactorAuth.userId!,
|
||||
id: userTotpAuth.userId!,
|
||||
select: {
|
||||
email: true,
|
||||
},
|
||||
@@ -75,8 +75,8 @@ export default class UserTwoFactorAuthAPI extends BaseAPI<
|
||||
throw new BadDataException("User email not found");
|
||||
}
|
||||
|
||||
const isValid: boolean = TwoFactorAuth.verifyToken({
|
||||
secret: userTwoFactorAuth.twoFactorSecret || "",
|
||||
const isValid: boolean = TotpAuth.verifyToken({
|
||||
secret: userTotpAuth.twoFactorSecret || "",
|
||||
token: req.body["code"] || "",
|
||||
email: user.email!,
|
||||
});
|
||||
@@ -87,8 +87,8 @@ export default class UserTwoFactorAuthAPI extends BaseAPI<
|
||||
|
||||
// update this 2fa code as verified
|
||||
|
||||
await UserTwoFactorAuthService.updateOneById({
|
||||
id: userTwoFactorAuth.id!,
|
||||
await UserTotpAuthService.updateOneById({
|
||||
id: userTotpAuth.id!,
|
||||
data: {
|
||||
isVerified: true,
|
||||
},
|
||||
103
Common/Server/API/UserWebAuthnAPI.ts
Normal file
103
Common/Server/API/UserWebAuthnAPI.ts
Normal file
@@ -0,0 +1,103 @@
|
||||
import ObjectID from "../../Types/ObjectID";
|
||||
import UserMiddleware from "../Middleware/UserAuthorization";
|
||||
import UserWebAuthnService, {
|
||||
Service as UserWebAuthnServiceType,
|
||||
} from "../Services/UserWebAuthnService";
|
||||
import {
|
||||
ExpressRequest,
|
||||
ExpressResponse,
|
||||
NextFunction,
|
||||
OneUptimeRequest,
|
||||
} from "../Utils/Express";
|
||||
import BaseAPI from "./BaseAPI";
|
||||
import UserWebAuthn from "../../Models/DatabaseModels/UserWebAuthn";
|
||||
import BadDataException from "../../Types/Exception/BadDataException";
|
||||
import Response from "../Utils/Response";
|
||||
import { JSONObject } from "../../Types/JSON";
|
||||
import CommonAPI from "./CommonAPI";
|
||||
import DatabaseCommonInteractionProps from "../../Types/BaseDatabase/DatabaseCommonInteractionProps";
|
||||
|
||||
export default class UserWebAuthnAPI extends BaseAPI<
|
||||
UserWebAuthn,
|
||||
UserWebAuthnServiceType
|
||||
> {
|
||||
public constructor() {
|
||||
super(UserWebAuthn, UserWebAuthnService);
|
||||
|
||||
this.router.post(
|
||||
`${new this.entityType().getCrudApiPath()?.toString()}/generate-registration-options`,
|
||||
UserMiddleware.getUserMiddleware,
|
||||
async (req: ExpressRequest, res: ExpressResponse, next: NextFunction) => {
|
||||
try {
|
||||
const userId: ObjectID = (req as OneUptimeRequest).userAuthorization!
|
||||
.userId;
|
||||
|
||||
const result: { options: any; challenge: string } =
|
||||
await UserWebAuthnService.generateRegistrationOptions({
|
||||
userId: userId,
|
||||
});
|
||||
|
||||
return Response.sendJsonObjectResponse(req, res, result);
|
||||
} catch (err) {
|
||||
next(err);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
this.router.post(
|
||||
`${new this.entityType().getCrudApiPath()?.toString()}/verify-registration`,
|
||||
UserMiddleware.getUserMiddleware,
|
||||
async (req: ExpressRequest, res: ExpressResponse, next: NextFunction) => {
|
||||
try {
|
||||
const data: JSONObject = req.body;
|
||||
|
||||
const databaseProps: DatabaseCommonInteractionProps =
|
||||
await CommonAPI.getDatabaseCommonInteractionProps(req);
|
||||
|
||||
const expectedChallenge: string = data["challenge"] as string;
|
||||
const credential: any = data["credential"];
|
||||
const name: string = data["name"] as string;
|
||||
|
||||
await UserWebAuthnService.verifyRegistration({
|
||||
challenge: expectedChallenge,
|
||||
credential: credential,
|
||||
name: name,
|
||||
props: databaseProps,
|
||||
});
|
||||
|
||||
return Response.sendEmptySuccessResponse(req, res);
|
||||
} catch (err) {
|
||||
next(err);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
this.router.post(
|
||||
`${new this.entityType().getCrudApiPath()?.toString()}/generate-authentication-options`,
|
||||
async (req: ExpressRequest, res: ExpressResponse, next: NextFunction) => {
|
||||
try {
|
||||
const data: JSONObject = req.body["data"] as JSONObject;
|
||||
|
||||
if (!data) {
|
||||
throw new BadDataException("Data is required");
|
||||
}
|
||||
|
||||
const email: string | undefined = data["email"] as string | undefined;
|
||||
|
||||
if (!email) {
|
||||
throw new BadDataException("Email is required");
|
||||
}
|
||||
|
||||
const result: { options: any; challenge: string; userId: string } =
|
||||
await UserWebAuthnService.generateAuthenticationOptions({
|
||||
email: email,
|
||||
});
|
||||
|
||||
return Response.sendJsonObjectResponse(req, res, result);
|
||||
} catch (err) {
|
||||
next(err);
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
136
Common/Server/API/UserWhatsAppAPI.ts
Normal file
136
Common/Server/API/UserWhatsAppAPI.ts
Normal file
@@ -0,0 +1,136 @@
|
||||
import UserMiddleware from "../Middleware/UserAuthorization";
|
||||
import UserWhatsAppService, {
|
||||
Service as UserWhatsAppServiceType,
|
||||
} from "../Services/UserWhatsAppService";
|
||||
import {
|
||||
ExpressRequest,
|
||||
ExpressResponse,
|
||||
NextFunction,
|
||||
OneUptimeRequest,
|
||||
} from "../Utils/Express";
|
||||
import Response from "../Utils/Response";
|
||||
import BaseAPI from "./BaseAPI";
|
||||
import BadDataException from "../../Types/Exception/BadDataException";
|
||||
import UserWhatsApp from "../../Models/DatabaseModels/UserWhatsApp";
|
||||
|
||||
export default class UserWhatsAppAPI extends BaseAPI<
|
||||
UserWhatsApp,
|
||||
UserWhatsAppServiceType
|
||||
> {
|
||||
public constructor() {
|
||||
super(UserWhatsApp, UserWhatsAppService);
|
||||
|
||||
this.router.post(
|
||||
`${new this.entityType().getCrudApiPath()?.toString()}/verify`,
|
||||
UserMiddleware.getUserMiddleware,
|
||||
async (req: ExpressRequest, res: ExpressResponse, next: NextFunction) => {
|
||||
try {
|
||||
req = req as OneUptimeRequest;
|
||||
|
||||
if (!req.body.itemId) {
|
||||
return Response.sendErrorResponse(
|
||||
req,
|
||||
res,
|
||||
new BadDataException("Invalid item ID"),
|
||||
);
|
||||
}
|
||||
|
||||
if (!req.body.code) {
|
||||
return Response.sendErrorResponse(
|
||||
req,
|
||||
res,
|
||||
new BadDataException("Invalid code"),
|
||||
);
|
||||
}
|
||||
|
||||
const item: UserWhatsApp | null = await this.service.findOneById({
|
||||
id: req.body["itemId"],
|
||||
props: {
|
||||
isRoot: true,
|
||||
},
|
||||
select: {
|
||||
userId: true,
|
||||
verificationCode: true,
|
||||
isVerified: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!item) {
|
||||
return Response.sendErrorResponse(
|
||||
req,
|
||||
res,
|
||||
new BadDataException("Item not found"),
|
||||
);
|
||||
}
|
||||
|
||||
if (item.isVerified) {
|
||||
return Response.sendErrorResponse(
|
||||
req,
|
||||
res,
|
||||
new BadDataException("WhatsApp number already verified"),
|
||||
);
|
||||
}
|
||||
|
||||
if (
|
||||
item.userId?.toString() !==
|
||||
(req as OneUptimeRequest)?.userAuthorization?.userId?.toString()
|
||||
) {
|
||||
return Response.sendErrorResponse(
|
||||
req,
|
||||
res,
|
||||
new BadDataException("Invalid user ID"),
|
||||
);
|
||||
}
|
||||
|
||||
if (item.verificationCode !== req.body["code"]) {
|
||||
return Response.sendErrorResponse(
|
||||
req,
|
||||
res,
|
||||
new BadDataException("Invalid code"),
|
||||
);
|
||||
}
|
||||
|
||||
await this.service.updateOneById({
|
||||
id: item.id!,
|
||||
props: {
|
||||
isRoot: true,
|
||||
},
|
||||
data: {
|
||||
isVerified: true,
|
||||
},
|
||||
});
|
||||
|
||||
return Response.sendEmptySuccessResponse(req, res);
|
||||
} catch (err) {
|
||||
return next(err);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
this.router.post(
|
||||
`${new this.entityType()
|
||||
.getCrudApiPath()
|
||||
?.toString()}/resend-verification-code`,
|
||||
UserMiddleware.getUserMiddleware,
|
||||
async (req: ExpressRequest, res: ExpressResponse, next: NextFunction) => {
|
||||
try {
|
||||
req = req as OneUptimeRequest;
|
||||
|
||||
if (!req.body.itemId) {
|
||||
return Response.sendErrorResponse(
|
||||
req,
|
||||
res,
|
||||
new BadDataException("Invalid item ID"),
|
||||
);
|
||||
}
|
||||
|
||||
await this.service.resendVerificationCode(req.body.itemId);
|
||||
|
||||
return Response.sendEmptySuccessResponse(req, res);
|
||||
} catch (err) {
|
||||
return next(err);
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
AppApiRoute,
|
||||
StatusPageApiRoute,
|
||||
DocsRoute,
|
||||
HomeRoute,
|
||||
} from "../ServiceRoute";
|
||||
import BillingConfig from "./BillingConfig";
|
||||
import Protocol from "../Types/API/Protocol";
|
||||
@@ -325,6 +326,8 @@ export const AccountsClientUrl: URL = new URL(
|
||||
AccountsRoute,
|
||||
);
|
||||
|
||||
export const HomeClientUrl: URL = new URL(HttpProtocol, Host, HomeRoute);
|
||||
|
||||
export const DocsClientUrl: URL = new URL(HttpProtocol, Host, DocsRoute);
|
||||
|
||||
export const DisableTelemetry: boolean =
|
||||
@@ -337,6 +340,12 @@ export const SlackAppClientSecret: string | null =
|
||||
export const SlackAppSigningSecret: string | null =
|
||||
process.env["SLACK_APP_SIGNING_SECRET"] || null;
|
||||
|
||||
// Microsoft Teams Configuration
|
||||
export const MicrosoftTeamsAppClientId: string | null =
|
||||
process.env["MICROSOFT_TEAMS_APP_CLIENT_ID"] || null;
|
||||
export const MicrosoftTeamsAppClientSecret: string | null =
|
||||
process.env["MICROSOFT_TEAMS_APP_CLIENT_SECRET"] || null;
|
||||
|
||||
// VAPID Configuration for Web Push Notifications
|
||||
export const VapidPublicKey: string | undefined =
|
||||
process.env["VAPID_PUBLIC_KEY"] || undefined;
|
||||
|
||||
BIN
Common/Server/Images/MicrosoftTeams/color.png
Normal file
BIN
Common/Server/Images/MicrosoftTeams/color.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 6.5 KiB |
BIN
Common/Server/Images/MicrosoftTeams/outline.png
Normal file
BIN
Common/Server/Images/MicrosoftTeams/outline.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.4 KiB |
@@ -122,7 +122,9 @@ export default class ClickhouseDatabase {
|
||||
}
|
||||
|
||||
const result: HTTPErrorResponse | HTTPResponse<JSONObject> =
|
||||
await API.get(URL.fromString(dbUrl.toString()));
|
||||
await API.get({
|
||||
url: URL.fromString(dbUrl.toString()),
|
||||
});
|
||||
|
||||
logger.debug("Clickhouse Connection Status Result");
|
||||
logger.debug(result);
|
||||
|
||||
@@ -11,8 +11,10 @@ export class AddEnableCustomSubscriberEmailNotificationFooterText1753131488925
|
||||
`ALTER TABLE "StatusPage" ADD "enableCustomSubscriberEmailNotificationFooterText" boolean NOT NULL DEFAULT false`,
|
||||
);
|
||||
|
||||
// Data migration: Set existing status pages to have enableCustomSubscriberEmailNotificationFooterText = true
|
||||
// This ensures backward compatibility for existing status pages that already have custom footer text
|
||||
/*
|
||||
* Data migration: Set existing status pages to have enableCustomSubscriberEmailNotificationFooterText = true
|
||||
* This ensures backward compatibility for existing status pages that already have custom footer text
|
||||
*/
|
||||
await queryRunner.query(
|
||||
`UPDATE "StatusPage" SET "enableCustomSubscriberEmailNotificationFooterText" = true WHERE "subscriberEmailNotificationFooterText" IS NOT NULL AND "subscriberEmailNotificationFooterText" != ''`,
|
||||
);
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
import { MigrationInterface, QueryRunner } from "typeorm";
|
||||
|
||||
export class MigrationName1758313975491 implements MigrationInterface {
|
||||
public name = "MigrationName1758313975491";
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "Project" ADD "discountPercent" integer NOT NULL DEFAULT '0'`,
|
||||
);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "Project" DROP COLUMN "discountPercent"`,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
import { MigrationInterface, QueryRunner } from "typeorm";
|
||||
|
||||
export class MigrationName1758626094132 implements MigrationInterface {
|
||||
public name = "MigrationName1758626094132";
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
`CREATE TABLE "TeamComplianceSetting" ("_id" uuid NOT NULL DEFAULT uuid_generate_v4(), "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "updatedAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "deletedAt" TIMESTAMP WITH TIME ZONE, "version" integer NOT NULL, "projectId" uuid NOT NULL, "teamId" uuid NOT NULL, "createdByUserId" uuid, "deletedByUserId" uuid, "ruleType" character varying(100) NOT NULL, "enabled" boolean NOT NULL DEFAULT false, "options" jsonb, CONSTRAINT "PK_7fdfcc67b176bbc26d188e84c3f" PRIMARY KEY ("_id"))`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`CREATE INDEX "IDX_510f0d225fc3cec252790630e7" ON "TeamComplianceSetting" ("projectId") `,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`CREATE INDEX "IDX_1afaa319cf5f476122b131e2b8" ON "TeamComplianceSetting" ("teamId") `,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "OnCallDutyPolicyScheduleLayer" ALTER COLUMN "rotation" SET DEFAULT '{"_type":"Recurring","value":{"intervalType":"Day","intervalCount":{"_type":"PositiveNumber","value":1}}}'`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "OnCallDutyPolicyScheduleLayer" ALTER COLUMN "restrictionTimes" SET DEFAULT '{"_type":"RestrictionTimes","value":{"restictionType":"None","dayRestrictionTimes":null,"weeklyRestrictionTimes":[]}}'`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "TeamComplianceSetting" ADD CONSTRAINT "FK_510f0d225fc3cec252790630e77" FOREIGN KEY ("projectId") REFERENCES "Project"("_id") ON DELETE CASCADE ON UPDATE NO ACTION`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "TeamComplianceSetting" ADD CONSTRAINT "FK_1afaa319cf5f476122b131e2b8f" FOREIGN KEY ("teamId") REFERENCES "Team"("_id") ON DELETE CASCADE ON UPDATE NO ACTION`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "TeamComplianceSetting" ADD CONSTRAINT "FK_a639b6b7654f5b6ee6eb431a036" FOREIGN KEY ("createdByUserId") REFERENCES "User"("_id") ON DELETE SET NULL ON UPDATE NO ACTION`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "TeamComplianceSetting" ADD CONSTRAINT "FK_380d3ad2f032b7a77956ef29095" FOREIGN KEY ("deletedByUserId") REFERENCES "User"("_id") ON DELETE SET NULL ON UPDATE NO ACTION`,
|
||||
);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "TeamComplianceSetting" DROP CONSTRAINT "FK_380d3ad2f032b7a77956ef29095"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "TeamComplianceSetting" DROP CONSTRAINT "FK_a639b6b7654f5b6ee6eb431a036"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "TeamComplianceSetting" DROP CONSTRAINT "FK_1afaa319cf5f476122b131e2b8f"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "TeamComplianceSetting" DROP CONSTRAINT "FK_510f0d225fc3cec252790630e77"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "OnCallDutyPolicyScheduleLayer" ALTER COLUMN "restrictionTimes" SET DEFAULT '{"_type": "RestrictionTimes", "value": {"restictionType": "None", "dayRestrictionTimes": null, "weeklyRestrictionTimes": []}}'`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "OnCallDutyPolicyScheduleLayer" ALTER COLUMN "rotation" SET DEFAULT '{"_type": "Recurring", "value": {"intervalType": "Day", "intervalCount": {"_type": "PositiveNumber", "value": 1}}}'`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`DROP INDEX "public"."IDX_1afaa319cf5f476122b131e2b8"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`DROP INDEX "public"."IDX_510f0d225fc3cec252790630e7"`,
|
||||
);
|
||||
await queryRunner.query(`DROP TABLE "TeamComplianceSetting"`);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
import { MigrationInterface, QueryRunner } from "typeorm";
|
||||
|
||||
export class MigrationName1758629540993 implements MigrationInterface {
|
||||
public name = "MigrationName1758629540993";
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "OnCallDutyPolicyScheduleLayer" ALTER COLUMN "rotation" SET DEFAULT '{"_type":"Recurring","value":{"intervalType":"Day","intervalCount":{"_type":"PositiveNumber","value":1}}}'`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "OnCallDutyPolicyScheduleLayer" ALTER COLUMN "restrictionTimes" SET DEFAULT '{"_type":"RestrictionTimes","value":{"restictionType":"None","dayRestrictionTimes":null,"weeklyRestrictionTimes":[]}}'`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "TeamComplianceSetting" DROP COLUMN "ruleType"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "TeamComplianceSetting" ADD "ruleType" character varying(500) NOT NULL`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`CREATE UNIQUE INDEX "IDX_ef7342919ff02501c2b0ddc029" ON "TeamComplianceSetting" ("teamId", "ruleType") `,
|
||||
);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
`DROP INDEX "public"."IDX_ef7342919ff02501c2b0ddc029"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "TeamComplianceSetting" DROP COLUMN "ruleType"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "TeamComplianceSetting" ADD "ruleType" character varying(100) NOT NULL`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "OnCallDutyPolicyScheduleLayer" ALTER COLUMN "restrictionTimes" SET DEFAULT '{"_type": "RestrictionTimes", "value": {"restictionType": "None", "dayRestrictionTimes": null, "weeklyRestrictionTimes": []}}'`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "OnCallDutyPolicyScheduleLayer" ALTER COLUMN "rotation" SET DEFAULT '{"_type": "Recurring", "value": {"intervalType": "Day", "intervalCount": {"_type": "PositiveNumber", "value": 1}}}'`,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
import { MigrationInterface, QueryRunner } from "typeorm";
|
||||
|
||||
export class MigrationName1758798730753 implements MigrationInterface {
|
||||
public name = "MigrationName1758798730753";
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "ProjectSCIM" ADD "enablePushGroups" boolean NOT NULL DEFAULT false`,
|
||||
);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "ProjectSCIM" DROP COLUMN "enablePushGroups"`,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
import { MigrationInterface, QueryRunner } from "typeorm";
|
||||
|
||||
export class MigrationName1759175457008 implements MigrationInterface {
|
||||
public name = "MigrationName1759175457008";
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
`CREATE TABLE "UserWebAuthn" ("_id" uuid NOT NULL DEFAULT uuid_generate_v4(), "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "updatedAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "deletedAt" TIMESTAMP WITH TIME ZONE, "version" integer NOT NULL, "name" character varying(100) NOT NULL, "credentialId" text NOT NULL, "publicKey" text NOT NULL, "counter" text NOT NULL, "transports" text, "isVerified" boolean NOT NULL DEFAULT false, "deletedByUserId" uuid, "userId" uuid, CONSTRAINT "UQ_ed9d287cb27cc360b9c3a4542e9" UNIQUE ("credentialId"), CONSTRAINT "PK_76a58e093d632ac5a9036bfac57" PRIMARY KEY ("_id"))`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "UserWebAuthn" ADD CONSTRAINT "FK_e14966d27e4991f5f53ef54cad5" FOREIGN KEY ("deletedByUserId") REFERENCES "User"("_id") ON DELETE SET NULL ON UPDATE NO ACTION`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "UserWebAuthn" ADD CONSTRAINT "FK_e7a7d2869a90899c5f76ec997c0" FOREIGN KEY ("userId") REFERENCES "User"("_id") ON DELETE CASCADE ON UPDATE NO ACTION`,
|
||||
);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "UserWebAuthn" DROP CONSTRAINT "FK_e7a7d2869a90899c5f76ec997c0"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "UserWebAuthn" DROP CONSTRAINT "FK_e14966d27e4991f5f53ef54cad5"`,
|
||||
);
|
||||
await queryRunner.query(`DROP TABLE "UserWebAuthn"`);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
import { MigrationInterface, QueryRunner } from "typeorm";
|
||||
|
||||
export class MigrationName1759232954703 implements MigrationInterface {
|
||||
public name = "MigrationName1759232954703";
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "User" DROP COLUMN "twoFactorSecretCode"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "User" DROP COLUMN "twoFactorAuthUrl"`,
|
||||
);
|
||||
await queryRunner.query(`ALTER TABLE "User" DROP COLUMN "backupCodes"`);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`ALTER TABLE "User" ADD "backupCodes" text`);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "User" ADD "twoFactorAuthUrl" character varying(100)`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "User" ADD "twoFactorSecretCode" character varying(100)`,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
import { MigrationInterface, QueryRunner } from "typeorm";
|
||||
|
||||
export class RenameUserTwoFactorAuthToUserTotpAuth1759234532998
|
||||
implements MigrationInterface
|
||||
{
|
||||
public name = "RenameUserTwoFactorAuthToUserTotpAuth1759234532998";
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.renameTable("UserTwoFactorAuth", "UserTotpAuth");
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.renameTable("UserTotpAuth", "UserTwoFactorAuth");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,331 @@
|
||||
import { MigrationInterface, QueryRunner } from "typeorm";
|
||||
|
||||
export class MigrationName1759943124812 implements MigrationInterface {
|
||||
public name = "MigrationName1759943124812";
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
`CREATE TABLE "WhatsAppLog" ("_id" uuid NOT NULL DEFAULT uuid_generate_v4(), "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "updatedAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "deletedAt" TIMESTAMP WITH TIME ZONE, "version" integer NOT NULL, "projectId" uuid NOT NULL, "toNumber" character varying(30) NOT NULL, "fromNumber" character varying(30), "messageText" text, "statusMessage" character varying(500), "status" character varying(100) NOT NULL, "whatsAppCostInUSDCents" integer NOT NULL DEFAULT '0', "incidentId" uuid, "userId" uuid, "alertId" uuid, "scheduledMaintenanceId" uuid, "statusPageId" uuid, "statusPageAnnouncementId" uuid, "teamId" uuid, "onCallDutyPolicyId" uuid, "onCallDutyPolicyEscalationRuleId" uuid, "onCallDutyPolicyScheduleId" uuid, "deletedByUserId" uuid, CONSTRAINT "PK_9800b27ad5072db21ff1e453300" PRIMARY KEY ("_id"))`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`CREATE INDEX "IDX_20e246495b31ec9720529ec13a" ON "WhatsAppLog" ("projectId") `,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`CREATE INDEX "IDX_ee24b8a69670171de6c19fdcaf" ON "WhatsAppLog" ("toNumber") `,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`CREATE INDEX "IDX_f9cf8acb2f63698431f4f18f48" ON "WhatsAppLog" ("fromNumber") `,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`CREATE INDEX "IDX_98e71cf97956e7938195be8451" ON "WhatsAppLog" ("whatsAppCostInUSDCents") `,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`CREATE INDEX "IDX_2f4c03d17243b8b3ddae2677ae" ON "WhatsAppLog" ("incidentId") `,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`CREATE INDEX "IDX_020a8349a02b6cfc79129a8deb" ON "WhatsAppLog" ("userId") `,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`CREATE INDEX "IDX_431a6f11acfd2795b17c652fbb" ON "WhatsAppLog" ("alertId") `,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`CREATE INDEX "IDX_048963c43c534478290408bdd7" ON "WhatsAppLog" ("scheduledMaintenanceId") `,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`CREATE INDEX "IDX_16379405298dc2b312ff456fc8" ON "WhatsAppLog" ("statusPageId") `,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`CREATE INDEX "IDX_4136974e8f1832f03df27057a7" ON "WhatsAppLog" ("statusPageAnnouncementId") `,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`CREATE INDEX "IDX_a638be5c4d5e8551f7be91dd8b" ON "WhatsAppLog" ("teamId") `,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`CREATE INDEX "IDX_dc907429d63e2d860bb290124e" ON "WhatsAppLog" ("onCallDutyPolicyId") `,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`CREATE INDEX "IDX_486f56105b72ee019cd9634272" ON "WhatsAppLog" ("onCallDutyPolicyEscalationRuleId") `,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`CREATE INDEX "IDX_f78a2014c70e4846df79f9e681" ON "WhatsAppLog" ("onCallDutyPolicyScheduleId") `,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`CREATE TABLE "UserWhatsApp" ("_id" uuid NOT NULL DEFAULT uuid_generate_v4(), "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "updatedAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "deletedAt" TIMESTAMP WITH TIME ZONE, "version" integer NOT NULL, "projectId" uuid NOT NULL, "phone" character varying(30) NOT NULL, "userId" uuid, "createdByUserId" uuid, "deletedByUserId" uuid, "isVerified" boolean NOT NULL DEFAULT false, "verificationCode" character varying(100) NOT NULL, CONSTRAINT "PK_19ab8aa5949cb38d08930e959ad" PRIMARY KEY ("_id"))`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`CREATE INDEX "IDX_cacaefed4f479bf300d4065c80" ON "UserWhatsApp" ("projectId") `,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`CREATE INDEX "IDX_b99e3db0cecd0e5f15b1f6738a" ON "UserWhatsApp" ("userId") `,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "Project" ADD "enableWhatsAppNotifications" boolean NOT NULL DEFAULT false`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "GlobalConfig" ADD "metaWhatsAppAccessToken" text`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "GlobalConfig" ADD CONSTRAINT "UQ_83bd5d0c54a21bfe12316fa6520" UNIQUE ("metaWhatsAppAccessToken")`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "GlobalConfig" ADD "metaWhatsAppPhoneNumberId" character varying(100)`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "GlobalConfig" ADD CONSTRAINT "UQ_ef032cda9dd2fac68daeedd7bd2" UNIQUE ("metaWhatsAppPhoneNumberId")`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "GlobalConfig" ADD "metaWhatsAppBusinessAccountId" character varying(100)`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "GlobalConfig" ADD CONSTRAINT "UQ_607e6e88215689951d9b3645f00" UNIQUE ("metaWhatsAppBusinessAccountId")`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "GlobalConfig" ADD "metaWhatsAppAppId" character varying(100)`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "GlobalConfig" ADD CONSTRAINT "UQ_e67fd0998ca781ec7db0e573e91" UNIQUE ("metaWhatsAppAppId")`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "GlobalConfig" ADD "metaWhatsAppAppSecret" text`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "GlobalConfig" ADD CONSTRAINT "UQ_d4669bf754f937bae16c4a1837c" UNIQUE ("metaWhatsAppAppSecret")`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "UserNotificationRule" ADD "userWhatsAppId" uuid`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "UserNotificationSetting" ADD "alertByWhatsApp" boolean NOT NULL DEFAULT false`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "UserOnCallLogTimeline" ADD "userWhatsAppId" uuid`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "OnCallDutyPolicyScheduleLayer" ALTER COLUMN "rotation" SET DEFAULT '{"_type":"Recurring","value":{"intervalType":"Day","intervalCount":{"_type":"PositiveNumber","value":1}}}'`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "OnCallDutyPolicyScheduleLayer" ALTER COLUMN "restrictionTimes" SET DEFAULT '{"_type":"RestrictionTimes","value":{"restictionType":"None","dayRestrictionTimes":null,"weeklyRestrictionTimes":[]}}'`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`CREATE INDEX "IDX_73297560a1a70e4fe47eac2986" ON "UserNotificationRule" ("userWhatsAppId") `,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`CREATE INDEX "IDX_0a67c82e4e093ae5c89d2d76bd" ON "UserOnCallLogTimeline" ("userWhatsAppId") `,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "WhatsAppLog" ADD CONSTRAINT "FK_20e246495b31ec9720529ec13a6" FOREIGN KEY ("projectId") REFERENCES "Project"("_id") ON DELETE CASCADE ON UPDATE NO ACTION`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "WhatsAppLog" ADD CONSTRAINT "FK_2f4c03d17243b8b3ddae2677ae1" FOREIGN KEY ("incidentId") REFERENCES "Incident"("_id") ON DELETE CASCADE ON UPDATE NO ACTION`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "WhatsAppLog" ADD CONSTRAINT "FK_020a8349a02b6cfc79129a8deba" FOREIGN KEY ("userId") REFERENCES "User"("_id") ON DELETE CASCADE ON UPDATE NO ACTION`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "WhatsAppLog" ADD CONSTRAINT "FK_431a6f11acfd2795b17c652fbb5" FOREIGN KEY ("alertId") REFERENCES "Alert"("_id") ON DELETE CASCADE ON UPDATE NO ACTION`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "WhatsAppLog" ADD CONSTRAINT "FK_048963c43c534478290408bdd78" FOREIGN KEY ("scheduledMaintenanceId") REFERENCES "ScheduledMaintenance"("_id") ON DELETE CASCADE ON UPDATE NO ACTION`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "WhatsAppLog" ADD CONSTRAINT "FK_16379405298dc2b312ff456fc88" FOREIGN KEY ("statusPageId") REFERENCES "StatusPage"("_id") ON DELETE CASCADE ON UPDATE NO ACTION`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "WhatsAppLog" ADD CONSTRAINT "FK_4136974e8f1832f03df27057a7e" FOREIGN KEY ("statusPageAnnouncementId") REFERENCES "StatusPageAnnouncement"("_id") ON DELETE CASCADE ON UPDATE NO ACTION`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "WhatsAppLog" ADD CONSTRAINT "FK_a638be5c4d5e8551f7be91dd8be" FOREIGN KEY ("teamId") REFERENCES "Team"("_id") ON DELETE CASCADE ON UPDATE NO ACTION`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "WhatsAppLog" ADD CONSTRAINT "FK_dc907429d63e2d860bb290124e3" FOREIGN KEY ("onCallDutyPolicyId") REFERENCES "OnCallDutyPolicy"("_id") ON DELETE CASCADE ON UPDATE NO ACTION`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "WhatsAppLog" ADD CONSTRAINT "FK_486f56105b72ee019cd96342723" FOREIGN KEY ("onCallDutyPolicyEscalationRuleId") REFERENCES "OnCallDutyPolicyEscalationRule"("_id") ON DELETE CASCADE ON UPDATE NO ACTION`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "WhatsAppLog" ADD CONSTRAINT "FK_f78a2014c70e4846df79f9e681a" FOREIGN KEY ("onCallDutyPolicyScheduleId") REFERENCES "OnCallDutyPolicySchedule"("_id") ON DELETE CASCADE ON UPDATE NO ACTION`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "WhatsAppLog" ADD CONSTRAINT "FK_2f75dca0a039aa9384de646f759" FOREIGN KEY ("deletedByUserId") REFERENCES "User"("_id") ON DELETE SET NULL ON UPDATE NO ACTION`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "UserWhatsApp" ADD CONSTRAINT "FK_cacaefed4f479bf300d4065c802" FOREIGN KEY ("projectId") REFERENCES "Project"("_id") ON DELETE CASCADE ON UPDATE NO ACTION`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "UserWhatsApp" ADD CONSTRAINT "FK_b99e3db0cecd0e5f15b1f6738aa" FOREIGN KEY ("userId") REFERENCES "User"("_id") ON DELETE CASCADE ON UPDATE NO ACTION`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "UserWhatsApp" ADD CONSTRAINT "FK_57d2f22db228562775e3274975a" FOREIGN KEY ("createdByUserId") REFERENCES "User"("_id") ON DELETE SET NULL ON UPDATE NO ACTION`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "UserWhatsApp" ADD CONSTRAINT "FK_e90592dde8357dd1afbf19073d8" FOREIGN KEY ("deletedByUserId") REFERENCES "User"("_id") ON DELETE SET NULL ON UPDATE NO ACTION`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "UserNotificationRule" ADD CONSTRAINT "FK_73297560a1a70e4fe47eac29861" FOREIGN KEY ("userWhatsAppId") REFERENCES "UserWhatsApp"("_id") ON DELETE CASCADE ON UPDATE NO ACTION`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "UserOnCallLogTimeline" ADD CONSTRAINT "FK_0a67c82e4e093ae5c89d2d76bdf" FOREIGN KEY ("userWhatsAppId") REFERENCES "UserWhatsApp"("_id") ON DELETE CASCADE ON UPDATE NO ACTION`,
|
||||
);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "UserOnCallLogTimeline" DROP CONSTRAINT "FK_0a67c82e4e093ae5c89d2d76bdf"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "UserNotificationRule" DROP CONSTRAINT "FK_73297560a1a70e4fe47eac29861"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "UserWhatsApp" DROP CONSTRAINT "FK_e90592dde8357dd1afbf19073d8"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "UserWhatsApp" DROP CONSTRAINT "FK_57d2f22db228562775e3274975a"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "UserWhatsApp" DROP CONSTRAINT "FK_b99e3db0cecd0e5f15b1f6738aa"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "UserWhatsApp" DROP CONSTRAINT "FK_cacaefed4f479bf300d4065c802"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "WhatsAppLog" DROP CONSTRAINT "FK_2f75dca0a039aa9384de646f759"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "WhatsAppLog" DROP CONSTRAINT "FK_f78a2014c70e4846df79f9e681a"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "WhatsAppLog" DROP CONSTRAINT "FK_486f56105b72ee019cd96342723"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "WhatsAppLog" DROP CONSTRAINT "FK_dc907429d63e2d860bb290124e3"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "WhatsAppLog" DROP CONSTRAINT "FK_a638be5c4d5e8551f7be91dd8be"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "WhatsAppLog" DROP CONSTRAINT "FK_4136974e8f1832f03df27057a7e"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "WhatsAppLog" DROP CONSTRAINT "FK_16379405298dc2b312ff456fc88"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "WhatsAppLog" DROP CONSTRAINT "FK_048963c43c534478290408bdd78"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "WhatsAppLog" DROP CONSTRAINT "FK_431a6f11acfd2795b17c652fbb5"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "WhatsAppLog" DROP CONSTRAINT "FK_020a8349a02b6cfc79129a8deba"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "WhatsAppLog" DROP CONSTRAINT "FK_2f4c03d17243b8b3ddae2677ae1"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "WhatsAppLog" DROP CONSTRAINT "FK_20e246495b31ec9720529ec13a6"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`DROP INDEX "public"."IDX_0a67c82e4e093ae5c89d2d76bd"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`DROP INDEX "public"."IDX_73297560a1a70e4fe47eac2986"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "OnCallDutyPolicyScheduleLayer" ALTER COLUMN "restrictionTimes" SET DEFAULT '{"_type": "RestrictionTimes", "value": {"restictionType": "None", "dayRestrictionTimes": null, "weeklyRestrictionTimes": []}}'`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "OnCallDutyPolicyScheduleLayer" ALTER COLUMN "rotation" SET DEFAULT '{"_type": "Recurring", "value": {"intervalType": "Day", "intervalCount": {"_type": "PositiveNumber", "value": 1}}}'`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "UserOnCallLogTimeline" DROP COLUMN "userWhatsAppId"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "UserNotificationSetting" DROP COLUMN "alertByWhatsApp"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "UserNotificationRule" DROP COLUMN "userWhatsAppId"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "GlobalConfig" DROP CONSTRAINT "UQ_d4669bf754f937bae16c4a1837c"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "GlobalConfig" DROP COLUMN "metaWhatsAppAppSecret"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "GlobalConfig" DROP CONSTRAINT "UQ_e67fd0998ca781ec7db0e573e91"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "GlobalConfig" DROP COLUMN "metaWhatsAppAppId"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "GlobalConfig" DROP CONSTRAINT "UQ_607e6e88215689951d9b3645f00"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "GlobalConfig" DROP COLUMN "metaWhatsAppBusinessAccountId"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "GlobalConfig" DROP CONSTRAINT "UQ_ef032cda9dd2fac68daeedd7bd2"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "GlobalConfig" DROP COLUMN "metaWhatsAppPhoneNumberId"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "GlobalConfig" DROP CONSTRAINT "UQ_83bd5d0c54a21bfe12316fa6520"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "GlobalConfig" DROP COLUMN "metaWhatsAppAccessToken"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "Project" DROP COLUMN "enableWhatsAppNotifications"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`DROP INDEX "public"."IDX_b99e3db0cecd0e5f15b1f6738a"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`DROP INDEX "public"."IDX_cacaefed4f479bf300d4065c80"`,
|
||||
);
|
||||
await queryRunner.query(`DROP TABLE "UserWhatsApp"`);
|
||||
await queryRunner.query(
|
||||
`DROP INDEX "public"."IDX_f78a2014c70e4846df79f9e681"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`DROP INDEX "public"."IDX_486f56105b72ee019cd9634272"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`DROP INDEX "public"."IDX_dc907429d63e2d860bb290124e"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`DROP INDEX "public"."IDX_a638be5c4d5e8551f7be91dd8b"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`DROP INDEX "public"."IDX_4136974e8f1832f03df27057a7"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`DROP INDEX "public"."IDX_16379405298dc2b312ff456fc8"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`DROP INDEX "public"."IDX_048963c43c534478290408bdd7"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`DROP INDEX "public"."IDX_431a6f11acfd2795b17c652fbb"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`DROP INDEX "public"."IDX_020a8349a02b6cfc79129a8deb"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`DROP INDEX "public"."IDX_2f4c03d17243b8b3ddae2677ae"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`DROP INDEX "public"."IDX_98e71cf97956e7938195be8451"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`DROP INDEX "public"."IDX_f9cf8acb2f63698431f4f18f48"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`DROP INDEX "public"."IDX_ee24b8a69670171de6c19fdcaf"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`DROP INDEX "public"."IDX_20e246495b31ec9720529ec13a"`,
|
||||
);
|
||||
await queryRunner.query(`DROP TABLE "WhatsAppLog"`);
|
||||
}
|
||||
}
|
||||
@@ -167,6 +167,14 @@ import { MigrationName1756300358095 } from "./1756300358095-MigrationName";
|
||||
import { MigrationName1756821449686 } from "./1756821449686-MigrationName";
|
||||
import { MigrationName1757416939595 } from "./1757416939595-MigrationName";
|
||||
import { MigrationName1757423505855 } from "./1757423505855-MigrationName";
|
||||
import { MigrationName1758313975491 } from "./1758313975491-MigrationName";
|
||||
import { MigrationName1758626094132 } from "./1758626094132-MigrationName";
|
||||
import { MigrationName1758629540993 } from "./1758629540993-MigrationName";
|
||||
import { MigrationName1758798730753 } from "./1758798730753-MigrationName";
|
||||
import { MigrationName1759175457008 } from "./1759175457008-MigrationName";
|
||||
import { MigrationName1759232954703 } from "./1759232954703-MigrationName";
|
||||
import { RenameUserTwoFactorAuthToUserTotpAuth1759234532998 } from "./1759234532998-MigrationName";
|
||||
import { MigrationName1759943124812 } from "./1759943124812-MigrationName";
|
||||
|
||||
export default [
|
||||
InitialMigration,
|
||||
@@ -338,4 +346,12 @@ export default [
|
||||
MigrationName1756821449686,
|
||||
MigrationName1757416939595,
|
||||
MigrationName1757423505855,
|
||||
MigrationName1758313975491,
|
||||
MigrationName1758626094132,
|
||||
MigrationName1758629540993,
|
||||
MigrationName1758798730753,
|
||||
MigrationName1759175457008,
|
||||
MigrationName1759232954703,
|
||||
RenameUserTwoFactorAuthToUserTotpAuth1759234532998,
|
||||
MigrationName1759943124812,
|
||||
];
|
||||
|
||||
@@ -12,6 +12,7 @@ import { createBullBoard } from "@bull-board/api";
|
||||
import { BullMQAdapter } from "@bull-board/api/bullMQAdapter";
|
||||
import { ExpressRouter } from "../Utils/Express";
|
||||
import CaptureSpan from "../Utils/Telemetry/CaptureSpan";
|
||||
import logger from "../Utils/Logger";
|
||||
|
||||
export enum QueueName {
|
||||
Workflow = "Workflow",
|
||||
@@ -29,6 +30,49 @@ export default class Queue {
|
||||
private static queueDict: Dictionary<BullQueue> = {};
|
||||
// track queues we have already run initial cleanup on
|
||||
private static cleanedQueueNames: Set<string> = new Set<string>();
|
||||
// store repeatable jobs to re-add on reconnect
|
||||
private static repeatableJobs: Dictionary<
|
||||
Dictionary<{
|
||||
jobName: string;
|
||||
data: JSONObject;
|
||||
options: JobsOptions;
|
||||
}>
|
||||
> = {};
|
||||
|
||||
private static async setupReconnectListener(
|
||||
queue: BullQueue,
|
||||
queueName: QueueName,
|
||||
): Promise<void> {
|
||||
const client: Awaited<typeof queue.client> = await queue.client;
|
||||
client.on("ready", async () => {
|
||||
logger.debug(`Queue ${queueName} reconnected, re-adding repeatable jobs`);
|
||||
const jobs:
|
||||
| Dictionary<{
|
||||
jobName: string;
|
||||
data: JSONObject;
|
||||
options: JobsOptions;
|
||||
}>
|
||||
| undefined = Queue.repeatableJobs[queueName];
|
||||
if (jobs) {
|
||||
for (const jobId in jobs) {
|
||||
const job:
|
||||
| { jobName: string; data: JSONObject; options: JobsOptions }
|
||||
| undefined = jobs[jobId];
|
||||
if (job) {
|
||||
try {
|
||||
logger.debug(
|
||||
`Re-adding repeatable job ${job.jobName} to queue ${queueName}`,
|
||||
);
|
||||
await queue.add(job.jobName, job.data, job.options);
|
||||
} catch (err: unknown) {
|
||||
logger.error("Error re-adding repeatable job");
|
||||
logger.error(err);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@CaptureSpan()
|
||||
public static getQueue(queueName: QueueName): BullQueue {
|
||||
@@ -49,8 +93,10 @@ export default class Queue {
|
||||
removeOnComplete: { count: 500 }, // keep last 1000 completed jobs
|
||||
removeOnFail: { count: 100 }, // keep last 500 failed jobs
|
||||
},
|
||||
// Optionally cap the event stream length (supported in BullMQ >= v5)
|
||||
// This helps prevent the :events stream from growing indefinitely
|
||||
/*
|
||||
* Optionally cap the event stream length (supported in BullMQ >= v5)
|
||||
* This helps prevent the :events stream from growing indefinitely
|
||||
*/
|
||||
streams: {
|
||||
events: { maxLen: 1000 },
|
||||
},
|
||||
@@ -59,6 +105,12 @@ export default class Queue {
|
||||
// save it to the dictionary
|
||||
this.queueDict[queueName] = queue;
|
||||
|
||||
// Add event listener to re-add repeatable jobs on reconnect
|
||||
this.setupReconnectListener(queue, queueName).catch((err: unknown) => {
|
||||
logger.error("Error setting up reconnect listener for queue");
|
||||
logger.error(err);
|
||||
});
|
||||
|
||||
// Fire-and-forget initial cleanup for legacy/old data if not done before
|
||||
if (!this.cleanedQueueNames.has(queueName)) {
|
||||
this.cleanedQueueNames.add(queueName);
|
||||
@@ -158,6 +210,18 @@ export default class Queue {
|
||||
);
|
||||
}
|
||||
|
||||
// Store repeatable jobs for re-adding on reconnect
|
||||
if (options && options.scheduleAt) {
|
||||
if (!this.repeatableJobs[queueName]) {
|
||||
this.repeatableJobs[queueName] = {};
|
||||
}
|
||||
this.repeatableJobs[queueName]![jobId] = {
|
||||
jobName,
|
||||
data,
|
||||
options: optionsObject,
|
||||
};
|
||||
}
|
||||
|
||||
const jobAdded: Job = await this.getQueue(queueName).add(
|
||||
jobName,
|
||||
data,
|
||||
|
||||
@@ -26,8 +26,10 @@ export default abstract class IO {
|
||||
);
|
||||
}
|
||||
|
||||
// const pubClient: ClientType = Redis.getClient()!.duplicate();
|
||||
// const subClient: ClientType = Redis.getClient()!.duplicate();
|
||||
/*
|
||||
* const pubClient: ClientType = Redis.getClient()!.duplicate();
|
||||
* const subClient: ClientType = Redis.getClient()!.duplicate();
|
||||
*/
|
||||
|
||||
// this.socketServer.adapter(createAdapter(pubClient, subClient));
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user