mirror of
https://github.com/OneUptime/oneuptime.git
synced 2026-04-06 08:42:13 +02:00
Compare commits
1041 Commits
9.3.19
...
api-ref-re
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c6ab306a08 | ||
|
|
14cd9d249f | ||
|
|
83149665e8 | ||
|
|
c2574d52da | ||
|
|
1ede78e669 | ||
|
|
c3875137a5 | ||
|
|
b1ed2c00c4 | ||
|
|
1809a6f726 | ||
|
|
3e48478b4f | ||
|
|
8ba6d5d058 | ||
|
|
d75e7f8d10 | ||
|
|
b23236e85f | ||
|
|
2fe2d2c614 | ||
|
|
f2cce35a04 | ||
|
|
93cead93fd | ||
|
|
de2a1b463e | ||
|
|
88871cac78 | ||
|
|
39bf5da168 | ||
|
|
23d2341051 | ||
|
|
c6d13d3647 | ||
|
|
1011f0704e | ||
|
|
5ff6942b93 | ||
|
|
93eff373a6 | ||
|
|
2fbc1b426d | ||
|
|
288874be45 | ||
|
|
47717fff17 | ||
|
|
5f58043b38 | ||
|
|
2bce40d993 | ||
|
|
81e8931c19 | ||
|
|
314ec696ce | ||
|
|
3e80060ff8 | ||
|
|
8e5f5fc854 | ||
|
|
5655d72fe1 | ||
|
|
c4063fecdc | ||
|
|
3e416c1679 | ||
|
|
006c41472c | ||
|
|
f6d8761fe8 | ||
|
|
a9e16a96d3 | ||
|
|
387f948abe | ||
|
|
e2ece21a8e | ||
|
|
70651ace2e | ||
|
|
3fd6582bc4 | ||
|
|
a6682624fc | ||
|
|
7bb7837ec9 | ||
|
|
f5d724a829 | ||
|
|
b3081e109d | ||
|
|
4aafdb2d28 | ||
|
|
6afc99f54d | ||
|
|
b971482aaa | ||
|
|
00ff39a0cd | ||
|
|
b14e319b66 | ||
|
|
43472de521 | ||
|
|
cf72280ffa | ||
|
|
049dd870d0 | ||
|
|
02000e1a28 | ||
|
|
32884930ae | ||
|
|
1a80b89fb8 | ||
|
|
ab83172d60 | ||
|
|
3a0d85174f | ||
|
|
6eee812701 | ||
|
|
319093d3d8 | ||
|
|
7b99129c2e | ||
|
|
ee1996e961 | ||
|
|
d82d63b54a | ||
|
|
5d2ab103ee | ||
|
|
c57bda87ab | ||
|
|
54554d50c1 | ||
|
|
3e6760dc7f | ||
|
|
c6d0200a98 | ||
|
|
fd9bd6ba6f | ||
|
|
3348f44bf1 | ||
|
|
b01ccf6318 | ||
|
|
9a8b989e85 | ||
|
|
f546126ee4 | ||
|
|
f903d0bea2 | ||
|
|
32fcb9398c | ||
|
|
eee7b4cb67 | ||
|
|
afb172c345 | ||
|
|
1f9fad972c | ||
|
|
d9d9e70fd6 | ||
|
|
8b3912d369 | ||
|
|
7bf03074b2 | ||
|
|
b05db3486e | ||
|
|
5e5fdb5402 | ||
|
|
9ec5efbd1e | ||
|
|
7d6e81cb8b | ||
|
|
a5e3e05f86 | ||
|
|
266fcb274e | ||
|
|
70a8c9974c | ||
|
|
87501d8d1b | ||
|
|
a31f9d9d28 | ||
|
|
d89be8ed57 | ||
|
|
509a010261 | ||
|
|
54754ff5ae | ||
|
|
7f9ed4d439 | ||
|
|
d62816dd49 | ||
|
|
7dd6129dad | ||
|
|
7ccea02340 | ||
|
|
0af41725b4 | ||
|
|
9f6bcddc1e | ||
|
|
97c461f7a3 | ||
|
|
736f8bb83c | ||
|
|
eb33daf64f | ||
|
|
c3c90eef03 | ||
|
|
e92e9f08d3 | ||
|
|
2b313a7702 | ||
|
|
3cf7c7d1ae | ||
|
|
76cfa7186e | ||
|
|
afaff717c0 | ||
|
|
fde0d5f2c6 | ||
|
|
d5c5387621 | ||
|
|
e0ef6e9a77 | ||
|
|
2dc0dc4c96 | ||
|
|
c9eb72ba2c | ||
|
|
92e247d168 | ||
|
|
14988c438a | ||
|
|
d81682d02f | ||
|
|
9d5faca3ec | ||
|
|
89ccde1bc4 | ||
|
|
3aab280dcd | ||
|
|
b8e44a1bcf | ||
|
|
4c3b4d23ff | ||
|
|
a4ff718d61 | ||
|
|
3433a815f3 | ||
|
|
2a20807126 | ||
|
|
991dc1c842 | ||
|
|
2026e7fd77 | ||
|
|
1d0016412e | ||
|
|
917f27fe11 | ||
|
|
c07c89e3dd | ||
|
|
32c4c1666d | ||
|
|
636a419cbd | ||
|
|
61699b9f4a | ||
|
|
b6ed3643c3 | ||
|
|
9e73ac45a1 | ||
|
|
7a3dbd0e8e | ||
|
|
1ec25c27ee | ||
|
|
5286527155 | ||
|
|
895af10755 | ||
|
|
77ccca7e2a | ||
|
|
66f46e9b84 | ||
|
|
91edae50b2 | ||
|
|
7ab3dfe043 | ||
|
|
fb661126d4 | ||
|
|
94c57f3189 | ||
|
|
4de6021905 | ||
|
|
c62a49d499 | ||
|
|
01fd5263ca | ||
|
|
d87eee68e8 | ||
|
|
3f4db5b7e0 | ||
|
|
462ad9d6ab | ||
|
|
6444d3d5cc | ||
|
|
415222561b | ||
|
|
8cf2661c63 | ||
|
|
a820f817ff | ||
|
|
576927c6c7 | ||
|
|
e866db9e18 | ||
|
|
8e91a786f9 | ||
|
|
02d16446f1 | ||
|
|
5d5517258b | ||
|
|
5df632c46c | ||
|
|
c1ee79b339 | ||
|
|
67265c0fc8 | ||
|
|
72e5384012 | ||
|
|
dc8e9d44b1 | ||
|
|
91102ee952 | ||
|
|
e46d1ae7da | ||
|
|
008005415a | ||
|
|
c7362f3ada | ||
|
|
1f634576fe | ||
|
|
d25a97fe17 | ||
|
|
b89ff11db8 | ||
|
|
5ac5ffede5 | ||
|
|
d9167b89ba | ||
|
|
66b995c64a | ||
|
|
f383bbba4d | ||
|
|
43f1a59042 | ||
|
|
7d49872edc | ||
|
|
6d2d5892b9 | ||
|
|
756217e19e | ||
|
|
ca3cf01be7 | ||
|
|
fd0a81a0b1 | ||
|
|
14016d188b | ||
|
|
3a2aff7f34 | ||
|
|
4a6edfee06 | ||
|
|
2dc1b8aa8c | ||
|
|
eb0f0e742d | ||
|
|
23c82c5239 | ||
|
|
2b61e4f4b7 | ||
|
|
9b21abf78d | ||
|
|
bd54b38a69 | ||
|
|
4dc799d238 | ||
|
|
b4d90e3bef | ||
|
|
6c8d4203da | ||
|
|
f7e9745624 | ||
|
|
f7d133adba | ||
|
|
b06c2cb1c3 | ||
|
|
b51c5d9677 | ||
|
|
9a1e265d1c | ||
|
|
e18d75fc8e | ||
|
|
5a68d2f726 | ||
|
|
dfa7c4875a | ||
|
|
8a568e0495 | ||
|
|
7152058ee2 | ||
|
|
b198dc0ec8 | ||
|
|
b0a3f8d60f | ||
|
|
83a13635cf | ||
|
|
a2c1744e8c | ||
|
|
9dfbc05618 | ||
|
|
4271ddbdcb | ||
|
|
6ffb081a02 | ||
|
|
16a9edbfcd | ||
|
|
e32d4395a3 | ||
|
|
b8cd3ce1c1 | ||
|
|
b86aee7f2a | ||
|
|
2cde167445 | ||
|
|
9bd6b011fe | ||
|
|
538eef5660 | ||
|
|
e1a343ae38 | ||
|
|
8b42af35c1 | ||
|
|
fc9026a8d8 | ||
|
|
86edee35c1 | ||
|
|
109c276bc5 | ||
|
|
8040dd0f56 | ||
|
|
00d4148b6b | ||
|
|
dec03bc3a8 | ||
|
|
46a9f95fc0 | ||
|
|
8b2f9bc778 | ||
|
|
fcc6223850 | ||
|
|
c9bc214e86 | ||
|
|
2897a937ba | ||
|
|
f3cd7be143 | ||
|
|
f324a4e864 | ||
|
|
f6a8cef649 | ||
|
|
770ef007a4 | ||
|
|
dafa0cc5d9 | ||
|
|
196e9cae10 | ||
|
|
d0d26d20b2 | ||
|
|
6a90ee97bf | ||
|
|
f2a2644b0e | ||
|
|
5cb2ac8c8b | ||
|
|
6751d59b2f | ||
|
|
aefc649743 | ||
|
|
cfba73665c | ||
|
|
049c5d003c | ||
|
|
fd8998952d | ||
|
|
5ca85e4915 | ||
|
|
aa401291b6 | ||
|
|
bf3d90871d | ||
|
|
4a4dff9264 | ||
|
|
fd3f75e4e2 | ||
|
|
43fc5acdda | ||
|
|
c7ca6138f3 | ||
|
|
87475b00c4 | ||
|
|
d5613cc4bd | ||
|
|
b1c9d9a645 | ||
|
|
01c6101ae9 | ||
|
|
ec56609bf4 | ||
|
|
e5f652a950 | ||
|
|
749bd2e41d | ||
|
|
cc23416ad8 | ||
|
|
86fda9ba16 | ||
|
|
969983043b | ||
|
|
2b64dd0b1d | ||
|
|
3a514969dc | ||
|
|
10d006890c | ||
|
|
2cb719d53a | ||
|
|
eafb543371 | ||
|
|
5800fe4f7a | ||
|
|
8d3712c36a | ||
|
|
f8e26246dd | ||
|
|
8560ecab41 | ||
|
|
5b2a6924d9 | ||
|
|
e047143974 | ||
|
|
d23dc791e2 | ||
|
|
a4b3b340c8 | ||
|
|
2173e4e611 | ||
|
|
dc19f87404 | ||
|
|
a3045c5f26 | ||
|
|
5d6907be97 | ||
|
|
e2ace9fc11 | ||
|
|
5a11bf228a | ||
|
|
cdd8d5523f | ||
|
|
f5029fada7 | ||
|
|
8131c9d42f | ||
|
|
946c7d4c48 | ||
|
|
9cbc7d9646 | ||
|
|
72d95871f7 | ||
|
|
3545a221bc | ||
|
|
9a5bcb9f31 | ||
|
|
91a4d3601c | ||
|
|
ea99dd4873 | ||
|
|
34863dbcb6 | ||
|
|
effeb3a0b6 | ||
|
|
86bdcb416a | ||
|
|
665f194f6d | ||
|
|
1378445dc5 | ||
|
|
236be5b60e | ||
|
|
256f4334eb | ||
|
|
28d5ad4292 | ||
|
|
5c169ccd5b | ||
|
|
e05f15d3f6 | ||
|
|
de0cbe1f42 | ||
|
|
fc48a0efdb | ||
|
|
e623c973ee | ||
|
|
8d56287892 | ||
|
|
0950d4288f | ||
|
|
56ea1c4690 | ||
|
|
08d2b6f5a2 | ||
|
|
2cabdde5bd | ||
|
|
3e48a706bd | ||
|
|
7c672e14a1 | ||
|
|
80a3bbac3d | ||
|
|
25f9b826cf | ||
|
|
c478e6af30 | ||
|
|
555a722732 | ||
|
|
6b5f981424 | ||
|
|
e0e614cf21 | ||
|
|
44cc072d98 | ||
|
|
c2c97dae0a | ||
|
|
3978374ccb | ||
|
|
6950daf10a | ||
|
|
7a07e669c9 | ||
|
|
67ece0fcca | ||
|
|
5413e24bd4 | ||
|
|
59b3fc0334 | ||
|
|
2bc72dbdb6 | ||
|
|
15ccf00503 | ||
|
|
b3d73a5523 | ||
|
|
43e6291608 | ||
|
|
09d82f64de | ||
|
|
51ed9fc2bb | ||
|
|
b23ccdcc57 | ||
|
|
147e687bac | ||
|
|
b84cebcb10 | ||
|
|
7374e3bf9a | ||
|
|
413ba90b02 | ||
|
|
2fd61385bd | ||
|
|
822bc9f8d5 | ||
|
|
e53a490606 | ||
|
|
cc53460e7a | ||
|
|
7d6e0488ba | ||
|
|
385a0fb9e5 | ||
|
|
584b79f48c | ||
|
|
92901b1647 | ||
|
|
bcbc4f6d99 | ||
|
|
04dd1260ac | ||
|
|
882f9f6ae4 | ||
|
|
549dc3546b | ||
|
|
25edcf7d9b | ||
|
|
46378fc3db | ||
|
|
f9f5bff4ce | ||
|
|
12b78249c5 | ||
|
|
f8cbc3a551 | ||
|
|
670b984cee | ||
|
|
e677e54ea9 | ||
|
|
928a2589c2 | ||
|
|
45f7a86888 | ||
|
|
0fa7848ab9 | ||
|
|
ae6e49da8f | ||
|
|
e80e22b1fa | ||
|
|
2adefd1cee | ||
|
|
36cbb3159a | ||
|
|
54909116b9 | ||
|
|
4582f6100a | ||
|
|
49a01eca8c | ||
|
|
349df0e181 | ||
|
|
c52116bec1 | ||
|
|
098a18005f | ||
|
|
6dbcd69ecd | ||
|
|
09a6827709 | ||
|
|
dbb1fa6c18 | ||
|
|
cd450bc3b6 | ||
|
|
047195116d | ||
|
|
564f21388b | ||
|
|
c69d7c949e | ||
|
|
dd47b9c3a9 | ||
|
|
ce731cb489 | ||
|
|
f725fdd2d9 | ||
|
|
1aec570c83 | ||
|
|
97b7e15ece | ||
|
|
7cdac5fe66 | ||
|
|
4add175070 | ||
|
|
711cfd2f6b | ||
|
|
6869ee670a | ||
|
|
c4d978cc3b | ||
|
|
1dffc2fbbe | ||
|
|
6b0756cd3a | ||
|
|
fbfa7747e0 | ||
|
|
a7c38dcbf2 | ||
|
|
6b8dd9e8b5 | ||
|
|
c5e7429b3d | ||
|
|
13ccee4e69 | ||
|
|
f9c9ac5ef0 | ||
|
|
10654a0a04 | ||
|
|
0d900dca78 | ||
|
|
bf5846e7f4 | ||
|
|
9fa48c7a25 | ||
|
|
9efb070334 | ||
|
|
85e7dd1150 | ||
|
|
072f162b6e | ||
|
|
9e01b0b75a | ||
|
|
3d7b98d1ee | ||
|
|
62f6900dd2 | ||
|
|
51cf4a88bf | ||
|
|
0dfd38d263 | ||
|
|
66424eee24 | ||
|
|
93adee4b16 | ||
|
|
d7efe2445c | ||
|
|
1bf4c52518 | ||
|
|
8348bf6897 | ||
|
|
7f2192206f | ||
|
|
ddf7636965 | ||
|
|
52514fbb7e | ||
|
|
2c3521561d | ||
|
|
8d6ef5a277 | ||
|
|
726ae7ef98 | ||
|
|
fde974d968 | ||
|
|
eae5e026fa | ||
|
|
5b01743e74 | ||
|
|
2cfc6a5e68 | ||
|
|
0976df1bee | ||
|
|
caa59aea7e | ||
|
|
ab5e0ec3c4 | ||
|
|
f4eda526c5 | ||
|
|
19f347a826 | ||
|
|
7eb84c2fb0 | ||
|
|
a27f3953ab | ||
|
|
4ec162208b | ||
|
|
13482b13d7 | ||
|
|
69c0253862 | ||
|
|
92d8b7b425 | ||
|
|
0ef053dc3d | ||
|
|
fa0bd99bc8 | ||
|
|
68b6ca9fd3 | ||
|
|
cd9b711ee4 | ||
|
|
b05d1652e1 | ||
|
|
d971573db0 | ||
|
|
5a11518c31 | ||
|
|
ce4f41367b | ||
|
|
f553726186 | ||
|
|
1bd746b285 | ||
|
|
82558fda59 | ||
|
|
3f1fe2bf1c | ||
|
|
d00fa80e47 | ||
|
|
dc4805c3b2 | ||
|
|
642fb95209 | ||
|
|
01baf60b2e | ||
|
|
225679f5d3 | ||
|
|
51f16e2213 | ||
|
|
0af23bbacb | ||
|
|
7b446a853c | ||
|
|
df480577ab | ||
|
|
c031cc2af3 | ||
|
|
ae17820d0d | ||
|
|
f2f3900506 | ||
|
|
4c8b92144c | ||
|
|
4c45e16f56 | ||
|
|
53e39724e7 | ||
|
|
849882d868 | ||
|
|
e3f8af83e5 | ||
|
|
18a5559116 | ||
|
|
3c3ecfc698 | ||
|
|
c22f7fec46 | ||
|
|
75d473f6d7 | ||
|
|
5d8f8e248e | ||
|
|
a7c3ea274f | ||
|
|
da324f49d9 | ||
|
|
6fd9223ee9 | ||
|
|
c062d651e8 | ||
|
|
1c264ac5a1 | ||
|
|
8923a4bff6 | ||
|
|
ab4e0cf85f | ||
|
|
8f63e93eba | ||
|
|
c06697f299 | ||
|
|
2590a8d671 | ||
|
|
e4eadc297b | ||
|
|
d0bf351dc1 | ||
|
|
3234cc7d09 | ||
|
|
0158271e6a | ||
|
|
9d50bf2535 | ||
|
|
c8ca2eacc5 | ||
|
|
66699901a7 | ||
|
|
68a33eee2f | ||
|
|
016793d77d | ||
|
|
7b040b659d | ||
|
|
c6db71f383 | ||
|
|
16923c750b | ||
|
|
f57173f43c | ||
|
|
9716d138ea | ||
|
|
8e1b6859f5 | ||
|
|
9751dd0d5f | ||
|
|
e84bd95f49 | ||
|
|
02b76539ab | ||
|
|
c40c33773b | ||
|
|
b0cebf9338 | ||
|
|
674e35dc70 | ||
|
|
d5f42141a0 | ||
|
|
8fb6da7d41 | ||
|
|
d14e77ee7f | ||
|
|
71e8a70717 | ||
|
|
370bdc6e21 | ||
|
|
d2a5d037c1 | ||
|
|
1ffa87d322 | ||
|
|
28ffde7983 | ||
|
|
4655e207a5 | ||
|
|
62a5b216a0 | ||
|
|
79fb9d18ca | ||
|
|
ba5e8fdaeb | ||
|
|
aa31bbab45 | ||
|
|
3c84365d61 | ||
|
|
ae59bd8300 | ||
|
|
b868206e82 | ||
|
|
ddbf971f1f | ||
|
|
483578ba4d | ||
|
|
614cb4413e | ||
|
|
ad43fc2df2 | ||
|
|
aa2a6deb9e | ||
|
|
dcb13bb401 | ||
|
|
5260364e91 | ||
|
|
1938e620bb | ||
|
|
c3fd71dcd4 | ||
|
|
aba191c533 | ||
|
|
847c019aea | ||
|
|
edf05944c1 | ||
|
|
5293876943 | ||
|
|
ca7a702c13 | ||
|
|
4020b4b647 | ||
|
|
650849f4ad | ||
|
|
9098261ac0 | ||
|
|
70c6abbb86 | ||
|
|
23bc5531f0 | ||
|
|
20404458e2 | ||
|
|
31d3ce949d | ||
|
|
61ed224ad0 | ||
|
|
71ea76ee62 | ||
|
|
7a4a0553ca | ||
|
|
cb57fa4a07 | ||
|
|
22bc222689 | ||
|
|
9d96170c42 | ||
|
|
d0f4d21177 | ||
|
|
e9a2167484 | ||
|
|
a8af991a80 | ||
|
|
8bb3a5b7ac | ||
|
|
d0b1efb660 | ||
|
|
4f3259c3b1 | ||
|
|
30b53a90a4 | ||
|
|
cf8377ceec | ||
|
|
ffc7dbc35f | ||
|
|
97fd817db4 | ||
|
|
aa7caaa193 | ||
|
|
a05853ea09 | ||
|
|
2ba96c093d | ||
|
|
ed3df77ca4 | ||
|
|
9ec363d222 | ||
|
|
a090ec2747 | ||
|
|
9fde4fece9 | ||
|
|
596798801a | ||
|
|
dcc87c46b2 | ||
|
|
33fdabaea3 | ||
|
|
35deea863b | ||
|
|
2b2bbbdd55 | ||
|
|
d8cd92c504 | ||
|
|
f6ef2fa97d | ||
|
|
8a86f6a94f | ||
|
|
4b30274915 | ||
|
|
01f7d7cc78 | ||
|
|
616e64110a | ||
|
|
903a72a4e1 | ||
|
|
699c1d4341 | ||
|
|
078a4e8180 | ||
|
|
29232e7052 | ||
|
|
a221f7247c | ||
|
|
c4b5aca463 | ||
|
|
9de4be6661 | ||
|
|
a532dcdd5f | ||
|
|
bde09d2326 | ||
|
|
b36ac68026 | ||
|
|
200a94692e | ||
|
|
3afc3a3302 | ||
|
|
fa52c30462 | ||
|
|
c229936d5c | ||
|
|
70b2fb8c16 | ||
|
|
264613c676 | ||
|
|
61209f967f | ||
|
|
f82de89f3f | ||
|
|
b94a095bef | ||
|
|
c08de3da35 | ||
|
|
748e18fd1b | ||
|
|
c0d7c34018 | ||
|
|
e249ee6e59 | ||
|
|
9eca0153ce | ||
|
|
c4deb0d0b4 | ||
|
|
7ecd86eca7 | ||
|
|
f3312a2417 | ||
|
|
e0558a4a0a | ||
|
|
e2a238e3e3 | ||
|
|
543c62df5a | ||
|
|
382c838d40 | ||
|
|
1a88832efc | ||
|
|
d1f97a3193 | ||
|
|
70a269b662 | ||
|
|
a4ae42fd08 | ||
|
|
6c0161543a | ||
|
|
4519292cc8 | ||
|
|
83993fc2a4 | ||
|
|
5340b04b26 | ||
|
|
2a6003e78f | ||
|
|
bfed03a10e | ||
|
|
d99b20327f | ||
|
|
3a317a8b55 | ||
|
|
0c64ba30b0 | ||
|
|
4400a7e5dd | ||
|
|
45aab853c4 | ||
|
|
b37c13d347 | ||
|
|
d13407494b | ||
|
|
724ab97874 | ||
|
|
15e2fdcf48 | ||
|
|
3a546f9b5a | ||
|
|
291a0f12f1 | ||
|
|
3d62b67bca | ||
|
|
4c31c2b651 | ||
|
|
acdcb2d5da | ||
|
|
43084263ab | ||
|
|
1bbc953462 | ||
|
|
7cef3956e8 | ||
|
|
4904a535d1 | ||
|
|
5db511036e | ||
|
|
8b9023d93d | ||
|
|
0546d1fb12 | ||
|
|
d523ae822d | ||
|
|
9fd781c083 | ||
|
|
8d743dbb59 | ||
|
|
5b3e97c10d | ||
|
|
252a81c9ae | ||
|
|
e6b414a94b | ||
|
|
f521091f8e | ||
|
|
19e112a8a8 | ||
|
|
84dd084dae | ||
|
|
439c1f8716 | ||
|
|
647b713375 | ||
|
|
a2e6b7a4fc | ||
|
|
b06bc71a2c | ||
|
|
367a80c413 | ||
|
|
f49e4bd5d0 | ||
|
|
a74a7e0a9a | ||
|
|
f4946449f3 | ||
|
|
9640732e29 | ||
|
|
6ef5e409da | ||
|
|
bcdfa034f6 | ||
|
|
959267a174 | ||
|
|
767db415d2 | ||
|
|
7ca81aa9f8 | ||
|
|
26bd4c7a90 | ||
|
|
9d29a1d00b | ||
|
|
935608d23d | ||
|
|
4a3000d3c3 | ||
|
|
b5df7042f7 | ||
|
|
a345390b9b | ||
|
|
d0a8c049ba | ||
|
|
56037adcf0 | ||
|
|
73e2fcf3c6 | ||
|
|
710bdea813 | ||
|
|
f07ba35310 | ||
|
|
9ef2163bc0 | ||
|
|
1f53a56b8f | ||
|
|
6998b63f59 | ||
|
|
dccddf3ebc | ||
|
|
2a7d076407 | ||
|
|
0e82b17f6b | ||
|
|
0aa7838fc5 | ||
|
|
b1d243896f | ||
|
|
ab70c2c041 | ||
|
|
22b6c5ace0 | ||
|
|
ad32579214 | ||
|
|
e525cc3708 | ||
|
|
db7eaacd14 | ||
|
|
a549daf9ab | ||
|
|
d9e65ce633 | ||
|
|
be2d33591d | ||
|
|
88897004a2 | ||
|
|
dceccf00fa | ||
|
|
f079a2b9e6 | ||
|
|
4044d705d6 | ||
|
|
a2cbe4e241 | ||
|
|
b509b57bb8 | ||
|
|
4a7f27a372 | ||
|
|
214ac678d3 | ||
|
|
9dabce1b7a | ||
|
|
9bc260847d | ||
|
|
229775935e | ||
|
|
668c35ba2e | ||
|
|
a808913049 | ||
|
|
54da185280 | ||
|
|
b1bc7bfde4 | ||
|
|
09d5ce0e1a | ||
|
|
3935446071 | ||
|
|
2463dcb2db | ||
|
|
0eb239a469 | ||
|
|
b865caae7a | ||
|
|
a3e25723af | ||
|
|
bae0338c36 | ||
|
|
f656f23836 | ||
|
|
a8a79162e4 | ||
|
|
04556835b0 | ||
|
|
84c5e50199 | ||
|
|
06fd44ecaf | ||
|
|
c535b68056 | ||
|
|
34c8e4fdec | ||
|
|
bf04796637 | ||
|
|
16e6c0c601 | ||
|
|
75a733e9b8 | ||
|
|
30217a64ec | ||
|
|
488295e303 | ||
|
|
973131b70a | ||
|
|
52cb00a1c4 | ||
|
|
ab0027a042 | ||
|
|
26a6d12809 | ||
|
|
0bdf74cab2 | ||
|
|
dd996539bc | ||
|
|
ad2ee2b0d6 | ||
|
|
ed6630c2d6 | ||
|
|
3af9121d6a | ||
|
|
e9d5a560ff | ||
|
|
d87b6da7c5 | ||
|
|
34d6c8edbe | ||
|
|
e9f63fb1e2 | ||
|
|
7a515f7ad8 | ||
|
|
4564baae70 | ||
|
|
4316fdbf81 | ||
|
|
be98736f4e | ||
|
|
ce7e10e3d9 | ||
|
|
21683de677 | ||
|
|
4dddec9966 | ||
|
|
b79a287791 | ||
|
|
6bd4b7257d | ||
|
|
438f8f4b6f | ||
|
|
75c1fedfba | ||
|
|
731a8e8b8f | ||
|
|
74768efea1 | ||
|
|
716d05b105 | ||
|
|
8ea7b26299 | ||
|
|
1538852bc4 | ||
|
|
a85607b996 | ||
|
|
a53db3b673 | ||
|
|
f6ec592cb6 | ||
|
|
d2e82fe50e | ||
|
|
30cb030470 | ||
|
|
3b84f5cece | ||
|
|
7f84f5c34d | ||
|
|
47b42d92c1 | ||
|
|
1928244a8e | ||
|
|
c3af14c3fe | ||
|
|
324666bafe | ||
|
|
8d4862f39f | ||
|
|
8c42627d46 | ||
|
|
590caa0563 | ||
|
|
15ac2bf749 | ||
|
|
d98103ca94 | ||
|
|
f8a6354fb4 | ||
|
|
c7cb3b3b20 | ||
|
|
978998b3f8 | ||
|
|
738e464c1e | ||
|
|
8882d62eac | ||
|
|
90cd819b0a | ||
|
|
a6873c687a | ||
|
|
4b54bd6b91 | ||
|
|
eaf2cbcb71 | ||
|
|
d9a1876ad4 | ||
|
|
c09f75faf0 | ||
|
|
674e32b95b | ||
|
|
84cab49386 | ||
|
|
479b83f6bf | ||
|
|
81ed9e0fc1 | ||
|
|
1f162461ad | ||
|
|
ad4f901e2f | ||
|
|
a885e2e8a8 | ||
|
|
8f11156011 | ||
|
|
1ef1101134 | ||
|
|
5664ad48dd | ||
|
|
e2608f56db | ||
|
|
78df267145 | ||
|
|
20e7e68e71 | ||
|
|
877d69f22e | ||
|
|
0791029f4a | ||
|
|
69c91a2c41 | ||
|
|
499d28c34c | ||
|
|
ec57a9ddbc | ||
|
|
e59db99b22 | ||
|
|
aaa3c4f602 | ||
|
|
29842c06e3 | ||
|
|
402ccf01d0 | ||
|
|
b7114304ee | ||
|
|
dca72856a2 | ||
|
|
1d7c758096 | ||
|
|
1e748365a5 | ||
|
|
a8022762a2 | ||
|
|
9c1ed659e9 | ||
|
|
42accb4204 | ||
|
|
529e5954d4 | ||
|
|
75ea34ef9e | ||
|
|
c4e1b8d97d | ||
|
|
b5626ef352 | ||
|
|
692d15159c | ||
|
|
91c163af9e | ||
|
|
fe71be64dd | ||
|
|
9e6587cb62 | ||
|
|
42b2f58d6a | ||
|
|
98afb63880 | ||
|
|
6d352c8579 | ||
|
|
21b761ddbf | ||
|
|
e51009e94d | ||
|
|
619136c9da | ||
|
|
d622334bd3 | ||
|
|
5508bf6302 | ||
|
|
4ba9151400 | ||
|
|
97fe212d15 | ||
|
|
af3738924e | ||
|
|
92f77c7ce2 | ||
|
|
ecb54381d8 | ||
|
|
0a9435ef1a | ||
|
|
2bc52c7b5d | ||
|
|
392b6dda9a | ||
|
|
0a6035ed65 | ||
|
|
70f9444aab | ||
|
|
088333c91c | ||
|
|
7fc7276207 | ||
|
|
631bf12c23 | ||
|
|
5ce158ebf3 | ||
|
|
4684f25f22 | ||
|
|
3d36d86bd6 | ||
|
|
93c017dbab | ||
|
|
c74204ed1f | ||
|
|
80125f500c | ||
|
|
2771efcd87 | ||
|
|
0e27802f1a | ||
|
|
a96c270a94 | ||
|
|
66d76676f5 | ||
|
|
cc0eb6a4b9 | ||
|
|
2d687a3275 | ||
|
|
47bca3fb9b | ||
|
|
79d3548492 | ||
|
|
3156302dbc | ||
|
|
84307250b7 | ||
|
|
c3986bd66a | ||
|
|
a4a56bf2c7 | ||
|
|
6644523c54 | ||
|
|
44aa046fec | ||
|
|
da5b9b4955 | ||
|
|
81dd803b62 | ||
|
|
efffa82cbf | ||
|
|
64b0c9f137 | ||
|
|
a3661e1626 | ||
|
|
4115deadc4 | ||
|
|
6fbd112964 | ||
|
|
a171c52c8d | ||
|
|
4f7d3ed2be | ||
|
|
9db97b3919 | ||
|
|
8bc545c90f | ||
|
|
19d1629e37 | ||
|
|
e7fd472c14 | ||
|
|
dd4a1416fc | ||
|
|
09b65b9a5b | ||
|
|
8fb1a1daf9 | ||
|
|
d3d0dedfee | ||
|
|
46635f4251 | ||
|
|
1068a5d96e | ||
|
|
d1e200a54f | ||
|
|
8220b0356c | ||
|
|
bc9b09aed5 | ||
|
|
6822718c46 | ||
|
|
9a935c4e90 | ||
|
|
6137199e63 | ||
|
|
6e6d989be4 | ||
|
|
5a7af27543 | ||
|
|
7c422b4384 | ||
|
|
b06de38f69 | ||
|
|
71723675d6 | ||
|
|
e699e323cb | ||
|
|
8e8bc54aed | ||
|
|
d3cf309aef | ||
|
|
a24f9d37a9 | ||
|
|
d5332ed494 | ||
|
|
23fdd3bfd7 | ||
|
|
714f8b4edf | ||
|
|
02f920a152 | ||
|
|
378663b03c | ||
|
|
78257ebda8 | ||
|
|
d29e876b96 | ||
|
|
88c1e23da9 | ||
|
|
cb50f89a12 | ||
|
|
869cc6d2b8 | ||
|
|
1566bd6a21 | ||
|
|
d175841b2a | ||
|
|
811fb49c2d | ||
|
|
290b59dfc9 | ||
|
|
acb57b6b32 | ||
|
|
ff1feb1a9f | ||
|
|
a99c09c05a | ||
|
|
b400965384 | ||
|
|
48a9523bb6 | ||
|
|
aa5ff55a9c | ||
|
|
08f8200d5b | ||
|
|
7030f27076 | ||
|
|
d7611b895b | ||
|
|
ec8afb2d0b | ||
|
|
81aeb373c4 | ||
|
|
cbc779ae0b | ||
|
|
9906115faf | ||
|
|
86d60f4688 | ||
|
|
d7f329fcff | ||
|
|
120d36f3dd | ||
|
|
13f22b1611 | ||
|
|
f9a89548e2 | ||
|
|
88c55f9e14 | ||
|
|
91965f3cc9 | ||
|
|
1383b1f3b0 | ||
|
|
cf45f089af | ||
|
|
ecfcbae86b | ||
|
|
b714ad168c | ||
|
|
59d6aeb2b4 | ||
|
|
be6d122879 | ||
|
|
b9fcfd5c61 | ||
|
|
9e902e5b76 | ||
|
|
4fa44b40c9 | ||
|
|
3baa081850 | ||
|
|
bcc7218091 | ||
|
|
91937ea9bc | ||
|
|
79835d411e | ||
|
|
8a965dcf1a | ||
|
|
549cbe7102 | ||
|
|
2f727b7707 | ||
|
|
5dd1f1a7f1 | ||
|
|
80defab7b2 | ||
|
|
5dea6fcbad | ||
|
|
5e6d5aebff | ||
|
|
d7e8dc3d92 | ||
|
|
0e21d2755b | ||
|
|
52ed66fafe | ||
|
|
37215aca51 | ||
|
|
441cd5c73d | ||
|
|
046e0c00cd | ||
|
|
e0a9ab8cfb | ||
|
|
ea47d2bd2c | ||
|
|
7b249f55b5 | ||
|
|
f261fe98f0 | ||
|
|
3070b549e3 | ||
|
|
4731a67ab4 | ||
|
|
b204b05bc3 | ||
|
|
9217d869dd | ||
|
|
02e512e6e8 | ||
|
|
475ad54a8f | ||
|
|
739cf632e5 | ||
|
|
e069e94971 | ||
|
|
391c9ea2e7 | ||
|
|
72a6b426ea | ||
|
|
86aca6a48e | ||
|
|
9b81a82eed | ||
|
|
c4ab245824 | ||
|
|
06cf878446 | ||
|
|
cd068f9219 | ||
|
|
53c0b1fb92 | ||
|
|
642a5a2982 | ||
|
|
c0c162cca5 | ||
|
|
c94a2db6fa | ||
|
|
3b4828eea1 | ||
|
|
5907bfe4d1 | ||
|
|
a2d9cda7d9 | ||
|
|
a8be03d3c9 | ||
|
|
272ae08048 | ||
|
|
5f2bda119a | ||
|
|
64cfeb5400 | ||
|
|
1e1d3e939e | ||
|
|
6894cae68c | ||
|
|
65b4a8217b | ||
|
|
5452342f2f | ||
|
|
d1f583fb47 | ||
|
|
41f3a4ce21 | ||
|
|
41f151b8eb | ||
|
|
60276876bd | ||
|
|
86fb8fbb30 | ||
|
|
5e9b5be0ad | ||
|
|
a9734dd18e | ||
|
|
511c65a01b | ||
|
|
a619f323e7 | ||
|
|
a021ad41ef | ||
|
|
1eddfff608 | ||
|
|
578774df08 | ||
|
|
82d0d68a7c | ||
|
|
8d9ba58964 | ||
|
|
52dbab88f6 | ||
|
|
387ebc9375 | ||
|
|
26f3e5bd5e | ||
|
|
7ed06d7391 | ||
|
|
3c2811000e | ||
|
|
5979e4f345 | ||
|
|
5c84699bae | ||
|
|
d153fc4cd4 | ||
|
|
34475f76f9 | ||
|
|
6565b7c803 | ||
|
|
c63923ed5b | ||
|
|
33d51932c5 | ||
|
|
557d14106c | ||
|
|
8d5395ae74 | ||
|
|
06e0100ede | ||
|
|
3db29ab264 | ||
|
|
7442e36b18 | ||
|
|
1fa446ec0c | ||
|
|
ef85d98362 | ||
|
|
46bccfb596 | ||
|
|
f7b2588647 | ||
|
|
b4106eb580 | ||
|
|
de05f727d7 | ||
|
|
5a3d6d9ccc | ||
|
|
7d5f813bac | ||
|
|
d4cb2587c9 | ||
|
|
75ca86d92d | ||
|
|
ddf3dcd8a8 | ||
|
|
bc1a30f877 | ||
|
|
194d87041c | ||
|
|
ba950928a4 | ||
|
|
449f780201 | ||
|
|
b95fe3ad4f | ||
|
|
0dc3e5fe8d | ||
|
|
36e0b18f13 | ||
|
|
c9e1a3b2b6 | ||
|
|
1a15e446ff | ||
|
|
3947b0bba1 | ||
|
|
3f24c910c0 | ||
|
|
8619ba379a | ||
|
|
34396c764e | ||
|
|
7ab6b8e135 | ||
|
|
54c7955c78 | ||
|
|
d5ae25545c | ||
|
|
f449191f84 | ||
|
|
3168b57e28 | ||
|
|
dbff165c34 | ||
|
|
497394e5ee | ||
|
|
bdc9683c04 | ||
|
|
ad5372e354 | ||
|
|
72a31714a8 |
@@ -33,6 +33,15 @@ stop
|
||||
|
||||
nohup.out*
|
||||
|
||||
# Large directories not needed for Docker builds
|
||||
E2E/playwright-report
|
||||
E2E/test-results
|
||||
Terraform
|
||||
HelmChart
|
||||
Scripts
|
||||
.git
|
||||
GoSDK
|
||||
|
||||
encrypted-credentials.tar
|
||||
encrypted-credentials/
|
||||
|
||||
|
||||
23
.github/workflows/build.yml
vendored
23
.github/workflows/build.yml
vendored
@@ -125,29 +125,6 @@ jobs:
|
||||
max_attempts: 3
|
||||
command: sudo docker build --no-cache -f ./Workflow/Dockerfile .
|
||||
|
||||
docker-build-api-reference:
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
CI_PIPELINE_ID: ${{github.run_number}}
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Preinstall
|
||||
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
|
||||
uses: nick-fields/retry@v3
|
||||
with:
|
||||
timeout_minutes: 45
|
||||
max_attempts: 3
|
||||
command: sudo docker build --no-cache -f ./APIReference/Dockerfile .
|
||||
|
||||
docker-build-docs:
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
|
||||
53
.github/workflows/compile.yml
vendored
53
.github/workflows/compile.yml
vendored
@@ -128,23 +128,6 @@ jobs:
|
||||
max_attempts: 3
|
||||
command: cd Workflow && npm install && npm run compile && npm run dep-check
|
||||
|
||||
compile-api-reference:
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
CI_PIPELINE_ID: ${{github.run_number}}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: latest
|
||||
- run: cd Common && npm install
|
||||
- name: Compile API Reference
|
||||
uses: nick-fields/retry@v3
|
||||
with:
|
||||
timeout_minutes: 30
|
||||
max_attempts: 3
|
||||
command: cd APIReference && npm install && npm run compile && npm run dep-check
|
||||
|
||||
compile-docs-reference:
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
@@ -389,6 +372,23 @@ jobs:
|
||||
max_attempts: 3
|
||||
command: cd MCP && npm update @oneuptime/common && npm install && npm run compile && npm run dep-check
|
||||
|
||||
compile-mobile-app:
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
CI_PIPELINE_ID: ${{github.run_number}}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: latest
|
||||
- run: cd Common && npm install && npm run compile
|
||||
- name: Compile MobileApp
|
||||
uses: nick-fields/retry@v3
|
||||
with:
|
||||
timeout_minutes: 30
|
||||
max_attempts: 3
|
||||
command: cd MobileApp && npm install && npm run compile
|
||||
|
||||
compile-ai-agent:
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
@@ -404,4 +404,21 @@ jobs:
|
||||
with:
|
||||
timeout_minutes: 30
|
||||
max_attempts: 3
|
||||
command: cd AIAgent && npm install && npm run compile && npm run dep-check
|
||||
command: cd AIAgent && npm install && npm run compile && npm run dep-check
|
||||
|
||||
compile-cli:
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
CI_PIPELINE_ID: ${{github.run_number}}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: latest
|
||||
- run: cd Common && npm install
|
||||
- name: Compile CLI
|
||||
uses: nick-fields/retry@v3
|
||||
with:
|
||||
timeout_minutes: 30
|
||||
max_attempts: 3
|
||||
command: cd CLI && npm install && npm run compile && npm run dep-check
|
||||
395
.github/workflows/release.yml
vendored
395
.github/workflows/release.yml
vendored
@@ -4,6 +4,13 @@ on:
|
||||
push:
|
||||
branches:
|
||||
- "release"
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
publish_android_to_store:
|
||||
description: 'Publish Android app to Google Play Store'
|
||||
required: false
|
||||
type: boolean
|
||||
default: false
|
||||
|
||||
jobs:
|
||||
generate-build-number:
|
||||
@@ -1701,7 +1708,7 @@ jobs:
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: '1.21'
|
||||
go-version: 'stable'
|
||||
cache: true
|
||||
|
||||
- name: Install GoReleaser
|
||||
@@ -1736,75 +1743,6 @@ jobs:
|
||||
|
||||
|
||||
|
||||
api-reference-docker-image-deploy:
|
||||
needs: [generate-build-number, read-version]
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
QEMU_CPU: max
|
||||
steps:
|
||||
- name: Free Disk Space (Ubuntu)
|
||||
uses: jlumbroso/free-disk-space@main
|
||||
with:
|
||||
tool-cache: false
|
||||
android: true
|
||||
dotnet: true
|
||||
haskell: true
|
||||
large-packages: true
|
||||
docker-images: true
|
||||
swap-storage: true
|
||||
- name: Docker Meta
|
||||
id: meta
|
||||
uses: docker/metadata-action@v4
|
||||
with:
|
||||
images: |
|
||||
oneuptime/api-reference
|
||||
ghcr.io/oneuptime/api-reference
|
||||
tags: |
|
||||
type=raw,value=release,enable=true
|
||||
type=semver,value=${{needs.read-version.outputs.major_minor}},pattern={{version}},enable=true
|
||||
|
||||
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ github.ref }}
|
||||
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: latest
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3
|
||||
with:
|
||||
image: tonistiigi/binfmt:qemu-v10.0.4
|
||||
|
||||
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Generate Dockerfile from Dockerfile.tpl
|
||||
run: npm run prerun
|
||||
|
||||
# Build and deploy nginx.
|
||||
|
||||
- name: Login to Docker Hub
|
||||
run: |
|
||||
echo "${{ secrets.DOCKERHUB_PASSWORD }}" | docker login --username "${{ secrets.DOCKERHUB_USERNAME }}" --password-stdin
|
||||
|
||||
- name: Login to GitHub Container Registry
|
||||
run: |
|
||||
echo "${{ secrets.GITHUB_TOKEN }}" | docker login ghcr.io --username "${{ github.repository_owner }}" --password-stdin
|
||||
|
||||
- name: Build and push
|
||||
run: |
|
||||
bash ./Scripts/GHA/build_docker_images.sh \
|
||||
--image api-reference \
|
||||
--version "${{needs.read-version.outputs.major_minor}}" \
|
||||
--dockerfile ./APIReference/Dockerfile \
|
||||
--context . \
|
||||
--platforms linux/amd64,linux/arm64 \
|
||||
--git-sha "${{ github.sha }}"
|
||||
|
||||
push-release-tags:
|
||||
name: Push release tags before GitHub release
|
||||
needs:
|
||||
@@ -1832,7 +1770,6 @@ jobs:
|
||||
- docs-docker-image-deploy
|
||||
- worker-docker-image-deploy
|
||||
- workflow-docker-image-deploy
|
||||
- api-reference-docker-image-deploy
|
||||
- test-e2e-release-saas
|
||||
- test-e2e-release-self-hosted
|
||||
runs-on: ubuntu-latest
|
||||
@@ -1861,8 +1798,7 @@ jobs:
|
||||
"ai-agent",
|
||||
"docs",
|
||||
"worker",
|
||||
"workflow",
|
||||
"api-reference"
|
||||
"workflow"
|
||||
]
|
||||
steps:
|
||||
- name: Set up Docker Buildx
|
||||
@@ -1909,16 +1845,55 @@ jobs:
|
||||
|
||||
test-e2e-release-saas:
|
||||
runs-on: ubuntu-latest
|
||||
needs: [telemetry-docker-image-deploy, mcp-docker-image-deploy, docs-docker-image-deploy, api-reference-docker-image-deploy, workflow-docker-image-deploy, accounts-docker-image-deploy, ai-agent-docker-image-deploy, admin-dashboard-docker-image-deploy, app-docker-image-deploy, dashboard-docker-image-deploy, probe-ingest-docker-image-deploy, server-monitor-ingest-docker-image-deploy, isolated-vm-docker-image-deploy, home-docker-image-deploy, worker-docker-image-deploy, otel-collector-docker-image-deploy, probe-docker-image-deploy, status-page-docker-image-deploy, test-docker-image-deploy, test-server-docker-image-deploy, publish-npm-packages, e2e-docker-image-deploy, helm-chart-deploy, generate-build-number, read-version, nginx-docker-image-deploy, incoming-request-ingest-docker-image-deploy]
|
||||
needs: [telemetry-docker-image-deploy, mcp-docker-image-deploy, docs-docker-image-deploy, workflow-docker-image-deploy, accounts-docker-image-deploy, ai-agent-docker-image-deploy, admin-dashboard-docker-image-deploy, app-docker-image-deploy, dashboard-docker-image-deploy, probe-ingest-docker-image-deploy, server-monitor-ingest-docker-image-deploy, isolated-vm-docker-image-deploy, home-docker-image-deploy, worker-docker-image-deploy, otel-collector-docker-image-deploy, probe-docker-image-deploy, status-page-docker-image-deploy, test-docker-image-deploy, test-server-docker-image-deploy, publish-npm-packages, e2e-docker-image-deploy, helm-chart-deploy, generate-build-number, read-version, nginx-docker-image-deploy, incoming-request-ingest-docker-image-deploy]
|
||||
env:
|
||||
CI_PIPELINE_ID: ${{github.run_number}}
|
||||
steps:
|
||||
# Docker compose needs a lot of space to build images, so we need to free up some space first in the GitHub Actions runner
|
||||
# Aggressively free disk space before anything else
|
||||
- name: Aggressive Disk Cleanup
|
||||
run: |
|
||||
echo "=== Disk space BEFORE cleanup ==="
|
||||
df -h /
|
||||
# Remove pre-installed software not needed for this job
|
||||
sudo rm -rf /usr/share/dotnet || true
|
||||
sudo rm -rf /usr/local/lib/android || true
|
||||
sudo rm -rf /opt/ghc || true
|
||||
sudo rm -rf /opt/hostedtoolcache || true
|
||||
sudo rm -rf /usr/local/share/boost || true
|
||||
sudo rm -rf /usr/local/graalvm/ || true
|
||||
sudo rm -rf /usr/local/share/powershell || true
|
||||
sudo rm -rf /usr/local/share/chromium || true
|
||||
sudo rm -rf /usr/local/lib/node_modules || true
|
||||
sudo rm -rf /usr/share/swift || true
|
||||
sudo rm -rf /usr/share/miniconda || true
|
||||
sudo rm -rf /usr/lib/google-cloud-sdk || true
|
||||
sudo rm -rf /usr/lib/jvm || true
|
||||
sudo rm -rf /usr/lib/firefox || true
|
||||
sudo rm -rf /usr/lib/heroku || true
|
||||
sudo rm -rf /usr/local/julia* || true
|
||||
sudo rm -rf /opt/az || true
|
||||
sudo rm -rf /opt/microsoft || true
|
||||
sudo rm -rf /opt/pipx || true
|
||||
sudo rm -rf /opt/actionarchivecache || true
|
||||
sudo rm -rf /imagegeneration || true
|
||||
sudo rm -rf /usr/share/az_* || true
|
||||
sudo rm -rf /usr/share/sbt || true
|
||||
sudo rm -rf /usr/share/gradle* || true
|
||||
sudo rm -rf /usr/share/kotlinc || true
|
||||
sudo rm -rf /usr/share/ri || true
|
||||
sudo rm -rf /usr/local/.ghcup || true
|
||||
# Clean apt cache
|
||||
sudo apt-get clean || true
|
||||
sudo rm -rf /var/lib/apt/lists/* || true
|
||||
# Clean temp files
|
||||
sudo rm -rf /tmp/* || true
|
||||
# Docker cleanup
|
||||
docker system prune -af --volumes || true
|
||||
echo "=== Disk space AFTER aggressive cleanup ==="
|
||||
df -h /
|
||||
- name: Free Disk Space (Ubuntu)
|
||||
uses: jlumbroso/free-disk-space@main
|
||||
with:
|
||||
# this might remove tools that are actually needed,
|
||||
# if set to "true" but frees about 6 GB
|
||||
tool-cache: true
|
||||
android: true
|
||||
dotnet: true
|
||||
@@ -1926,6 +1901,10 @@ jobs:
|
||||
large-packages: true
|
||||
docker-images: true
|
||||
swap-storage: true
|
||||
- name: Final Disk Space Check
|
||||
run: |
|
||||
echo "=== Disk space after all cleanup ==="
|
||||
df -h /
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
@@ -1997,16 +1976,55 @@ jobs:
|
||||
test-e2e-release-self-hosted:
|
||||
runs-on: ubuntu-latest
|
||||
# After all the jobs runs
|
||||
needs: [telemetry-docker-image-deploy, mcp-docker-image-deploy, incoming-request-ingest-docker-image-deploy, docs-docker-image-deploy, api-reference-docker-image-deploy, workflow-docker-image-deploy, accounts-docker-image-deploy, ai-agent-docker-image-deploy, admin-dashboard-docker-image-deploy, app-docker-image-deploy, dashboard-docker-image-deploy, probe-ingest-docker-image-deploy, server-monitor-ingest-docker-image-deploy, isolated-vm-docker-image-deploy, home-docker-image-deploy, worker-docker-image-deploy, otel-collector-docker-image-deploy, probe-docker-image-deploy, status-page-docker-image-deploy, test-docker-image-deploy, test-server-docker-image-deploy, publish-npm-packages, e2e-docker-image-deploy, helm-chart-deploy, generate-build-number, read-version, nginx-docker-image-deploy]
|
||||
needs: [telemetry-docker-image-deploy, mcp-docker-image-deploy, incoming-request-ingest-docker-image-deploy, docs-docker-image-deploy, workflow-docker-image-deploy, accounts-docker-image-deploy, ai-agent-docker-image-deploy, admin-dashboard-docker-image-deploy, app-docker-image-deploy, dashboard-docker-image-deploy, probe-ingest-docker-image-deploy, server-monitor-ingest-docker-image-deploy, isolated-vm-docker-image-deploy, home-docker-image-deploy, worker-docker-image-deploy, otel-collector-docker-image-deploy, probe-docker-image-deploy, status-page-docker-image-deploy, test-docker-image-deploy, test-server-docker-image-deploy, publish-npm-packages, e2e-docker-image-deploy, helm-chart-deploy, generate-build-number, read-version, nginx-docker-image-deploy]
|
||||
env:
|
||||
CI_PIPELINE_ID: ${{github.run_number}}
|
||||
steps:
|
||||
# Docker compose needs a lot of space to build images, so we need to free up some space first in the GitHub Actions runner
|
||||
# Aggressively free disk space before anything else
|
||||
- name: Aggressive Disk Cleanup
|
||||
run: |
|
||||
echo "=== Disk space BEFORE cleanup ==="
|
||||
df -h /
|
||||
# Remove pre-installed software not needed for this job
|
||||
sudo rm -rf /usr/share/dotnet || true
|
||||
sudo rm -rf /usr/local/lib/android || true
|
||||
sudo rm -rf /opt/ghc || true
|
||||
sudo rm -rf /opt/hostedtoolcache || true
|
||||
sudo rm -rf /usr/local/share/boost || true
|
||||
sudo rm -rf /usr/local/graalvm/ || true
|
||||
sudo rm -rf /usr/local/share/powershell || true
|
||||
sudo rm -rf /usr/local/share/chromium || true
|
||||
sudo rm -rf /usr/local/lib/node_modules || true
|
||||
sudo rm -rf /usr/share/swift || true
|
||||
sudo rm -rf /usr/share/miniconda || true
|
||||
sudo rm -rf /usr/lib/google-cloud-sdk || true
|
||||
sudo rm -rf /usr/lib/jvm || true
|
||||
sudo rm -rf /usr/lib/firefox || true
|
||||
sudo rm -rf /usr/lib/heroku || true
|
||||
sudo rm -rf /usr/local/julia* || true
|
||||
sudo rm -rf /opt/az || true
|
||||
sudo rm -rf /opt/microsoft || true
|
||||
sudo rm -rf /opt/pipx || true
|
||||
sudo rm -rf /opt/actionarchivecache || true
|
||||
sudo rm -rf /imagegeneration || true
|
||||
sudo rm -rf /usr/share/az_* || true
|
||||
sudo rm -rf /usr/share/sbt || true
|
||||
sudo rm -rf /usr/share/gradle* || true
|
||||
sudo rm -rf /usr/share/kotlinc || true
|
||||
sudo rm -rf /usr/share/ri || true
|
||||
sudo rm -rf /usr/local/.ghcup || true
|
||||
# Clean apt cache
|
||||
sudo apt-get clean || true
|
||||
sudo rm -rf /var/lib/apt/lists/* || true
|
||||
# Clean temp files
|
||||
sudo rm -rf /tmp/* || true
|
||||
# Docker cleanup
|
||||
docker system prune -af --volumes || true
|
||||
echo "=== Disk space AFTER aggressive cleanup ==="
|
||||
df -h /
|
||||
- name: Free Disk Space (Ubuntu)
|
||||
uses: jlumbroso/free-disk-space@main
|
||||
with:
|
||||
# this might remove tools that are actually needed,
|
||||
# if set to "true" but frees about 6 GB
|
||||
tool-cache: true
|
||||
android: true
|
||||
dotnet: true
|
||||
@@ -2014,6 +2032,10 @@ jobs:
|
||||
large-packages: true
|
||||
docker-images: true
|
||||
swap-storage: true
|
||||
- name: Final Disk Space Check
|
||||
run: |
|
||||
echo "=== Disk space after all cleanup ==="
|
||||
df -h /
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
@@ -2200,6 +2222,151 @@ jobs:
|
||||
tag_name: ${{needs.read-version.outputs.major_minor}}
|
||||
|
||||
|
||||
# Build Android release APK and attach to GitHub Release.
|
||||
# Required secrets setup guide: MobileApp/docs/RELEASE_SIGNING.md
|
||||
mobile-app-android-deploy:
|
||||
needs: [draft-github-release, generate-build-number, read-version]
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ github.ref }}
|
||||
|
||||
- name: Setup Java 17
|
||||
uses: actions/setup-java@v4
|
||||
with:
|
||||
distribution: 'temurin'
|
||||
java-version: '17'
|
||||
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: latest
|
||||
|
||||
- name: Install dependencies
|
||||
run: cd MobileApp && npm install
|
||||
|
||||
- name: Generate native Android project
|
||||
run: cd MobileApp && npx expo prebuild --platform android --no-install
|
||||
|
||||
- name: Decode Android keystore
|
||||
run: |
|
||||
echo "${{ secrets.ANDROID_KEYSTORE_BASE64 }}" | base64 --decode > /tmp/release.keystore
|
||||
|
||||
- name: Build release APK
|
||||
env:
|
||||
ANDROID_KEYSTORE_FILE: /tmp/release.keystore
|
||||
ANDROID_KEYSTORE_PASSWORD: ${{ secrets.ANDROID_KEYSTORE_PASSWORD }}
|
||||
ANDROID_KEY_ALIAS: ${{ secrets.ANDROID_KEY_ALIAS }}
|
||||
ANDROID_KEY_PASSWORD: ${{ secrets.ANDROID_KEY_PASSWORD }}
|
||||
run: |
|
||||
cd MobileApp/android
|
||||
./gradlew assembleRelease \
|
||||
-PversionName=${{ needs.read-version.outputs.major_minor }} \
|
||||
-PversionCode=${{ needs.generate-build-number.outputs.build_number }}
|
||||
|
||||
- name: Upload APK to GitHub Release
|
||||
uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
files: MobileApp/android/app/build/outputs/apk/release/*.apk
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
draft: true
|
||||
prerelease: false
|
||||
tag_name: ${{ needs.read-version.outputs.major_minor }}
|
||||
|
||||
# Build iOS release IPA and attach to GitHub Release.
|
||||
# Required secrets setup guide: MobileApp/docs/RELEASE_SIGNING.md
|
||||
mobile-app-ios-deploy:
|
||||
needs: [draft-github-release, generate-build-number, read-version]
|
||||
runs-on: macos-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ github.ref }}
|
||||
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: latest
|
||||
|
||||
- name: Install dependencies
|
||||
run: cd MobileApp && npm install
|
||||
|
||||
- name: Generate native iOS project
|
||||
run: cd MobileApp && npx expo prebuild --platform ios --no-install
|
||||
|
||||
- name: Install CocoaPods dependencies
|
||||
run: cd MobileApp/ios && pod install
|
||||
|
||||
- name: Import signing certificate
|
||||
env:
|
||||
IOS_DISTRIBUTION_CERTIFICATE_BASE64: ${{ secrets.IOS_DISTRIBUTION_CERTIFICATE_BASE64 }}
|
||||
IOS_DISTRIBUTION_CERTIFICATE_PASSWORD: ${{ secrets.IOS_DISTRIBUTION_CERTIFICATE_PASSWORD }}
|
||||
run: |
|
||||
CERTIFICATE_PATH=$RUNNER_TEMP/distribution.p12
|
||||
KEYCHAIN_PATH=$RUNNER_TEMP/app-signing.keychain-db
|
||||
KEYCHAIN_PASSWORD=$(openssl rand -base64 32)
|
||||
|
||||
echo "$IOS_DISTRIBUTION_CERTIFICATE_BASE64" | base64 --decode > "$CERTIFICATE_PATH"
|
||||
|
||||
security create-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH"
|
||||
security set-keychain-settings -lut 21600 "$KEYCHAIN_PATH"
|
||||
security unlock-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH"
|
||||
|
||||
security import "$CERTIFICATE_PATH" -P "$IOS_DISTRIBUTION_CERTIFICATE_PASSWORD" \
|
||||
-A -t cert -f pkcs12 -k "$KEYCHAIN_PATH"
|
||||
security set-key-partition-list -S apple-tool:,apple: -k "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH"
|
||||
security list-keychain -d user -s "$KEYCHAIN_PATH"
|
||||
|
||||
- name: Install provisioning profile
|
||||
env:
|
||||
IOS_PROVISIONING_PROFILE_BASE64: ${{ secrets.IOS_PROVISIONING_PROFILE_BASE64 }}
|
||||
run: |
|
||||
PROFILE_PATH=$RUNNER_TEMP/profile.mobileprovision
|
||||
echo "$IOS_PROVISIONING_PROFILE_BASE64" | base64 --decode > "$PROFILE_PATH"
|
||||
|
||||
mkdir -p ~/Library/MobileDevice/Provisioning\ Profiles
|
||||
cp "$PROFILE_PATH" ~/Library/MobileDevice/Provisioning\ Profiles/
|
||||
|
||||
- name: Build archive
|
||||
run: |
|
||||
cd MobileApp
|
||||
xcodebuild -workspace ios/OneUptime.xcworkspace \
|
||||
-scheme OneUptime \
|
||||
-configuration Release \
|
||||
-sdk iphoneos \
|
||||
-archivePath $RUNNER_TEMP/OneUptime.xcarchive \
|
||||
archive \
|
||||
MARKETING_VERSION=${{ needs.read-version.outputs.major_minor }} \
|
||||
CURRENT_PROJECT_VERSION=${{ needs.generate-build-number.outputs.build_number }}
|
||||
|
||||
- name: Prepare ExportOptions.plist with team ID
|
||||
env:
|
||||
IOS_TEAM_ID: ${{ secrets.IOS_TEAM_ID }}
|
||||
run: |
|
||||
/usr/libexec/PlistBuddy -c "Add :teamID string $IOS_TEAM_ID" MobileApp/ios/ExportOptions.plist || \
|
||||
/usr/libexec/PlistBuddy -c "Set :teamID $IOS_TEAM_ID" MobileApp/ios/ExportOptions.plist
|
||||
|
||||
- name: Export IPA
|
||||
run: |
|
||||
cd MobileApp
|
||||
xcodebuild -exportArchive \
|
||||
-archivePath $RUNNER_TEMP/OneUptime.xcarchive \
|
||||
-exportOptionsPlist ios/ExportOptions.plist \
|
||||
-exportPath $RUNNER_TEMP/build
|
||||
|
||||
- name: Upload IPA to GitHub Release
|
||||
uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
files: ${{ runner.temp }}/build/*.ipa
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
draft: true
|
||||
prerelease: false
|
||||
tag_name: ${{ needs.read-version.outputs.major_minor }}
|
||||
|
||||
- name: Cleanup keychain
|
||||
if: always()
|
||||
run: |
|
||||
security delete-keychain $RUNNER_TEMP/app-signing.keychain-db || true
|
||||
|
||||
finalize-github-release:
|
||||
name: Publish GitHub release
|
||||
needs: [infrastructure-agent-deploy, generate-build-number, read-version]
|
||||
@@ -2241,3 +2408,69 @@ jobs:
|
||||
throw new Error(`Failed to publish release for tag ${tag}: ${error.message ?? error}`);
|
||||
}
|
||||
|
||||
# Publish Android app to Google Play Store.
|
||||
# This job only runs when manually triggered via workflow_dispatch with publish_android_to_store=true.
|
||||
# Required secrets:
|
||||
# - GOOGLE_PLAY_SERVICE_ACCOUNT_JSON: Service account JSON key with Play Store publishing access
|
||||
# - ANDROID_KEYSTORE_BASE64: Base64-encoded release keystore
|
||||
# - ANDROID_KEYSTORE_PASSWORD: Keystore password
|
||||
# - ANDROID_KEY_ALIAS: Signing key alias
|
||||
# - ANDROID_KEY_PASSWORD: Signing key password
|
||||
publish-android-to-play-store:
|
||||
needs: [generate-build-number, read-version]
|
||||
if: github.event_name == 'workflow_dispatch' && github.event.inputs.publish_android_to_store == 'true'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ github.ref }}
|
||||
|
||||
- name: Setup Java 17
|
||||
uses: actions/setup-java@v4
|
||||
with:
|
||||
distribution: 'temurin'
|
||||
java-version: '17'
|
||||
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: latest
|
||||
|
||||
- name: Install dependencies
|
||||
run: cd MobileApp && npm install
|
||||
|
||||
- name: Generate native Android project
|
||||
run: cd MobileApp && npx expo prebuild --platform android --no-install
|
||||
|
||||
- name: Decode Android keystore
|
||||
run: |
|
||||
echo "${{ secrets.ANDROID_KEYSTORE_BASE64 }}" | base64 --decode > /tmp/release.keystore
|
||||
|
||||
- name: Build release AAB for Play Store
|
||||
env:
|
||||
ANDROID_KEYSTORE_FILE: /tmp/release.keystore
|
||||
ANDROID_KEYSTORE_PASSWORD: ${{ secrets.ANDROID_KEYSTORE_PASSWORD }}
|
||||
ANDROID_KEY_ALIAS: ${{ secrets.ANDROID_KEY_ALIAS }}
|
||||
ANDROID_KEY_PASSWORD: ${{ secrets.ANDROID_KEY_PASSWORD }}
|
||||
run: |
|
||||
cd MobileApp/android
|
||||
./gradlew bundleRelease \
|
||||
-PversionName=${{ needs.read-version.outputs.major_minor }} \
|
||||
-PversionCode=${{ needs.generate-build-number.outputs.build_number }}
|
||||
|
||||
- name: Upload AAB as build artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
if: always()
|
||||
with:
|
||||
name: android-aab-${{ needs.read-version.outputs.major_minor }}
|
||||
path: MobileApp/android/app/build/outputs/bundle/release/*.aab
|
||||
retention-days: 90
|
||||
|
||||
- name: Upload AAB to Google Play Store
|
||||
uses: r0adkll/upload-google-play@v1
|
||||
with:
|
||||
serviceAccountJsonPlainText: ${{ secrets.GOOGLE_PLAY_SERVICE_ACCOUNT_JSON }}
|
||||
packageName: com.oneuptime.oncall
|
||||
releaseFiles: MobileApp/android/app/build/outputs/bundle/release/*.aab
|
||||
track: production
|
||||
status: completed
|
||||
|
||||
|
||||
143
.github/workflows/terraform-provider-e2e.yml
vendored
Normal file
143
.github/workflows/terraform-provider-e2e.yml
vendored
Normal file
@@ -0,0 +1,143 @@
|
||||
name: Terraform Provider E2E Tests
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
- master
|
||||
- develop
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
terraform-e2e-tests:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 120
|
||||
env:
|
||||
CI_PIPELINE_ID: ${{ github.run_number }}
|
||||
APP_TAG: latest
|
||||
|
||||
steps:
|
||||
- name: Free Disk Space (Ubuntu)
|
||||
uses: jlumbroso/free-disk-space@main
|
||||
with:
|
||||
tool-cache: true
|
||||
android: true
|
||||
dotnet: true
|
||||
haskell: true
|
||||
large-packages: true
|
||||
docker-images: true
|
||||
swap-storage: true
|
||||
|
||||
- name: Additional Disk Cleanup
|
||||
run: |
|
||||
echo "=== Initial disk space ==="
|
||||
df -h
|
||||
|
||||
echo "=== Removing unnecessary tools and libraries ==="
|
||||
# Remove Android SDK (if not already removed)
|
||||
sudo rm -rf /usr/local/lib/android || true
|
||||
|
||||
# Remove .NET SDK and runtime
|
||||
sudo rm -rf /usr/share/dotnet || true
|
||||
sudo rm -rf /etc/skel/.dotnet || true
|
||||
|
||||
# Remove Haskell/GHC
|
||||
sudo rm -rf /opt/ghc || true
|
||||
sudo rm -rf /usr/local/.ghcup || true
|
||||
|
||||
# Remove CodeQL
|
||||
sudo rm -rf /opt/hostedtoolcache/CodeQL || true
|
||||
|
||||
# Remove Boost
|
||||
sudo rm -rf /usr/local/share/boost || true
|
||||
|
||||
# Remove Swift
|
||||
sudo rm -rf /usr/share/swift || true
|
||||
|
||||
# Remove Julia
|
||||
sudo rm -rf /usr/local/julia* || true
|
||||
|
||||
# Remove Rust (cargo/rustup)
|
||||
sudo rm -rf /usr/share/rust || true
|
||||
sudo rm -rf /home/runner/.rustup || true
|
||||
sudo rm -rf /home/runner/.cargo || true
|
||||
|
||||
# Remove unnecessary hostedtoolcache items
|
||||
sudo rm -rf /opt/hostedtoolcache/Python || true
|
||||
sudo rm -rf /opt/hostedtoolcache/PyPy || true
|
||||
sudo rm -rf /opt/hostedtoolcache/Ruby || true
|
||||
sudo rm -rf /opt/hostedtoolcache/Java* || true
|
||||
|
||||
# Remove additional large directories
|
||||
sudo rm -rf /usr/share/miniconda || true
|
||||
sudo rm -rf /usr/local/graalvm || true
|
||||
sudo rm -rf /usr/local/share/chromium || true
|
||||
sudo rm -rf /usr/local/share/powershell || true
|
||||
sudo rm -rf /usr/share/az_* || true
|
||||
|
||||
# Remove documentation
|
||||
sudo rm -rf /usr/share/doc || true
|
||||
sudo rm -rf /usr/share/man || true
|
||||
|
||||
# Remove unnecessary locales
|
||||
sudo rm -rf /usr/share/locale || true
|
||||
|
||||
# Clean apt cache
|
||||
sudo apt-get clean || true
|
||||
sudo rm -rf /var/lib/apt/lists/* || true
|
||||
sudo rm -rf /var/cache/apt/archives/* || true
|
||||
|
||||
# Clean tmp
|
||||
sudo rm -rf /tmp/* || true
|
||||
|
||||
echo "=== Moving Docker data to /mnt for more space ==="
|
||||
# Stop docker
|
||||
sudo systemctl stop docker || true
|
||||
|
||||
# Move docker data directory to /mnt (which has ~70GB)
|
||||
sudo mv /var/lib/docker /mnt/docker || true
|
||||
sudo mkdir -p /var/lib/docker || true
|
||||
sudo mount --bind /mnt/docker /var/lib/docker || true
|
||||
|
||||
# Restart docker
|
||||
sudo systemctl start docker || true
|
||||
|
||||
echo "=== Final disk space ==="
|
||||
df -h
|
||||
|
||||
echo "=== Docker info ==="
|
||||
docker info | grep -E "Docker Root Dir|Storage Driver" || true
|
||||
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: latest
|
||||
cache: 'npm'
|
||||
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: 'stable'
|
||||
cache: true
|
||||
|
||||
- name: Setup Terraform
|
||||
uses: hashicorp/setup-terraform@v3
|
||||
with:
|
||||
terraform_version: "1.6.0"
|
||||
terraform_wrapper: false
|
||||
|
||||
- name: Run E2E Tests
|
||||
uses: nick-fields/retry@v3
|
||||
with:
|
||||
timeout_minutes: 60
|
||||
max_attempts: 3
|
||||
command: |
|
||||
chmod +x ./E2E/Terraform/e2e-tests/scripts/*.sh
|
||||
./E2E/Terraform/e2e-tests/scripts/index.sh
|
||||
@@ -28,7 +28,7 @@ jobs:
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: '1.21'
|
||||
go-version: 'stable'
|
||||
cache: true
|
||||
|
||||
- name: Install Common dependencies
|
||||
|
||||
73
.github/workflows/test-release.yaml
vendored
73
.github/workflows/test-release.yaml
vendored
@@ -1287,77 +1287,6 @@ jobs:
|
||||
|
||||
|
||||
|
||||
api-reference-docker-image-deploy:
|
||||
needs: [read-version, generate-build-number]
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
QEMU_CPU: max
|
||||
steps:
|
||||
- name: Free Disk Space (Ubuntu)
|
||||
uses: jlumbroso/free-disk-space@main
|
||||
with:
|
||||
tool-cache: false
|
||||
android: true
|
||||
dotnet: true
|
||||
haskell: true
|
||||
large-packages: true
|
||||
docker-images: true
|
||||
swap-storage: true
|
||||
- name: Docker Meta
|
||||
id: meta
|
||||
uses: docker/metadata-action@v4
|
||||
with:
|
||||
images: |
|
||||
oneuptime/api-reference
|
||||
ghcr.io/oneuptime/api-reference
|
||||
tags: |
|
||||
type=raw,value=test,enable=true
|
||||
type=raw,value=${{needs.read-version.outputs.major_minor}}-test,enable=true
|
||||
|
||||
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ github.ref }}
|
||||
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: latest
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3
|
||||
with:
|
||||
image: tonistiigi/binfmt:qemu-v10.0.4
|
||||
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Generate Dockerfile from Dockerfile.tpl
|
||||
run: npm run prerun
|
||||
|
||||
# Build and deploy app.
|
||||
|
||||
- name: Login to Docker Hub
|
||||
run: |
|
||||
echo "${{ secrets.DOCKERHUB_PASSWORD }}" | docker login --username "${{ secrets.DOCKERHUB_USERNAME }}" --password-stdin
|
||||
|
||||
- name: Login to GitHub Container Registry
|
||||
run: |
|
||||
echo "${{ secrets.GITHUB_TOKEN }}" | docker login ghcr.io --username "${{ github.repository_owner }}" --password-stdin
|
||||
|
||||
- name: Build and push
|
||||
run: |
|
||||
bash ./Scripts/GHA/build_docker_images.sh \
|
||||
--image api-reference \
|
||||
--version "${{needs.read-version.outputs.major_minor}}-test" \
|
||||
--dockerfile ./APIReference/Dockerfile \
|
||||
--context . \
|
||||
--platforms linux/amd64,linux/arm64 \
|
||||
--git-sha "${{ github.sha }}" \
|
||||
--extra-tags test \
|
||||
--extra-enterprise-tags enterprise-test
|
||||
|
||||
|
||||
accounts-docker-image-deploy:
|
||||
needs: [read-version, generate-build-number]
|
||||
runs-on: ubuntu-latest
|
||||
@@ -1725,7 +1654,7 @@ jobs:
|
||||
|
||||
test-helm-chart:
|
||||
runs-on: ubuntu-latest
|
||||
needs: [infrastructure-agent-deploy, mcp-docker-image-deploy, publish-terraform-provider, telemetry-docker-image-deploy, docs-docker-image-deploy, worker-docker-image-deploy, workflow-docker-image-deploy, isolated-vm-docker-image-deploy, home-docker-image-deploy, api-reference-docker-image-deploy, test-server-docker-image-deploy, test-docker-image-deploy, probe-ingest-docker-image-deploy, server-monitor-ingest-docker-image-deploy, probe-docker-image-deploy, dashboard-docker-image-deploy, admin-dashboard-docker-image-deploy, app-docker-image-deploy, accounts-docker-image-deploy, ai-agent-docker-image-deploy, otel-collector-docker-image-deploy, status-page-docker-image-deploy, nginx-docker-image-deploy, e2e-docker-image-deploy, incoming-request-ingest-docker-image-deploy]
|
||||
needs: [infrastructure-agent-deploy, mcp-docker-image-deploy, publish-terraform-provider, telemetry-docker-image-deploy, docs-docker-image-deploy, worker-docker-image-deploy, workflow-docker-image-deploy, isolated-vm-docker-image-deploy, home-docker-image-deploy, test-server-docker-image-deploy, test-docker-image-deploy, probe-ingest-docker-image-deploy, server-monitor-ingest-docker-image-deploy, probe-docker-image-deploy, dashboard-docker-image-deploy, admin-dashboard-docker-image-deploy, app-docker-image-deploy, accounts-docker-image-deploy, ai-agent-docker-image-deploy, otel-collector-docker-image-deploy, status-page-docker-image-deploy, nginx-docker-image-deploy, e2e-docker-image-deploy, incoming-request-ingest-docker-image-deploy]
|
||||
env:
|
||||
CI_PIPELINE_ID: ${{github.run_number}}
|
||||
steps:
|
||||
|
||||
21
.github/workflows/test.cli.yaml
vendored
Normal file
21
.github/workflows/test.cli.yaml
vendored
Normal file
@@ -0,0 +1,21 @@
|
||||
name: CLI Test
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
push:
|
||||
branches-ignore:
|
||||
- 'hotfix-*' # excludes hotfix branches
|
||||
- 'release'
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
CI_PIPELINE_ID: ${{github.run_number}}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: latest
|
||||
- run: cd Common && npm install
|
||||
- run: cd CLI && npm install && npm run test
|
||||
39
.github/workflows/test.mobile-app.yaml
vendored
Normal file
39
.github/workflows/test.mobile-app.yaml
vendored
Normal file
@@ -0,0 +1,39 @@
|
||||
name: MobileApp Test
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
push:
|
||||
branches-ignore:
|
||||
- 'hotfix-*' # excludes hotfix branches
|
||||
- 'release'
|
||||
|
||||
jobs:
|
||||
expo-doctor:
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
CI_PIPELINE_ID: ${{github.run_number}}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: latest
|
||||
- run: cd MobileApp && npm install
|
||||
- name: Run Expo Doctor
|
||||
run: cd MobileApp && npx expo-doctor@latest
|
||||
|
||||
expo-web-export:
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
CI_PIPELINE_ID: ${{github.run_number}}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: latest
|
||||
- run: cd MobileApp && npm install
|
||||
- name: Export Web Bundle
|
||||
uses: nick-fields/retry@v3
|
||||
with:
|
||||
timeout_minutes: 30
|
||||
max_attempts: 3
|
||||
command: cd MobileApp && npx expo export --platform web
|
||||
13
.gitignore
vendored
13
.gitignore
vendored
@@ -116,8 +116,8 @@ InfrastructureAgent/oneuptime-infrastructure-agent
|
||||
# Terraform generated files
|
||||
openapi.json
|
||||
|
||||
Terraform/**
|
||||
|
||||
Terraform/terraform-provider-oneuptime/**
|
||||
Terraform/openapi.json
|
||||
TerraformTest/**
|
||||
|
||||
terraform-provider-example/**
|
||||
@@ -129,3 +129,12 @@ MCP/node_modules
|
||||
Dashboard/public/sw.js
|
||||
.claude/settings.local.json
|
||||
Common/.claude/settings.local.json
|
||||
E2E/Terraform/e2e-tests/test-env.sh
|
||||
|
||||
# Terraform state and plan files
|
||||
*.tfplan
|
||||
tfplan
|
||||
terraform.tfstate
|
||||
terraform.tfstate.backup
|
||||
.terraform/
|
||||
.terraform.lock.hcl
|
||||
|
||||
@@ -49,5 +49,4 @@ LICENSE
|
||||
marketing/*/*
|
||||
licenses/*
|
||||
certifications/*
|
||||
ApiReference/public/assets/*
|
||||
JavaScriptSDK/src/cli/server-monitor/out/scripts/prettify/*
|
||||
|
||||
14
.vscode/launch.json
vendored
14
.vscode/launch.json
vendored
@@ -105,20 +105,6 @@
|
||||
"restart": true,
|
||||
"autoAttachChildProcesses": true
|
||||
},
|
||||
{
|
||||
"address": "127.0.0.1",
|
||||
"localRoot": "${workspaceFolder}/APIReference",
|
||||
"name": "API Reference: Debug with Docker",
|
||||
"port": 8737,
|
||||
"remoteRoot": "/usr/src/app",
|
||||
"request": "attach",
|
||||
"skipFiles": [
|
||||
"<node_internals>/**"
|
||||
],
|
||||
"type": "node",
|
||||
"restart": true,
|
||||
"autoAttachChildProcesses": true
|
||||
},
|
||||
{
|
||||
"address": "127.0.0.1",
|
||||
"localRoot": "${workspaceFolder}/TestServer",
|
||||
|
||||
13
AIAgent/package-lock.json
generated
13
AIAgent/package-lock.json
generated
@@ -73,15 +73,18 @@
|
||||
"ejs": "^3.1.10",
|
||||
"elkjs": "^0.10.0",
|
||||
"esbuild": "^0.25.5",
|
||||
"expo-server-sdk": "^3.15.0",
|
||||
"express": "^4.21.1",
|
||||
"formik": "^2.4.6",
|
||||
"history": "^5.3.0",
|
||||
"ioredis": "^5.3.2",
|
||||
"isolated-vm": "^6.0.2",
|
||||
"json2csv": "^5.0.7",
|
||||
"json5": "^2.2.3",
|
||||
"jsonwebtoken": "^9.0.0",
|
||||
"jwt-decode": "^4.0.0",
|
||||
"marked": "^12.0.2",
|
||||
"mermaid": "^11.12.2",
|
||||
"moment": "^2.30.1",
|
||||
"moment-timezone": "^0.5.45",
|
||||
"multer": "^2.0.2",
|
||||
@@ -101,7 +104,7 @@
|
||||
"react-dropzone": "^14.2.2",
|
||||
"react-error-boundary": "^4.0.13",
|
||||
"react-highlight": "^0.15.0",
|
||||
"react-markdown": "^8.0.3",
|
||||
"react-markdown": "^9.0.0",
|
||||
"react-router-dom": "^6.30.1",
|
||||
"react-select": "^5.4.0",
|
||||
"react-spinners": "^0.14.1",
|
||||
@@ -111,7 +114,7 @@
|
||||
"recharts": "^2.12.7",
|
||||
"redis-semaphore": "^5.5.1",
|
||||
"reflect-metadata": "^0.2.2",
|
||||
"remark-gfm": "^3.0.1",
|
||||
"remark-gfm": "^4.0.0",
|
||||
"slackify-markdown": "^4.4.0",
|
||||
"slugify": "^1.6.5",
|
||||
"socket.io": "^4.7.4",
|
||||
@@ -1808,9 +1811,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/diff": {
|
||||
"version": "4.0.2",
|
||||
"resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz",
|
||||
"integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==",
|
||||
"version": "4.0.4",
|
||||
"resolved": "https://registry.npmjs.org/diff/-/diff-4.0.4.tgz",
|
||||
"integrity": "sha512-X07nttJQkwkfKfvTPG/KSnE2OMdcUCao6+eXF3wmnIQRn2aPAHH3VxDbDOdegkd6JbPsXqShpvEOHfAT+nCNwQ==",
|
||||
"license": "BSD-3-Clause",
|
||||
"engines": {
|
||||
"node": ">=0.3.1"
|
||||
|
||||
@@ -1,56 +0,0 @@
|
||||
.git
|
||||
|
||||
node_modules
|
||||
# See https://help.github.com/ignore-files/ for more about ignoring files.
|
||||
|
||||
# dependencies
|
||||
/node_modules
|
||||
node_modules
|
||||
|
||||
.idea
|
||||
# testing
|
||||
/coverage
|
||||
|
||||
# production
|
||||
/build
|
||||
|
||||
# misc
|
||||
.DS_Store
|
||||
|
||||
env.js
|
||||
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
|
||||
yarn.lock
|
||||
Untitled-1
|
||||
*.local.sh
|
||||
*.local.yaml
|
||||
run
|
||||
stop
|
||||
|
||||
nohup.out*
|
||||
|
||||
encrypted-credentials.tar
|
||||
encrypted-credentials/
|
||||
|
||||
_README.md
|
||||
|
||||
# Important Add production values to gitignore.
|
||||
values-saas-production.yaml
|
||||
kubernetes/values-saas-production.yaml
|
||||
|
||||
/private
|
||||
|
||||
/tls_cert.pem
|
||||
/tls_key.pem
|
||||
/keys
|
||||
|
||||
temp_readme.md
|
||||
|
||||
tests/coverage
|
||||
|
||||
settings.json
|
||||
|
||||
GoSDK/tester/
|
||||
1
APIReference/.gitattributes
vendored
1
APIReference/.gitattributes
vendored
@@ -1 +0,0 @@
|
||||
*.js text eol=lf
|
||||
30
APIReference/.gitignore
vendored
30
APIReference/.gitignore
vendored
@@ -1,30 +0,0 @@
|
||||
# See https://help.github.com/ignore-files/ for more about ignoring files.
|
||||
|
||||
# dependencies
|
||||
#/backend/node_modules
|
||||
/kubernetes
|
||||
/node_modules
|
||||
.idea
|
||||
|
||||
# misc
|
||||
.DS_Store
|
||||
|
||||
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
yarn.lock
|
||||
|
||||
**/*/paymentService.test.js
|
||||
apiTest.rest
|
||||
|
||||
application_security_dir
|
||||
container_security_dir
|
||||
|
||||
# coverage
|
||||
/coverage
|
||||
/.nyc_output
|
||||
|
||||
/greenlock.d/config.json
|
||||
/greenlock.d/config.json.bak
|
||||
/.greenlockrc
|
||||
@@ -1,75 +0,0 @@
|
||||
#
|
||||
# OneUptime-App Dockerfile
|
||||
#
|
||||
|
||||
# Pull base image nodejs image.
|
||||
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
|
||||
RUN npm config set fetch-retry-mintimeout 20000
|
||||
RUN npm config set fetch-retry-maxtimeout 60000
|
||||
|
||||
|
||||
|
||||
ARG GIT_SHA
|
||||
ARG APP_VERSION
|
||||
ARG IS_ENTERPRISE_EDITION=false
|
||||
|
||||
ENV GIT_SHA=${GIT_SHA}
|
||||
ENV APP_VERSION=${APP_VERSION}
|
||||
ENV IS_ENTERPRISE_EDITION=${IS_ENTERPRISE_EDITION}
|
||||
ENV PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD=1
|
||||
|
||||
|
||||
# IF APP_VERSION is not set, set it to 1.0.0
|
||||
RUN if [ -z "$APP_VERSION" ]; then export APP_VERSION=1.0.0; fi
|
||||
|
||||
|
||||
# Install bash.
|
||||
RUN apk add bash && apk add curl
|
||||
|
||||
|
||||
# Install python
|
||||
RUN apk update && apk add --no-cache --virtual .gyp python3 make g++
|
||||
|
||||
#Use bash shell by default
|
||||
SHELL ["/bin/bash", "-c"]
|
||||
|
||||
|
||||
RUN mkdir /usr/src
|
||||
|
||||
WORKDIR /usr/src/Common
|
||||
COPY ./Common/package*.json /usr/src/Common/
|
||||
# Set version in ./Common/package.json to the APP_VERSION
|
||||
RUN sed -i "s/\"version\": \".*\"/\"version\": \"$APP_VERSION\"/g" /usr/src/Common/package.json
|
||||
RUN npm install
|
||||
COPY ./Common /usr/src/Common
|
||||
|
||||
ENV PRODUCTION=true
|
||||
|
||||
WORKDIR /usr/src/app
|
||||
|
||||
# Install app dependencies
|
||||
COPY ./APIReference/package*.json /usr/src/app/
|
||||
# Set version in ./App/package.json to the APP_VERSION
|
||||
RUN sed -i "s/\"version\": \".*\"/\"version\": \"$APP_VERSION\"/g" /usr/src/app/package.json
|
||||
RUN npm install
|
||||
|
||||
# Expose ports.
|
||||
# - 1446: OneUptime-api-reference
|
||||
EXPOSE 1446
|
||||
|
||||
{{ if eq .Env.ENVIRONMENT "development" }}
|
||||
#Run the app
|
||||
CMD [ "npm", "run", "dev" ]
|
||||
{{ else }}
|
||||
# Copy app source
|
||||
COPY ./APIReference /usr/src/app
|
||||
# Bundle app source
|
||||
RUN npm run compile
|
||||
# Set permission to write logs and cache in case container run as non root
|
||||
RUN chown -R 1000:1000 "/tmp/npm" && chmod -R 2777 "/tmp/npm"
|
||||
#Run the app
|
||||
CMD [ "npm", "start" ]
|
||||
{{ end }}
|
||||
@@ -1,52 +0,0 @@
|
||||
import APIReferenceRoutes from "./Routes";
|
||||
import { PromiseVoidFunction } from "Common/Types/FunctionTypes";
|
||||
import InfrastructureStatus from "Common/Server/Infrastructure/Status";
|
||||
import logger from "Common/Server/Utils/Logger";
|
||||
import App from "Common/Server/Utils/StartServer";
|
||||
import Telemetry from "Common/Server/Utils/Telemetry";
|
||||
import "ejs";
|
||||
|
||||
const APP_NAME: string = "reference";
|
||||
|
||||
const init: PromiseVoidFunction = async (): Promise<void> => {
|
||||
try {
|
||||
// Initialize telemetry
|
||||
Telemetry.init({
|
||||
serviceName: APP_NAME,
|
||||
});
|
||||
|
||||
const statusCheck: PromiseVoidFunction = async (): Promise<void> => {
|
||||
// Check the status of infrastructure components
|
||||
return await InfrastructureStatus.checkStatusWithRetry({
|
||||
checkClickhouseStatus: false,
|
||||
checkPostgresStatus: false,
|
||||
checkRedisStatus: false,
|
||||
retryCount: 3,
|
||||
});
|
||||
};
|
||||
|
||||
// Initialize the app with service name and status checks
|
||||
await App.init({
|
||||
appName: APP_NAME,
|
||||
statusOptions: {
|
||||
liveCheck: statusCheck,
|
||||
readyCheck: statusCheck,
|
||||
},
|
||||
});
|
||||
|
||||
await APIReferenceRoutes.init();
|
||||
|
||||
// Add default routes to the app
|
||||
await App.addDefaultRoutes();
|
||||
} catch (err) {
|
||||
logger.error("App Init Failed:");
|
||||
logger.error(err);
|
||||
throw err;
|
||||
}
|
||||
};
|
||||
|
||||
init().catch((err: Error) => {
|
||||
logger.error(err);
|
||||
logger.error("Exiting node process");
|
||||
process.exit(1);
|
||||
});
|
||||
@@ -1,29 +0,0 @@
|
||||
# README
|
||||
|
||||
This README would normally document whatever steps are necessary to get your application up and running.
|
||||
|
||||
### What is this repository for?
|
||||
|
||||
- Quick summary
|
||||
- Version
|
||||
- [Learn Markdown](https://bitbucket.org/tutorials/markdowndemo)
|
||||
|
||||
### How do I get set up?
|
||||
|
||||
- Summary of set up
|
||||
- Configuration
|
||||
- Dependencies
|
||||
- Database configuration
|
||||
- How to run tests
|
||||
- Deployment instructions
|
||||
|
||||
### Contribution guidelines
|
||||
|
||||
- Writing tests
|
||||
- Code review
|
||||
- Other guidelines
|
||||
|
||||
### Who do I talk to?
|
||||
|
||||
- Repo owner or admin
|
||||
- Other community or team contact
|
||||
@@ -1,3 +0,0 @@
|
||||
export const ViewsPath: string = "/usr/src/app/views";
|
||||
export const StaticPath: string = "/usr/src/app/Static";
|
||||
export const CodeExamplesPath: string = "/usr/src/app/CodeExamples";
|
||||
@@ -1,14 +0,0 @@
|
||||
{
|
||||
"watch": ["./","../Common/Server", "../Common/Types", "../Common/Utils", "../Common/Models"],
|
||||
"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 -r ts-node/register/transpile-only Index.ts"
|
||||
}
|
||||
4265
APIReference/package-lock.json
generated
4265
APIReference/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -1,33 +0,0 @@
|
||||
{
|
||||
"name": "@oneuptime/api-reference",
|
||||
"version": "1.0.0",
|
||||
"description": "",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/OneUptime/oneuptime"
|
||||
},
|
||||
"main": "Index.ts",
|
||||
"scripts": {
|
||||
"start": "export NODE_OPTIONS='--max-old-space-size=8096' && node --require ts-node/register Index.ts",
|
||||
"compile": "tsc",
|
||||
"clear-modules": "rm -rf node_modules && rm package-lock.json && npm install",
|
||||
"dev": "npx nodemon",
|
||||
"audit": "npm audit --audit-level=low",
|
||||
"dep-check": "npm install -g depcheck && depcheck ./ --skip-missing=true",
|
||||
"test": "rm -rf build && jest --detectOpenHandles --passWithNoTests",
|
||||
"coverage": "jest --detectOpenHandles --coverage"
|
||||
},
|
||||
"author": "OneUptime <hello@oneuptime.com> (https://oneuptime.com/)",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"Common": "file:../Common",
|
||||
"ejs": "^3.1.9",
|
||||
"ts-node": "^10.9.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/jest": "^29.5.11",
|
||||
"@types/node": "^17.0.31",
|
||||
"jest": "^28.1.0",
|
||||
"nodemon": "^2.0.20"
|
||||
}
|
||||
}
|
||||
@@ -1,112 +0,0 @@
|
||||
{
|
||||
"ts-node": {
|
||||
// these options are overrides used only by ts-node
|
||||
// same as the --compilerOptions flag and the TS_NODE_COMPILER_OPTIONS environment variable
|
||||
"compilerOptions": {
|
||||
"module": "commonjs",
|
||||
"resolveJsonModule": true,
|
||||
}
|
||||
},
|
||||
"compilerOptions": {
|
||||
/* Visit https://aka.ms/tsconfig.json to read more about this file */
|
||||
|
||||
/* Projects */
|
||||
// "incremental": true, /* Enable incremental compilation */
|
||||
// "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */
|
||||
// "tsBuildInfoFile": "./", /* Specify the folder for .tsbuildinfo incremental compilation files. */
|
||||
// "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects */
|
||||
// "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */
|
||||
// "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */
|
||||
|
||||
/* Language and Environment */
|
||||
"target": "es2017" /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */,
|
||||
// "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */
|
||||
"jsx": "react" /* Specify what JSX code is generated. */,
|
||||
"experimentalDecorators": true, /* Enable experimental support for TC39 stage 2 draft decorators. */
|
||||
"emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */
|
||||
// "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h' */
|
||||
// "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */
|
||||
// "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using `jsx: react-jsx*`.` */
|
||||
// "reactNamespace": "", /* Specify the object invoked for `createElement`. This only applies when targeting `react` JSX emit. */
|
||||
// "noLib": true, /* Disable including any library files, including the default lib.d.ts. */
|
||||
// "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */
|
||||
|
||||
/* Modules */
|
||||
// "module": "es2022" /* Specify what module code is generated. */,
|
||||
// "rootDir": "./", /* Specify the root folder within your source files. */
|
||||
"moduleResolution": "node", /* Specify how TypeScript looks up a file from a given module specifier. */
|
||||
// "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */
|
||||
// "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */
|
||||
// "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */
|
||||
"typeRoots": [
|
||||
"./node_modules/@types"
|
||||
], /* Specify multiple folders that act like `./node_modules/@types`. */
|
||||
"types": ["node", "jest"], /* Specify type package names to be included without being referenced in a source file. */
|
||||
// "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */
|
||||
// "resolveJsonModule": true, /* Enable importing .json files */
|
||||
// "noResolve": true, /* Disallow `import`s, `require`s or `<reference>`s from expanding the number of files TypeScript should add to a project. */
|
||||
|
||||
/* JavaScript Support */
|
||||
// "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the `checkJS` option to get errors from these files. */
|
||||
// "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */
|
||||
// "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from `node_modules`. Only applicable with `allowJs`. */
|
||||
|
||||
/* Emit */
|
||||
// "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */
|
||||
// "declarationMap": true, /* Create sourcemaps for d.ts files. */
|
||||
// "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */
|
||||
"sourceMap": true, /* Create source map files for emitted JavaScript files. */
|
||||
// "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If `declaration` is true, also designates a file that bundles all .d.ts output. */
|
||||
"outDir": "./build/dist", /* Specify an output folder for all emitted files. */
|
||||
// "removeComments": true, /* Disable emitting comments. */
|
||||
// "noEmit": true, /* Disable emitting files from a compilation. */
|
||||
// "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */
|
||||
// "importsNotUsedAsValues": "remove", /* Specify emit/checking behavior for imports that are only used for types */
|
||||
// "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */
|
||||
// "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */
|
||||
// "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */
|
||||
// "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */
|
||||
// "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */
|
||||
// "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */
|
||||
// "newLine": "crlf", /* Set the newline character for emitting files. */
|
||||
// "stripInternal": true, /* Disable emitting declarations that have `@internal` in their JSDoc comments. */
|
||||
// "noEmitHelpers": true, /* Disable generating custom helper functions like `__extends` in compiled output. */
|
||||
// "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */
|
||||
// "preserveConstEnums": true, /* Disable erasing `const enum` declarations in generated code. */
|
||||
// "declarationDir": "./", /* Specify the output directory for generated declaration files. */
|
||||
// "preserveValueImports": true, /* Preserve unused imported values in the JavaScript output that would otherwise be removed. */
|
||||
|
||||
/* Interop Constraints */
|
||||
// "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */
|
||||
// "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */
|
||||
"esModuleInterop": true /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables `allowSyntheticDefaultImports` for type compatibility. */,
|
||||
// "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */
|
||||
"forceConsistentCasingInFileNames": true /* Ensure that casing is correct in imports. */,
|
||||
|
||||
/* Type Checking */
|
||||
"strict": true /* Enable all strict type-checking options. */,
|
||||
"noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied `any` type.. */
|
||||
"strictNullChecks": true, /* When type checking, take into account `null` and `undefined`. */
|
||||
"strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */
|
||||
"strictBindCallApply": true, /* Check that the arguments for `bind`, `call`, and `apply` methods match the original function. */
|
||||
"strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */
|
||||
"noImplicitThis": true, /* Enable error reporting when `this` is given the type `any`. */
|
||||
"useUnknownInCatchVariables": true, /* Type catch clause variables as 'unknown' instead of 'any'. */
|
||||
"alwaysStrict": true, /* Ensure 'use strict' is always emitted. */
|
||||
"noUnusedLocals": true, /* Enable error reporting when a local variables aren't read. */
|
||||
"noUnusedParameters": true, /* Raise an error when a function parameter isn't read */
|
||||
"exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */
|
||||
"noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */
|
||||
"noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */
|
||||
"noUncheckedIndexedAccess": true, /* Include 'undefined' in index signature results */
|
||||
"noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */
|
||||
"noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type */
|
||||
// "allowUnusedLabels": true, /* Disable error reporting for unused labels. */
|
||||
// "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */
|
||||
|
||||
/* Completeness */
|
||||
// "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */
|
||||
"skipLibCheck": true, /* Skip type checking all .d.ts files. */
|
||||
"resolveJsonModule": true
|
||||
}
|
||||
}
|
||||
@@ -1,36 +0,0 @@
|
||||
<main class="py-12">
|
||||
<article class="prose">
|
||||
<!-- Hero Section -->
|
||||
<div class="mb-10">
|
||||
<div class="flex items-center gap-3 mb-4">
|
||||
<div class="flex items-center justify-center w-10 h-10 rounded-xl bg-indigo-600 shadow-lg shadow-indigo-500/30">
|
||||
<svg class="w-5 h-5 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z"></path>
|
||||
</svg>
|
||||
</div>
|
||||
<span class="text-xs font-semibold text-indigo-600 uppercase tracking-wider">Guide</span>
|
||||
</div>
|
||||
<h1 class="font-bold text-3xl text-slate-900 tracking-tight mb-3">Permissions</h1>
|
||||
<p class="text-lg text-slate-600 leading-relaxed max-w-2xl">Your API Token needs permissions to create, update, read or delete any resource. If you do not have permissions to make a request a <code class="inline-code">4xx</code> status will be sent as response. You can manage permissions for your API Key in Project Settings > API Keys.</p>
|
||||
</div>
|
||||
|
||||
<h2 id="consuming-webhooks" class="scroll-mt-24 text-xl font-semibold text-slate-900 mb-6 mt-12">
|
||||
Permissions List
|
||||
</h2>
|
||||
<p class="text-slate-600 leading-relaxed mb-6">Here is a list of all the permissions:</p>
|
||||
|
||||
<div class="rounded-xl border border-slate-200 bg-white overflow-hidden">
|
||||
<ul role="list" class="m-0 divide-y divide-slate-100 p-0">
|
||||
<% for(var i=0; i<pageData.permissions.length; i++) {%>
|
||||
<li class="m-0 px-5 py-4 hover:bg-slate-50/50 transition-colors">
|
||||
<dl class="m-0 flex flex-wrap items-center gap-x-3 gap-y-2">
|
||||
<dd><code class="inline-code"><%= pageData.permissions[i].permission -%></code></dd>
|
||||
<dd class="font-mono text-xs text-slate-500"><%= pageData.permissions[i].title -%></dd>
|
||||
<dd class="w-full flex-none text-sm text-slate-600 mt-1"><%= pageData.permissions[i].description -%></dd>
|
||||
</dl>
|
||||
</li>
|
||||
<% } %>
|
||||
</ul>
|
||||
</div>
|
||||
</article>
|
||||
</main>
|
||||
26
Accounts/index.d.ts
vendored
26
Accounts/index.d.ts
vendored
@@ -2,3 +2,29 @@ declare module "*.png";
|
||||
declare module "*.svg";
|
||||
declare module "*.jpg";
|
||||
declare module "*.gif";
|
||||
|
||||
declare module "react-syntax-highlighter/dist/esm/prism-light";
|
||||
declare module "react-syntax-highlighter/dist/esm/styles/prism";
|
||||
declare module "react-syntax-highlighter/dist/esm/languages/prism/javascript";
|
||||
declare module "react-syntax-highlighter/dist/esm/languages/prism/typescript";
|
||||
declare module "react-syntax-highlighter/dist/esm/languages/prism/jsx";
|
||||
declare module "react-syntax-highlighter/dist/esm/languages/prism/tsx";
|
||||
declare module "react-syntax-highlighter/dist/esm/languages/prism/python";
|
||||
declare module "react-syntax-highlighter/dist/esm/languages/prism/bash";
|
||||
declare module "react-syntax-highlighter/dist/esm/languages/prism/json";
|
||||
declare module "react-syntax-highlighter/dist/esm/languages/prism/yaml";
|
||||
declare module "react-syntax-highlighter/dist/esm/languages/prism/sql";
|
||||
declare module "react-syntax-highlighter/dist/esm/languages/prism/go";
|
||||
declare module "react-syntax-highlighter/dist/esm/languages/prism/java";
|
||||
declare module "react-syntax-highlighter/dist/esm/languages/prism/css";
|
||||
declare module "react-syntax-highlighter/dist/esm/languages/prism/markup";
|
||||
declare module "react-syntax-highlighter/dist/esm/languages/prism/markdown";
|
||||
declare module "react-syntax-highlighter/dist/esm/languages/prism/docker";
|
||||
declare module "react-syntax-highlighter/dist/esm/languages/prism/rust";
|
||||
declare module "react-syntax-highlighter/dist/esm/languages/prism/c";
|
||||
declare module "react-syntax-highlighter/dist/esm/languages/prism/cpp";
|
||||
declare module "react-syntax-highlighter/dist/esm/languages/prism/csharp";
|
||||
declare module "react-syntax-highlighter/dist/esm/languages/prism/ruby";
|
||||
declare module "react-syntax-highlighter/dist/esm/languages/prism/php";
|
||||
declare module "react-syntax-highlighter/dist/esm/languages/prism/graphql";
|
||||
declare module "react-syntax-highlighter/dist/esm/languages/prism/http";
|
||||
|
||||
100
Accounts/package-lock.json
generated
100
Accounts/package-lock.json
generated
@@ -77,15 +77,18 @@
|
||||
"ejs": "^3.1.10",
|
||||
"elkjs": "^0.10.0",
|
||||
"esbuild": "^0.25.5",
|
||||
"expo-server-sdk": "^3.15.0",
|
||||
"express": "^4.21.1",
|
||||
"formik": "^2.4.6",
|
||||
"history": "^5.3.0",
|
||||
"ioredis": "^5.3.2",
|
||||
"isolated-vm": "^6.0.2",
|
||||
"json2csv": "^5.0.7",
|
||||
"json5": "^2.2.3",
|
||||
"jsonwebtoken": "^9.0.0",
|
||||
"jwt-decode": "^4.0.0",
|
||||
"marked": "^12.0.2",
|
||||
"mermaid": "^11.12.2",
|
||||
"moment": "^2.30.1",
|
||||
"moment-timezone": "^0.5.45",
|
||||
"multer": "^2.0.2",
|
||||
@@ -105,7 +108,7 @@
|
||||
"react-dropzone": "^14.2.2",
|
||||
"react-error-boundary": "^4.0.13",
|
||||
"react-highlight": "^0.15.0",
|
||||
"react-markdown": "^8.0.3",
|
||||
"react-markdown": "^9.0.0",
|
||||
"react-router-dom": "^6.30.1",
|
||||
"react-select": "^5.4.0",
|
||||
"react-spinners": "^0.14.1",
|
||||
@@ -115,7 +118,7 @@
|
||||
"recharts": "^2.12.7",
|
||||
"redis-semaphore": "^5.5.1",
|
||||
"reflect-metadata": "^0.2.2",
|
||||
"remark-gfm": "^3.0.1",
|
||||
"remark-gfm": "^4.0.0",
|
||||
"slackify-markdown": "^4.4.0",
|
||||
"slugify": "^1.6.5",
|
||||
"socket.io": "^4.7.4",
|
||||
@@ -474,20 +477,6 @@
|
||||
"node": ">=0.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/ansi-styles": {
|
||||
"version": "4.3.0",
|
||||
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
|
||||
"integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
|
||||
"dependencies": {
|
||||
"color-convert": "^2.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/anymatch": {
|
||||
"version": "3.1.3",
|
||||
"resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz",
|
||||
@@ -531,6 +520,7 @@
|
||||
"version": "1.1.12",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
|
||||
"integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"balanced-match": "^1.0.0",
|
||||
@@ -550,40 +540,6 @@
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/chalk": {
|
||||
"version": "4.1.2",
|
||||
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
|
||||
"integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
|
||||
"dependencies": {
|
||||
"ansi-styles": "^4.1.0",
|
||||
"supports-color": "^7.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/chalk/chalk?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/chalk/node_modules/has-flag": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
|
||||
"integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/chalk/node_modules/supports-color": {
|
||||
"version": "7.2.0",
|
||||
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
|
||||
"integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
|
||||
"dependencies": {
|
||||
"has-flag": "^4.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/chokidar": {
|
||||
"version": "3.5.3",
|
||||
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz",
|
||||
@@ -611,22 +567,6 @@
|
||||
"fsevents": "~2.3.2"
|
||||
}
|
||||
},
|
||||
"node_modules/color-convert": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
|
||||
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
|
||||
"dependencies": {
|
||||
"color-name": "~1.1.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=7.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/color-name": {
|
||||
"version": "1.1.4",
|
||||
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
|
||||
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="
|
||||
},
|
||||
"node_modules/Common": {
|
||||
"resolved": "../Common",
|
||||
"link": true
|
||||
@@ -634,7 +574,8 @@
|
||||
"node_modules/concat-map": {
|
||||
"version": "0.0.1",
|
||||
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
|
||||
"integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="
|
||||
"integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/create-require": {
|
||||
"version": "1.1.1",
|
||||
@@ -658,10 +599,11 @@
|
||||
}
|
||||
},
|
||||
"node_modules/diff": {
|
||||
"version": "4.0.2",
|
||||
"resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz",
|
||||
"integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==",
|
||||
"version": "4.0.4",
|
||||
"resolved": "https://registry.npmjs.org/diff/-/diff-4.0.4.tgz",
|
||||
"integrity": "sha512-X07nttJQkwkfKfvTPG/KSnE2OMdcUCao6+eXF3wmnIQRn2aPAHH3VxDbDOdegkd6JbPsXqShpvEOHfAT+nCNwQ==",
|
||||
"dev": true,
|
||||
"license": "BSD-3-Clause",
|
||||
"engines": {
|
||||
"node": ">=0.3.1"
|
||||
}
|
||||
@@ -809,15 +751,14 @@
|
||||
}
|
||||
},
|
||||
"node_modules/jake": {
|
||||
"version": "10.9.2",
|
||||
"resolved": "https://registry.npmjs.org/jake/-/jake-10.9.2.tgz",
|
||||
"integrity": "sha512-2P4SQ0HrLQ+fw6llpLnOaGAvN2Zu6778SJMrCUwns4fOoG9ayrTiZk3VV8sCPkVZF8ab0zksVpS8FDY5pRCNBA==",
|
||||
"version": "10.9.4",
|
||||
"resolved": "https://registry.npmjs.org/jake/-/jake-10.9.4.tgz",
|
||||
"integrity": "sha512-wpHYzhxiVQL+IV05BLE2Xn34zW1S223hvjtqk0+gsPrwd/8JNLXJgZZM/iPFsYc1xyphF+6M6EvdE5E9MBGkDA==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"async": "^3.2.3",
|
||||
"chalk": "^4.0.2",
|
||||
"async": "^3.2.6",
|
||||
"filelist": "^1.0.4",
|
||||
"minimatch": "^3.1.2"
|
||||
"picocolors": "^1.1.1"
|
||||
},
|
||||
"bin": {
|
||||
"jake": "bin/cli.js"
|
||||
@@ -852,6 +793,7 @@
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
|
||||
"integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"brace-expansion": "^1.1.7"
|
||||
},
|
||||
@@ -926,6 +868,12 @@
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/picocolors": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
|
||||
"integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/picomatch": {
|
||||
"version": "2.3.1",
|
||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
|
||||
|
||||
@@ -32,6 +32,7 @@ import Reseller from "Common/Models/DatabaseModels/Reseller";
|
||||
import User from "Common/Models/DatabaseModels/User";
|
||||
import React, { useState } from "react";
|
||||
import useAsyncEffect from "use-async-effect";
|
||||
import { IsBillingEnabled } from "Common/Server/EnvironmentConfig";
|
||||
|
||||
const RegisterPage: () => JSX.Element = () => {
|
||||
const apiUrl: URL = SIGNUP_API_URL;
|
||||
@@ -172,6 +173,36 @@ const RegisterPage: () => JSX.Element = () => {
|
||||
}
|
||||
}
|
||||
|
||||
if (!BILLING_ENABLED) {
|
||||
formFields = formFields.concat([
|
||||
{
|
||||
overrideField: {
|
||||
selfHostedCompanyName: true,
|
||||
},
|
||||
overrideFieldKey: "selfHostedCompanyName",
|
||||
fieldType: FormFieldSchemaType.Text,
|
||||
placeholder: "Acme, Inc.",
|
||||
required: false,
|
||||
title: "Company Name",
|
||||
dataTestId: "selfHostedCompanyName",
|
||||
showEvenIfPermissionDoesNotExist: true,
|
||||
disableSpellCheck: true,
|
||||
},
|
||||
{
|
||||
overrideField: {
|
||||
selfHostedPhoneNumber: true,
|
||||
},
|
||||
overrideFieldKey: "selfHostedPhoneNumber",
|
||||
fieldType: FormFieldSchemaType.Phone,
|
||||
required: false,
|
||||
placeholder: "+11234567890",
|
||||
title: "Phone Number",
|
||||
dataTestId: "selfHostedPhoneNumber",
|
||||
showEvenIfPermissionDoesNotExist: true,
|
||||
},
|
||||
]);
|
||||
}
|
||||
|
||||
formFields = formFields.concat([
|
||||
{
|
||||
field: {
|
||||
@@ -206,6 +237,25 @@ const RegisterPage: () => JSX.Element = () => {
|
||||
},
|
||||
]);
|
||||
|
||||
if (!IsBillingEnabled) {
|
||||
formFields = formFields.concat([
|
||||
{
|
||||
overrideField: {
|
||||
notifySelfHosted: true,
|
||||
},
|
||||
overrideFieldKey: "notifySelfHosted",
|
||||
|
||||
fieldType: FormFieldSchemaType.Checkbox,
|
||||
required: false,
|
||||
defaultValue: true,
|
||||
title: "Notify me about security patches and new releases",
|
||||
dataTestId: "notifySelfHosted",
|
||||
showEvenIfPermissionDoesNotExist: true,
|
||||
spanFullRow: true,
|
||||
},
|
||||
]);
|
||||
}
|
||||
|
||||
if (isCaptchaEnabled) {
|
||||
formFields = formFields.concat([
|
||||
{
|
||||
@@ -330,6 +380,7 @@ const RegisterPage: () => JSX.Element = () => {
|
||||
if (value && value.email) {
|
||||
UiAnalytics.userAuth(value.email);
|
||||
UiAnalytics.capture("accounts/register");
|
||||
UiAnalytics.capture("sign_up");
|
||||
}
|
||||
|
||||
LoginUtil.login({
|
||||
|
||||
@@ -42,10 +42,11 @@
|
||||
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link rel="preload" href="https://fonts.googleapis.com/css2?family=Inter:wght@100;200;300;400;500;600;700;800;900&display=swap" as="style">
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@100;200;300;400;500;600;700;800;900&display=swap" rel="stylesheet">
|
||||
<style>
|
||||
* {
|
||||
font-family: Inter;
|
||||
font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
|
||||
}
|
||||
|
||||
|
||||
@@ -75,6 +76,7 @@
|
||||
width: auto;
|
||||
}
|
||||
</style>
|
||||
<link rel="preload" href="/accounts/assets/js/tailwind-3.4.5.js" as="script">
|
||||
<script src="/accounts/assets/js/tailwind-3.4.5.js"></script>
|
||||
|
||||
<title>OneUptime Accounts</title>
|
||||
|
||||
26
AdminDashboard/index.d.ts
vendored
26
AdminDashboard/index.d.ts
vendored
@@ -2,3 +2,29 @@ declare module "*.png";
|
||||
declare module "*.svg";
|
||||
declare module "*.jpg";
|
||||
declare module "*.gif";
|
||||
|
||||
declare module "react-syntax-highlighter/dist/esm/prism-light";
|
||||
declare module "react-syntax-highlighter/dist/esm/styles/prism";
|
||||
declare module "react-syntax-highlighter/dist/esm/languages/prism/javascript";
|
||||
declare module "react-syntax-highlighter/dist/esm/languages/prism/typescript";
|
||||
declare module "react-syntax-highlighter/dist/esm/languages/prism/jsx";
|
||||
declare module "react-syntax-highlighter/dist/esm/languages/prism/tsx";
|
||||
declare module "react-syntax-highlighter/dist/esm/languages/prism/python";
|
||||
declare module "react-syntax-highlighter/dist/esm/languages/prism/bash";
|
||||
declare module "react-syntax-highlighter/dist/esm/languages/prism/json";
|
||||
declare module "react-syntax-highlighter/dist/esm/languages/prism/yaml";
|
||||
declare module "react-syntax-highlighter/dist/esm/languages/prism/sql";
|
||||
declare module "react-syntax-highlighter/dist/esm/languages/prism/go";
|
||||
declare module "react-syntax-highlighter/dist/esm/languages/prism/java";
|
||||
declare module "react-syntax-highlighter/dist/esm/languages/prism/css";
|
||||
declare module "react-syntax-highlighter/dist/esm/languages/prism/markup";
|
||||
declare module "react-syntax-highlighter/dist/esm/languages/prism/markdown";
|
||||
declare module "react-syntax-highlighter/dist/esm/languages/prism/docker";
|
||||
declare module "react-syntax-highlighter/dist/esm/languages/prism/rust";
|
||||
declare module "react-syntax-highlighter/dist/esm/languages/prism/c";
|
||||
declare module "react-syntax-highlighter/dist/esm/languages/prism/cpp";
|
||||
declare module "react-syntax-highlighter/dist/esm/languages/prism/csharp";
|
||||
declare module "react-syntax-highlighter/dist/esm/languages/prism/ruby";
|
||||
declare module "react-syntax-highlighter/dist/esm/languages/prism/php";
|
||||
declare module "react-syntax-highlighter/dist/esm/languages/prism/graphql";
|
||||
declare module "react-syntax-highlighter/dist/esm/languages/prism/http";
|
||||
|
||||
108
AdminDashboard/package-lock.json
generated
108
AdminDashboard/package-lock.json
generated
@@ -76,15 +76,18 @@
|
||||
"ejs": "^3.1.10",
|
||||
"elkjs": "^0.10.0",
|
||||
"esbuild": "^0.25.5",
|
||||
"expo-server-sdk": "^3.15.0",
|
||||
"express": "^4.21.1",
|
||||
"formik": "^2.4.6",
|
||||
"history": "^5.3.0",
|
||||
"ioredis": "^5.3.2",
|
||||
"isolated-vm": "^6.0.2",
|
||||
"json2csv": "^5.0.7",
|
||||
"json5": "^2.2.3",
|
||||
"jsonwebtoken": "^9.0.0",
|
||||
"jwt-decode": "^4.0.0",
|
||||
"marked": "^12.0.2",
|
||||
"mermaid": "^11.12.2",
|
||||
"moment": "^2.30.1",
|
||||
"moment-timezone": "^0.5.45",
|
||||
"multer": "^2.0.2",
|
||||
@@ -104,7 +107,7 @@
|
||||
"react-dropzone": "^14.2.2",
|
||||
"react-error-boundary": "^4.0.13",
|
||||
"react-highlight": "^0.15.0",
|
||||
"react-markdown": "^8.0.3",
|
||||
"react-markdown": "^9.0.0",
|
||||
"react-router-dom": "^6.30.1",
|
||||
"react-select": "^5.4.0",
|
||||
"react-spinners": "^0.14.1",
|
||||
@@ -114,7 +117,7 @@
|
||||
"recharts": "^2.12.7",
|
||||
"redis-semaphore": "^5.5.1",
|
||||
"reflect-metadata": "^0.2.2",
|
||||
"remark-gfm": "^3.0.1",
|
||||
"remark-gfm": "^4.0.0",
|
||||
"slackify-markdown": "^4.4.0",
|
||||
"slugify": "^1.6.5",
|
||||
"socket.io": "^4.7.4",
|
||||
@@ -478,9 +481,10 @@
|
||||
}
|
||||
},
|
||||
"node_modules/async": {
|
||||
"version": "3.2.5",
|
||||
"resolved": "https://registry.npmjs.org/async/-/async-3.2.5.tgz",
|
||||
"integrity": "sha512-baNZyqaaLhyLVKm/DlvdW051MSgO6b8eVfIezl9E5PqWxFgzLm/wQntEW4zOytVburDEr0JlALEpdOFwvErLsg=="
|
||||
"version": "3.2.6",
|
||||
"resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz",
|
||||
"integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/balanced-match": {
|
||||
"version": "1.0.2",
|
||||
@@ -500,6 +504,7 @@
|
||||
"version": "1.1.12",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
|
||||
"integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"balanced-match": "^1.0.0",
|
||||
@@ -553,7 +558,8 @@
|
||||
"node_modules/concat-map": {
|
||||
"version": "0.0.1",
|
||||
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
|
||||
"integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="
|
||||
"integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/create-require": {
|
||||
"version": "1.1.1",
|
||||
@@ -577,10 +583,11 @@
|
||||
}
|
||||
},
|
||||
"node_modules/diff": {
|
||||
"version": "4.0.2",
|
||||
"resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz",
|
||||
"integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==",
|
||||
"version": "4.0.4",
|
||||
"resolved": "https://registry.npmjs.org/diff/-/diff-4.0.4.tgz",
|
||||
"integrity": "sha512-X07nttJQkwkfKfvTPG/KSnE2OMdcUCao6+eXF3wmnIQRn2aPAHH3VxDbDOdegkd6JbPsXqShpvEOHfAT+nCNwQ==",
|
||||
"dev": true,
|
||||
"license": "BSD-3-Clause",
|
||||
"engines": {
|
||||
"node": ">=0.3.1"
|
||||
}
|
||||
@@ -726,14 +733,14 @@
|
||||
}
|
||||
},
|
||||
"node_modules/jake": {
|
||||
"version": "10.8.7",
|
||||
"resolved": "https://registry.npmjs.org/jake/-/jake-10.8.7.tgz",
|
||||
"integrity": "sha512-ZDi3aP+fG/LchyBzUM804VjddnwfSfsdeYkwt8NcbKRvo4rFkjhs456iLFn3k2ZUWvNe4i48WACDbza8fhq2+w==",
|
||||
"version": "10.9.4",
|
||||
"resolved": "https://registry.npmjs.org/jake/-/jake-10.9.4.tgz",
|
||||
"integrity": "sha512-wpHYzhxiVQL+IV05BLE2Xn34zW1S223hvjtqk0+gsPrwd/8JNLXJgZZM/iPFsYc1xyphF+6M6EvdE5E9MBGkDA==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"async": "^3.2.3",
|
||||
"chalk": "^4.0.2",
|
||||
"async": "^3.2.6",
|
||||
"filelist": "^1.0.4",
|
||||
"minimatch": "^3.1.2"
|
||||
"picocolors": "^1.1.1"
|
||||
},
|
||||
"bin": {
|
||||
"jake": "bin/cli.js"
|
||||
@@ -742,70 +749,6 @@
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/jake/node_modules/ansi-styles": {
|
||||
"version": "4.3.0",
|
||||
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
|
||||
"integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
|
||||
"dependencies": {
|
||||
"color-convert": "^2.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/jake/node_modules/chalk": {
|
||||
"version": "4.1.2",
|
||||
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
|
||||
"integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
|
||||
"dependencies": {
|
||||
"ansi-styles": "^4.1.0",
|
||||
"supports-color": "^7.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/chalk/chalk?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/jake/node_modules/color-convert": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
|
||||
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
|
||||
"dependencies": {
|
||||
"color-name": "~1.1.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=7.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/jake/node_modules/color-name": {
|
||||
"version": "1.1.4",
|
||||
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
|
||||
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="
|
||||
},
|
||||
"node_modules/jake/node_modules/has-flag": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
|
||||
"integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/jake/node_modules/supports-color": {
|
||||
"version": "7.2.0",
|
||||
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
|
||||
"integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
|
||||
"dependencies": {
|
||||
"has-flag": "^4.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/js-tokens": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
|
||||
@@ -832,6 +775,7 @@
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
|
||||
"integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"brace-expansion": "^1.1.7"
|
||||
},
|
||||
@@ -906,6 +850,12 @@
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/picocolors": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
|
||||
"integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/picomatch": {
|
||||
"version": "2.3.1",
|
||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
|
||||
|
||||
@@ -4,6 +4,7 @@ import Logout from "./Pages/Logout/Logout";
|
||||
import Projects from "./Pages/Projects/Index";
|
||||
import SettingsAPIKey from "./Pages/Settings/APIKey/Index";
|
||||
import SettingsAuthentication from "./Pages/Settings/Authentication/Index";
|
||||
import SettingsDataRetention from "./Pages/Settings/DataRetention/Index";
|
||||
import SettingsCallSMS from "./Pages/Settings/CallSMS/Index";
|
||||
import SettingsWhatsApp from "./Pages/Settings/WhatsApp/Index";
|
||||
// Settings Pages.
|
||||
@@ -143,6 +144,11 @@ const App: () => JSX.Element = () => {
|
||||
path={RouteMap[PageMap.SETTINGS_API_KEY]?.toString() || ""}
|
||||
element={<SettingsAPIKey />}
|
||||
/>
|
||||
|
||||
<PageRoute
|
||||
path={RouteMap[PageMap.SETTINGS_DATA_RETENTION]?.toString() || ""}
|
||||
element={<SettingsDataRetention />}
|
||||
/>
|
||||
</Routes>
|
||||
</MasterPage>
|
||||
);
|
||||
|
||||
@@ -12,7 +12,6 @@ import Toggle from "Common/UI/Components/Toggle/Toggle";
|
||||
import FieldType from "Common/UI/Components/Types/FieldType";
|
||||
import { BILLING_ENABLED, getAllEnvVars } from "Common/UI/Config";
|
||||
import { GetReactElementFunction } from "Common/UI/Types/FunctionTypes";
|
||||
import Navigation from "Common/UI/Utils/Navigation";
|
||||
import Project from "Common/Models/DatabaseModels/Project";
|
||||
import User from "Common/Models/DatabaseModels/User";
|
||||
import React, {
|
||||
@@ -21,6 +20,7 @@ import React, {
|
||||
useEffect,
|
||||
useState,
|
||||
} from "react";
|
||||
import Navigation from "Common/UI/Utils/Navigation";
|
||||
|
||||
const Projects: FunctionComponent = (): ReactElement => {
|
||||
const [isSubscriptionPlanYearly, setIsSubscriptionPlanYearly] =
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import AdminModelAPI from "../../../Utils/ModelAPI";
|
||||
import PageMap from "../../../Utils/PageMap";
|
||||
import RouteMap, { RouteUtil } from "../../../Utils/RouteMap";
|
||||
import Route from "Common/Types/API/Route";
|
||||
@@ -17,6 +18,7 @@ const DeletePage: FunctionComponent = (): ReactElement => {
|
||||
modelId={modelId}
|
||||
modelNameField="name"
|
||||
modelType={Project}
|
||||
modelAPI={AdminModelAPI}
|
||||
title={"Project"}
|
||||
breadcrumbLinks={[
|
||||
{
|
||||
@@ -41,6 +43,7 @@ const DeletePage: FunctionComponent = (): ReactElement => {
|
||||
<ModelDelete
|
||||
modelType={Project}
|
||||
modelId={modelId}
|
||||
modelAPI={AdminModelAPI}
|
||||
onDeleteSuccess={() => {
|
||||
Navigation.navigate(RouteMap[PageMap.PROJECTS] as Route);
|
||||
}}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import AdminModelAPI from "../../../Utils/ModelAPI";
|
||||
import ObjectID from "Common/Types/ObjectID";
|
||||
import PageMap from "../../../Utils/PageMap";
|
||||
import RouteMap, { RouteUtil } from "../../../Utils/RouteMap";
|
||||
@@ -19,6 +20,7 @@ const Projects: FunctionComponent = (): ReactElement => {
|
||||
modelId={modelId}
|
||||
modelNameField="name"
|
||||
modelType={Project}
|
||||
modelAPI={AdminModelAPI}
|
||||
title={"Project"}
|
||||
breadcrumbLinks={[
|
||||
{
|
||||
@@ -43,6 +45,7 @@ const Projects: FunctionComponent = (): ReactElement => {
|
||||
<div>
|
||||
<CardModelDetail<Project>
|
||||
name="Project"
|
||||
modelAPI={AdminModelAPI}
|
||||
cardProps={{
|
||||
title: "Project",
|
||||
description: "Project details",
|
||||
|
||||
@@ -73,6 +73,46 @@ const Settings: FunctionComponent = (): ReactElement => {
|
||||
modelId: ObjectID.getZeroObjectID(),
|
||||
}}
|
||||
/>
|
||||
|
||||
<CardModelDetail
|
||||
name="Project Creation Settings"
|
||||
cardProps={{
|
||||
title: "Project Creation",
|
||||
description:
|
||||
"Control who can create new projects on this OneUptime Server.",
|
||||
}}
|
||||
isEditable={true}
|
||||
editButtonText="Edit Settings"
|
||||
formFields={[
|
||||
{
|
||||
field: {
|
||||
disableUserProjectCreation: true,
|
||||
},
|
||||
title: "Restrict Project Creation to Admins Only",
|
||||
fieldType: FormFieldSchemaType.Toggle,
|
||||
required: false,
|
||||
description:
|
||||
"When enabled, only master admin users can create new projects.",
|
||||
},
|
||||
]}
|
||||
modelDetailProps={{
|
||||
modelType: GlobalConfig,
|
||||
id: "model-detail-project-creation",
|
||||
fields: [
|
||||
{
|
||||
field: {
|
||||
disableUserProjectCreation: true,
|
||||
},
|
||||
fieldType: FieldType.Boolean,
|
||||
title: "Restrict Project Creation to Admins Only",
|
||||
placeholder: "No",
|
||||
description:
|
||||
"When enabled, only master admin users can create new projects.",
|
||||
},
|
||||
],
|
||||
modelId: ObjectID.getZeroObjectID(),
|
||||
}}
|
||||
/>
|
||||
</Page>
|
||||
);
|
||||
};
|
||||
|
||||
85
AdminDashboard/src/Pages/Settings/DataRetention/Index.tsx
Normal file
85
AdminDashboard/src/Pages/Settings/DataRetention/Index.tsx
Normal file
@@ -0,0 +1,85 @@
|
||||
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 } from "react";
|
||||
|
||||
const Settings: FunctionComponent = (): ReactElement => {
|
||||
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: "Data Retention",
|
||||
to: RouteUtil.populateRouteParams(
|
||||
RouteMap[PageMap.SETTINGS_DATA_RETENTION] as Route,
|
||||
),
|
||||
},
|
||||
]}
|
||||
sideMenu={<DashboardSideMenu />}
|
||||
>
|
||||
<CardModelDetail
|
||||
name="Monitor Log Retention Settings"
|
||||
cardProps={{
|
||||
title: "Monitor Log Retention",
|
||||
description:
|
||||
"Configure how long monitor logs are retained before being automatically deleted.",
|
||||
}}
|
||||
isEditable={true}
|
||||
editButtonText="Edit Settings"
|
||||
formFields={[
|
||||
{
|
||||
field: {
|
||||
monitorLogRetentionInDays: true,
|
||||
},
|
||||
title: "Monitor Log Retention (Days)",
|
||||
fieldType: FormFieldSchemaType.PositiveNumber,
|
||||
required: false,
|
||||
description:
|
||||
"Number of days to retain monitor logs. Monitor logs older than this will be automatically deleted. Default is 1 day if not set. Minimum: 1 day, Maximum: 365 days.",
|
||||
validation: {
|
||||
minValue: 1,
|
||||
maxValue: 365,
|
||||
},
|
||||
placeholder: "1",
|
||||
},
|
||||
]}
|
||||
modelDetailProps={{
|
||||
modelType: GlobalConfig,
|
||||
id: "model-detail-global-config-data-retention",
|
||||
fields: [
|
||||
{
|
||||
field: {
|
||||
monitorLogRetentionInDays: true,
|
||||
},
|
||||
fieldType: FieldType.Number,
|
||||
title: "Monitor Log Retention (Days)",
|
||||
placeholder: "1 (default)",
|
||||
description:
|
||||
"Number of days to retain monitor logs. Monitor logs older than this will be automatically deleted.",
|
||||
},
|
||||
],
|
||||
modelId: ObjectID.getZeroObjectID(),
|
||||
}}
|
||||
/>
|
||||
</Page>
|
||||
);
|
||||
};
|
||||
|
||||
export default Settings;
|
||||
@@ -72,6 +72,17 @@ const DashboardSideMenu: () => JSX.Element = (): ReactElement => {
|
||||
icon={IconProp.Signal}
|
||||
/>
|
||||
</SideMenuSection>
|
||||
<SideMenuSection title="Data Retention">
|
||||
<SideMenuItem
|
||||
link={{
|
||||
title: "Data Retention",
|
||||
to: RouteUtil.populateRouteParams(
|
||||
RouteMap[PageMap.SETTINGS_DATA_RETENTION] as Route,
|
||||
),
|
||||
}}
|
||||
icon={IconProp.Database}
|
||||
/>
|
||||
</SideMenuSection>
|
||||
<SideMenuSection title="AI">
|
||||
<SideMenuItem
|
||||
link={{
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import AdminModelAPI from "../../../Utils/ModelAPI";
|
||||
import PageMap from "../../../Utils/PageMap";
|
||||
import RouteMap, { RouteUtil } from "../../../Utils/RouteMap";
|
||||
import Route from "Common/Types/API/Route";
|
||||
@@ -17,6 +18,7 @@ const DeletePage: FunctionComponent = (): ReactElement => {
|
||||
modelId={modelId}
|
||||
modelNameField="email"
|
||||
modelType={User}
|
||||
modelAPI={AdminModelAPI}
|
||||
title={"User"}
|
||||
breadcrumbLinks={[
|
||||
{
|
||||
@@ -39,6 +41,7 @@ const DeletePage: FunctionComponent = (): ReactElement => {
|
||||
<ModelDelete
|
||||
modelType={User}
|
||||
modelId={modelId}
|
||||
modelAPI={AdminModelAPI}
|
||||
onDeleteSuccess={() => {
|
||||
Navigation.navigate(RouteMap[PageMap.USERS] as Route);
|
||||
}}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import AdminModelAPI from "../../../Utils/ModelAPI";
|
||||
import ObjectID from "Common/Types/ObjectID";
|
||||
import PageMap from "../../../Utils/PageMap";
|
||||
import RouteMap, { RouteUtil } from "../../../Utils/RouteMap";
|
||||
@@ -19,6 +20,7 @@ const Users: FunctionComponent = (): ReactElement => {
|
||||
modelId={modelId}
|
||||
modelNameField="email"
|
||||
modelType={User}
|
||||
modelAPI={AdminModelAPI}
|
||||
title={"User"}
|
||||
breadcrumbLinks={[
|
||||
{
|
||||
@@ -41,6 +43,7 @@ const Users: FunctionComponent = (): ReactElement => {
|
||||
<div>
|
||||
<CardModelDetail<User>
|
||||
name="User"
|
||||
modelAPI={AdminModelAPI}
|
||||
cardProps={{
|
||||
title: "User",
|
||||
description: "User details",
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import AdminModelAPI from "../../../Utils/ModelAPI";
|
||||
import PageMap from "../../../Utils/PageMap";
|
||||
import RouteMap, { RouteUtil } from "../../../Utils/RouteMap";
|
||||
import Route from "Common/Types/API/Route";
|
||||
@@ -19,6 +20,7 @@ const UserSettings: FunctionComponent = (): ReactElement => {
|
||||
modelId={modelId}
|
||||
modelNameField="email"
|
||||
modelType={User}
|
||||
modelAPI={AdminModelAPI}
|
||||
title={"User"}
|
||||
breadcrumbLinks={[
|
||||
{
|
||||
@@ -52,6 +54,7 @@ const UserSettings: FunctionComponent = (): ReactElement => {
|
||||
>
|
||||
<CardModelDetail<User>
|
||||
name="user-master-admin-settings"
|
||||
modelAPI={AdminModelAPI}
|
||||
cardProps={{
|
||||
title: "Master Admin Access",
|
||||
description:
|
||||
|
||||
@@ -22,6 +22,7 @@ enum PageMap {
|
||||
SETTINGS_LLM_PROVIDERS = "SETTINGS_LLM_PROVIDERS",
|
||||
SETTINGS_AUTHENTICATION = "SETTINGS_AUTHENTICATION",
|
||||
SETTINGS_API_KEY = "SETTINGS_API_KEY",
|
||||
SETTINGS_DATA_RETENTION = "SETTINGS_DATA_RETENTION",
|
||||
}
|
||||
|
||||
export default PageMap;
|
||||
|
||||
@@ -36,6 +36,9 @@ const RouteMap: Dictionary<Route> = {
|
||||
`/admin/settings/authentication`,
|
||||
),
|
||||
[PageMap.SETTINGS_API_KEY]: new Route(`/admin/settings/api-key`),
|
||||
[PageMap.SETTINGS_DATA_RETENTION]: new Route(
|
||||
`/admin/settings/data-retention`,
|
||||
),
|
||||
};
|
||||
|
||||
export class RouteUtil {
|
||||
|
||||
@@ -34,10 +34,11 @@
|
||||
<meta name="theme-color" content="#121212">
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link rel="preload" href="https://fonts.googleapis.com/css2?family=Inter:wght@100;200;300;400;500;600;700;800;900&display=swap" as="style">
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@100;200;300;400;500;600;700;800;900&display=swap" rel="stylesheet">
|
||||
<style>
|
||||
* {
|
||||
font-family: Inter;
|
||||
font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
|
||||
}
|
||||
|
||||
|
||||
@@ -67,6 +68,7 @@
|
||||
width: auto;
|
||||
}
|
||||
</style>
|
||||
<link rel="preload" href="/admin/assets/js/tailwind-3.4.5.js" as="script">
|
||||
<script src="/admin/assets/js/tailwind-3.4.5.js"></script>
|
||||
|
||||
<script src="/admin/env.js"></script>
|
||||
|
||||
@@ -23,6 +23,7 @@ import WhatsAppLogAPI from "./WhatsAppLogAPI";
|
||||
// Import API
|
||||
import ResellerPlanAPI from "Common/Server/API/ResellerPlanAPI";
|
||||
import EnterpriseLicenseAPI from "Common/Server/API/EnterpriseLicenseAPI";
|
||||
import OpenSourceDeploymentAPI from "Common/Server/API/OpenSourceDeploymentAPI";
|
||||
import MonitorAPI from "Common/Server/API/MonitorAPI";
|
||||
import ShortLinkAPI from "Common/Server/API/ShortLinkAPI";
|
||||
import StatusPageAPI from "Common/Server/API/StatusPageAPI";
|
||||
@@ -35,15 +36,18 @@ import UserWebAuthnAPI from "Common/Server/API/UserWebAuthnAPI";
|
||||
import MonitorTest from "Common/Models/DatabaseModels/MonitorTest";
|
||||
import IncidentInternalNoteAPI from "Common/Server/API/IncidentInternalNoteAPI";
|
||||
import IncidentPublicNoteAPI from "Common/Server/API/IncidentPublicNoteAPI";
|
||||
import IncidentEpisodePublicNoteAPI from "Common/Server/API/IncidentEpisodePublicNoteAPI";
|
||||
import ScheduledMaintenanceInternalNoteAPI from "Common/Server/API/ScheduledMaintenanceInternalNoteAPI";
|
||||
import ScheduledMaintenancePublicNoteAPI from "Common/Server/API/ScheduledMaintenancePublicNoteAPI";
|
||||
import IncidentAPI from "Common/Server/API/IncidentAPI";
|
||||
import IncidentEpisodeAPI from "Common/Server/API/IncidentEpisodeAPI";
|
||||
import ScheduledMaintenanceAPI from "Common/Server/API/ScheduledMaintenanceAPI";
|
||||
import AlertAPI from "Common/Server/API/AlertAPI";
|
||||
// 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 UserIncomingCallNumberAPI from "Common/Server/API/UserIncomingCallNumberAPI";
|
||||
import UserWhatsAppAPI from "Common/Server/API/UserWhatsAppAPI";
|
||||
import UserPushAPI from "Common/Server/API/UserPushAPI";
|
||||
import UserAPI from "Common/Server/API/UserAPI";
|
||||
@@ -62,6 +66,12 @@ import DomainService, {
|
||||
import EmailLogService, {
|
||||
Service as EmailLogServiceType,
|
||||
} from "Common/Server/Services/EmailLogService";
|
||||
import ProjectSCIMLogService, {
|
||||
Service as ProjectSCIMLogServiceType,
|
||||
} from "Common/Server/Services/ProjectSCIMLogService";
|
||||
import StatusPageSCIMLogService, {
|
||||
Service as StatusPageSCIMLogServiceType,
|
||||
} from "Common/Server/Services/StatusPageSCIMLogService";
|
||||
import TelemetryIngestionKeyService, {
|
||||
Service as TelemetryIngestionKeyServiceType,
|
||||
} from "Common/Server/Services/TelemetryIngestionKeyService";
|
||||
@@ -99,6 +109,70 @@ import AlertStateTimelineService, {
|
||||
Service as AlertStateTimelineServiceType,
|
||||
} from "Common/Server/Services/AlertStateTimelineService";
|
||||
|
||||
// AlertEpisode Services
|
||||
import AlertEpisodeService, {
|
||||
Service as AlertEpisodeServiceType,
|
||||
} from "Common/Server/Services/AlertEpisodeService";
|
||||
import AlertEpisodeFeedService, {
|
||||
Service as AlertEpisodeFeedServiceType,
|
||||
} from "Common/Server/Services/AlertEpisodeFeedService";
|
||||
import AlertEpisodeInternalNoteService, {
|
||||
Service as AlertEpisodeInternalNoteServiceType,
|
||||
} from "Common/Server/Services/AlertEpisodeInternalNoteService";
|
||||
import AlertEpisodeMemberService, {
|
||||
Service as AlertEpisodeMemberServiceType,
|
||||
} from "Common/Server/Services/AlertEpisodeMemberService";
|
||||
import AlertEpisodeOwnerTeamService, {
|
||||
Service as AlertEpisodeOwnerTeamServiceType,
|
||||
} from "Common/Server/Services/AlertEpisodeOwnerTeamService";
|
||||
import AlertEpisodeOwnerUserService, {
|
||||
Service as AlertEpisodeOwnerUserServiceType,
|
||||
} from "Common/Server/Services/AlertEpisodeOwnerUserService";
|
||||
import AlertEpisodeStateTimelineService, {
|
||||
Service as AlertEpisodeStateTimelineServiceType,
|
||||
} from "Common/Server/Services/AlertEpisodeStateTimelineService";
|
||||
|
||||
// IncidentEpisode Services
|
||||
|
||||
import IncidentEpisodeFeedService, {
|
||||
Service as IncidentEpisodeFeedServiceType,
|
||||
} from "Common/Server/Services/IncidentEpisodeFeedService";
|
||||
import IncidentEpisodeInternalNoteService, {
|
||||
Service as IncidentEpisodeInternalNoteServiceType,
|
||||
} from "Common/Server/Services/IncidentEpisodeInternalNoteService";
|
||||
import IncidentEpisodeMemberService, {
|
||||
Service as IncidentEpisodeMemberServiceType,
|
||||
} from "Common/Server/Services/IncidentEpisodeMemberService";
|
||||
import IncidentEpisodeOwnerTeamService, {
|
||||
Service as IncidentEpisodeOwnerTeamServiceType,
|
||||
} from "Common/Server/Services/IncidentEpisodeOwnerTeamService";
|
||||
import IncidentEpisodeOwnerUserService, {
|
||||
Service as IncidentEpisodeOwnerUserServiceType,
|
||||
} from "Common/Server/Services/IncidentEpisodeOwnerUserService";
|
||||
import IncidentEpisodeStateTimelineService, {
|
||||
Service as IncidentEpisodeStateTimelineServiceType,
|
||||
} from "Common/Server/Services/IncidentEpisodeStateTimelineService";
|
||||
|
||||
import IncidentEpisodeRoleMemberService, {
|
||||
Service as IncidentEpisodeRoleMemberServiceType,
|
||||
} from "Common/Server/Services/IncidentEpisodeRoleMemberService";
|
||||
|
||||
import AlertGroupingRuleService, {
|
||||
Service as AlertGroupingRuleServiceType,
|
||||
} from "Common/Server/Services/AlertGroupingRuleService";
|
||||
|
||||
import IncidentGroupingRuleService, {
|
||||
Service as IncidentGroupingRuleServiceType,
|
||||
} from "Common/Server/Services/IncidentGroupingRuleService";
|
||||
|
||||
import IncidentSlaService, {
|
||||
Service as IncidentSlaServiceType,
|
||||
} from "Common/Server/Services/IncidentSlaService";
|
||||
|
||||
import IncidentSlaRuleService, {
|
||||
Service as IncidentSlaRuleServiceType,
|
||||
} from "Common/Server/Services/IncidentSlaRuleService";
|
||||
|
||||
import IncidentCustomFieldService, {
|
||||
Service as IncidentCustomFieldServiceType,
|
||||
} from "Common/Server/Services/IncidentCustomFieldService";
|
||||
@@ -120,6 +194,12 @@ import IncidentOwnerUserService, {
|
||||
import IncidentSeverityService, {
|
||||
Service as IncidentSeverityServiceType,
|
||||
} from "Common/Server/Services/IncidentSeverityService";
|
||||
import IncidentRoleService, {
|
||||
Service as IncidentRoleServiceType,
|
||||
} from "Common/Server/Services/IncidentRoleService";
|
||||
import IncidentMemberService, {
|
||||
Service as IncidentMemberServiceType,
|
||||
} from "Common/Server/Services/IncidentMemberService";
|
||||
import IncidentStateService, {
|
||||
Service as IncidentStateServiceType,
|
||||
} from "Common/Server/Services/IncidentStateService";
|
||||
@@ -189,6 +269,20 @@ import OnCallDutyPolicyCustomFieldService, {
|
||||
import OnCallDutyPolicyEscalationRuleScheduleService, {
|
||||
Service as OnCallDutyPolicyEscalationRuleScheduleServiceType,
|
||||
} from "Common/Server/Services/OnCallDutyPolicyEscalationRuleScheduleService";
|
||||
|
||||
// Incoming Call Policy
|
||||
import IncomingCallPolicyService, {
|
||||
Service as IncomingCallPolicyServiceType,
|
||||
} from "Common/Server/Services/IncomingCallPolicyService";
|
||||
import IncomingCallPolicyEscalationRuleService, {
|
||||
Service as IncomingCallPolicyEscalationRuleServiceType,
|
||||
} from "Common/Server/Services/IncomingCallPolicyEscalationRuleService";
|
||||
import IncomingCallLogService, {
|
||||
Service as IncomingCallLogServiceType,
|
||||
} from "Common/Server/Services/IncomingCallLogService";
|
||||
import IncomingCallLogItemService, {
|
||||
Service as IncomingCallLogItemServiceType,
|
||||
} from "Common/Server/Services/IncomingCallLogItemService";
|
||||
import OnCallDutyPolicyEscalationRuleService, {
|
||||
Service as OnCallDutyPolicyEscalationRuleServiceType,
|
||||
} from "Common/Server/Services/OnCallDutyPolicyEscalationRuleService";
|
||||
@@ -216,6 +310,9 @@ import OnCallDutyPolicyScheduleService, {
|
||||
import ProjectCallSMSConfigService, {
|
||||
Service as ProjectCallSMSConfigServiceType,
|
||||
} from "Common/Server/Services/ProjectCallSMSConfigService";
|
||||
import ProjectUserProfileService, {
|
||||
Service as ProjectUserProfileServiceType,
|
||||
} from "Common/Server/Services/ProjectUserProfileService";
|
||||
import ProjectSmtpConfigService, {
|
||||
Service as ProjectSMTPConfigServiceType,
|
||||
} from "Common/Server/Services/ProjectSmtpConfigService";
|
||||
@@ -315,6 +412,9 @@ import StatusPageSSOService, {
|
||||
import TeamMemberService, {
|
||||
TeamMemberService as TeamMemberServiceType,
|
||||
} from "Common/Server/Services/TeamMemberService";
|
||||
import TeamMemberCustomFieldService, {
|
||||
Service as TeamMemberCustomFieldServiceType,
|
||||
} from "Common/Server/Services/TeamMemberCustomFieldService";
|
||||
import TeamPermissionService, {
|
||||
Service as TeamPermissionServiceType,
|
||||
} from "Common/Server/Services/TeamPermissionService";
|
||||
@@ -388,6 +488,8 @@ import PushNotificationLog from "Common/Models/DatabaseModels/PushNotificationLo
|
||||
import WorkspaceNotificationLog from "Common/Models/DatabaseModels/WorkspaceNotificationLog";
|
||||
import Domain from "Common/Models/DatabaseModels/Domain";
|
||||
import EmailLog from "Common/Models/DatabaseModels/EmailLog";
|
||||
import ProjectSCIMLog from "Common/Models/DatabaseModels/ProjectSCIMLog";
|
||||
import StatusPageSCIMLog from "Common/Models/DatabaseModels/StatusPageSCIMLog";
|
||||
import EmailVerificationToken from "Common/Models/DatabaseModels/EmailVerificationToken";
|
||||
import Dashboard from "Common/Models/DatabaseModels/Dashboard";
|
||||
|
||||
@@ -399,12 +501,36 @@ import AlertSeverity from "Common/Models/DatabaseModels/AlertSeverity";
|
||||
import AlertState from "Common/Models/DatabaseModels/AlertState";
|
||||
import AlertStateTimeline from "Common/Models/DatabaseModels/AlertStateTimeline";
|
||||
|
||||
// AlertEpisode Models
|
||||
import AlertEpisode from "Common/Models/DatabaseModels/AlertEpisode";
|
||||
import AlertEpisodeFeed from "Common/Models/DatabaseModels/AlertEpisodeFeed";
|
||||
import AlertEpisodeInternalNote from "Common/Models/DatabaseModels/AlertEpisodeInternalNote";
|
||||
import AlertEpisodeMember from "Common/Models/DatabaseModels/AlertEpisodeMember";
|
||||
import AlertEpisodeOwnerTeam from "Common/Models/DatabaseModels/AlertEpisodeOwnerTeam";
|
||||
import AlertEpisodeOwnerUser from "Common/Models/DatabaseModels/AlertEpisodeOwnerUser";
|
||||
import AlertEpisodeStateTimeline from "Common/Models/DatabaseModels/AlertEpisodeStateTimeline";
|
||||
import AlertGroupingRule from "Common/Models/DatabaseModels/AlertGroupingRule";
|
||||
import IncidentGroupingRule from "Common/Models/DatabaseModels/IncidentGroupingRule";
|
||||
import IncidentSla from "Common/Models/DatabaseModels/IncidentSla";
|
||||
import IncidentSlaRule from "Common/Models/DatabaseModels/IncidentSlaRule";
|
||||
|
||||
// IncidentEpisode Models
|
||||
import IncidentEpisodeFeed from "Common/Models/DatabaseModels/IncidentEpisodeFeed";
|
||||
import IncidentEpisodeInternalNote from "Common/Models/DatabaseModels/IncidentEpisodeInternalNote";
|
||||
import IncidentEpisodeMember from "Common/Models/DatabaseModels/IncidentEpisodeMember";
|
||||
import IncidentEpisodeOwnerTeam from "Common/Models/DatabaseModels/IncidentEpisodeOwnerTeam";
|
||||
import IncidentEpisodeOwnerUser from "Common/Models/DatabaseModels/IncidentEpisodeOwnerUser";
|
||||
import IncidentEpisodeStateTimeline from "Common/Models/DatabaseModels/IncidentEpisodeStateTimeline";
|
||||
import IncidentEpisodeRoleMember from "Common/Models/DatabaseModels/IncidentEpisodeRoleMember";
|
||||
|
||||
import IncidentCustomField from "Common/Models/DatabaseModels/IncidentCustomField";
|
||||
import IncidentNoteTemplate from "Common/Models/DatabaseModels/IncidentNoteTemplate";
|
||||
import IncidentPostmortemTemplate from "Common/Models/DatabaseModels/IncidentPostmortemTemplate";
|
||||
import IncidentOwnerTeam from "Common/Models/DatabaseModels/IncidentOwnerTeam";
|
||||
import IncidentOwnerUser from "Common/Models/DatabaseModels/IncidentOwnerUser";
|
||||
import IncidentSeverity from "Common/Models/DatabaseModels/IncidentSeverity";
|
||||
import IncidentRole from "Common/Models/DatabaseModels/IncidentRole";
|
||||
import IncidentMember from "Common/Models/DatabaseModels/IncidentMember";
|
||||
import IncidentState from "Common/Models/DatabaseModels/IncidentState";
|
||||
import IncidentStateTimeline from "Common/Models/DatabaseModels/IncidentStateTimeline";
|
||||
import IncidentTemplate from "Common/Models/DatabaseModels/IncidentTemplate";
|
||||
@@ -424,6 +550,12 @@ import MonitorStatus from "Common/Models/DatabaseModels/MonitorStatus";
|
||||
import MonitorTimelineStatus from "Common/Models/DatabaseModels/MonitorStatusTimeline";
|
||||
import OnCallDutyPolicyCustomField from "Common/Models/DatabaseModels/OnCallDutyPolicyCustomField";
|
||||
import OnCallDutyPolicyEscalationRule from "Common/Models/DatabaseModels/OnCallDutyPolicyEscalationRule";
|
||||
|
||||
// Incoming Call Policy Models
|
||||
import IncomingCallPolicy from "Common/Models/DatabaseModels/IncomingCallPolicy";
|
||||
import IncomingCallPolicyEscalationRule from "Common/Models/DatabaseModels/IncomingCallPolicyEscalationRule";
|
||||
import IncomingCallLog from "Common/Models/DatabaseModels/IncomingCallLog";
|
||||
import IncomingCallLogItem from "Common/Models/DatabaseModels/IncomingCallLogItem";
|
||||
import OnCallDutyPolicyEscalationRuleSchedule from "Common/Models/DatabaseModels/OnCallDutyPolicyEscalationRuleSchedule";
|
||||
import OnCallDutyPolicyEscalationRuleTeam from "Common/Models/DatabaseModels/OnCallDutyPolicyEscalationRuleTeam";
|
||||
import OnCallDutyPolicyEscalationRuleUser from "Common/Models/DatabaseModels/OnCallDutyPolicyEscalationRuleUser";
|
||||
@@ -434,6 +566,7 @@ import OnCallDutyPolicyScheduleLayer from "Common/Models/DatabaseModels/OnCallDu
|
||||
import OnCallDutyPolicyScheduleLayerUser from "Common/Models/DatabaseModels/OnCallDutyPolicyScheduleLayerUser";
|
||||
import ProjectCallSMSConfig from "Common/Models/DatabaseModels/ProjectCallSMSConfig";
|
||||
import ProjectSmtpConfig from "Common/Models/DatabaseModels/ProjectSmtpConfig";
|
||||
import ProjectUserProfile from "Common/Models/DatabaseModels/ProjectUserProfile";
|
||||
import PromoCode from "Common/Models/DatabaseModels/PromoCode";
|
||||
import CodeRepository from "Common/Models/DatabaseModels/CodeRepository";
|
||||
import Reseller from "Common/Models/DatabaseModels/Reseller";
|
||||
@@ -462,6 +595,7 @@ import StatusPageResource from "Common/Models/DatabaseModels/StatusPageResource"
|
||||
import StatusPageSSO from "Common/Models/DatabaseModels/StatusPageSso";
|
||||
import Team from "Common/Models/DatabaseModels/Team";
|
||||
import TeamMember from "Common/Models/DatabaseModels/TeamMember";
|
||||
import TeamMemberCustomField from "Common/Models/DatabaseModels/TeamMemberCustomField";
|
||||
import TeamPermission from "Common/Models/DatabaseModels/TeamPermission";
|
||||
import TeamComplianceSetting from "Common/Models/DatabaseModels/TeamComplianceSetting";
|
||||
import TelemetryUsageBilling from "Common/Models/DatabaseModels/TelemetryUsageBilling";
|
||||
@@ -876,6 +1010,171 @@ const BaseAPIFeatureSet: FeatureSet = {
|
||||
).getRouter(),
|
||||
);
|
||||
|
||||
// AlertEpisode Routes
|
||||
app.use(
|
||||
`/${APP_NAME.toLocaleLowerCase()}`,
|
||||
new BaseAPI<AlertEpisode, AlertEpisodeServiceType>(
|
||||
AlertEpisode,
|
||||
AlertEpisodeService,
|
||||
).getRouter(),
|
||||
);
|
||||
|
||||
app.use(
|
||||
`/${APP_NAME.toLocaleLowerCase()}`,
|
||||
new BaseAPI<AlertEpisodeFeed, AlertEpisodeFeedServiceType>(
|
||||
AlertEpisodeFeed,
|
||||
AlertEpisodeFeedService,
|
||||
).getRouter(),
|
||||
);
|
||||
|
||||
app.use(
|
||||
`/${APP_NAME.toLocaleLowerCase()}`,
|
||||
new BaseAPI<
|
||||
AlertEpisodeInternalNote,
|
||||
AlertEpisodeInternalNoteServiceType
|
||||
>(AlertEpisodeInternalNote, AlertEpisodeInternalNoteService).getRouter(),
|
||||
);
|
||||
|
||||
app.use(
|
||||
`/${APP_NAME.toLocaleLowerCase()}`,
|
||||
new BaseAPI<AlertEpisodeMember, AlertEpisodeMemberServiceType>(
|
||||
AlertEpisodeMember,
|
||||
AlertEpisodeMemberService,
|
||||
).getRouter(),
|
||||
);
|
||||
|
||||
app.use(
|
||||
`/${APP_NAME.toLocaleLowerCase()}`,
|
||||
new BaseAPI<AlertEpisodeOwnerTeam, AlertEpisodeOwnerTeamServiceType>(
|
||||
AlertEpisodeOwnerTeam,
|
||||
AlertEpisodeOwnerTeamService,
|
||||
).getRouter(),
|
||||
);
|
||||
|
||||
app.use(
|
||||
`/${APP_NAME.toLocaleLowerCase()}`,
|
||||
new BaseAPI<AlertEpisodeOwnerUser, AlertEpisodeOwnerUserServiceType>(
|
||||
AlertEpisodeOwnerUser,
|
||||
AlertEpisodeOwnerUserService,
|
||||
).getRouter(),
|
||||
);
|
||||
|
||||
app.use(
|
||||
`/${APP_NAME.toLocaleLowerCase()}`,
|
||||
new BaseAPI<
|
||||
AlertEpisodeStateTimeline,
|
||||
AlertEpisodeStateTimelineServiceType
|
||||
>(
|
||||
AlertEpisodeStateTimeline,
|
||||
AlertEpisodeStateTimelineService,
|
||||
).getRouter(),
|
||||
);
|
||||
|
||||
// IncidentEpisode Routes
|
||||
app.use(
|
||||
`/${APP_NAME.toLocaleLowerCase()}`,
|
||||
new IncidentEpisodeAPI().getRouter(),
|
||||
);
|
||||
|
||||
app.use(
|
||||
`/${APP_NAME.toLocaleLowerCase()}`,
|
||||
new BaseAPI<IncidentEpisodeFeed, IncidentEpisodeFeedServiceType>(
|
||||
IncidentEpisodeFeed,
|
||||
IncidentEpisodeFeedService,
|
||||
).getRouter(),
|
||||
);
|
||||
|
||||
app.use(
|
||||
`/${APP_NAME.toLocaleLowerCase()}`,
|
||||
new BaseAPI<
|
||||
IncidentEpisodeInternalNote,
|
||||
IncidentEpisodeInternalNoteServiceType
|
||||
>(
|
||||
IncidentEpisodeInternalNote,
|
||||
IncidentEpisodeInternalNoteService,
|
||||
).getRouter(),
|
||||
);
|
||||
|
||||
app.use(
|
||||
`/${APP_NAME.toLocaleLowerCase()}`,
|
||||
new BaseAPI<IncidentEpisodeMember, IncidentEpisodeMemberServiceType>(
|
||||
IncidentEpisodeMember,
|
||||
IncidentEpisodeMemberService,
|
||||
).getRouter(),
|
||||
);
|
||||
|
||||
app.use(
|
||||
`/${APP_NAME.toLocaleLowerCase()}`,
|
||||
new BaseAPI<
|
||||
IncidentEpisodeOwnerTeam,
|
||||
IncidentEpisodeOwnerTeamServiceType
|
||||
>(IncidentEpisodeOwnerTeam, IncidentEpisodeOwnerTeamService).getRouter(),
|
||||
);
|
||||
|
||||
app.use(
|
||||
`/${APP_NAME.toLocaleLowerCase()}`,
|
||||
new BaseAPI<
|
||||
IncidentEpisodeOwnerUser,
|
||||
IncidentEpisodeOwnerUserServiceType
|
||||
>(IncidentEpisodeOwnerUser, IncidentEpisodeOwnerUserService).getRouter(),
|
||||
);
|
||||
|
||||
app.use(
|
||||
`/${APP_NAME.toLocaleLowerCase()}`,
|
||||
new BaseAPI<
|
||||
IncidentEpisodeStateTimeline,
|
||||
IncidentEpisodeStateTimelineServiceType
|
||||
>(
|
||||
IncidentEpisodeStateTimeline,
|
||||
IncidentEpisodeStateTimelineService,
|
||||
).getRouter(),
|
||||
);
|
||||
|
||||
app.use(
|
||||
`/${APP_NAME.toLocaleLowerCase()}`,
|
||||
new BaseAPI<
|
||||
IncidentEpisodeRoleMember,
|
||||
IncidentEpisodeRoleMemberServiceType
|
||||
>(
|
||||
IncidentEpisodeRoleMember,
|
||||
IncidentEpisodeRoleMemberService,
|
||||
).getRouter(),
|
||||
);
|
||||
|
||||
app.use(
|
||||
`/${APP_NAME.toLocaleLowerCase()}`,
|
||||
new BaseAPI<AlertGroupingRule, AlertGroupingRuleServiceType>(
|
||||
AlertGroupingRule,
|
||||
AlertGroupingRuleService,
|
||||
).getRouter(),
|
||||
);
|
||||
|
||||
app.use(
|
||||
`/${APP_NAME.toLocaleLowerCase()}`,
|
||||
new BaseAPI<IncidentGroupingRule, IncidentGroupingRuleServiceType>(
|
||||
IncidentGroupingRule,
|
||||
IncidentGroupingRuleService,
|
||||
).getRouter(),
|
||||
);
|
||||
|
||||
// IncidentSla
|
||||
app.use(
|
||||
`/${APP_NAME.toLocaleLowerCase()}`,
|
||||
new BaseAPI<IncidentSla, IncidentSlaServiceType>(
|
||||
IncidentSla,
|
||||
IncidentSlaService,
|
||||
).getRouter(),
|
||||
);
|
||||
|
||||
// IncidentSlaRule
|
||||
app.use(
|
||||
`/${APP_NAME.toLocaleLowerCase()}`,
|
||||
new BaseAPI<IncidentSlaRule, IncidentSlaRuleServiceType>(
|
||||
IncidentSlaRule,
|
||||
IncidentSlaRuleService,
|
||||
).getRouter(),
|
||||
);
|
||||
|
||||
app.use(
|
||||
`/${APP_NAME.toLocaleLowerCase()}`,
|
||||
new BaseAnalyticsAPI<ExceptionInstance, ExceptionInstanceServiceType>(
|
||||
@@ -1131,6 +1430,14 @@ const BaseAPIFeatureSet: FeatureSet = {
|
||||
).getRouter(),
|
||||
);
|
||||
|
||||
app.use(
|
||||
`/${APP_NAME.toLocaleLowerCase()}`,
|
||||
new BaseAPI<ProjectUserProfile, ProjectUserProfileServiceType>(
|
||||
ProjectUserProfile,
|
||||
ProjectUserProfileService,
|
||||
).getRouter(),
|
||||
);
|
||||
|
||||
app.use(
|
||||
`/${APP_NAME.toLocaleLowerCase()}`,
|
||||
new BaseAPI<MonitorGroupResource, MonitorGroupResourceServiceType>(
|
||||
@@ -1147,6 +1454,14 @@ const BaseAPIFeatureSet: FeatureSet = {
|
||||
).getRouter(),
|
||||
);
|
||||
|
||||
app.use(
|
||||
`/${APP_NAME.toLocaleLowerCase()}`,
|
||||
new BaseAPI<TeamMemberCustomField, TeamMemberCustomFieldServiceType>(
|
||||
TeamMemberCustomField,
|
||||
TeamMemberCustomFieldService,
|
||||
).getRouter(),
|
||||
);
|
||||
|
||||
app.use(
|
||||
`/${APP_NAME.toLocaleLowerCase()}`,
|
||||
new BaseAPI<TeamPermission, TeamPermissionServiceType>(
|
||||
@@ -1333,6 +1648,22 @@ const BaseAPIFeatureSet: FeatureSet = {
|
||||
).getRouter(),
|
||||
);
|
||||
|
||||
app.use(
|
||||
`/${APP_NAME.toLocaleLowerCase()}`,
|
||||
new BaseAPI<IncidentRole, IncidentRoleServiceType>(
|
||||
IncidentRole,
|
||||
IncidentRoleService,
|
||||
).getRouter(),
|
||||
);
|
||||
|
||||
app.use(
|
||||
`/${APP_NAME.toLocaleLowerCase()}`,
|
||||
new BaseAPI<IncidentMember, IncidentMemberServiceType>(
|
||||
IncidentMember,
|
||||
IncidentMemberService,
|
||||
).getRouter(),
|
||||
);
|
||||
|
||||
app.use(
|
||||
`/${APP_NAME.toLocaleLowerCase()}`,
|
||||
new BaseAPI<IncidentOwnerUser, IncidentOwnerUserServiceType>(
|
||||
@@ -1545,6 +1876,22 @@ const BaseAPIFeatureSet: FeatureSet = {
|
||||
).getRouter(),
|
||||
);
|
||||
|
||||
app.use(
|
||||
`/${APP_NAME.toLocaleLowerCase()}`,
|
||||
new BaseAPI<ProjectSCIMLog, ProjectSCIMLogServiceType>(
|
||||
ProjectSCIMLog,
|
||||
ProjectSCIMLogService,
|
||||
).getRouter(),
|
||||
);
|
||||
|
||||
app.use(
|
||||
`/${APP_NAME.toLocaleLowerCase()}`,
|
||||
new BaseAPI<StatusPageSCIMLog, StatusPageSCIMLogServiceType>(
|
||||
StatusPageSCIMLog,
|
||||
StatusPageSCIMLogService,
|
||||
).getRouter(),
|
||||
);
|
||||
|
||||
app.use(
|
||||
`/${APP_NAME.toLocaleLowerCase()}`,
|
||||
new BaseAPI<Reseller, ResellerServiceType>(
|
||||
@@ -1589,6 +1936,45 @@ const BaseAPIFeatureSet: FeatureSet = {
|
||||
new OnCallDutyPolicyAPI().getRouter(),
|
||||
);
|
||||
|
||||
// IncomingCallPolicy
|
||||
app.use(
|
||||
`/${APP_NAME.toLocaleLowerCase()}`,
|
||||
new BaseAPI<IncomingCallPolicy, IncomingCallPolicyServiceType>(
|
||||
IncomingCallPolicy,
|
||||
IncomingCallPolicyService,
|
||||
).getRouter(),
|
||||
);
|
||||
|
||||
// IncomingCallPolicyEscalationRule
|
||||
app.use(
|
||||
`/${APP_NAME.toLocaleLowerCase()}`,
|
||||
new BaseAPI<
|
||||
IncomingCallPolicyEscalationRule,
|
||||
IncomingCallPolicyEscalationRuleServiceType
|
||||
>(
|
||||
IncomingCallPolicyEscalationRule,
|
||||
IncomingCallPolicyEscalationRuleService,
|
||||
).getRouter(),
|
||||
);
|
||||
|
||||
// IncomingCallLog
|
||||
app.use(
|
||||
`/${APP_NAME.toLocaleLowerCase()}`,
|
||||
new BaseAPI<IncomingCallLog, IncomingCallLogServiceType>(
|
||||
IncomingCallLog,
|
||||
IncomingCallLogService,
|
||||
).getRouter(),
|
||||
);
|
||||
|
||||
// IncomingCallLogItem
|
||||
app.use(
|
||||
`/${APP_NAME.toLocaleLowerCase()}`,
|
||||
new BaseAPI<IncomingCallLogItem, IncomingCallLogItemServiceType>(
|
||||
IncomingCallLogItem,
|
||||
IncomingCallLogItemService,
|
||||
).getRouter(),
|
||||
);
|
||||
|
||||
// TeamComplianceAPI
|
||||
app.use(
|
||||
`/${APP_NAME.toLocaleLowerCase()}`,
|
||||
@@ -1622,6 +2008,10 @@ const BaseAPIFeatureSet: FeatureSet = {
|
||||
`/${APP_NAME.toLocaleLowerCase()}`,
|
||||
new EnterpriseLicenseAPI().getRouter(),
|
||||
);
|
||||
app.use(
|
||||
`/${APP_NAME.toLocaleLowerCase()}`,
|
||||
new OpenSourceDeploymentAPI().getRouter(),
|
||||
);
|
||||
app.use(`/${APP_NAME.toLocaleLowerCase()}`, new SlackAPI().getRouter());
|
||||
app.use(
|
||||
`/${APP_NAME.toLocaleLowerCase()}`,
|
||||
@@ -1648,6 +2038,10 @@ const BaseAPIFeatureSet: FeatureSet = {
|
||||
);
|
||||
app.use(`/${APP_NAME.toLocaleLowerCase()}`, new UserEmailAPI().getRouter());
|
||||
app.use(`/${APP_NAME.toLocaleLowerCase()}`, new UserSMSAPI().getRouter());
|
||||
app.use(
|
||||
`/${APP_NAME.toLocaleLowerCase()}`,
|
||||
new UserIncomingCallNumberAPI().getRouter(),
|
||||
);
|
||||
app.use(
|
||||
`/${APP_NAME.toLocaleLowerCase()}`,
|
||||
new UserWhatsAppAPI().getRouter(),
|
||||
@@ -1752,6 +2146,11 @@ const BaseAPIFeatureSet: FeatureSet = {
|
||||
new IncidentPublicNoteAPI().getRouter(),
|
||||
);
|
||||
|
||||
app.use(
|
||||
`/${APP_NAME.toLocaleLowerCase()}`,
|
||||
new IncidentEpisodePublicNoteAPI().getRouter(),
|
||||
);
|
||||
|
||||
app.use(
|
||||
`/${APP_NAME.toLocaleLowerCase()}`,
|
||||
new IncidentInternalNoteAPI().getRouter(),
|
||||
|
||||
@@ -17,9 +17,11 @@ import ObjectID from "Common/Types/ObjectID";
|
||||
import PositiveNumber from "Common/Types/PositiveNumber";
|
||||
import DatabaseConfig from "Common/Server/DatabaseConfig";
|
||||
import {
|
||||
AppVersion,
|
||||
EncryptionSecret,
|
||||
IsBillingEnabled,
|
||||
} from "Common/Server/EnvironmentConfig";
|
||||
import API from "Common/Utils/API";
|
||||
import AccessTokenService from "Common/Server/Services/AccessTokenService";
|
||||
import EmailVerificationTokenService from "Common/Server/Services/EmailVerificationTokenService";
|
||||
import MailService from "Common/Server/Services/MailService";
|
||||
@@ -29,6 +31,7 @@ import UserSessionService, {
|
||||
SessionMetadata,
|
||||
} from "Common/Server/Services/UserSessionService";
|
||||
import CookieUtil from "Common/Server/Utils/Cookie";
|
||||
import JSONWebToken from "Common/Server/Utils/JsonWebToken";
|
||||
import Express, {
|
||||
ExpressRequest,
|
||||
ExpressResponse,
|
||||
@@ -54,6 +57,11 @@ const router: ExpressRouter = Express.getRouter();
|
||||
|
||||
const ACCESS_TOKEN_EXPIRY_SECONDS: number = 15 * 60;
|
||||
|
||||
interface FinalizeUserLoginResult {
|
||||
sessionMetadata: SessionMetadata;
|
||||
accessToken: string;
|
||||
}
|
||||
|
||||
type FinalizeUserLoginInput = {
|
||||
req: ExpressRequest;
|
||||
res: ExpressResponse;
|
||||
@@ -63,9 +71,9 @@ type FinalizeUserLoginInput = {
|
||||
|
||||
const finalizeUserLogin: (
|
||||
data: FinalizeUserLoginInput,
|
||||
) => Promise<SessionMetadata> = async (
|
||||
) => Promise<FinalizeUserLoginResult> = async (
|
||||
data: FinalizeUserLoginInput,
|
||||
): Promise<SessionMetadata> => {
|
||||
): Promise<FinalizeUserLoginResult> => {
|
||||
const { req, res, user, isGlobalLogin } = data;
|
||||
|
||||
const sessionMetadata: SessionMetadata =
|
||||
@@ -87,7 +95,21 @@ const finalizeUserLogin: (
|
||||
accessTokenExpiresInSeconds: ACCESS_TOKEN_EXPIRY_SECONDS,
|
||||
});
|
||||
|
||||
return sessionMetadata;
|
||||
// Generate access token for response body (used by mobile clients)
|
||||
const accessToken: string = JSONWebToken.signUserLoginToken({
|
||||
tokenData: {
|
||||
userId: user.id!,
|
||||
email: user.email!,
|
||||
name: user.name!,
|
||||
timezone: user.timezone || null,
|
||||
isMasterAdmin: user.isMasterAdmin!,
|
||||
isGlobalLogin: isGlobalLogin,
|
||||
sessionId: sessionMetadata.session.id!,
|
||||
},
|
||||
expiresInSeconds: ACCESS_TOKEN_EXPIRY_SECONDS,
|
||||
});
|
||||
|
||||
return { sessionMetadata, accessToken };
|
||||
};
|
||||
|
||||
router.post(
|
||||
@@ -251,6 +273,28 @@ router.post(
|
||||
|
||||
logger.info("User signed up: " + savedUser.email?.toString());
|
||||
|
||||
if (!IsBillingEnabled && miscDataProps["notifySelfHosted"] === true) {
|
||||
const instanceUrl: string = new URL(httpProtocol, host).toString();
|
||||
|
||||
API.post({
|
||||
url: URL.fromString(
|
||||
"https://oneuptime.com/api/open-source-deployment/register",
|
||||
),
|
||||
data: {
|
||||
email: savedUser.email?.toString() || "",
|
||||
name: savedUser.name?.toString() || "",
|
||||
companyName:
|
||||
(miscDataProps["selfHostedCompanyName"] as string) || undefined,
|
||||
companyPhoneNumber:
|
||||
(miscDataProps["selfHostedPhoneNumber"] as string) || undefined,
|
||||
oneuptimeVersion: AppVersion,
|
||||
instanceUrl: instanceUrl,
|
||||
},
|
||||
}).catch((err: Error) => {
|
||||
logger.error(err);
|
||||
});
|
||||
}
|
||||
|
||||
return Response.sendEntityResponse(req, res, savedUser, User);
|
||||
}
|
||||
|
||||
@@ -552,8 +596,10 @@ router.post(
|
||||
next: NextFunction,
|
||||
): Promise<void> => {
|
||||
try {
|
||||
// Try cookie first, then fallback to request body (for mobile clients)
|
||||
const refreshToken: string | undefined =
|
||||
CookieUtil.getRefreshTokenFromExpressRequest(req);
|
||||
CookieUtil.getRefreshTokenFromExpressRequest(req) ||
|
||||
(req.body.refreshToken as string | undefined);
|
||||
|
||||
if (!refreshToken) {
|
||||
CookieUtil.removeAllCookies(req, res);
|
||||
@@ -658,7 +704,26 @@ router.post(
|
||||
accessTokenExpiresInSeconds: ACCESS_TOKEN_EXPIRY_SECONDS,
|
||||
});
|
||||
|
||||
return Response.sendEmptySuccessResponse(req, res);
|
||||
// Generate access token for response body (used by mobile clients)
|
||||
const newAccessToken: string = JSONWebToken.signUserLoginToken({
|
||||
tokenData: {
|
||||
userId: user.id!,
|
||||
email: user.email!,
|
||||
name: user.name!,
|
||||
timezone: user.timezone || null,
|
||||
isMasterAdmin: user.isMasterAdmin!,
|
||||
isGlobalLogin: isGlobalLogin,
|
||||
sessionId: renewedSession.session.id!,
|
||||
},
|
||||
expiresInSeconds: ACCESS_TOKEN_EXPIRY_SECONDS,
|
||||
});
|
||||
|
||||
return Response.sendJsonObjectResponse(req, res, {
|
||||
accessToken: newAccessToken,
|
||||
refreshToken: renewedSession.refreshToken,
|
||||
refreshTokenExpiresAt:
|
||||
renewedSession.refreshTokenExpiresAt.toISOString(),
|
||||
});
|
||||
} catch (err) {
|
||||
return next(err);
|
||||
}
|
||||
@@ -673,8 +738,10 @@ router.post(
|
||||
next: NextFunction,
|
||||
): Promise<void> => {
|
||||
try {
|
||||
// Try cookie first, then fallback to request body (for mobile clients)
|
||||
const refreshToken: string | undefined =
|
||||
CookieUtil.getRefreshTokenFromExpressRequest(req);
|
||||
CookieUtil.getRefreshTokenFromExpressRequest(req) ||
|
||||
(req.body.refreshToken as string | undefined);
|
||||
|
||||
if (refreshToken) {
|
||||
await UserSessionService.revokeSessionByRefreshToken(refreshToken, {
|
||||
@@ -987,14 +1054,21 @@ const login: LoginFunction = async (options: {
|
||||
if (alreadySavedUser.password.toString() === user.password!.toString()) {
|
||||
logger.info("User logged in: " + alreadySavedUser.email?.toString());
|
||||
|
||||
await finalizeUserLogin({
|
||||
const loginResult: FinalizeUserLoginResult = await finalizeUserLogin({
|
||||
req,
|
||||
res,
|
||||
user: alreadySavedUser,
|
||||
isGlobalLogin: true,
|
||||
});
|
||||
|
||||
return Response.sendEntityResponse(req, res, alreadySavedUser, User);
|
||||
return Response.sendEntityResponse(req, res, alreadySavedUser, User, {
|
||||
miscData: {
|
||||
accessToken: loginResult.accessToken,
|
||||
refreshToken: loginResult.sessionMetadata.refreshToken,
|
||||
refreshTokenExpiresAt:
|
||||
loginResult.sessionMetadata.refreshTokenExpiresAt.toISOString(),
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
return Response.sendErrorResponse(
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -26,6 +26,7 @@ import UserSessionService, {
|
||||
import QueryHelper from "Common/Server/Types/Database/QueryHelper";
|
||||
import Select from "Common/Server/Types/Database/Select";
|
||||
import CookieUtil from "Common/Server/Utils/Cookie";
|
||||
import JSONWebToken from "Common/Server/Utils/JsonWebToken";
|
||||
import Express, {
|
||||
ExpressRequest,
|
||||
ExpressResponse,
|
||||
@@ -240,6 +241,8 @@ router.get(
|
||||
);
|
||||
}
|
||||
|
||||
const isMobileRequest: boolean = req.query["mobile"] === "true";
|
||||
|
||||
const samlRequestUrl: URL = SSOUtil.createSAMLRequestUrl({
|
||||
acsUrl: URL.fromString(
|
||||
`${HttpProtocol}${Host}/identity/idp-login/${projectSSO.projectId?.toString()}/${projectSSO.id?.toString()}`,
|
||||
@@ -250,6 +253,10 @@ router.get(
|
||||
),
|
||||
});
|
||||
|
||||
if (isMobileRequest) {
|
||||
samlRequestUrl.addQueryParam("RelayState", "mobile");
|
||||
}
|
||||
|
||||
return Response.redirect(req, res, samlRequestUrl);
|
||||
} catch (err) {
|
||||
logger.error(err);
|
||||
@@ -538,15 +545,11 @@ const loginUserWithSso: LoginUserWithSsoFunction = async (
|
||||
}
|
||||
|
||||
const projectId: ObjectID = new ObjectID(req.params["projectId"] as string);
|
||||
const isMobileRequest: boolean =
|
||||
req.body.RelayState === "mobile" || req.query["RelayState"] === "mobile";
|
||||
|
||||
alreadySavedUser.email = email;
|
||||
|
||||
CookieUtil.setSSOCookie({
|
||||
user: alreadySavedUser,
|
||||
projectId: projectId,
|
||||
expressResponse: res,
|
||||
});
|
||||
|
||||
// Refresh Permissions for this user here.
|
||||
await AccessTokenService.refreshUserAllPermissions(alreadySavedUser.id!);
|
||||
|
||||
@@ -562,6 +565,66 @@ const loginUserWithSso: LoginUserWithSsoFunction = async (
|
||||
},
|
||||
});
|
||||
|
||||
if (isMobileRequest) {
|
||||
// For mobile SSO, generate an access token and redirect to the app deep link
|
||||
const accessToken: string = JSONWebToken.signUserLoginToken({
|
||||
tokenData: {
|
||||
userId: alreadySavedUser.id!,
|
||||
email: alreadySavedUser.email!,
|
||||
name: alreadySavedUser.name!,
|
||||
timezone: alreadySavedUser.timezone || null,
|
||||
isMasterAdmin: alreadySavedUser.isMasterAdmin!,
|
||||
isGlobalLogin: false,
|
||||
sessionId: sessionMetadata.session.id!,
|
||||
},
|
||||
expiresInSeconds: ACCESS_TOKEN_EXPIRY_SECONDS,
|
||||
});
|
||||
|
||||
// Generate SSO token for per-project authentication (same as setSSOCookie)
|
||||
const ssoToken: string = JSONWebToken.sign({
|
||||
data: {
|
||||
userId: alreadySavedUser.id!,
|
||||
projectId: projectId,
|
||||
name: alreadySavedUser.name!,
|
||||
email: alreadySavedUser.email,
|
||||
isMasterAdmin: false,
|
||||
isGeneralLogin: false,
|
||||
},
|
||||
expiresInSeconds: OneUptimeDate.getSecondsInDays(
|
||||
new PositiveNumber(30),
|
||||
),
|
||||
});
|
||||
|
||||
const params: URLSearchParams = new URLSearchParams();
|
||||
params.set("accessToken", accessToken);
|
||||
params.set("refreshToken", sessionMetadata.refreshToken);
|
||||
params.set(
|
||||
"refreshTokenExpiresAt",
|
||||
sessionMetadata.refreshTokenExpiresAt.toISOString(),
|
||||
);
|
||||
params.set("userId", alreadySavedUser.id!.toString());
|
||||
params.set("email", alreadySavedUser.email!.toString());
|
||||
params.set("name", alreadySavedUser.name?.toString() || "");
|
||||
params.set(
|
||||
"isMasterAdmin",
|
||||
String(alreadySavedUser.isMasterAdmin || false),
|
||||
);
|
||||
params.set("ssoToken", ssoToken);
|
||||
params.set("projectId", projectId.toString());
|
||||
|
||||
const deepLinkUrl: string = `oneuptime://sso-callback?${params.toString()}`;
|
||||
|
||||
logger.info("User logged in with SSO (mobile): " + email.toString());
|
||||
|
||||
return res.redirect(deepLinkUrl);
|
||||
}
|
||||
|
||||
CookieUtil.setSSOCookie({
|
||||
user: alreadySavedUser,
|
||||
projectId: projectId,
|
||||
expressResponse: res,
|
||||
});
|
||||
|
||||
CookieUtil.setUserCookie({
|
||||
expressResponse: res,
|
||||
user: alreadySavedUser,
|
||||
@@ -575,7 +638,7 @@ const loginUserWithSso: LoginUserWithSsoFunction = async (
|
||||
const host: Hostname = await DatabaseConfig.getHost();
|
||||
const httpProtocol: Protocol = await DatabaseConfig.getHttpProtocol();
|
||||
|
||||
logger.info("User logged in with SSO" + email.toString());
|
||||
logger.info("User logged in with SSO: " + email.toString());
|
||||
|
||||
return Response.redirect(
|
||||
req,
|
||||
|
||||
@@ -13,6 +13,7 @@ import ObjectID from "Common/Types/ObjectID";
|
||||
import DatabaseConfig from "Common/Server/DatabaseConfig";
|
||||
import { EncryptionSecret } from "Common/Server/EnvironmentConfig";
|
||||
import MailService from "Common/Server/Services/MailService";
|
||||
import ProjectSMTPConfigService from "Common/Server/Services/ProjectSmtpConfigService";
|
||||
import StatusPagePrivateUserService from "Common/Server/Services/StatusPagePrivateUserService";
|
||||
import StatusPageService from "Common/Server/Services/StatusPageService";
|
||||
import StatusPagePrivateUserSessionService, {
|
||||
@@ -468,6 +469,16 @@ router.post(
|
||||
logoFileId: true,
|
||||
requireSsoForLogin: true,
|
||||
projectId: true,
|
||||
smtpConfig: {
|
||||
_id: true,
|
||||
hostname: true,
|
||||
port: true,
|
||||
username: true,
|
||||
password: true,
|
||||
fromEmail: true,
|
||||
fromName: true,
|
||||
secure: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
);
|
||||
@@ -547,6 +558,9 @@ router.post(
|
||||
},
|
||||
{
|
||||
projectId: statusPage.projectId!,
|
||||
mailServer: ProjectSMTPConfigService.toEmailServer(
|
||||
statusPage.smtpConfig,
|
||||
),
|
||||
statusPageId: statusPage.id!,
|
||||
},
|
||||
).catch((err: Error) => {
|
||||
@@ -632,6 +646,16 @@ router.post(
|
||||
logoFileId: true,
|
||||
requireSsoForLogin: true,
|
||||
projectId: true,
|
||||
smtpConfig: {
|
||||
_id: true,
|
||||
hostname: true,
|
||||
port: true,
|
||||
username: true,
|
||||
password: true,
|
||||
fromEmail: true,
|
||||
fromName: true,
|
||||
secure: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
);
|
||||
@@ -689,6 +713,9 @@ router.post(
|
||||
},
|
||||
{
|
||||
projectId: statusPage.projectId!,
|
||||
mailServer: ProjectSMTPConfigService.toEmailServer(
|
||||
statusPage.smtpConfig,
|
||||
),
|
||||
statusPageId: statusPage.id!,
|
||||
},
|
||||
).catch((err: Error) => {
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
262
App/FeatureSet/Identity/Utils/SCIMLogger.ts
Normal file
262
App/FeatureSet/Identity/Utils/SCIMLogger.ts
Normal file
@@ -0,0 +1,262 @@
|
||||
import ProjectSCIMLog from "Common/Models/DatabaseModels/ProjectSCIMLog";
|
||||
import StatusPageSCIMLog from "Common/Models/DatabaseModels/StatusPageSCIMLog";
|
||||
import ProjectSCIMLogService from "Common/Server/Services/ProjectSCIMLogService";
|
||||
import StatusPageSCIMLogService from "Common/Server/Services/StatusPageSCIMLogService";
|
||||
import logger from "Common/Server/Utils/Logger";
|
||||
import ObjectID from "Common/Types/ObjectID";
|
||||
import SCIMLogStatus from "Common/Types/SCIM/SCIMLogStatus";
|
||||
import { JSONObject, JSONValue, JSONArray } from "Common/Types/JSON";
|
||||
|
||||
export interface ProjectSCIMLogData {
|
||||
projectId: ObjectID;
|
||||
projectScimId: ObjectID;
|
||||
operationType: string;
|
||||
status: SCIMLogStatus;
|
||||
statusMessage?: string | undefined;
|
||||
httpMethod?: string | undefined;
|
||||
requestPath?: string | undefined;
|
||||
httpStatusCode?: number | undefined;
|
||||
affectedUserEmail?: string | undefined;
|
||||
affectedGroupName?: string | undefined;
|
||||
requestBody?: JSONObject | undefined;
|
||||
responseBody?: JSONObject | undefined;
|
||||
queryParams?: JSONObject | undefined;
|
||||
steps?: string[] | undefined;
|
||||
userInfo?: JSONObject | undefined;
|
||||
groupInfo?: JSONObject | undefined;
|
||||
additionalContext?: JSONObject | undefined;
|
||||
}
|
||||
|
||||
export interface StatusPageSCIMLogData {
|
||||
projectId: ObjectID;
|
||||
statusPageId: ObjectID;
|
||||
statusPageScimId: ObjectID;
|
||||
operationType: string;
|
||||
status: SCIMLogStatus;
|
||||
statusMessage?: string | undefined;
|
||||
httpMethod?: string | undefined;
|
||||
requestPath?: string | undefined;
|
||||
httpStatusCode?: number | undefined;
|
||||
affectedUserEmail?: string | undefined;
|
||||
requestBody?: JSONObject | undefined;
|
||||
responseBody?: JSONObject | undefined;
|
||||
queryParams?: JSONObject | undefined;
|
||||
steps?: string[] | undefined;
|
||||
userInfo?: JSONObject | undefined;
|
||||
additionalContext?: JSONObject | undefined;
|
||||
}
|
||||
|
||||
const sanitizeSensitiveData: (
|
||||
data: JSONObject | undefined,
|
||||
) => JSONObject | undefined = (
|
||||
data: JSONObject | undefined,
|
||||
): JSONObject | undefined => {
|
||||
if (!data) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const sanitized: JSONObject = { ...data };
|
||||
const sensitiveKeys: string[] = [
|
||||
"password",
|
||||
"bearerToken",
|
||||
"bearer_token",
|
||||
"authorization",
|
||||
"Authorization",
|
||||
"token",
|
||||
"secret",
|
||||
"apiKey",
|
||||
"api_key",
|
||||
];
|
||||
|
||||
const sanitizeRecursive: (obj: JSONObject) => JSONObject = (
|
||||
obj: JSONObject,
|
||||
): JSONObject => {
|
||||
const result: JSONObject = {};
|
||||
for (const key in obj) {
|
||||
if (Object.prototype.hasOwnProperty.call(obj, key)) {
|
||||
const value: JSONValue = obj[key];
|
||||
if (
|
||||
sensitiveKeys.some((k: string) => {
|
||||
return key.toLowerCase().includes(k.toLowerCase());
|
||||
})
|
||||
) {
|
||||
result[key] = "[REDACTED]";
|
||||
} else if (
|
||||
typeof value === "object" &&
|
||||
value !== null &&
|
||||
!Array.isArray(value)
|
||||
) {
|
||||
result[key] = sanitizeRecursive(value as JSONObject);
|
||||
} else if (Array.isArray(value)) {
|
||||
result[key] = (value as JSONArray).map((item: JSONValue) => {
|
||||
if (
|
||||
typeof item === "object" &&
|
||||
item !== null &&
|
||||
!Array.isArray(item)
|
||||
) {
|
||||
return sanitizeRecursive(item as JSONObject);
|
||||
}
|
||||
return item;
|
||||
}) as JSONArray;
|
||||
} else {
|
||||
result[key] = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
return result;
|
||||
};
|
||||
|
||||
return sanitizeRecursive(sanitized);
|
||||
};
|
||||
|
||||
export interface LogBodyDetails {
|
||||
requestBody?: JSONObject | undefined;
|
||||
responseBody?: JSONObject | undefined;
|
||||
timestamp: Date;
|
||||
queryParams?: JSONObject | undefined;
|
||||
steps?: string[] | undefined;
|
||||
userInfo?: JSONObject | undefined;
|
||||
groupInfo?: JSONObject | undefined;
|
||||
additionalContext?: JSONObject | undefined;
|
||||
}
|
||||
|
||||
const buildLogBody: (data: LogBodyDetails) => string = (
|
||||
data: LogBodyDetails,
|
||||
): string => {
|
||||
const logBody: JSONObject = {
|
||||
timestamp: data.timestamp.toISOString(),
|
||||
executedAt: data.timestamp.toISOString(),
|
||||
};
|
||||
|
||||
if (data.queryParams && Object.keys(data.queryParams).length > 0) {
|
||||
logBody["queryParameters"] = data.queryParams;
|
||||
}
|
||||
|
||||
if (data.requestBody) {
|
||||
logBody["request"] = sanitizeSensitiveData(data.requestBody);
|
||||
}
|
||||
|
||||
if (data.responseBody) {
|
||||
logBody["response"] = sanitizeSensitiveData(data.responseBody);
|
||||
}
|
||||
|
||||
if (data.steps && data.steps.length > 0) {
|
||||
logBody["executionSteps"] = data.steps;
|
||||
}
|
||||
|
||||
if (data.userInfo) {
|
||||
logBody["userDetails"] = sanitizeSensitiveData(data.userInfo);
|
||||
}
|
||||
|
||||
if (data.groupInfo) {
|
||||
logBody["groupDetails"] = sanitizeSensitiveData(data.groupInfo);
|
||||
}
|
||||
|
||||
if (data.additionalContext) {
|
||||
logBody["additionalContext"] = sanitizeSensitiveData(
|
||||
data.additionalContext,
|
||||
);
|
||||
}
|
||||
|
||||
return JSON.stringify(logBody, null, 2);
|
||||
};
|
||||
|
||||
export const createProjectSCIMLog: (
|
||||
data: ProjectSCIMLogData,
|
||||
) => Promise<void> = async (data: ProjectSCIMLogData): Promise<void> => {
|
||||
try {
|
||||
const log: ProjectSCIMLog = new ProjectSCIMLog();
|
||||
log.projectId = data.projectId;
|
||||
log.projectScimId = data.projectScimId;
|
||||
log.operationType = data.operationType;
|
||||
log.status = data.status;
|
||||
if (data.statusMessage !== undefined) {
|
||||
log.statusMessage = data.statusMessage;
|
||||
}
|
||||
if (data.httpMethod !== undefined) {
|
||||
log.httpMethod = data.httpMethod;
|
||||
}
|
||||
if (data.requestPath !== undefined) {
|
||||
log.requestPath = data.requestPath;
|
||||
}
|
||||
if (data.httpStatusCode !== undefined) {
|
||||
log.httpStatusCode = data.httpStatusCode;
|
||||
}
|
||||
if (data.affectedUserEmail !== undefined) {
|
||||
log.affectedUserEmail = data.affectedUserEmail;
|
||||
}
|
||||
if (data.affectedGroupName !== undefined) {
|
||||
log.affectedGroupName = data.affectedGroupName;
|
||||
}
|
||||
log.logBody = buildLogBody({
|
||||
requestBody: data.requestBody,
|
||||
responseBody: data.responseBody,
|
||||
timestamp: new Date(),
|
||||
queryParams: data.queryParams,
|
||||
steps: data.steps,
|
||||
userInfo: data.userInfo,
|
||||
groupInfo: data.groupInfo,
|
||||
additionalContext: data.additionalContext,
|
||||
});
|
||||
|
||||
await ProjectSCIMLogService.create({
|
||||
data: log,
|
||||
props: { isRoot: true },
|
||||
});
|
||||
} catch (err) {
|
||||
// Log errors silently to not affect SCIM operations
|
||||
logger.error("Failed to create Project SCIM log entry:");
|
||||
logger.error(err);
|
||||
}
|
||||
};
|
||||
|
||||
export const createStatusPageSCIMLog: (
|
||||
data: StatusPageSCIMLogData,
|
||||
) => Promise<void> = async (data: StatusPageSCIMLogData): Promise<void> => {
|
||||
try {
|
||||
const log: StatusPageSCIMLog = new StatusPageSCIMLog();
|
||||
log.projectId = data.projectId;
|
||||
log.statusPageId = data.statusPageId;
|
||||
log.statusPageScimId = data.statusPageScimId;
|
||||
log.operationType = data.operationType;
|
||||
log.status = data.status;
|
||||
if (data.statusMessage !== undefined) {
|
||||
log.statusMessage = data.statusMessage;
|
||||
}
|
||||
if (data.httpMethod !== undefined) {
|
||||
log.httpMethod = data.httpMethod;
|
||||
}
|
||||
if (data.requestPath !== undefined) {
|
||||
log.requestPath = data.requestPath;
|
||||
}
|
||||
if (data.httpStatusCode !== undefined) {
|
||||
log.httpStatusCode = data.httpStatusCode;
|
||||
}
|
||||
if (data.affectedUserEmail !== undefined) {
|
||||
log.affectedUserEmail = data.affectedUserEmail;
|
||||
}
|
||||
log.logBody = buildLogBody({
|
||||
requestBody: data.requestBody,
|
||||
responseBody: data.responseBody,
|
||||
timestamp: new Date(),
|
||||
queryParams: data.queryParams,
|
||||
steps: data.steps,
|
||||
userInfo: data.userInfo,
|
||||
additionalContext: data.additionalContext,
|
||||
});
|
||||
|
||||
await StatusPageSCIMLogService.create({
|
||||
data: log,
|
||||
props: { isRoot: true },
|
||||
});
|
||||
} catch (err) {
|
||||
// Log errors silently to not affect SCIM operations
|
||||
logger.error("Failed to create Status Page SCIM log entry:");
|
||||
logger.error(err);
|
||||
}
|
||||
};
|
||||
|
||||
export default {
|
||||
createProjectSCIMLog,
|
||||
createStatusPageSCIMLog,
|
||||
};
|
||||
@@ -1,10 +1,11 @@
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link rel="preload" href="https://fonts.googleapis.com/css2?family=Inter:wght@100;200;300;400;500;600;700;800;900&display=swap" as="style">
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@100;200;300;400;500;600;700;800;900&display=swap"
|
||||
rel="stylesheet">
|
||||
<style>
|
||||
* {
|
||||
font-family: Inter;
|
||||
font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
|
||||
}
|
||||
|
||||
|
||||
@@ -34,6 +35,7 @@
|
||||
width: auto;
|
||||
}
|
||||
</style>
|
||||
<link rel="preload" href="https://cdn.tailwindcss.com" as="script">
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
|
||||
<!-- Google Tag Manager -->
|
||||
|
||||
743
App/FeatureSet/Notification/API/IncomingCall.ts
Normal file
743
App/FeatureSet/Notification/API/IncomingCall.ts
Normal file
@@ -0,0 +1,743 @@
|
||||
import CallProviderFactory from "../Providers/CallProviderFactory";
|
||||
import { getProjectTwilioConfig } from "../Utils/TwilioConfigHelper";
|
||||
import {
|
||||
DialStatusData,
|
||||
ICallProvider,
|
||||
IncomingCallData,
|
||||
WebhookRequest,
|
||||
} from "Common/Types/Call/CallProvider";
|
||||
import TwilioConfig from "Common/Types/CallAndSMS/TwilioConfig";
|
||||
import IncomingCallStatus from "Common/Types/IncomingCall/IncomingCallStatus";
|
||||
import BadDataException from "Common/Types/Exception/BadDataException";
|
||||
import ObjectID from "Common/Types/ObjectID";
|
||||
import IncomingCallPolicyService from "Common/Server/Services/IncomingCallPolicyService";
|
||||
import IncomingCallPolicyEscalationRuleService from "Common/Server/Services/IncomingCallPolicyEscalationRuleService";
|
||||
import IncomingCallLogService from "Common/Server/Services/IncomingCallLogService";
|
||||
import IncomingCallLogItemService from "Common/Server/Services/IncomingCallLogItemService";
|
||||
import OnCallDutyPolicyScheduleService from "Common/Server/Services/OnCallDutyPolicyScheduleService";
|
||||
import UserService from "Common/Server/Services/UserService";
|
||||
import UserIncomingCallNumberService from "Common/Server/Services/UserIncomingCallNumberService";
|
||||
import UserIncomingCallNumber from "Common/Models/DatabaseModels/UserIncomingCallNumber";
|
||||
import Express, {
|
||||
ExpressRequest,
|
||||
ExpressResponse,
|
||||
ExpressRouter,
|
||||
NextFunction,
|
||||
} from "Common/Server/Utils/Express";
|
||||
import logger from "Common/Server/Utils/Logger";
|
||||
import IncomingCallPolicy from "Common/Models/DatabaseModels/IncomingCallPolicy";
|
||||
import IncomingCallPolicyEscalationRule from "Common/Models/DatabaseModels/IncomingCallPolicyEscalationRule";
|
||||
import IncomingCallLog from "Common/Models/DatabaseModels/IncomingCallLog";
|
||||
import IncomingCallLogItem from "Common/Models/DatabaseModels/IncomingCallLogItem";
|
||||
import User from "Common/Models/DatabaseModels/User";
|
||||
import Phone from "Common/Types/Phone";
|
||||
import { Host, HttpProtocol } from "Common/Server/EnvironmentConfig";
|
||||
|
||||
const router: ExpressRouter = Express.getRouter();
|
||||
|
||||
// Handle incoming voice call - single endpoint for all phone numbers
|
||||
router.post(
|
||||
"/voice",
|
||||
async (req: ExpressRequest, res: ExpressResponse, next: NextFunction) => {
|
||||
try {
|
||||
// Parse the called phone number from the request body (Twilio sends this)
|
||||
const calledPhoneNumber: string = req.body["To"] || req.body["Called"];
|
||||
|
||||
if (!calledPhoneNumber) {
|
||||
logger.error("No called phone number in request");
|
||||
res.status(400).send("Bad Request");
|
||||
return;
|
||||
}
|
||||
|
||||
// Find the policy by the called phone number
|
||||
const policy: IncomingCallPolicy | null =
|
||||
await IncomingCallPolicyService.findOneBy({
|
||||
query: {
|
||||
routingPhoneNumber: new Phone(calledPhoneNumber),
|
||||
},
|
||||
select: {
|
||||
_id: true,
|
||||
projectId: true,
|
||||
projectCallSMSConfigId: true,
|
||||
isEnabled: true,
|
||||
greetingMessage: true,
|
||||
noAnswerMessage: true,
|
||||
noOneAvailableMessage: true,
|
||||
repeatPolicyIfNoOneAnswers: true,
|
||||
repeatPolicyIfNoOneAnswersTimes: true,
|
||||
routingPhoneNumber: true,
|
||||
},
|
||||
props: {
|
||||
isRoot: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!policy) {
|
||||
logger.error(
|
||||
`Incoming call policy not found for phone number: ${calledPhoneNumber}`,
|
||||
);
|
||||
res.status(404).send("Policy not found");
|
||||
return;
|
||||
}
|
||||
|
||||
// Require project-level Twilio config
|
||||
if (!policy.projectCallSMSConfigId) {
|
||||
logger.error(
|
||||
`Policy ${policy.id?.toString()} does not have a project Twilio config`,
|
||||
);
|
||||
res.status(400).send("Policy not configured correctly");
|
||||
return;
|
||||
}
|
||||
|
||||
// Get project Twilio config
|
||||
const customTwilioConfig: TwilioConfig | null =
|
||||
await getProjectTwilioConfig(policy.projectCallSMSConfigId);
|
||||
|
||||
if (!customTwilioConfig) {
|
||||
logger.error(
|
||||
`Project Twilio config not found for policy ${policy.id?.toString()}`,
|
||||
);
|
||||
res.status(400).send("Twilio configuration not found");
|
||||
return;
|
||||
}
|
||||
|
||||
// Get provider with project config
|
||||
const provider: ICallProvider =
|
||||
CallProviderFactory.getProviderWithConfig(customTwilioConfig);
|
||||
|
||||
// Validate webhook signature to ensure request is from the call provider
|
||||
const signature: string =
|
||||
(req.headers["x-twilio-signature"] as string) || "";
|
||||
|
||||
// Debug logging
|
||||
logger.debug("=== Incoming Call Webhook Debug ===");
|
||||
logger.debug(`Original URL: ${req.originalUrl}`);
|
||||
logger.debug(`Base URL: ${req.baseUrl}`);
|
||||
logger.debug(`Path: ${req.path}`);
|
||||
logger.debug(`Protocol: ${req.protocol}`);
|
||||
logger.debug(`Host header: ${req.get("host")}`);
|
||||
logger.debug(`X-Forwarded-Proto: ${req.get("x-forwarded-proto")}`);
|
||||
logger.debug(`X-Forwarded-Host: ${req.get("x-forwarded-host")}`);
|
||||
logger.debug(`Twilio Signature: ${signature}`);
|
||||
logger.debug(`Environment HOST: ${Host}`);
|
||||
logger.debug(`Environment HttpProtocol: ${HttpProtocol}`);
|
||||
logger.debug("=== End Debug ===");
|
||||
|
||||
if (
|
||||
!provider.validateWebhookSignature(
|
||||
req as unknown as WebhookRequest,
|
||||
signature,
|
||||
)
|
||||
) {
|
||||
logger.error("Invalid webhook signature for incoming call");
|
||||
res.status(403).send("Forbidden");
|
||||
return;
|
||||
}
|
||||
|
||||
// Parse incoming call data
|
||||
const callData: IncomingCallData = provider.parseIncomingCallWebhook(
|
||||
req as unknown as WebhookRequest,
|
||||
);
|
||||
|
||||
const policyId: string = policy.id!.toString();
|
||||
|
||||
// Create call log early so we can track all outcomes
|
||||
const callLog: IncomingCallLog = new IncomingCallLog();
|
||||
if (policy.projectId) {
|
||||
callLog.projectId = policy.projectId;
|
||||
}
|
||||
callLog.incomingCallPolicyId = new ObjectID(policyId);
|
||||
callLog.callerPhoneNumber = new Phone(callData.callerPhoneNumber);
|
||||
if (policy.routingPhoneNumber) {
|
||||
callLog.routingPhoneNumber = policy.routingPhoneNumber;
|
||||
}
|
||||
callLog.callProviderCallId = callData.callId;
|
||||
callLog.status = IncomingCallStatus.Initiated;
|
||||
callLog.startedAt = new Date();
|
||||
callLog.currentEscalationRuleOrder = 1;
|
||||
callLog.repeatCount = 0;
|
||||
|
||||
// Check if policy is enabled
|
||||
if (!policy.isEnabled) {
|
||||
callLog.status = IncomingCallStatus.Failed;
|
||||
callLog.statusMessage = "Policy is disabled";
|
||||
callLog.endedAt = new Date();
|
||||
await IncomingCallLogService.create({
|
||||
data: callLog,
|
||||
props: { isRoot: true },
|
||||
});
|
||||
|
||||
const twiml: string = provider.generateHangupResponse(
|
||||
"Sorry, this service is currently disabled.",
|
||||
);
|
||||
res.type("text/xml");
|
||||
return res.send(twiml);
|
||||
}
|
||||
|
||||
// Save the call log now that initial checks passed
|
||||
const createdCallLog: IncomingCallLog =
|
||||
await IncomingCallLogService.create({
|
||||
data: callLog,
|
||||
props: {
|
||||
isRoot: true,
|
||||
},
|
||||
});
|
||||
|
||||
// Get the first escalation rule
|
||||
const firstRule: IncomingCallPolicyEscalationRule | null =
|
||||
await IncomingCallPolicyEscalationRuleService.findOneBy({
|
||||
query: {
|
||||
incomingCallPolicyId: new ObjectID(policyId),
|
||||
order: 1,
|
||||
},
|
||||
select: {
|
||||
_id: true,
|
||||
name: true,
|
||||
escalateAfterSeconds: true,
|
||||
onCallDutyPolicyScheduleId: true,
|
||||
userId: true,
|
||||
},
|
||||
props: {
|
||||
isRoot: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!firstRule) {
|
||||
await IncomingCallLogService.updateOneById({
|
||||
id: createdCallLog.id!,
|
||||
data: {
|
||||
status: IncomingCallStatus.Failed,
|
||||
statusMessage: "No escalation rules configured",
|
||||
endedAt: new Date(),
|
||||
},
|
||||
props: { isRoot: true },
|
||||
});
|
||||
|
||||
const twiml: string = provider.generateHangupResponse(
|
||||
policy.noOneAvailableMessage ||
|
||||
"We're sorry, but no on-call engineer is currently available.",
|
||||
);
|
||||
res.type("text/xml");
|
||||
return res.send(twiml);
|
||||
}
|
||||
|
||||
// Get the user to call
|
||||
const userToCall: UserToCall | null = await getUserToCall(
|
||||
firstRule,
|
||||
policy.projectId!,
|
||||
);
|
||||
|
||||
if (!userToCall) {
|
||||
await IncomingCallLogService.updateOneById({
|
||||
id: createdCallLog.id!,
|
||||
data: {
|
||||
status: IncomingCallStatus.Failed,
|
||||
statusMessage:
|
||||
"No on-call user available or user has no phone number",
|
||||
endedAt: new Date(),
|
||||
},
|
||||
props: { isRoot: true },
|
||||
});
|
||||
|
||||
const twiml: string = provider.generateHangupResponse(
|
||||
policy.noOneAvailableMessage ||
|
||||
"We're sorry, but no on-call engineer is currently available.",
|
||||
);
|
||||
res.type("text/xml");
|
||||
return res.send(twiml);
|
||||
}
|
||||
|
||||
// Create call log item
|
||||
const callLogItem: IncomingCallLogItem = new IncomingCallLogItem();
|
||||
if (policy.projectId) {
|
||||
callLogItem.projectId = policy.projectId;
|
||||
}
|
||||
callLogItem.incomingCallLogId = createdCallLog.id!;
|
||||
if (firstRule.id) {
|
||||
callLogItem.incomingCallPolicyEscalationRuleId = firstRule.id;
|
||||
}
|
||||
callLogItem.userId = userToCall.userId;
|
||||
callLogItem.userPhoneNumber = userToCall.phoneNumber;
|
||||
callLogItem.status = IncomingCallStatus.Ringing;
|
||||
callLogItem.startedAt = new Date();
|
||||
callLogItem.isAnswered = false;
|
||||
|
||||
const createdCallLogItem: IncomingCallLogItem =
|
||||
await IncomingCallLogItemService.create({
|
||||
data: callLogItem,
|
||||
props: {
|
||||
isRoot: true,
|
||||
},
|
||||
});
|
||||
|
||||
// Generate TwiML response
|
||||
const greetingMessage: string =
|
||||
policy.greetingMessage ||
|
||||
"Please wait while we connect you to the on-call engineer.";
|
||||
|
||||
// Construct status callback URL
|
||||
const statusCallbackUrl: string = `${HttpProtocol}${Host}/notification/incoming-call/dial-status/${createdCallLog.id?.toString()}/${createdCallLogItem.id?.toString()}`;
|
||||
|
||||
// Generate greeting + dial TwiML
|
||||
const twiml: string = generateGreetingAndDialTwiml(
|
||||
provider,
|
||||
greetingMessage,
|
||||
userToCall.phoneNumber.toString(),
|
||||
policy.routingPhoneNumber?.toString() || callData.calledPhoneNumber,
|
||||
firstRule.escalateAfterSeconds || 30,
|
||||
statusCallbackUrl,
|
||||
);
|
||||
|
||||
res.type("text/xml");
|
||||
return res.send(twiml);
|
||||
} catch (err) {
|
||||
logger.error(err);
|
||||
return next(err);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
// Handle dial status callback
|
||||
router.post(
|
||||
"/dial-status/:callLogId/:callLogItemId",
|
||||
async (req: ExpressRequest, res: ExpressResponse, next: NextFunction) => {
|
||||
try {
|
||||
const callLogId: string = req.params["callLogId"] as string;
|
||||
const callLogItemId: string = req.params["callLogItemId"] as string;
|
||||
|
||||
if (!callLogId || !callLogItemId) {
|
||||
throw new BadDataException("Invalid webhook URL");
|
||||
}
|
||||
|
||||
// Get the call log to find the policy and its Twilio config
|
||||
const callLog: IncomingCallLog | null =
|
||||
await IncomingCallLogService.findOneById({
|
||||
id: new ObjectID(callLogId),
|
||||
select: {
|
||||
_id: true,
|
||||
currentEscalationRuleOrder: true,
|
||||
repeatCount: true,
|
||||
incomingCallPolicyId: true,
|
||||
},
|
||||
props: {
|
||||
isRoot: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!callLog) {
|
||||
logger.error(`Call log not found: ${callLogId}`);
|
||||
res.status(404).send("Call log not found");
|
||||
return;
|
||||
}
|
||||
|
||||
// Get the policy with its Twilio config
|
||||
const policy: IncomingCallPolicy | null =
|
||||
await IncomingCallPolicyService.findOneById({
|
||||
id: callLog.incomingCallPolicyId!,
|
||||
select: {
|
||||
_id: true,
|
||||
projectId: true,
|
||||
projectCallSMSConfigId: true,
|
||||
noAnswerMessage: true,
|
||||
noOneAvailableMessage: true,
|
||||
repeatPolicyIfNoOneAnswers: true,
|
||||
repeatPolicyIfNoOneAnswersTimes: true,
|
||||
routingPhoneNumber: true,
|
||||
},
|
||||
props: {
|
||||
isRoot: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!policy || !policy.projectCallSMSConfigId) {
|
||||
logger.error("Policy or Twilio config not found");
|
||||
res.status(400).send("Configuration error");
|
||||
return;
|
||||
}
|
||||
|
||||
// Get project Twilio config
|
||||
const customTwilioConfig: TwilioConfig | null =
|
||||
await getProjectTwilioConfig(policy.projectCallSMSConfigId);
|
||||
|
||||
if (!customTwilioConfig) {
|
||||
logger.error("Twilio config not found for policy");
|
||||
res.status(400).send("Configuration error");
|
||||
return;
|
||||
}
|
||||
|
||||
// Get provider with project config
|
||||
const provider: ICallProvider =
|
||||
CallProviderFactory.getProviderWithConfig(customTwilioConfig);
|
||||
|
||||
// Validate webhook signature to ensure request is from the call provider
|
||||
const signature: string =
|
||||
(req.headers["x-twilio-signature"] as string) || "";
|
||||
if (
|
||||
!provider.validateWebhookSignature(
|
||||
req as unknown as WebhookRequest,
|
||||
signature,
|
||||
)
|
||||
) {
|
||||
logger.error("Invalid webhook signature for dial status callback");
|
||||
res.status(403).send("Forbidden");
|
||||
return;
|
||||
}
|
||||
|
||||
// Parse dial status
|
||||
const dialStatus: DialStatusData = provider.parseDialStatusWebhook(
|
||||
req as unknown as WebhookRequest,
|
||||
);
|
||||
|
||||
// Get the call log item
|
||||
const callLogItem: IncomingCallLogItem | null =
|
||||
await IncomingCallLogItemService.findOneById({
|
||||
id: new ObjectID(callLogItemId),
|
||||
select: {
|
||||
_id: true,
|
||||
incomingCallLogId: true,
|
||||
},
|
||||
props: {
|
||||
isRoot: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!callLogItem) {
|
||||
logger.error(`Call log item not found: ${callLogItemId}`);
|
||||
const twiml: string = provider.generateHangupResponse();
|
||||
res.type("text/xml");
|
||||
return res.send(twiml);
|
||||
}
|
||||
|
||||
// Update call log item
|
||||
const now: Date = new Date();
|
||||
await IncomingCallLogItemService.updateOneById({
|
||||
id: new ObjectID(callLogItemId),
|
||||
data: {
|
||||
status:
|
||||
dialStatus.dialStatus === "completed"
|
||||
? IncomingCallStatus.Connected
|
||||
: IncomingCallStatus.NoAnswer,
|
||||
dialDurationInSeconds: dialStatus.dialDurationSeconds || 0,
|
||||
endedAt: now,
|
||||
isAnswered: dialStatus.dialStatus === "completed",
|
||||
},
|
||||
props: {
|
||||
isRoot: true,
|
||||
},
|
||||
});
|
||||
|
||||
// If call was answered, mark as completed
|
||||
if (dialStatus.dialStatus === "completed") {
|
||||
await IncomingCallLogService.updateOneById({
|
||||
id: new ObjectID(callLogId),
|
||||
data: {
|
||||
status: IncomingCallStatus.Completed,
|
||||
endedAt: now,
|
||||
},
|
||||
props: {
|
||||
isRoot: true,
|
||||
},
|
||||
});
|
||||
|
||||
// Hang up - the call is complete
|
||||
const twiml: string = provider.generateHangupResponse();
|
||||
res.type("text/xml");
|
||||
return res.send(twiml);
|
||||
}
|
||||
|
||||
// Call was not answered, try next escalation rule
|
||||
const nextOrder: number = (callLog.currentEscalationRuleOrder || 1) + 1;
|
||||
|
||||
// Get the next escalation rule
|
||||
const nextRule: IncomingCallPolicyEscalationRule | null =
|
||||
await IncomingCallPolicyEscalationRuleService.findOneBy({
|
||||
query: {
|
||||
incomingCallPolicyId: callLog.incomingCallPolicyId!,
|
||||
order: nextOrder,
|
||||
},
|
||||
select: {
|
||||
_id: true,
|
||||
name: true,
|
||||
escalateAfterSeconds: true,
|
||||
onCallDutyPolicyScheduleId: true,
|
||||
userId: true,
|
||||
},
|
||||
props: {
|
||||
isRoot: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!nextRule) {
|
||||
// No more rules, check if we should repeat
|
||||
if (
|
||||
policy.repeatPolicyIfNoOneAnswers &&
|
||||
(callLog.repeatCount || 0) <
|
||||
(policy.repeatPolicyIfNoOneAnswersTimes || 1)
|
||||
) {
|
||||
// Restart from first rule
|
||||
await IncomingCallLogService.updateOneById({
|
||||
id: new ObjectID(callLogId),
|
||||
data: {
|
||||
currentEscalationRuleOrder: 1,
|
||||
repeatCount: (callLog.repeatCount || 0) + 1,
|
||||
},
|
||||
props: {
|
||||
isRoot: true,
|
||||
},
|
||||
});
|
||||
|
||||
// Get first rule again
|
||||
const firstRule: IncomingCallPolicyEscalationRule | null =
|
||||
await IncomingCallPolicyEscalationRuleService.findOneBy({
|
||||
query: {
|
||||
incomingCallPolicyId: callLog.incomingCallPolicyId!,
|
||||
order: 1,
|
||||
},
|
||||
select: {
|
||||
_id: true,
|
||||
name: true,
|
||||
escalateAfterSeconds: true,
|
||||
onCallDutyPolicyScheduleId: true,
|
||||
userId: true,
|
||||
},
|
||||
props: {
|
||||
isRoot: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (firstRule && policy.projectId) {
|
||||
const userToCall: UserToCall | null = await getUserToCall(
|
||||
firstRule,
|
||||
policy.projectId,
|
||||
);
|
||||
if (userToCall) {
|
||||
// Continue with the call
|
||||
return await dialNextUser(
|
||||
res,
|
||||
provider,
|
||||
policy,
|
||||
callLog,
|
||||
firstRule,
|
||||
userToCall,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// No more options, end the call
|
||||
await IncomingCallLogService.updateOneById({
|
||||
id: new ObjectID(callLogId),
|
||||
data: {
|
||||
status: IncomingCallStatus.NoAnswer,
|
||||
endedAt: now,
|
||||
},
|
||||
props: {
|
||||
isRoot: true,
|
||||
},
|
||||
});
|
||||
|
||||
const twiml: string = provider.generateHangupResponse(
|
||||
policy.noAnswerMessage ||
|
||||
"No one is available. Please try again later.",
|
||||
);
|
||||
res.type("text/xml");
|
||||
return res.send(twiml);
|
||||
}
|
||||
|
||||
// Update call log with new escalation rule order
|
||||
await IncomingCallLogService.updateOneById({
|
||||
id: new ObjectID(callLogId),
|
||||
data: {
|
||||
currentEscalationRuleOrder: nextOrder,
|
||||
status: IncomingCallStatus.Escalated,
|
||||
},
|
||||
props: {
|
||||
isRoot: true,
|
||||
},
|
||||
});
|
||||
|
||||
// Get the user to call
|
||||
const userToCall: UserToCall | null = await getUserToCall(
|
||||
nextRule,
|
||||
policy.projectId!,
|
||||
);
|
||||
|
||||
if (!userToCall) {
|
||||
/*
|
||||
* Skip this rule and try the next one (recursive approach via TwiML redirect would be complex)
|
||||
* For simplicity, end the call if no user available
|
||||
*/
|
||||
await IncomingCallLogService.updateOneById({
|
||||
id: new ObjectID(callLogId),
|
||||
data: {
|
||||
status: IncomingCallStatus.Failed,
|
||||
statusMessage:
|
||||
"No on-call user available or user has no phone number",
|
||||
endedAt: new Date(),
|
||||
},
|
||||
props: { isRoot: true },
|
||||
});
|
||||
|
||||
const twiml: string = provider.generateHangupResponse(
|
||||
policy.noOneAvailableMessage ||
|
||||
"We're sorry, but no on-call engineer is currently available.",
|
||||
);
|
||||
res.type("text/xml");
|
||||
return res.send(twiml);
|
||||
}
|
||||
|
||||
// Dial the next user
|
||||
return await dialNextUser(
|
||||
res,
|
||||
provider,
|
||||
policy,
|
||||
callLog,
|
||||
nextRule,
|
||||
userToCall,
|
||||
);
|
||||
} catch (err) {
|
||||
logger.error(err);
|
||||
return next(err);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
// Interface for user with phone number to call
|
||||
interface UserToCall {
|
||||
userId: ObjectID;
|
||||
phoneNumber: Phone;
|
||||
name?: string | undefined;
|
||||
email?: string | undefined;
|
||||
}
|
||||
|
||||
// Helper function to get user to call from escalation rule
|
||||
async function getUserToCall(
|
||||
rule: IncomingCallPolicyEscalationRule,
|
||||
projectId: ObjectID,
|
||||
): Promise<UserToCall | null> {
|
||||
let userId: ObjectID | null = null;
|
||||
|
||||
// If rule has a direct user, use that
|
||||
if (rule.userId) {
|
||||
userId = rule.userId;
|
||||
} else if (rule.onCallDutyPolicyScheduleId) {
|
||||
// If rule has an on-call schedule, get the current on-call user
|
||||
userId = await OnCallDutyPolicyScheduleService.getCurrentUserIdInSchedule(
|
||||
rule.onCallDutyPolicyScheduleId,
|
||||
);
|
||||
}
|
||||
|
||||
if (!userId) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Check if the user has a verified incoming call number for this project
|
||||
const verifiedIncomingCallNumber: UserIncomingCallNumber | null =
|
||||
await UserIncomingCallNumberService.findOneBy({
|
||||
query: {
|
||||
userId: userId,
|
||||
projectId: projectId,
|
||||
isVerified: true,
|
||||
},
|
||||
select: {
|
||||
phone: true,
|
||||
},
|
||||
props: {
|
||||
isRoot: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!verifiedIncomingCallNumber || !verifiedIncomingCallNumber.phone) {
|
||||
// No verified incoming call number for this user in this project
|
||||
return null;
|
||||
}
|
||||
|
||||
// Get user details for logging
|
||||
const user: User | null = await UserService.findOneById({
|
||||
id: userId,
|
||||
select: {
|
||||
_id: true,
|
||||
name: true,
|
||||
email: true,
|
||||
},
|
||||
props: {
|
||||
isRoot: true,
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
userId: userId,
|
||||
phoneNumber: verifiedIncomingCallNumber.phone,
|
||||
name: user?.name?.toString(),
|
||||
email: user?.email?.toString(),
|
||||
};
|
||||
}
|
||||
|
||||
// Helper function to generate greeting and dial TwiML
|
||||
function generateGreetingAndDialTwiml(
|
||||
provider: ICallProvider,
|
||||
greetingMessage: string,
|
||||
toPhoneNumber: string,
|
||||
fromPhoneNumber: string,
|
||||
timeoutSeconds: number,
|
||||
statusCallbackUrl: string,
|
||||
): string {
|
||||
// Use the escalation response which says a message then dials
|
||||
return provider.generateEscalationResponse(greetingMessage, {
|
||||
toPhoneNumber,
|
||||
fromPhoneNumber,
|
||||
timeoutSeconds,
|
||||
statusCallbackUrl,
|
||||
});
|
||||
}
|
||||
|
||||
// Helper function to dial the next user
|
||||
async function dialNextUser(
|
||||
res: ExpressResponse,
|
||||
provider: ICallProvider,
|
||||
policy: IncomingCallPolicy,
|
||||
callLog: IncomingCallLog,
|
||||
rule: IncomingCallPolicyEscalationRule,
|
||||
userToCall: UserToCall,
|
||||
): Promise<ExpressResponse> {
|
||||
// Create call log item
|
||||
const callLogItem: IncomingCallLogItem = new IncomingCallLogItem();
|
||||
if (policy.projectId) {
|
||||
callLogItem.projectId = policy.projectId;
|
||||
}
|
||||
callLogItem.incomingCallLogId = callLog.id!;
|
||||
if (rule.id) {
|
||||
callLogItem.incomingCallPolicyEscalationRuleId = rule.id;
|
||||
}
|
||||
callLogItem.userId = userToCall.userId;
|
||||
callLogItem.userPhoneNumber = userToCall.phoneNumber;
|
||||
callLogItem.status = IncomingCallStatus.Ringing;
|
||||
callLogItem.startedAt = new Date();
|
||||
callLogItem.isAnswered = false;
|
||||
|
||||
const createdCallLogItem: IncomingCallLogItem =
|
||||
await IncomingCallLogItemService.create({
|
||||
data: callLogItem,
|
||||
props: {
|
||||
isRoot: true,
|
||||
},
|
||||
});
|
||||
|
||||
// Construct status callback URL
|
||||
const statusCallbackUrl: string = `${HttpProtocol}${Host}/notification/incoming-call/dial-status/${callLog.id?.toString()}/${createdCallLogItem.id?.toString()}`;
|
||||
|
||||
// Generate dial TwiML with escalation message
|
||||
const escalationMessage: string = `Connecting you to the next available engineer.`;
|
||||
|
||||
const twiml: string = provider.generateEscalationResponse(escalationMessage, {
|
||||
toPhoneNumber: userToCall.phoneNumber.toString(),
|
||||
fromPhoneNumber: policy.routingPhoneNumber?.toString() || "",
|
||||
timeoutSeconds: rule.escalateAfterSeconds || 30,
|
||||
statusCallbackUrl,
|
||||
});
|
||||
|
||||
res.type("text/xml");
|
||||
return res.send(twiml);
|
||||
}
|
||||
|
||||
export default router;
|
||||
612
App/FeatureSet/Notification/API/PhoneNumber.ts
Normal file
612
App/FeatureSet/Notification/API/PhoneNumber.ts
Normal file
@@ -0,0 +1,612 @@
|
||||
import CallProviderFactory from "../Providers/CallProviderFactory";
|
||||
import { getProjectTwilioConfig } from "../Utils/TwilioConfigHelper";
|
||||
import { HttpProtocol, Host } from "Common/Server/EnvironmentConfig";
|
||||
import {
|
||||
AvailablePhoneNumber,
|
||||
ICallProvider,
|
||||
OwnedPhoneNumber,
|
||||
PurchasedPhoneNumber,
|
||||
} from "Common/Types/Call/CallProvider";
|
||||
import TwilioConfig from "Common/Types/CallAndSMS/TwilioConfig";
|
||||
import BadDataException from "Common/Types/Exception/BadDataException";
|
||||
import { JSONObject } from "Common/Types/JSON";
|
||||
import ObjectID from "Common/Types/ObjectID";
|
||||
import IncomingCallPolicyService from "Common/Server/Services/IncomingCallPolicyService";
|
||||
import ProjectService from "Common/Server/Services/ProjectService";
|
||||
import Express, {
|
||||
ExpressRequest,
|
||||
ExpressResponse,
|
||||
ExpressRouter,
|
||||
NextFunction,
|
||||
} from "Common/Server/Utils/Express";
|
||||
import Response from "Common/Server/Utils/Response";
|
||||
import logger from "Common/Server/Utils/Logger";
|
||||
import IncomingCallPolicy from "Common/Models/DatabaseModels/IncomingCallPolicy";
|
||||
import Project from "Common/Models/DatabaseModels/Project";
|
||||
import Phone from "Common/Types/Phone";
|
||||
|
||||
const router: ExpressRouter = Express.getRouter();
|
||||
|
||||
// Search available phone numbers
|
||||
router.post(
|
||||
"/search",
|
||||
async (req: ExpressRequest, res: ExpressResponse, next: NextFunction) => {
|
||||
try {
|
||||
const body: JSONObject = req.body as JSONObject;
|
||||
|
||||
const projectId: ObjectID | undefined = body["projectId"]
|
||||
? new ObjectID(body["projectId"] as string)
|
||||
: undefined;
|
||||
|
||||
const projectCallSMSConfigId: ObjectID | undefined = body[
|
||||
"projectCallSMSConfigId"
|
||||
]
|
||||
? new ObjectID(body["projectCallSMSConfigId"] as string)
|
||||
: undefined;
|
||||
|
||||
if (!projectId) {
|
||||
throw new BadDataException("projectId is required");
|
||||
}
|
||||
|
||||
if (!projectCallSMSConfigId) {
|
||||
throw new BadDataException(
|
||||
"projectCallSMSConfigId is required. Please configure a project-level Twilio configuration.",
|
||||
);
|
||||
}
|
||||
|
||||
const countryCode: string | undefined = body["countryCode"] as
|
||||
| string
|
||||
| undefined;
|
||||
const areaCode: string | undefined = body["areaCode"] as
|
||||
| string
|
||||
| undefined;
|
||||
const contains: string | undefined = body["contains"] as
|
||||
| string
|
||||
| undefined;
|
||||
|
||||
if (!countryCode) {
|
||||
throw new BadDataException("countryCode is required");
|
||||
}
|
||||
|
||||
// Check if project exists
|
||||
const project: Project | null = await ProjectService.findOneById({
|
||||
id: projectId,
|
||||
select: {
|
||||
_id: true,
|
||||
name: true,
|
||||
},
|
||||
props: {
|
||||
isRoot: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!project) {
|
||||
throw new BadDataException("Project not found");
|
||||
}
|
||||
|
||||
// Get project Twilio config
|
||||
const customTwilioConfig: TwilioConfig | null =
|
||||
await getProjectTwilioConfig(projectCallSMSConfigId);
|
||||
if (!customTwilioConfig) {
|
||||
throw new BadDataException("Project Call/SMS Config not found");
|
||||
}
|
||||
|
||||
const provider: ICallProvider =
|
||||
CallProviderFactory.getProviderWithConfig(customTwilioConfig);
|
||||
|
||||
const searchOptions: {
|
||||
countryCode: string;
|
||||
areaCode?: string;
|
||||
contains?: string;
|
||||
limit?: number;
|
||||
} = {
|
||||
countryCode,
|
||||
limit: 10,
|
||||
};
|
||||
|
||||
if (areaCode) {
|
||||
searchOptions.areaCode = areaCode;
|
||||
}
|
||||
|
||||
if (contains) {
|
||||
searchOptions.contains = contains;
|
||||
}
|
||||
|
||||
const numbers: AvailablePhoneNumber[] =
|
||||
await provider.searchAvailableNumbers(searchOptions);
|
||||
|
||||
// Customer pays Twilio directly - just return the phone numbers
|
||||
type ResponseNumber = {
|
||||
phoneNumber: string;
|
||||
friendlyName: string;
|
||||
locality?: string;
|
||||
region?: string;
|
||||
country: string;
|
||||
};
|
||||
|
||||
const responseNumbers: Array<ResponseNumber> = numbers.map(
|
||||
(n: AvailablePhoneNumber): ResponseNumber => {
|
||||
const result: ResponseNumber = {
|
||||
phoneNumber: n.phoneNumber,
|
||||
friendlyName: n.friendlyName,
|
||||
country: n.country,
|
||||
};
|
||||
if (n.locality) {
|
||||
result.locality = n.locality;
|
||||
}
|
||||
if (n.region) {
|
||||
result.region = n.region;
|
||||
}
|
||||
return result;
|
||||
},
|
||||
);
|
||||
|
||||
return Response.sendJsonObjectResponse(req, res, {
|
||||
availableNumbers: responseNumbers,
|
||||
});
|
||||
} catch (err) {
|
||||
logger.error(err);
|
||||
return next(err);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
// List owned phone numbers (already purchased in Twilio account)
|
||||
router.post(
|
||||
"/list-owned",
|
||||
async (req: ExpressRequest, res: ExpressResponse, next: NextFunction) => {
|
||||
try {
|
||||
const body: JSONObject = req.body as JSONObject;
|
||||
|
||||
const projectId: ObjectID | undefined = body["projectId"]
|
||||
? new ObjectID(body["projectId"] as string)
|
||||
: undefined;
|
||||
|
||||
const projectCallSMSConfigId: ObjectID | undefined = body[
|
||||
"projectCallSMSConfigId"
|
||||
]
|
||||
? new ObjectID(body["projectCallSMSConfigId"] as string)
|
||||
: undefined;
|
||||
|
||||
if (!projectId) {
|
||||
throw new BadDataException("projectId is required");
|
||||
}
|
||||
|
||||
if (!projectCallSMSConfigId) {
|
||||
throw new BadDataException(
|
||||
"projectCallSMSConfigId is required. Please configure a project-level Twilio configuration.",
|
||||
);
|
||||
}
|
||||
|
||||
// Check if project exists
|
||||
const project: Project | null = await ProjectService.findOneById({
|
||||
id: projectId,
|
||||
select: {
|
||||
_id: true,
|
||||
name: true,
|
||||
},
|
||||
props: {
|
||||
isRoot: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!project) {
|
||||
throw new BadDataException("Project not found");
|
||||
}
|
||||
|
||||
// Get project Twilio config
|
||||
const customTwilioConfig: TwilioConfig | null =
|
||||
await getProjectTwilioConfig(projectCallSMSConfigId);
|
||||
if (!customTwilioConfig) {
|
||||
throw new BadDataException("Project Call/SMS Config not found");
|
||||
}
|
||||
|
||||
const provider: ICallProvider =
|
||||
CallProviderFactory.getProviderWithConfig(customTwilioConfig);
|
||||
|
||||
const numbers: OwnedPhoneNumber[] = await provider.listOwnedNumbers();
|
||||
|
||||
type ResponseNumber = {
|
||||
phoneNumberId: string;
|
||||
phoneNumber: string;
|
||||
friendlyName: string;
|
||||
voiceUrl?: string | undefined;
|
||||
};
|
||||
|
||||
const responseNumbers: Array<ResponseNumber> = numbers.map(
|
||||
(n: OwnedPhoneNumber): ResponseNumber => {
|
||||
return {
|
||||
phoneNumberId: n.phoneNumberId,
|
||||
phoneNumber: n.phoneNumber,
|
||||
friendlyName: n.friendlyName,
|
||||
voiceUrl: n.voiceUrl,
|
||||
};
|
||||
},
|
||||
);
|
||||
|
||||
return Response.sendJsonObjectResponse(req, res, {
|
||||
ownedNumbers: responseNumbers,
|
||||
});
|
||||
} catch (err) {
|
||||
logger.error(err);
|
||||
return next(err);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
// Assign an existing phone number to a policy
|
||||
router.post(
|
||||
"/assign-existing",
|
||||
async (req: ExpressRequest, res: ExpressResponse, next: NextFunction) => {
|
||||
try {
|
||||
const body: JSONObject = req.body as JSONObject;
|
||||
|
||||
const projectId: ObjectID | undefined = body["projectId"]
|
||||
? new ObjectID(body["projectId"] as string)
|
||||
: undefined;
|
||||
|
||||
const phoneNumberId: string | undefined = body["phoneNumberId"] as
|
||||
| string
|
||||
| undefined;
|
||||
|
||||
const phoneNumber: string | undefined = body["phoneNumber"] as
|
||||
| string
|
||||
| undefined;
|
||||
|
||||
const incomingCallPolicyId: ObjectID | undefined = body[
|
||||
"incomingCallPolicyId"
|
||||
]
|
||||
? new ObjectID(body["incomingCallPolicyId"] as string)
|
||||
: undefined;
|
||||
|
||||
if (!projectId) {
|
||||
throw new BadDataException("projectId is required");
|
||||
}
|
||||
|
||||
if (!phoneNumberId) {
|
||||
throw new BadDataException("phoneNumberId is required");
|
||||
}
|
||||
|
||||
if (!phoneNumber) {
|
||||
throw new BadDataException("phoneNumber is required");
|
||||
}
|
||||
|
||||
if (!incomingCallPolicyId) {
|
||||
throw new BadDataException("incomingCallPolicyId is required");
|
||||
}
|
||||
|
||||
// Check if project exists
|
||||
const project: Project | null = await ProjectService.findOneById({
|
||||
id: projectId,
|
||||
select: {
|
||||
_id: true,
|
||||
name: true,
|
||||
},
|
||||
props: {
|
||||
isRoot: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!project) {
|
||||
throw new BadDataException("Project not found");
|
||||
}
|
||||
|
||||
// Check if incoming call policy exists and get its project config
|
||||
const incomingCallPolicy: IncomingCallPolicy | null =
|
||||
await IncomingCallPolicyService.findOneById({
|
||||
id: incomingCallPolicyId,
|
||||
select: {
|
||||
_id: true,
|
||||
projectId: true,
|
||||
projectCallSMSConfigId: true,
|
||||
routingPhoneNumber: true,
|
||||
},
|
||||
props: {
|
||||
isRoot: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!incomingCallPolicy) {
|
||||
throw new BadDataException("Incoming Call Policy not found");
|
||||
}
|
||||
|
||||
if (incomingCallPolicy.projectId?.toString() !== projectId.toString()) {
|
||||
throw new BadDataException(
|
||||
"Incoming Call Policy does not belong to this project",
|
||||
);
|
||||
}
|
||||
|
||||
if (incomingCallPolicy.routingPhoneNumber) {
|
||||
throw new BadDataException(
|
||||
"This policy already has a phone number. Please release it first.",
|
||||
);
|
||||
}
|
||||
|
||||
// Require project-level Twilio config
|
||||
if (!incomingCallPolicy.projectCallSMSConfigId) {
|
||||
throw new BadDataException(
|
||||
"This policy does not have a project Twilio configuration. Please configure one first.",
|
||||
);
|
||||
}
|
||||
|
||||
// Get project Twilio config
|
||||
const customTwilioConfig: TwilioConfig | null =
|
||||
await getProjectTwilioConfig(incomingCallPolicy.projectCallSMSConfigId);
|
||||
if (!customTwilioConfig) {
|
||||
throw new BadDataException("Project Call/SMS Config not found");
|
||||
}
|
||||
|
||||
const provider: ICallProvider =
|
||||
CallProviderFactory.getProviderWithConfig(customTwilioConfig);
|
||||
|
||||
/*
|
||||
* Construct webhook URL - single endpoint for all phone numbers
|
||||
* Twilio sends the "To" phone number in every webhook, so we look up the policy by phone number
|
||||
*/
|
||||
const webhookUrl: string = `${HttpProtocol}${Host}/notification/incoming-call/voice`;
|
||||
|
||||
const assigned: PurchasedPhoneNumber =
|
||||
await provider.assignExistingNumber(phoneNumberId, webhookUrl);
|
||||
|
||||
// Get country code from phone number
|
||||
const countryCode: string =
|
||||
Phone.getCountryCodeFromPhoneNumber(phoneNumber);
|
||||
const areaCode: string = Phone.getAreaCodeFromPhoneNumber(phoneNumber);
|
||||
|
||||
/*
|
||||
* Update the incoming call policy with the assigned number
|
||||
*/
|
||||
await IncomingCallPolicyService.updateOneById({
|
||||
id: incomingCallPolicyId,
|
||||
data: {
|
||||
routingPhoneNumber: new Phone(assigned.phoneNumber),
|
||||
callProviderPhoneNumberId: assigned.phoneNumberId,
|
||||
phoneNumberCountryCode: countryCode,
|
||||
phoneNumberAreaCode: areaCode,
|
||||
phoneNumberPurchasedAt: new Date(),
|
||||
},
|
||||
props: {
|
||||
isRoot: true,
|
||||
},
|
||||
});
|
||||
|
||||
return Response.sendJsonObjectResponse(req, res, {
|
||||
success: true,
|
||||
phoneNumberId: assigned.phoneNumberId,
|
||||
phoneNumber: assigned.phoneNumber,
|
||||
});
|
||||
} catch (err) {
|
||||
logger.error(err);
|
||||
return next(err);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
// Purchase a phone number
|
||||
router.post(
|
||||
"/purchase",
|
||||
async (req: ExpressRequest, res: ExpressResponse, next: NextFunction) => {
|
||||
try {
|
||||
const body: JSONObject = req.body as JSONObject;
|
||||
|
||||
const projectId: ObjectID | undefined = body["projectId"]
|
||||
? new ObjectID(body["projectId"] as string)
|
||||
: undefined;
|
||||
|
||||
const phoneNumber: string | undefined = body["phoneNumber"] as
|
||||
| string
|
||||
| undefined;
|
||||
|
||||
const incomingCallPolicyId: ObjectID | undefined = body[
|
||||
"incomingCallPolicyId"
|
||||
]
|
||||
? new ObjectID(body["incomingCallPolicyId"] as string)
|
||||
: undefined;
|
||||
|
||||
if (!projectId) {
|
||||
throw new BadDataException("projectId is required");
|
||||
}
|
||||
|
||||
if (!phoneNumber) {
|
||||
throw new BadDataException("phoneNumber is required");
|
||||
}
|
||||
|
||||
if (!incomingCallPolicyId) {
|
||||
throw new BadDataException("incomingCallPolicyId is required");
|
||||
}
|
||||
|
||||
// Check if project exists
|
||||
const project: Project | null = await ProjectService.findOneById({
|
||||
id: projectId,
|
||||
select: {
|
||||
_id: true,
|
||||
name: true,
|
||||
},
|
||||
props: {
|
||||
isRoot: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!project) {
|
||||
throw new BadDataException("Project not found");
|
||||
}
|
||||
|
||||
// Check if incoming call policy exists and get its project config
|
||||
const incomingCallPolicy: IncomingCallPolicy | null =
|
||||
await IncomingCallPolicyService.findOneById({
|
||||
id: incomingCallPolicyId,
|
||||
select: {
|
||||
_id: true,
|
||||
projectId: true,
|
||||
projectCallSMSConfigId: true,
|
||||
routingPhoneNumber: true,
|
||||
},
|
||||
props: {
|
||||
isRoot: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!incomingCallPolicy) {
|
||||
throw new BadDataException("Incoming Call Policy not found");
|
||||
}
|
||||
|
||||
if (incomingCallPolicy.projectId?.toString() !== projectId.toString()) {
|
||||
throw new BadDataException(
|
||||
"Incoming Call Policy does not belong to this project",
|
||||
);
|
||||
}
|
||||
|
||||
if (incomingCallPolicy.routingPhoneNumber) {
|
||||
throw new BadDataException(
|
||||
"This policy already has a phone number. Please release it first.",
|
||||
);
|
||||
}
|
||||
|
||||
// Require project-level Twilio config
|
||||
if (!incomingCallPolicy.projectCallSMSConfigId) {
|
||||
throw new BadDataException(
|
||||
"This policy does not have a project Twilio configuration. Please configure one first.",
|
||||
);
|
||||
}
|
||||
|
||||
// Get project Twilio config
|
||||
const customTwilioConfig: TwilioConfig | null =
|
||||
await getProjectTwilioConfig(incomingCallPolicy.projectCallSMSConfigId);
|
||||
if (!customTwilioConfig) {
|
||||
throw new BadDataException("Project Call/SMS Config not found");
|
||||
}
|
||||
|
||||
const provider: ICallProvider =
|
||||
CallProviderFactory.getProviderWithConfig(customTwilioConfig);
|
||||
|
||||
/*
|
||||
* Construct webhook URL - single endpoint for all phone numbers
|
||||
* Twilio sends the "To" phone number in every webhook, so we look up the policy by phone number
|
||||
*/
|
||||
const webhookUrl: string = `${HttpProtocol}${Host}/notification/incoming-call/voice`;
|
||||
|
||||
const purchased: PurchasedPhoneNumber = await provider.purchaseNumber(
|
||||
phoneNumber,
|
||||
webhookUrl,
|
||||
);
|
||||
|
||||
// Get country code from phone number
|
||||
const countryCode: string =
|
||||
Phone.getCountryCodeFromPhoneNumber(phoneNumber);
|
||||
const areaCode: string = Phone.getAreaCodeFromPhoneNumber(phoneNumber);
|
||||
|
||||
/*
|
||||
* Update the incoming call policy with the purchased number
|
||||
* Customer pays Twilio directly - no billing cost stored
|
||||
*/
|
||||
await IncomingCallPolicyService.updateOneById({
|
||||
id: incomingCallPolicyId,
|
||||
data: {
|
||||
routingPhoneNumber: new Phone(purchased.phoneNumber),
|
||||
callProviderPhoneNumberId: purchased.phoneNumberId,
|
||||
phoneNumberCountryCode: countryCode,
|
||||
phoneNumberAreaCode: areaCode,
|
||||
phoneNumberPurchasedAt: new Date(),
|
||||
},
|
||||
props: {
|
||||
isRoot: true,
|
||||
},
|
||||
});
|
||||
|
||||
return Response.sendJsonObjectResponse(req, res, {
|
||||
success: true,
|
||||
phoneNumberId: purchased.phoneNumberId,
|
||||
phoneNumber: purchased.phoneNumber,
|
||||
});
|
||||
} catch (err) {
|
||||
logger.error(err);
|
||||
return next(err);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
// Release a phone number
|
||||
router.delete(
|
||||
"/release/:incomingCallPolicyId",
|
||||
async (req: ExpressRequest, res: ExpressResponse, next: NextFunction) => {
|
||||
try {
|
||||
const incomingCallPolicyId: ObjectID | undefined = req.params[
|
||||
"incomingCallPolicyId"
|
||||
]
|
||||
? new ObjectID(req.params["incomingCallPolicyId"] as string)
|
||||
: undefined;
|
||||
|
||||
if (!incomingCallPolicyId) {
|
||||
throw new BadDataException("incomingCallPolicyId is required");
|
||||
}
|
||||
|
||||
// Get the incoming call policy with its project config
|
||||
const incomingCallPolicy: IncomingCallPolicy | null =
|
||||
await IncomingCallPolicyService.findOneById({
|
||||
id: incomingCallPolicyId,
|
||||
select: {
|
||||
_id: true,
|
||||
callProviderPhoneNumberId: true,
|
||||
projectCallSMSConfigId: true,
|
||||
routingPhoneNumber: true,
|
||||
},
|
||||
props: {
|
||||
isRoot: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!incomingCallPolicy) {
|
||||
throw new BadDataException("Incoming Call Policy not found");
|
||||
}
|
||||
|
||||
if (!incomingCallPolicy.callProviderPhoneNumberId) {
|
||||
throw new BadDataException("This policy does not have a phone number");
|
||||
}
|
||||
|
||||
// Require project-level Twilio config
|
||||
if (!incomingCallPolicy.projectCallSMSConfigId) {
|
||||
throw new BadDataException(
|
||||
"This policy does not have a project Twilio configuration.",
|
||||
);
|
||||
}
|
||||
|
||||
// Get project Twilio config
|
||||
const customTwilioConfig: TwilioConfig | null =
|
||||
await getProjectTwilioConfig(incomingCallPolicy.projectCallSMSConfigId);
|
||||
if (!customTwilioConfig) {
|
||||
throw new BadDataException("Project Call/SMS Config not found");
|
||||
}
|
||||
|
||||
const provider: ICallProvider =
|
||||
CallProviderFactory.getProviderWithConfig(customTwilioConfig);
|
||||
|
||||
await provider.releaseNumber(
|
||||
incomingCallPolicy.callProviderPhoneNumberId,
|
||||
);
|
||||
|
||||
// Update the incoming call policy to remove the phone number
|
||||
await IncomingCallPolicyService.updateOneById({
|
||||
id: incomingCallPolicyId,
|
||||
data: {
|
||||
routingPhoneNumber: null,
|
||||
callProviderPhoneNumberId: null,
|
||||
phoneNumberCountryCode: null,
|
||||
phoneNumberAreaCode: null,
|
||||
phoneNumberPurchasedAt: null,
|
||||
} as any, // TypeORM allows null for nullable columns
|
||||
props: {
|
||||
isRoot: true,
|
||||
},
|
||||
});
|
||||
|
||||
return Response.sendJsonObjectResponse(req, res, {
|
||||
success: true,
|
||||
});
|
||||
} catch (err) {
|
||||
logger.error(err);
|
||||
return next(err);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
export default router;
|
||||
108
App/FeatureSet/Notification/API/PushRelay.ts
Normal file
108
App/FeatureSet/Notification/API/PushRelay.ts
Normal file
@@ -0,0 +1,108 @@
|
||||
import Express, {
|
||||
ExpressRequest,
|
||||
ExpressResponse,
|
||||
ExpressRouter,
|
||||
NextFunction,
|
||||
} from "Common/Server/Utils/Express";
|
||||
import Response from "Common/Server/Utils/Response";
|
||||
import BadDataException from "Common/Types/Exception/BadDataException";
|
||||
import { JSONObject } from "Common/Types/JSON";
|
||||
import PushNotificationService from "Common/Server/Services/PushNotificationService";
|
||||
|
||||
const router: ExpressRouter = Express.getRouter();
|
||||
|
||||
// Simple in-memory rate limiter by IP
|
||||
const rateLimitMap: Map<string, { count: number; resetTime: number }> =
|
||||
new Map();
|
||||
const RATE_LIMIT_WINDOW_MS: number = 60 * 1000; // 1 minute
|
||||
const RATE_LIMIT_MAX_REQUESTS: number = 60; // 60 requests per minute per IP
|
||||
|
||||
function isRateLimited(ip: string): boolean {
|
||||
const now: number = Date.now();
|
||||
const entry: { count: number; resetTime: number } | undefined =
|
||||
rateLimitMap.get(ip);
|
||||
|
||||
if (!entry || now > entry.resetTime) {
|
||||
rateLimitMap.set(ip, { count: 1, resetTime: now + RATE_LIMIT_WINDOW_MS });
|
||||
return false;
|
||||
}
|
||||
|
||||
entry.count++;
|
||||
|
||||
return entry.count > RATE_LIMIT_MAX_REQUESTS;
|
||||
}
|
||||
|
||||
// Clean up stale rate limit entries every 5 minutes
|
||||
setInterval(
|
||||
() => {
|
||||
const now: number = Date.now();
|
||||
for (const [ip, entry] of rateLimitMap.entries()) {
|
||||
if (now > entry.resetTime) {
|
||||
rateLimitMap.delete(ip);
|
||||
}
|
||||
}
|
||||
},
|
||||
5 * 60 * 1000,
|
||||
);
|
||||
|
||||
router.post(
|
||||
"/send",
|
||||
async (req: ExpressRequest, res: ExpressResponse, next: NextFunction) => {
|
||||
try {
|
||||
const clientIp: string =
|
||||
(req.headers["x-forwarded-for"] as string)?.split(",")[0]?.trim() ||
|
||||
req.socket.remoteAddress ||
|
||||
"unknown";
|
||||
|
||||
if (isRateLimited(clientIp)) {
|
||||
res.status(429).json({
|
||||
message: "Rate limit exceeded. Please try again later.",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (!PushNotificationService.hasExpoAccessToken()) {
|
||||
throw new BadDataException(
|
||||
"Push relay is not configured. EXPO_ACCESS_TOKEN is not set on this server.",
|
||||
);
|
||||
}
|
||||
|
||||
const body: JSONObject = req.body as JSONObject;
|
||||
|
||||
const to: string | undefined = body["to"] as string | undefined;
|
||||
|
||||
if (!to || !PushNotificationService.isValidExpoPushToken(to)) {
|
||||
throw new BadDataException(
|
||||
"Invalid or missing push token. Must be a valid Expo push token.",
|
||||
);
|
||||
}
|
||||
|
||||
const title: string | undefined = body["title"] as string | undefined;
|
||||
const messageBody: string | undefined = body["body"] as
|
||||
| string
|
||||
| undefined;
|
||||
|
||||
if (!title && !messageBody) {
|
||||
throw new BadDataException(
|
||||
"At least one of 'title' or 'body' must be provided.",
|
||||
);
|
||||
}
|
||||
|
||||
await PushNotificationService.sendRelayPushNotification({
|
||||
to: to,
|
||||
...(title !== undefined ? { title } : {}),
|
||||
...(messageBody !== undefined ? { body: messageBody } : {}),
|
||||
data: (body["data"] as { [key: string]: string }) || {},
|
||||
sound: (body["sound"] as string) || "default",
|
||||
priority: (body["priority"] as string) || "high",
|
||||
channelId: (body["channelId"] as string) || "default",
|
||||
});
|
||||
|
||||
return Response.sendJsonObjectResponse(req, res, { success: true });
|
||||
} catch (err) {
|
||||
return next(err);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
export default router;
|
||||
@@ -312,3 +312,6 @@ export const CallDefaultCostInCentsPerMinute: number = process.env[
|
||||
]
|
||||
? parseInt(process.env["CALL_DEFAULT_COST_IN_CENTS_PER_MINUTE"])
|
||||
: 0;
|
||||
|
||||
// Call provider type
|
||||
export const CallProvider: string = process.env["CALL_PROVIDER"] || "twilio";
|
||||
|
||||
@@ -4,7 +4,10 @@ import MailAPI from "./API/Mail";
|
||||
import SmsAPI from "./API/SMS";
|
||||
import WhatsAppAPI from "./API/WhatsApp";
|
||||
import PushNotificationAPI from "./API/PushNotification";
|
||||
import PushRelayAPI from "./API/PushRelay";
|
||||
import SMTPConfigAPI from "./API/SMTPConfig";
|
||||
import PhoneNumberAPI from "./API/PhoneNumber";
|
||||
import IncomingCallAPI from "./API/IncomingCall";
|
||||
import "./Utils/Handlebars";
|
||||
import FeatureSet from "Common/Server/Types/FeatureSet";
|
||||
import Express, { ExpressApplication } from "Common/Server/Utils/Express";
|
||||
@@ -19,8 +22,11 @@ const NotificationFeatureSet: FeatureSet = {
|
||||
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}/push-relay`, "/push-relay"], PushRelayAPI);
|
||||
app.use([`/${APP_NAME}/call`, "/call"], CallAPI);
|
||||
app.use([`/${APP_NAME}/smtp-config`, "/smtp-config"], SMTPConfigAPI);
|
||||
app.use([`/${APP_NAME}/phone-number`, "/phone-number"], PhoneNumberAPI);
|
||||
app.use([`/${APP_NAME}/incoming-call`, "/incoming-call"], IncomingCallAPI);
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
88
App/FeatureSet/Notification/Providers/CallProviderFactory.ts
Normal file
88
App/FeatureSet/Notification/Providers/CallProviderFactory.ts
Normal file
@@ -0,0 +1,88 @@
|
||||
import { ICallProvider } from "Common/Types/Call/CallProvider";
|
||||
import CallProviderType from "Common/Types/Call/CallProviderType";
|
||||
import TwilioCallProvider from "./TwilioCallProvider";
|
||||
import { getTwilioConfig, CallProvider } from "../Config";
|
||||
import TwilioConfig from "Common/Types/CallAndSMS/TwilioConfig";
|
||||
import BadDataException from "Common/Types/Exception/BadDataException";
|
||||
|
||||
export default class CallProviderFactory {
|
||||
private static instance: ICallProvider | null = null;
|
||||
private static currentProviderType: CallProviderType | null = null;
|
||||
|
||||
// Get a provider with the global configuration (cached)
|
||||
public static async getProvider(): Promise<ICallProvider> {
|
||||
const providerType: CallProviderType = this.getProviderType();
|
||||
|
||||
// Return cached instance if provider type hasn't changed
|
||||
if (this.instance && this.currentProviderType === providerType) {
|
||||
return this.instance;
|
||||
}
|
||||
|
||||
switch (providerType) {
|
||||
case CallProviderType.Twilio: {
|
||||
const twilioConfig: TwilioConfig | null = await getTwilioConfig();
|
||||
|
||||
if (!twilioConfig) {
|
||||
throw new BadDataException(
|
||||
"Twilio configuration not found. Please configure Twilio in Admin Dashboard.",
|
||||
);
|
||||
}
|
||||
|
||||
this.instance = new TwilioCallProvider(twilioConfig);
|
||||
this.currentProviderType = providerType;
|
||||
break;
|
||||
}
|
||||
default:
|
||||
throw new BadDataException(`Unknown call provider: ${providerType}`);
|
||||
}
|
||||
|
||||
return this.instance;
|
||||
}
|
||||
|
||||
/*
|
||||
* Get a provider with a custom configuration (not cached)
|
||||
* Used when a project has its own Twilio configuration
|
||||
*/
|
||||
public static getProviderWithConfig(
|
||||
customConfig: TwilioConfig,
|
||||
): ICallProvider {
|
||||
const providerType: CallProviderType = this.getProviderType();
|
||||
|
||||
switch (providerType) {
|
||||
case CallProviderType.Twilio: {
|
||||
/*
|
||||
* Create a new provider instance with the custom config
|
||||
* This is not cached since it's project-specific
|
||||
*/
|
||||
return new TwilioCallProvider(customConfig);
|
||||
}
|
||||
default:
|
||||
throw new BadDataException(`Unknown call provider: ${providerType}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Get a provider, using custom config if provided, otherwise global config
|
||||
public static async getProviderWithOptionalConfig(
|
||||
customConfig?: TwilioConfig,
|
||||
): Promise<ICallProvider> {
|
||||
if (customConfig) {
|
||||
return this.getProviderWithConfig(customConfig);
|
||||
}
|
||||
return this.getProvider();
|
||||
}
|
||||
|
||||
public static getProviderType(): CallProviderType {
|
||||
switch (CallProvider.toLowerCase()) {
|
||||
case "twilio":
|
||||
return CallProviderType.Twilio;
|
||||
default:
|
||||
return CallProviderType.Twilio;
|
||||
}
|
||||
}
|
||||
|
||||
// Method to reset the cached instance (useful for testing or config changes)
|
||||
public static resetProvider(): void {
|
||||
this.instance = null;
|
||||
this.currentProviderType = null;
|
||||
}
|
||||
}
|
||||
327
App/FeatureSet/Notification/Providers/TwilioCallProvider.ts
Normal file
327
App/FeatureSet/Notification/Providers/TwilioCallProvider.ts
Normal file
@@ -0,0 +1,327 @@
|
||||
import {
|
||||
AvailablePhoneNumber,
|
||||
DialOptions,
|
||||
DialStatusData,
|
||||
ICallProvider,
|
||||
IncomingCallData,
|
||||
OwnedPhoneNumber,
|
||||
PurchasedPhoneNumber,
|
||||
SearchNumberOptions,
|
||||
WebhookRequest,
|
||||
} from "Common/Types/Call/CallProvider";
|
||||
import TwilioConfig from "Common/Types/CallAndSMS/TwilioConfig";
|
||||
import BadDataException from "Common/Types/Exception/BadDataException";
|
||||
import Twilio, { validateRequest } from "twilio";
|
||||
import logger from "Common/Server/Utils/Logger";
|
||||
|
||||
export default class TwilioCallProvider implements ICallProvider {
|
||||
private client: Twilio.Twilio;
|
||||
private config: TwilioConfig;
|
||||
|
||||
public constructor(config: TwilioConfig) {
|
||||
this.config = config;
|
||||
this.client = new Twilio.Twilio(config.accountSid, config.authToken);
|
||||
}
|
||||
|
||||
public async searchAvailableNumbers(
|
||||
options: SearchNumberOptions,
|
||||
): Promise<AvailablePhoneNumber[]> {
|
||||
const searchOptions: {
|
||||
voiceEnabled: boolean;
|
||||
limit: number;
|
||||
areaCode?: number;
|
||||
contains?: string;
|
||||
} = {
|
||||
voiceEnabled: true,
|
||||
limit: options.limit || 10,
|
||||
};
|
||||
|
||||
if (options.areaCode) {
|
||||
searchOptions.areaCode = parseInt(options.areaCode);
|
||||
}
|
||||
|
||||
if (options.contains) {
|
||||
searchOptions.contains = options.contains;
|
||||
}
|
||||
|
||||
const numbers: Array<{
|
||||
phoneNumber: string;
|
||||
friendlyName: string;
|
||||
locality?: string;
|
||||
region?: string;
|
||||
}> = await this.client
|
||||
.availablePhoneNumbers(options.countryCode)
|
||||
.local.list(searchOptions);
|
||||
|
||||
return numbers.map(
|
||||
(n: {
|
||||
phoneNumber: string;
|
||||
friendlyName: string;
|
||||
locality?: string;
|
||||
region?: string;
|
||||
}): AvailablePhoneNumber => {
|
||||
const result: AvailablePhoneNumber = {
|
||||
phoneNumber: n.phoneNumber,
|
||||
friendlyName: n.friendlyName,
|
||||
country: options.countryCode,
|
||||
};
|
||||
if (n.locality) {
|
||||
result.locality = n.locality;
|
||||
}
|
||||
if (n.region) {
|
||||
result.region = n.region;
|
||||
}
|
||||
return result;
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
public async listOwnedNumbers(): Promise<OwnedPhoneNumber[]> {
|
||||
const numbers: Array<{
|
||||
sid: string;
|
||||
phoneNumber: string;
|
||||
friendlyName: string;
|
||||
voiceUrl?: string;
|
||||
}> = await this.client.incomingPhoneNumbers.list({
|
||||
limit: 100,
|
||||
});
|
||||
|
||||
return numbers.map(
|
||||
(n: {
|
||||
sid: string;
|
||||
phoneNumber: string;
|
||||
friendlyName: string;
|
||||
voiceUrl?: string;
|
||||
}): OwnedPhoneNumber => {
|
||||
return {
|
||||
phoneNumberId: n.sid,
|
||||
phoneNumber: n.phoneNumber,
|
||||
friendlyName: n.friendlyName,
|
||||
voiceUrl: n.voiceUrl,
|
||||
};
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
public async purchaseNumber(
|
||||
phoneNumber: string,
|
||||
webhookUrl: string,
|
||||
): Promise<PurchasedPhoneNumber> {
|
||||
const purchased: Twilio.Twilio["incomingPhoneNumbers"] extends {
|
||||
create: (opts: Record<string, unknown>) => Promise<infer R>;
|
||||
}
|
||||
? R
|
||||
: never = await this.client.incomingPhoneNumbers.create({
|
||||
phoneNumber,
|
||||
voiceUrl: webhookUrl,
|
||||
voiceMethod: "POST",
|
||||
});
|
||||
|
||||
return {
|
||||
phoneNumberId: purchased.sid,
|
||||
phoneNumber: purchased.phoneNumber,
|
||||
};
|
||||
}
|
||||
|
||||
public async assignExistingNumber(
|
||||
phoneNumberId: string,
|
||||
webhookUrl: string,
|
||||
): Promise<PurchasedPhoneNumber> {
|
||||
// Update the webhook URL for an existing phone number
|
||||
const updated: Twilio.Twilio["incomingPhoneNumbers"] extends {
|
||||
(sid: string): {
|
||||
update: (opts: Record<string, unknown>) => Promise<infer R>;
|
||||
};
|
||||
}
|
||||
? R
|
||||
: never = await this.client.incomingPhoneNumbers(phoneNumberId).update({
|
||||
voiceUrl: webhookUrl,
|
||||
voiceMethod: "POST",
|
||||
});
|
||||
|
||||
return {
|
||||
phoneNumberId: updated.sid,
|
||||
phoneNumber: updated.phoneNumber,
|
||||
};
|
||||
}
|
||||
|
||||
public async releaseNumber(phoneNumberId: string): Promise<void> {
|
||||
await this.client.incomingPhoneNumbers(phoneNumberId).remove();
|
||||
}
|
||||
|
||||
public async updateWebhookUrl(
|
||||
phoneNumberId: string,
|
||||
webhookUrl: string,
|
||||
): Promise<void> {
|
||||
await this.client.incomingPhoneNumbers(phoneNumberId).update({
|
||||
voiceUrl: webhookUrl,
|
||||
voiceMethod: "POST",
|
||||
});
|
||||
}
|
||||
|
||||
public generateGreetingResponse(message: string): string {
|
||||
const response: Twilio.twiml.VoiceResponse =
|
||||
new Twilio.twiml.VoiceResponse();
|
||||
response.say({ voice: "alice" }, message);
|
||||
return response.toString();
|
||||
}
|
||||
|
||||
public generateDialResponse(options: DialOptions): string {
|
||||
const response: Twilio.twiml.VoiceResponse =
|
||||
new Twilio.twiml.VoiceResponse();
|
||||
const dial: ReturnType<Twilio.twiml.VoiceResponse["dial"]> = response.dial({
|
||||
action: options.statusCallbackUrl,
|
||||
method: "POST",
|
||||
timeout: options.timeoutSeconds,
|
||||
callerId: options.fromPhoneNumber,
|
||||
});
|
||||
dial.number(options.toPhoneNumber);
|
||||
return response.toString();
|
||||
}
|
||||
|
||||
public generateHangupResponse(message?: string): string {
|
||||
const response: Twilio.twiml.VoiceResponse =
|
||||
new Twilio.twiml.VoiceResponse();
|
||||
if (message) {
|
||||
response.say({ voice: "alice" }, message);
|
||||
}
|
||||
response.hangup();
|
||||
return response.toString();
|
||||
}
|
||||
|
||||
public generateEscalationResponse(
|
||||
message: string,
|
||||
nextDialOptions: DialOptions,
|
||||
): string {
|
||||
const response: Twilio.twiml.VoiceResponse =
|
||||
new Twilio.twiml.VoiceResponse();
|
||||
response.say({ voice: "alice" }, message);
|
||||
const dial: ReturnType<Twilio.twiml.VoiceResponse["dial"]> = response.dial({
|
||||
action: nextDialOptions.statusCallbackUrl,
|
||||
method: "POST",
|
||||
timeout: nextDialOptions.timeoutSeconds,
|
||||
callerId: nextDialOptions.fromPhoneNumber,
|
||||
});
|
||||
dial.number(nextDialOptions.toPhoneNumber);
|
||||
return response.toString();
|
||||
}
|
||||
|
||||
public parseIncomingCallWebhook(request: WebhookRequest): IncomingCallData {
|
||||
const body: { CallSid?: string; From?: string; To?: string } =
|
||||
request.body as { CallSid?: string; From?: string; To?: string };
|
||||
|
||||
if (!body.CallSid) {
|
||||
throw new BadDataException("CallSid not found in webhook request");
|
||||
}
|
||||
|
||||
if (!body.From) {
|
||||
throw new BadDataException("From not found in webhook request");
|
||||
}
|
||||
|
||||
if (!body.To) {
|
||||
throw new BadDataException("To not found in webhook request");
|
||||
}
|
||||
|
||||
return {
|
||||
callId: body.CallSid,
|
||||
callerPhoneNumber: body.From,
|
||||
calledPhoneNumber: body.To,
|
||||
};
|
||||
}
|
||||
|
||||
public parseDialStatusWebhook(request: WebhookRequest): DialStatusData {
|
||||
const body: {
|
||||
CallSid?: string;
|
||||
DialCallStatus?: string;
|
||||
DialCallDuration?: string;
|
||||
} = request.body as {
|
||||
CallSid?: string;
|
||||
DialCallStatus?: string;
|
||||
DialCallDuration?: string;
|
||||
};
|
||||
|
||||
if (!body.CallSid) {
|
||||
throw new BadDataException("CallSid not found in webhook request");
|
||||
}
|
||||
|
||||
return {
|
||||
callId: body.CallSid,
|
||||
dialStatus: this.mapTwilioStatus(body.DialCallStatus || "failed"),
|
||||
dialDurationSeconds: parseInt(body.DialCallDuration || "0"),
|
||||
};
|
||||
}
|
||||
|
||||
public validateWebhookSignature(
|
||||
request: WebhookRequest,
|
||||
signature: string,
|
||||
): boolean {
|
||||
const authToken: string = this.config.authToken;
|
||||
|
||||
/*
|
||||
* Build the full URL that Twilio used to generate the signature
|
||||
* When behind a proxy, use X-Forwarded-Proto and X-Forwarded-Host headers
|
||||
* These headers are set by reverse proxies (nginx, load balancers, etc.)
|
||||
*/
|
||||
const forwardedProto: string | undefined = request.get(
|
||||
"x-forwarded-proto",
|
||||
) as string | undefined;
|
||||
const forwardedHost: string | undefined = request.get(
|
||||
"x-forwarded-host",
|
||||
) as string | undefined;
|
||||
|
||||
// Use forwarded headers if available, otherwise fall back to request properties
|
||||
const protocol: string = forwardedProto || request.protocol || "https";
|
||||
const host: string = forwardedHost || request.get("host") || "";
|
||||
|
||||
/*
|
||||
* Nginx rewrites /notification to /api/notification internally
|
||||
* But Twilio signed with the original external URL path (/notification/...)
|
||||
* So we need to remove the /api prefix for signature validation
|
||||
*/
|
||||
let originalUrl: string = request.originalUrl;
|
||||
if (originalUrl.startsWith("/api/notification")) {
|
||||
originalUrl = originalUrl.replace("/api/notification", "/notification");
|
||||
}
|
||||
|
||||
const url: string = `${protocol}://${host}${originalUrl}`;
|
||||
|
||||
const params: Record<string, string> = {};
|
||||
const body: Record<string, unknown> = request.body as Record<
|
||||
string,
|
||||
unknown
|
||||
>;
|
||||
for (const key of Object.keys(body)) {
|
||||
params[key] = String(body[key]);
|
||||
}
|
||||
|
||||
const isValid: boolean = validateRequest(authToken, signature, url, params);
|
||||
|
||||
// Debug logging for signature validation
|
||||
if (!isValid) {
|
||||
logger.debug("Twilio Webhook Signature Validation Debug:");
|
||||
logger.debug(` URL used for validation: ${url}`);
|
||||
logger.debug(` Signature received: ${signature}`);
|
||||
logger.debug(` Protocol: ${protocol}`);
|
||||
logger.debug(` Host: ${host}`);
|
||||
logger.debug(` Original URL (from request): ${request.originalUrl}`);
|
||||
logger.debug(` Corrected URL path: ${originalUrl}`);
|
||||
logger.debug(` X-Forwarded-Proto: ${forwardedProto}`);
|
||||
logger.debug(` X-Forwarded-Host: ${forwardedHost}`);
|
||||
logger.debug(` Request protocol: ${request.protocol}`);
|
||||
logger.debug(` Request host header: ${request.get("host")}`);
|
||||
}
|
||||
|
||||
return isValid;
|
||||
}
|
||||
|
||||
private mapTwilioStatus(status: string): DialStatusData["dialStatus"] {
|
||||
const map: Record<string, DialStatusData["dialStatus"]> = {
|
||||
completed: "completed",
|
||||
busy: "busy",
|
||||
"no-answer": "no-answer",
|
||||
failed: "failed",
|
||||
canceled: "canceled",
|
||||
};
|
||||
return map[status] || "failed";
|
||||
}
|
||||
}
|
||||
@@ -70,6 +70,7 @@ export default class CallService {
|
||||
customTwilioConfig?: TwilioConfig | undefined;
|
||||
incidentId?: ObjectID | undefined;
|
||||
alertId?: ObjectID | undefined;
|
||||
monitorId?: ObjectID | undefined;
|
||||
scheduledMaintenanceId?: ObjectID | undefined;
|
||||
statusPageId?: ObjectID | undefined;
|
||||
statusPageAnnouncementId?: ObjectID | undefined;
|
||||
@@ -120,7 +121,7 @@ export default class CallService {
|
||||
const fromNumber: Phone = Phone.pickPhoneNumberToSendSMSOrCallFrom({
|
||||
to: callRequest.to,
|
||||
primaryPhoneNumberToPickFrom: twilioConfig.primaryPhoneNumber,
|
||||
seocndaryPhoneNumbersToPickFrom:
|
||||
secondaryPhoneNumbersToPickFrom:
|
||||
twilioConfig.secondaryPhoneNumbers || [],
|
||||
});
|
||||
callLog.fromNumber = fromNumber;
|
||||
@@ -144,6 +145,10 @@ export default class CallService {
|
||||
callLog.alertId = options.alertId;
|
||||
}
|
||||
|
||||
if (options.monitorId) {
|
||||
callLog.monitorId = options.monitorId;
|
||||
}
|
||||
|
||||
if (options.scheduledMaintenanceId) {
|
||||
callLog.scheduledMaintenanceId = options.scheduledMaintenanceId;
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ import {
|
||||
getSendgridConfig,
|
||||
} from "../Config";
|
||||
import SMTPOAuthService from "./SMTPOAuthService";
|
||||
import SendgridMail, { MailDataRequired } from "@sendgrid/mail";
|
||||
import SendgridMail, { ClientResponse, MailDataRequired } from "@sendgrid/mail";
|
||||
import Hostname from "Common/Types/API/Hostname";
|
||||
import OneUptimeDate from "Common/Types/Date";
|
||||
import Dictionary from "Common/Types/Dictionary";
|
||||
@@ -433,12 +433,17 @@ export default class MailService {
|
||||
try {
|
||||
for (let attempt: number = 1; attempt <= maxRetries; attempt++) {
|
||||
try {
|
||||
await mailer.sendMail({
|
||||
from: `${options.emailServer.fromName.toString()} <${options.emailServer.fromEmail.toString()}>`,
|
||||
to: mail.toEmail.toString(),
|
||||
subject: mail.subject,
|
||||
html: mail.body,
|
||||
});
|
||||
const sendMailResponse: SMTPTransport.SentMessageInfo =
|
||||
await mailer.sendMail({
|
||||
from: `${options.emailServer.fromName.toString()} <${options.emailServer.fromEmail.toString()}>`,
|
||||
to: mail.toEmail.toString(),
|
||||
subject: mail.subject,
|
||||
html: mail.body,
|
||||
});
|
||||
|
||||
logger.debug("SMTP Email Provider Response:");
|
||||
logger.debug(JSON.stringify(sendMailResponse, null, 2));
|
||||
|
||||
return; // Success, exit the function
|
||||
} catch (error) {
|
||||
lastError = error;
|
||||
@@ -478,6 +483,7 @@ export default class MailService {
|
||||
timeout?: number | undefined;
|
||||
incidentId?: ObjectID | undefined;
|
||||
alertId?: ObjectID | undefined;
|
||||
monitorId?: ObjectID | undefined;
|
||||
scheduledMaintenanceId?: ObjectID | undefined;
|
||||
statusPageId?: ObjectID | undefined;
|
||||
statusPageAnnouncementId?: ObjectID | undefined;
|
||||
@@ -511,6 +517,10 @@ export default class MailService {
|
||||
emailLog.alertId = options.alertId;
|
||||
}
|
||||
|
||||
if (options.monitorId) {
|
||||
emailLog.monitorId = options.monitorId;
|
||||
}
|
||||
|
||||
if (options.scheduledMaintenanceId) {
|
||||
emailLog.scheduledMaintenanceId = options.scheduledMaintenanceId;
|
||||
}
|
||||
@@ -652,7 +662,21 @@ export default class MailService {
|
||||
emailLog.fromEmail = sendgridConfig.fromEmail;
|
||||
}
|
||||
|
||||
await SendgridMail.send(msg);
|
||||
const sendgridResponse: [ClientResponse, Record<string, unknown>] =
|
||||
await SendgridMail.send(msg);
|
||||
|
||||
logger.debug("SendGrid Email Provider Response:");
|
||||
logger.debug(
|
||||
JSON.stringify(
|
||||
{
|
||||
statusCode: sendgridResponse[0]?.statusCode,
|
||||
headers: sendgridResponse[0]?.headers,
|
||||
body: sendgridResponse[0]?.body,
|
||||
},
|
||||
null,
|
||||
2,
|
||||
),
|
||||
);
|
||||
|
||||
if (emailLog) {
|
||||
emailLog.status = MailStatus.Success;
|
||||
|
||||
@@ -33,6 +33,7 @@ export default class SmsService {
|
||||
userOnCallLogTimelineId?: ObjectID | undefined;
|
||||
incidentId?: ObjectID | undefined;
|
||||
alertId?: ObjectID | undefined;
|
||||
monitorId?: ObjectID | undefined;
|
||||
scheduledMaintenanceId?: ObjectID | undefined;
|
||||
statusPageId?: ObjectID | undefined;
|
||||
statusPageAnnouncementId?: ObjectID | undefined;
|
||||
@@ -91,6 +92,10 @@ export default class SmsService {
|
||||
smsLog.alertId = options.alertId;
|
||||
}
|
||||
|
||||
if (options.monitorId) {
|
||||
smsLog.monitorId = options.monitorId;
|
||||
}
|
||||
|
||||
if (options.scheduledMaintenanceId) {
|
||||
smsLog.scheduledMaintenanceId = options.scheduledMaintenanceId;
|
||||
}
|
||||
@@ -140,7 +145,7 @@ export default class SmsService {
|
||||
const fromNumber: Phone = Phone.pickPhoneNumberToSendSMSOrCallFrom({
|
||||
to: to,
|
||||
primaryPhoneNumberToPickFrom: twilioConfig.primaryPhoneNumber,
|
||||
seocndaryPhoneNumbersToPickFrom:
|
||||
secondaryPhoneNumbersToPickFrom:
|
||||
twilioConfig.secondaryPhoneNumbers || [],
|
||||
});
|
||||
|
||||
|
||||
@@ -41,6 +41,7 @@ export default class WhatsAppService {
|
||||
userOnCallLogTimelineId?: ObjectID | undefined;
|
||||
incidentId?: ObjectID | undefined;
|
||||
alertId?: ObjectID | undefined;
|
||||
monitorId?: ObjectID | undefined;
|
||||
scheduledMaintenanceId?: ObjectID | undefined;
|
||||
statusPageId?: ObjectID | undefined;
|
||||
statusPageAnnouncementId?: ObjectID | undefined;
|
||||
@@ -96,6 +97,10 @@ export default class WhatsAppService {
|
||||
whatsAppLog.alertId = options.alertId;
|
||||
}
|
||||
|
||||
if (options.monitorId) {
|
||||
whatsAppLog.monitorId = options.monitorId;
|
||||
}
|
||||
|
||||
if (options.scheduledMaintenanceId) {
|
||||
whatsAppLog.scheduledMaintenanceId = options.scheduledMaintenanceId;
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
|
||||
{{> Logo this}}
|
||||
{{> EmailTitle title=(concat "Alert: " alertTitle) }}
|
||||
{{> EmailTitle title=(concat "Alert " alertNumber ": " alertTitle) }}
|
||||
|
||||
{{> InfoBlock info=(concat "A new alert has been created in the project - " projectName)}}
|
||||
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
{{> Start this}}
|
||||
|
||||
|
||||
{{> Logo this}}
|
||||
{{> EmailTitle title=(concat "Alert Episode " episodeNumber ": " alertEpisodeTitle) }}
|
||||
|
||||
{{> InfoBlock info=(concat "A new alert episode has been created in the project - " projectName)}}
|
||||
|
||||
{{> InfoBlock info="Here are the details: "}}
|
||||
|
||||
{{> DetailBoxStart this }}
|
||||
{{> DetailBoxField title="Alert Episode Title:" text=alertEpisodeTitle }}
|
||||
{{> DetailBoxField title="Current State: " text=currentState }}
|
||||
{{> DetailBoxField title="Resources Affected: " text=resourcesAffected }}
|
||||
{{> DetailBoxField title="Severity: " text=alertEpisodeSeverity }}
|
||||
{{> DetailBoxField title="Root Cause: " text=rootCause }}
|
||||
{{> DetailBoxField title="Description: " text=alertEpisodeDescription }}
|
||||
{{> DetailBoxEnd this }}
|
||||
|
||||
{{#if alertsList}}
|
||||
{{> TitleBlock title=(concat "Alerts in this Episode (" alertsCount ")") }}
|
||||
<!-- Alerts List Container -->
|
||||
<table class="st-Copy st-Width st-Width--mobile" border="0" cellpadding="0" cellspacing="0"
|
||||
width="600" style="min-width: 600px;">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td class="st-Spacer st-Spacer--gutter"
|
||||
style="border: 0; margin:0; padding: 0; font-size: 1px; line-height: 1px; mso-line-height-rule: exactly;"
|
||||
width="64">
|
||||
<div class="st-Spacer st-Spacer--filler"></div>
|
||||
</td>
|
||||
<td style="border: 0; margin: 0; padding: 0;">
|
||||
{{{alertsList}}}
|
||||
</td>
|
||||
<td class="st-Spacer st-Spacer--gutter"
|
||||
style="border: 0; margin:0; padding: 0; font-size: 1px; line-height: 1px; mso-line-height-rule: exactly;"
|
||||
width="64">
|
||||
<div class="st-Spacer st-Spacer--filler"></div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="st-Spacer st-Spacer--stacked" colspan="3" height="16"
|
||||
style="border: 0; margin: 0; padding: 0; font-size: 1px; line-height: 1px; mso-line-height-rule: exactly;">
|
||||
<div class="st-Spacer st-Spacer--filler"></div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<!-- /Alerts List Container -->
|
||||
{{/if}}
|
||||
|
||||
{{> InfoBlock info="ACTION REQUIRED: Please acknowledge this alert episode by clicking on the button below - "}}
|
||||
|
||||
{{> ButtonBlock buttonUrl=acknowledgeAlertEpisodeLink buttonText="Acknowledge Alert Episode"}}
|
||||
|
||||
{{> InfoBlock info="You can also copy and paste this link:"}}
|
||||
{{> InfoBlock info=acknowledgeAlertEpisodeLink}}
|
||||
|
||||
{{> InfoBlock info="You will be notified when the status of this alert episode changes."}}
|
||||
|
||||
{{> TitleBlock title="Why am I receiving this email?"}}
|
||||
{{> InfoBlock info="You are receiving this email because you are a member of the team that is responsible for this alert episode or you are currently on-call."}}
|
||||
|
||||
{{> Footer this }}
|
||||
|
||||
{{> End this}}
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
|
||||
{{> Logo this}}
|
||||
{{> EmailTitle title=(concat "Incident: " incidentTitle) }}
|
||||
{{> EmailTitle title=(concat "Incident " incidentNumber ": " incidentTitle) }}
|
||||
|
||||
{{> InfoBlock info=(concat "A new incident has been created in the project - " projectName)}}
|
||||
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
{{> Start this}}
|
||||
|
||||
|
||||
{{> Logo this}}
|
||||
{{> EmailTitle title=(concat "Incident Episode " episodeNumber ": " incidentEpisodeTitle) }}
|
||||
|
||||
{{> InfoBlock info=(concat "A new incident episode has been created in the project - " projectName)}}
|
||||
|
||||
{{> InfoBlock info="Here are the details: "}}
|
||||
|
||||
{{> DetailBoxStart this }}
|
||||
{{> DetailBoxField title="Incident Episode Title:" text=incidentEpisodeTitle }}
|
||||
{{> DetailBoxField title="Current State: " text=currentState }}
|
||||
{{> DetailBoxField title="Resources Affected: " text=resourcesAffected }}
|
||||
{{> DetailBoxField title="Severity: " text=incidentEpisodeSeverity }}
|
||||
{{> DetailBoxField title="Root Cause: " text=rootCause }}
|
||||
{{> DetailBoxField title="Description: " text=incidentEpisodeDescription }}
|
||||
{{> DetailBoxEnd this }}
|
||||
|
||||
{{#if incidentsList}}
|
||||
{{> TitleBlock title=(concat "Incidents in this Episode (" incidentsCount ")") }}
|
||||
<!-- Incidents List Container -->
|
||||
<table class="st-Copy st-Width st-Width--mobile" border="0" cellpadding="0" cellspacing="0"
|
||||
width="600" style="min-width: 600px;">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td class="st-Spacer st-Spacer--gutter"
|
||||
style="border: 0; margin:0; padding: 0; font-size: 1px; line-height: 1px; mso-line-height-rule: exactly;"
|
||||
width="64">
|
||||
<div class="st-Spacer st-Spacer--filler"></div>
|
||||
</td>
|
||||
<td style="border: 0; margin: 0; padding: 0;">
|
||||
{{{incidentsList}}}
|
||||
</td>
|
||||
<td class="st-Spacer st-Spacer--gutter"
|
||||
style="border: 0; margin:0; padding: 0; font-size: 1px; line-height: 1px; mso-line-height-rule: exactly;"
|
||||
width="64">
|
||||
<div class="st-Spacer st-Spacer--filler"></div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="st-Spacer st-Spacer--stacked" colspan="3" height="16"
|
||||
style="border: 0; margin: 0; padding: 0; font-size: 1px; line-height: 1px; mso-line-height-rule: exactly;">
|
||||
<div class="st-Spacer st-Spacer--filler"></div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<!-- /Incidents List Container -->
|
||||
{{/if}}
|
||||
|
||||
{{> InfoBlock info="ACTION REQUIRED: Please acknowledge this incident episode by clicking on the button below - "}}
|
||||
|
||||
{{> ButtonBlock buttonUrl=acknowledgeIncidentEpisodeLink buttonText="Acknowledge Incident Episode"}}
|
||||
|
||||
{{> InfoBlock info="You can also copy and paste this link:"}}
|
||||
{{> InfoBlock info=acknowledgeIncidentEpisodeLink}}
|
||||
|
||||
{{> InfoBlock info="You will be notified when the status of this incident episode changes."}}
|
||||
|
||||
{{> TitleBlock title="Why am I receiving this email?"}}
|
||||
{{> InfoBlock info="You are receiving this email because you are a member of the team that is responsible for this incident episode or you are currently on-call."}}
|
||||
|
||||
{{> Footer this }}
|
||||
|
||||
{{> End this}}
|
||||
@@ -0,0 +1,30 @@
|
||||
{{> Start this}}
|
||||
|
||||
|
||||
{{> Logo this}}
|
||||
{{> EmailTitle title=(concat "Alert Episode " episodeNumber ": " episodeTitle) }}
|
||||
|
||||
{{> InfoBlock info="You have been added as the owner of this alert episode."}}
|
||||
|
||||
{{> InfoBlock info="Here are the details: "}}
|
||||
|
||||
{{> DetailBoxStart this }}
|
||||
{{> DetailBoxField title="Episode Title:" text=episodeTitle }}
|
||||
{{> DetailBoxField title="Current State: " text=currentState }}
|
||||
{{> DetailBoxField title="Severity: " text=episodeSeverity }}
|
||||
{{> DetailBoxField title="Description: " text=episodeDescription }}
|
||||
{{> DetailBoxEnd this }}
|
||||
|
||||
|
||||
{{> InfoBlock info="You can view this alert episode by clicking on the button below - "}}
|
||||
|
||||
{{> ButtonBlock buttonUrl=episodeViewLink buttonText="View on Dashboard"}}
|
||||
|
||||
{{> InfoBlock info="You can also copy and paste this link:"}}
|
||||
{{> InfoBlock info=episodeViewLink}}
|
||||
|
||||
{{> InfoBlock info="You will be notified when the status of this alert episode changes."}}
|
||||
|
||||
{{> Footer this }}
|
||||
|
||||
{{> End this}}
|
||||
@@ -0,0 +1,37 @@
|
||||
{{> Start this}}
|
||||
|
||||
|
||||
{{> Logo this}}
|
||||
{{> EmailTitle title=(concat "Alert Episode " episodeNumber ": " episodeTitle) }}
|
||||
|
||||
{{> InfoBlock info="A new note has been posted on this alert episode."}}
|
||||
|
||||
{{> InfoBlock info="Here are the details: "}}
|
||||
|
||||
{{> DetailBoxStart this }}
|
||||
{{> DetailBoxField title="Episode Title:" text=episodeTitle }}
|
||||
{{> DetailBoxField title="Current State: " text=currentState }}
|
||||
{{> DetailBoxField title="Severity: " text=episodeSeverity }}
|
||||
{{#if isPrivateNote}}
|
||||
{{> DetailBoxField title="Private Note: " text=note }}
|
||||
{{else}}
|
||||
{{> DetailBoxField title="Public Note: " text=note }}
|
||||
{{/if}}
|
||||
{{> DetailBoxEnd this }}
|
||||
|
||||
|
||||
{{> InfoBlock info="You can view this alert episode by clicking on the button below - "}}
|
||||
|
||||
{{> ButtonBlock buttonUrl=episodeViewLink buttonText="View on Dashboard"}}
|
||||
|
||||
{{> InfoBlock info="You can also copy and paste this link:"}}
|
||||
{{> InfoBlock info=episodeViewLink}}
|
||||
|
||||
{{> InfoBlock info="You will be notified when the status of this alert episode changes."}}
|
||||
|
||||
{{> OwnerInfo this }}
|
||||
{{> UnsubscribeOwnerEmail this }}
|
||||
|
||||
{{> Footer this }}
|
||||
|
||||
{{> End this}}
|
||||
@@ -0,0 +1,35 @@
|
||||
{{> Start this}}
|
||||
|
||||
|
||||
{{> Logo this}}
|
||||
{{> EmailTitle title=(concat "Alert Episode " episodeNumber ": " episodeTitle) }}
|
||||
|
||||
{{> InfoBlock info=(concat "A new alert episode has been created in the project - " projectName)}}
|
||||
|
||||
{{> InfoBlock info="Here are the details: "}}
|
||||
|
||||
{{> DetailBoxStart this }}
|
||||
{{> DetailBoxField title="Episode Title:" text=episodeTitle }}
|
||||
{{> DetailBoxField title="Current State: " text=currentState }}
|
||||
{{> DetailBoxField title="Episode Created By: " text=declaredBy }}
|
||||
{{> DetailBoxField title="Episode Created At: " text=declaredAt }}
|
||||
{{> DetailBoxField title="Severity: " text=episodeSeverity }}
|
||||
{{> DetailBoxField title="Description: " text=episodeDescription }}
|
||||
{{> DetailBoxEnd this }}
|
||||
|
||||
|
||||
{{> InfoBlock info="You can view this alert episode by clicking on the button below - "}}
|
||||
|
||||
{{> ButtonBlock buttonUrl=episodeViewLink buttonText="View on Dashboard"}}
|
||||
|
||||
{{> InfoBlock info="You can also copy and paste this link:"}}
|
||||
{{> InfoBlock info=episodeViewLink}}
|
||||
|
||||
{{> InfoBlock info="You will be notified when the status of this alert episode changes."}}
|
||||
|
||||
{{> OwnerInfo this }}
|
||||
{{> UnsubscribeOwnerEmail this }}
|
||||
|
||||
{{> Footer this }}
|
||||
|
||||
{{> End this}}
|
||||
@@ -0,0 +1,37 @@
|
||||
{{> Start this}}
|
||||
|
||||
|
||||
{{> Logo this}}
|
||||
{{> EmailTitle title=(concat "Alert Episode " episodeNumber ": " episodeTitle) }}
|
||||
|
||||
{{> InfoBlock info="Alert episode state has changed"}}
|
||||
|
||||
{{> InfoBlock info="Here are the details: "}}
|
||||
|
||||
{{> DetailBoxStart this }}
|
||||
{{> StateTransition this}}
|
||||
{{#ifNotCond previousStateDurationText ""}}
|
||||
{{> DetailBoxField title="Duration in Previous State:" text=previousStateDurationText }}
|
||||
{{/ifNotCond}}
|
||||
{{> DetailBoxField title="Episode Title:" text=episodeTitle }}
|
||||
{{> DetailBoxField title="State changed at:" text=stateChangedAt }}
|
||||
{{> DetailBoxField title="Severity:" text=episodeSeverity }}
|
||||
{{> DetailBoxField title="Description:" text=episodeDescription }}
|
||||
{{> DetailBoxEnd this }}
|
||||
|
||||
|
||||
{{> InfoBlock info="You can view this alert episode by clicking on the button below - "}}
|
||||
|
||||
{{> ButtonBlock buttonUrl=episodeViewLink buttonText="View on Dashboard"}}
|
||||
|
||||
{{> InfoBlock info="You can also copy and paste this link:"}}
|
||||
{{> InfoBlock info=episodeViewLink}}
|
||||
|
||||
{{> InfoBlock info="You will be notified when the status of this alert episode changes."}}
|
||||
|
||||
{{> OwnerInfo this }}
|
||||
{{> UnsubscribeOwnerEmail this }}
|
||||
|
||||
{{> Footer this }}
|
||||
|
||||
{{> End this}}
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
|
||||
{{> Logo this}}
|
||||
{{> EmailTitle title=(concat "Alert: " alertTitle) }}
|
||||
{{> EmailTitle title=(concat "Alert " alertNumber ": " alertTitle) }}
|
||||
|
||||
{{> InfoBlock info="You have been added as the owner of this alert."}}
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
|
||||
{{> Logo this}}
|
||||
{{> EmailTitle title=(concat "Alert: " alertTitle) }}
|
||||
{{> EmailTitle title=(concat "Alert " alertNumber ": " alertTitle) }}
|
||||
|
||||
{{> InfoBlock info="A new note has been posted on this alert."}}
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
|
||||
{{> Logo this}}
|
||||
{{> EmailTitle title=(concat "Alert: " alertTitle) }}
|
||||
{{> EmailTitle title=(concat "Alert " alertNumber ": " alertTitle) }}
|
||||
|
||||
{{> InfoBlock info=(concat "A new alert has been created in the project - " projectName)}}
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
|
||||
{{> Logo this}}
|
||||
{{> EmailTitle title=(concat "Alert: " alertTitle) }}
|
||||
{{> EmailTitle title=(concat "Alert " alertNumber ": " alertTitle) }}
|
||||
|
||||
{{> InfoBlock info="Alert state has changed"}}
|
||||
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
{{> Start this}}
|
||||
|
||||
|
||||
{{> Logo this}}
|
||||
{{> EmailTitle title=(concat "Incident Episode " episodeNumber ": " episodeTitle) }}
|
||||
|
||||
{{> InfoBlock info="You have been added as the owner of this incident episode."}}
|
||||
|
||||
{{> InfoBlock info="Here are the details: "}}
|
||||
|
||||
{{> DetailBoxStart this }}
|
||||
{{> DetailBoxField title="Episode Title:" text=episodeTitle }}
|
||||
{{> DetailBoxField title="Current State: " text=currentState }}
|
||||
{{> DetailBoxField title="Severity: " text=episodeSeverity }}
|
||||
{{> DetailBoxField title="Description: " text=episodeDescription }}
|
||||
{{> DetailBoxEnd this }}
|
||||
|
||||
|
||||
{{> InfoBlock info="You can view this incident episode by clicking on the button below - "}}
|
||||
|
||||
{{> ButtonBlock buttonUrl=episodeViewLink buttonText="View on Dashboard"}}
|
||||
|
||||
{{> InfoBlock info="You can also copy and paste this link:"}}
|
||||
{{> InfoBlock info=episodeViewLink}}
|
||||
|
||||
{{> InfoBlock info="You will be notified when the status of this incident episode changes."}}
|
||||
|
||||
{{> Footer this }}
|
||||
|
||||
{{> End this}}
|
||||
@@ -0,0 +1,37 @@
|
||||
{{> Start this}}
|
||||
|
||||
|
||||
{{> Logo this}}
|
||||
{{> EmailTitle title=(concat "Incident Episode " episodeNumber ": " episodeTitle) }}
|
||||
|
||||
{{> InfoBlock info="A new note has been posted on this incident episode."}}
|
||||
|
||||
{{> InfoBlock info="Here are the details: "}}
|
||||
|
||||
{{> DetailBoxStart this }}
|
||||
{{> DetailBoxField title="Episode Title:" text=episodeTitle }}
|
||||
{{> DetailBoxField title="Current State: " text=currentState }}
|
||||
{{> DetailBoxField title="Severity: " text=episodeSeverity }}
|
||||
{{#if isPrivateNote}}
|
||||
{{> DetailBoxField title="Private Note: " text=note }}
|
||||
{{else}}
|
||||
{{> DetailBoxField title="Public Note: " text=note }}
|
||||
{{/if}}
|
||||
{{> DetailBoxEnd this }}
|
||||
|
||||
|
||||
{{> InfoBlock info="You can view this incident episode by clicking on the button below - "}}
|
||||
|
||||
{{> ButtonBlock buttonUrl=episodeViewLink buttonText="View on Dashboard"}}
|
||||
|
||||
{{> InfoBlock info="You can also copy and paste this link:"}}
|
||||
{{> InfoBlock info=episodeViewLink}}
|
||||
|
||||
{{> InfoBlock info="You will be notified when the status of this incident episode changes."}}
|
||||
|
||||
{{> OwnerInfo this }}
|
||||
{{> UnsubscribeOwnerEmail this }}
|
||||
|
||||
{{> Footer this }}
|
||||
|
||||
{{> End this}}
|
||||
@@ -0,0 +1,35 @@
|
||||
{{> Start this}}
|
||||
|
||||
|
||||
{{> Logo this}}
|
||||
{{> EmailTitle title=(concat "Incident Episode " episodeNumber ": " episodeTitle) }}
|
||||
|
||||
{{> InfoBlock info=(concat "A new incident episode has been created in the project - " projectName)}}
|
||||
|
||||
{{> InfoBlock info="Here are the details: "}}
|
||||
|
||||
{{> DetailBoxStart this }}
|
||||
{{> DetailBoxField title="Episode Title:" text=episodeTitle }}
|
||||
{{> DetailBoxField title="Current State: " text=currentState }}
|
||||
{{> DetailBoxField title="Episode Created By: " text=declaredBy }}
|
||||
{{> DetailBoxField title="Episode Created At: " text=declaredAt }}
|
||||
{{> DetailBoxField title="Severity: " text=episodeSeverity }}
|
||||
{{> DetailBoxField title="Description: " text=episodeDescription }}
|
||||
{{> DetailBoxEnd this }}
|
||||
|
||||
|
||||
{{> InfoBlock info="You can view this incident episode by clicking on the button below - "}}
|
||||
|
||||
{{> ButtonBlock buttonUrl=episodeViewLink buttonText="View on Dashboard"}}
|
||||
|
||||
{{> InfoBlock info="You can also copy and paste this link:"}}
|
||||
{{> InfoBlock info=episodeViewLink}}
|
||||
|
||||
{{> InfoBlock info="You will be notified when the status of this incident episode changes."}}
|
||||
|
||||
{{> OwnerInfo this }}
|
||||
{{> UnsubscribeOwnerEmail this }}
|
||||
|
||||
{{> Footer this }}
|
||||
|
||||
{{> End this}}
|
||||
@@ -0,0 +1,37 @@
|
||||
{{> Start this}}
|
||||
|
||||
|
||||
{{> Logo this}}
|
||||
{{> EmailTitle title=(concat "Incident Episode " episodeNumber ": " episodeTitle) }}
|
||||
|
||||
{{> InfoBlock info="Incident episode state has changed"}}
|
||||
|
||||
{{> InfoBlock info="Here are the details: "}}
|
||||
|
||||
{{> DetailBoxStart this }}
|
||||
{{> StateTransition this}}
|
||||
{{#ifNotCond previousStateDurationText ""}}
|
||||
{{> DetailBoxField title="Duration in Previous State:" text=previousStateDurationText }}
|
||||
{{/ifNotCond}}
|
||||
{{> DetailBoxField title="Episode Title:" text=episodeTitle }}
|
||||
{{> DetailBoxField title="State changed at:" text=stateChangedAt }}
|
||||
{{> DetailBoxField title="Severity:" text=episodeSeverity }}
|
||||
{{> DetailBoxField title="Description:" text=episodeDescription }}
|
||||
{{> DetailBoxEnd this }}
|
||||
|
||||
|
||||
{{> InfoBlock info="You can view this incident episode by clicking on the button below - "}}
|
||||
|
||||
{{> ButtonBlock buttonUrl=episodeViewLink buttonText="View on Dashboard"}}
|
||||
|
||||
{{> InfoBlock info="You can also copy and paste this link:"}}
|
||||
{{> InfoBlock info=episodeViewLink}}
|
||||
|
||||
{{> InfoBlock info="You will be notified when the status of this incident episode changes."}}
|
||||
|
||||
{{> OwnerInfo this }}
|
||||
{{> UnsubscribeOwnerEmail this }}
|
||||
|
||||
{{> Footer this }}
|
||||
|
||||
{{> End this}}
|
||||
@@ -0,0 +1,32 @@
|
||||
{{> Start this}}
|
||||
|
||||
|
||||
{{> Logo this}}
|
||||
{{> EmailTitle title=(concat "Incident " incidentNumber ": " incidentTitle) }}
|
||||
|
||||
{{> InfoBlock info=(concat "You have been assigned as " incidentRole " to this incident.")}}
|
||||
|
||||
{{> InfoBlock info="Here are the details: "}}
|
||||
|
||||
{{> DetailBoxStart this }}
|
||||
{{> DetailBoxField title="Incident Title:" text=incidentTitle }}
|
||||
{{> DetailBoxField title="Your Role: " text=incidentRole }}
|
||||
{{> DetailBoxField title="Current State: " text=currentState }}
|
||||
{{> DetailBoxField title="Resources Affected: " text=resourcesAffected }}
|
||||
{{> DetailBoxField title="Severity: " text=incidentSeverity }}
|
||||
{{> DetailBoxField title="Description: " text=incidentDescription }}
|
||||
{{> DetailBoxEnd this }}
|
||||
|
||||
|
||||
{{> InfoBlock info="You can view this incident by clicking on the button below - "}}
|
||||
|
||||
{{> ButtonBlock buttonUrl=incidentViewLink buttonText="View on Dashboard"}}
|
||||
|
||||
{{> InfoBlock info="You can also copy and paste this link:"}}
|
||||
{{> InfoBlock info=incidentViewLink}}
|
||||
|
||||
{{> InfoBlock info="You will be notified when the status of this incident changes."}}
|
||||
|
||||
{{> Footer this }}
|
||||
|
||||
{{> End this}}
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
|
||||
{{> Logo this}}
|
||||
{{> EmailTitle title=(concat "Incident: " incidentTitle) }}
|
||||
{{> EmailTitle title=(concat "Incident " incidentNumber ": " incidentTitle) }}
|
||||
|
||||
{{> InfoBlock info="You have been added as the owner of this incident."}}
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
|
||||
{{> Logo this}}
|
||||
{{> EmailTitle title=(concat "Incident: " incidentTitle) }}
|
||||
{{> EmailTitle title=(concat "Incident " incidentNumber ": " incidentTitle) }}
|
||||
|
||||
{{> InfoBlock info="A new note has been posted on this incident."}}
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
|
||||
{{> Logo this}}
|
||||
{{> EmailTitle title=(concat "Incident: " incidentTitle) }}
|
||||
{{> EmailTitle title=(concat "Incident " incidentNumber ": " incidentTitle) }}
|
||||
|
||||
{{> InfoBlock info=(concat "A new incident has been created in the project - " projectName)}}
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
|
||||
{{> Logo this}}
|
||||
{{> EmailTitle title=(concat "Incident: " incidentTitle) }}
|
||||
{{> EmailTitle title=(concat "Incident " incidentNumber ": " incidentTitle) }}
|
||||
|
||||
{{> InfoBlock info="Incident state has changed"}}
|
||||
|
||||
|
||||
37
App/FeatureSet/Notification/Templates/Invoice.hbs
Normal file
37
App/FeatureSet/Notification/Templates/Invoice.hbs
Normal file
@@ -0,0 +1,37 @@
|
||||
{{> Start this}}
|
||||
|
||||
{{> Logo this}}
|
||||
|
||||
{{> EmailTitle title="Invoice from OneUptime" }}
|
||||
|
||||
{{> InfoBlock info="A new invoice has been generated for your account. Here are the details:"}}
|
||||
|
||||
{{> DetailBoxStart this }}
|
||||
{{> DetailBoxField title="Invoice Number:" text=invoiceNumber }}
|
||||
{{> DetailBoxField title="Invoice Date:" text=invoiceDate }}
|
||||
{{> DetailBoxField title="Amount:" text=amount }}
|
||||
{{#if description}}
|
||||
{{> DetailBoxField title="Description:" text=description }}
|
||||
{{/if}}
|
||||
{{> DetailBoxEnd this }}
|
||||
|
||||
{{#if invoicePdfUrl}}
|
||||
{{> InfoBlock info="You can view and download your invoice by clicking the button below:"}}
|
||||
|
||||
{{> ButtonBlock buttonUrl=invoicePdfUrl buttonText="View Invoice PDF"}}
|
||||
|
||||
{{> InfoBlock info="Or copy and paste this link:"}}
|
||||
{{> InfoBlock info=invoicePdfUrl}}
|
||||
{{/if}}
|
||||
|
||||
{{#if dashboardLink}}
|
||||
{{> InfoBlock info="You can also view all your invoices in your dashboard:"}}
|
||||
|
||||
{{> ButtonBlock buttonUrl=dashboardLink buttonText="View Billing Dashboard"}}
|
||||
{{/if}}
|
||||
|
||||
{{> InfoBlock info="You have received this email because you are subscribed to receive invoice notifications for this project."}}
|
||||
|
||||
{{> Footer this }}
|
||||
|
||||
{{> End this}}
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
|
||||
{{> Logo this}}
|
||||
{{> EmailTitle title=(concat "Scheduled Maintenance: " scheduledMaintenanceTitle) }}
|
||||
{{> EmailTitle title=(concat "Scheduled Maintenance " scheduledMaintenanceNumber ": " scheduledMaintenanceTitle) }}
|
||||
|
||||
{{> InfoBlock info="You have been added as the owner of this scheduled maintenance event."}}
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
|
||||
{{> Logo this}}
|
||||
{{> EmailTitle title=(concat "Scheduled Maintenance: " scheduledMaintenanceTitle) }}
|
||||
{{> EmailTitle title=(concat "Scheduled Maintenance " scheduledMaintenanceNumber ": " scheduledMaintenanceTitle) }}
|
||||
|
||||
{{> InfoBlock info="A new note has been posted on this scheduled maintenance event"}}
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
|
||||
{{> Logo this}}
|
||||
{{> EmailTitle title=(concat "Scheduled Maintenance: " scheduledMaintenanceTitle) }}
|
||||
{{> EmailTitle title=(concat "Scheduled Maintenance " scheduledMaintenanceNumber ": " scheduledMaintenanceTitle) }}
|
||||
|
||||
{{> InfoBlock info=(concat "A new scheduled maintenance has been created in the project - " projectName)}}
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
|
||||
{{> Logo this}}
|
||||
{{> EmailTitle title=(concat "Scheduled Maintenance: " scheduledMaintenanceTitle) }}
|
||||
{{> EmailTitle title=(concat "Scheduled Maintenance " scheduledMaintenanceNumber ": " scheduledMaintenanceTitle) }}
|
||||
|
||||
{{> InfoBlock info="Scheduled Maintenance state has changed"}}
|
||||
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
{{> Start this}}
|
||||
|
||||
{{> CustomLogo this}}
|
||||
|
||||
{{> EmailTitle title=(concat "New Incident: " episodeTitle) }}
|
||||
|
||||
{{> InfoBlock info="A new incident has been reported that may affect the services you're subscribed to."}}
|
||||
|
||||
{{> DetailBoxStart this }}
|
||||
{{> DetailBoxField title="Incident" text=episodeTitle }}
|
||||
{{#if episodeSeverity}}
|
||||
{{> DetailBoxField title="Severity" text=episodeSeverity }}
|
||||
{{/if}}
|
||||
{{> DetailBoxField title="Affected Resources" text=resourcesAffected }}
|
||||
{{#if episodeDescription}}
|
||||
{{> DetailBoxField title="Description" text=episodeDescription }}
|
||||
{{/if}}
|
||||
{{> DetailBoxEnd this }}
|
||||
|
||||
{{#if detailsUrl}}
|
||||
{{> ButtonBlock buttonUrl=detailsUrl buttonText="View Incident Details"}}
|
||||
{{else}}
|
||||
{{> ButtonBlock buttonUrl=statusPageUrl buttonText="View Status Page"}}
|
||||
{{/if}}
|
||||
|
||||
{{> VerticalSpace this}}
|
||||
|
||||
{{#if subscriberEmailNotificationFooterText}}
|
||||
{{> InfoBlock info=subscriberEmailNotificationFooterText }}
|
||||
{{/if}}
|
||||
|
||||
{{> UnsubscribeBlock this}}
|
||||
|
||||
{{> Footer this}}
|
||||
|
||||
{{> End this}}
|
||||
@@ -0,0 +1,30 @@
|
||||
{{> Start this}}
|
||||
|
||||
{{> CustomLogo this}}
|
||||
{{> EmailTitle title=(concat "Incident: " episodeTitle) }}
|
||||
|
||||
{{> InfoBlock info="A new note has been added to the incident. Here are the details:"}}
|
||||
|
||||
{{> DetailBoxStart this }}
|
||||
{{> DetailBoxField title="Incident Title" text=episodeTitle }}
|
||||
{{> DetailBoxField title="Resources Affected" text=resourcesAffected }}
|
||||
{{#if episodeSeverity}}
|
||||
{{> DetailBoxField title="Severity" text=episodeSeverity }}
|
||||
{{/if}}
|
||||
{{> DetailBoxField title="Note" text=note }}
|
||||
{{> DetailBoxEnd this }}
|
||||
|
||||
|
||||
{{> InfoBlock info=(concat subscriberEmailNotificationFooterText "") }}
|
||||
|
||||
|
||||
{{#if detailsUrl}}
|
||||
{{> InfoBlock info=(concat "Find further information here: " detailsUrl)}}
|
||||
{{else}}
|
||||
{{> InfoBlock info=(concat "Find further information here: " statusPageUrl)}}
|
||||
{{/if}}
|
||||
|
||||
{{> UnsubscribeBlock this}}
|
||||
{{> VerticalSpace this}}
|
||||
|
||||
{{> End this}}
|
||||
@@ -0,0 +1,34 @@
|
||||
{{> Start this}}
|
||||
|
||||
{{> CustomLogo this}}
|
||||
|
||||
{{> EmailTitle title=emailTitle }}
|
||||
|
||||
{{> InfoBlock info="The status of an incident affecting services you're subscribed to has been updated."}}
|
||||
|
||||
{{> DetailBoxStart this }}
|
||||
{{> DetailBoxField title="Incident" text=episodeTitle }}
|
||||
{{> DetailBoxField title="Current State" text=episodeState }}
|
||||
{{#if episodeSeverity}}
|
||||
{{> DetailBoxField title="Severity" text=episodeSeverity }}
|
||||
{{/if}}
|
||||
{{> DetailBoxField title="Affected Resources" text=resourcesAffected }}
|
||||
{{> DetailBoxEnd this }}
|
||||
|
||||
{{#if detailsUrl}}
|
||||
{{> ButtonBlock buttonUrl=detailsUrl buttonText="View Incident Details"}}
|
||||
{{else}}
|
||||
{{> ButtonBlock buttonUrl=statusPageUrl buttonText="View Status Page"}}
|
||||
{{/if}}
|
||||
|
||||
{{> VerticalSpace this}}
|
||||
|
||||
{{#if subscriberEmailNotificationFooterText}}
|
||||
{{> InfoBlock info=subscriberEmailNotificationFooterText }}
|
||||
{{/if}}
|
||||
|
||||
{{> UnsubscribeBlock this}}
|
||||
|
||||
{{> Footer this}}
|
||||
|
||||
{{> End this}}
|
||||
34
App/FeatureSet/Notification/Utils/TwilioConfigHelper.ts
Normal file
34
App/FeatureSet/Notification/Utils/TwilioConfigHelper.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
import TwilioConfig from "Common/Types/CallAndSMS/TwilioConfig";
|
||||
import ObjectID from "Common/Types/ObjectID";
|
||||
import ProjectCallSMSConfigService from "Common/Server/Services/ProjectCallSMSConfigService";
|
||||
import ProjectCallSMSConfig from "Common/Models/DatabaseModels/ProjectCallSMSConfig";
|
||||
|
||||
/**
|
||||
* Helper function to get TwilioConfig from project config
|
||||
* Shared between IncomingCall and PhoneNumber APIs
|
||||
*/
|
||||
export async function getProjectTwilioConfig(
|
||||
projectCallSMSConfigId: ObjectID,
|
||||
): Promise<TwilioConfig | null> {
|
||||
const projectConfig: ProjectCallSMSConfig | null =
|
||||
await ProjectCallSMSConfigService.findOneById({
|
||||
id: projectCallSMSConfigId,
|
||||
select: {
|
||||
twilioAccountSID: true,
|
||||
twilioAuthToken: true,
|
||||
twilioPrimaryPhoneNumber: true,
|
||||
twilioSecondaryPhoneNumbers: true,
|
||||
},
|
||||
props: {
|
||||
isRoot: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!projectConfig) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const twilioConfig: TwilioConfig | undefined =
|
||||
ProjectCallSMSConfigService.toTwilioConfig(projectConfig);
|
||||
return twilioConfig || null;
|
||||
}
|
||||
69
App/package-lock.json
generated
69
App/package-lock.json
generated
@@ -83,15 +83,18 @@
|
||||
"ejs": "^3.1.10",
|
||||
"elkjs": "^0.10.0",
|
||||
"esbuild": "^0.25.5",
|
||||
"expo-server-sdk": "^3.15.0",
|
||||
"express": "^4.21.1",
|
||||
"formik": "^2.4.6",
|
||||
"history": "^5.3.0",
|
||||
"ioredis": "^5.3.2",
|
||||
"isolated-vm": "^6.0.2",
|
||||
"json2csv": "^5.0.7",
|
||||
"json5": "^2.2.3",
|
||||
"jsonwebtoken": "^9.0.0",
|
||||
"jwt-decode": "^4.0.0",
|
||||
"marked": "^12.0.2",
|
||||
"mermaid": "^11.12.2",
|
||||
"moment": "^2.30.1",
|
||||
"moment-timezone": "^0.5.45",
|
||||
"multer": "^2.0.2",
|
||||
@@ -111,7 +114,7 @@
|
||||
"react-dropzone": "^14.2.2",
|
||||
"react-error-boundary": "^4.0.13",
|
||||
"react-highlight": "^0.15.0",
|
||||
"react-markdown": "^8.0.3",
|
||||
"react-markdown": "^9.0.0",
|
||||
"react-router-dom": "^6.30.1",
|
||||
"react-select": "^5.4.0",
|
||||
"react-spinners": "^0.14.1",
|
||||
@@ -121,7 +124,7 @@
|
||||
"recharts": "^2.12.7",
|
||||
"redis-semaphore": "^5.5.1",
|
||||
"reflect-metadata": "^0.2.2",
|
||||
"remark-gfm": "^3.0.1",
|
||||
"remark-gfm": "^4.0.0",
|
||||
"slackify-markdown": "^4.4.0",
|
||||
"slugify": "^1.6.5",
|
||||
"socket.io": "^4.7.4",
|
||||
@@ -1559,6 +1562,7 @@
|
||||
"version": "4.3.0",
|
||||
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
|
||||
"integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"color-convert": "^2.0.1"
|
||||
},
|
||||
@@ -1597,9 +1601,10 @@
|
||||
}
|
||||
},
|
||||
"node_modules/async": {
|
||||
"version": "3.2.5",
|
||||
"resolved": "https://registry.npmjs.org/async/-/async-3.2.5.tgz",
|
||||
"integrity": "sha512-baNZyqaaLhyLVKm/DlvdW051MSgO6b8eVfIezl9E5PqWxFgzLm/wQntEW4zOytVburDEr0JlALEpdOFwvErLsg=="
|
||||
"version": "3.2.6",
|
||||
"resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz",
|
||||
"integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/asynckit": {
|
||||
"version": "0.4.0",
|
||||
@@ -1608,13 +1613,13 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/axios": {
|
||||
"version": "1.13.1",
|
||||
"resolved": "https://registry.npmjs.org/axios/-/axios-1.13.1.tgz",
|
||||
"integrity": "sha512-hU4EGxxt+j7TQijx1oYdAjw4xuIp1wRQSsbMFwSthCWeBQur1eF+qJ5iQ5sN3Tw8YRzQNKb8jszgBdMDVqwJcw==",
|
||||
"version": "1.13.5",
|
||||
"resolved": "https://registry.npmjs.org/axios/-/axios-1.13.5.tgz",
|
||||
"integrity": "sha512-cz4ur7Vb0xS4/KUN0tPWe44eqxrIu31me+fbang3ijiNscE129POzipJJA6zniq2C/Z6sJCjMimjS8Lc/GAs8Q==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"follow-redirects": "^1.15.6",
|
||||
"form-data": "^4.0.4",
|
||||
"follow-redirects": "^1.15.11",
|
||||
"form-data": "^4.0.5",
|
||||
"proxy-from-env": "^1.1.0"
|
||||
}
|
||||
},
|
||||
@@ -1727,6 +1732,7 @@
|
||||
"version": "1.1.12",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
|
||||
"integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"balanced-match": "^1.0.0",
|
||||
@@ -1870,6 +1876,7 @@
|
||||
"version": "4.1.2",
|
||||
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
|
||||
"integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"ansi-styles": "^4.1.0",
|
||||
"supports-color": "^7.1.0"
|
||||
@@ -1972,6 +1979,7 @@
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
|
||||
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"color-name": "~1.1.4"
|
||||
},
|
||||
@@ -1982,7 +1990,8 @@
|
||||
"node_modules/color-name": {
|
||||
"version": "1.1.4",
|
||||
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
|
||||
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="
|
||||
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/combined-stream": {
|
||||
"version": "1.0.8",
|
||||
@@ -2003,7 +2012,8 @@
|
||||
"node_modules/concat-map": {
|
||||
"version": "0.0.1",
|
||||
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
|
||||
"integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="
|
||||
"integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/convert-source-map": {
|
||||
"version": "1.9.0",
|
||||
@@ -2085,9 +2095,10 @@
|
||||
}
|
||||
},
|
||||
"node_modules/diff": {
|
||||
"version": "4.0.2",
|
||||
"resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz",
|
||||
"integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==",
|
||||
"version": "4.0.4",
|
||||
"resolved": "https://registry.npmjs.org/diff/-/diff-4.0.4.tgz",
|
||||
"integrity": "sha512-X07nttJQkwkfKfvTPG/KSnE2OMdcUCao6+eXF3wmnIQRn2aPAHH3VxDbDOdegkd6JbPsXqShpvEOHfAT+nCNwQ==",
|
||||
"license": "BSD-3-Clause",
|
||||
"engines": {
|
||||
"node": ">=0.3.1"
|
||||
}
|
||||
@@ -2386,9 +2397,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/form-data": {
|
||||
"version": "4.0.4",
|
||||
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz",
|
||||
"integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==",
|
||||
"version": "4.0.5",
|
||||
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz",
|
||||
"integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"asynckit": "^0.4.0",
|
||||
@@ -2588,6 +2599,7 @@
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
|
||||
"integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
@@ -2872,14 +2884,14 @@
|
||||
}
|
||||
},
|
||||
"node_modules/jake": {
|
||||
"version": "10.8.7",
|
||||
"resolved": "https://registry.npmjs.org/jake/-/jake-10.8.7.tgz",
|
||||
"integrity": "sha512-ZDi3aP+fG/LchyBzUM804VjddnwfSfsdeYkwt8NcbKRvo4rFkjhs456iLFn3k2ZUWvNe4i48WACDbza8fhq2+w==",
|
||||
"version": "10.9.4",
|
||||
"resolved": "https://registry.npmjs.org/jake/-/jake-10.9.4.tgz",
|
||||
"integrity": "sha512-wpHYzhxiVQL+IV05BLE2Xn34zW1S223hvjtqk0+gsPrwd/8JNLXJgZZM/iPFsYc1xyphF+6M6EvdE5E9MBGkDA==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"async": "^3.2.3",
|
||||
"chalk": "^4.0.2",
|
||||
"async": "^3.2.6",
|
||||
"filelist": "^1.0.4",
|
||||
"minimatch": "^3.1.2"
|
||||
"picocolors": "^1.1.1"
|
||||
},
|
||||
"bin": {
|
||||
"jake": "bin/cli.js"
|
||||
@@ -3804,6 +3816,7 @@
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
|
||||
"integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"brace-expansion": "^1.1.7"
|
||||
},
|
||||
@@ -4101,7 +4114,6 @@
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
|
||||
"integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
|
||||
"dev": true,
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/picomatch": {
|
||||
@@ -4189,9 +4201,9 @@
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/qs": {
|
||||
"version": "6.14.1",
|
||||
"resolved": "https://registry.npmjs.org/qs/-/qs-6.14.1.tgz",
|
||||
"integrity": "sha512-4EK3+xJl8Ts67nLYNwqw/dsFVnCf+qR7RgXSK9jEEm9unao3njwMDdmsdvoKBKHzxd7tCYz5e5M+SnMjdtXGQQ==",
|
||||
"version": "6.14.2",
|
||||
"resolved": "https://registry.npmjs.org/qs/-/qs-6.14.2.tgz",
|
||||
"integrity": "sha512-V/yCWTTF7VJ9hIh18Ugr2zhJMP01MY7c5kh4J870L7imm6/DIzBsNLTXzMwUA3yZ5b/KBqLx8Kp3uRvd7xSe3Q==",
|
||||
"license": "BSD-3-Clause",
|
||||
"dependencies": {
|
||||
"side-channel": "^1.1.0"
|
||||
@@ -4585,6 +4597,7 @@
|
||||
"version": "7.2.0",
|
||||
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
|
||||
"integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"has-flag": "^4.0.0"
|
||||
},
|
||||
|
||||
140
CLI/Commands/ConfigCommands.ts
Normal file
140
CLI/Commands/ConfigCommands.ts
Normal file
@@ -0,0 +1,140 @@
|
||||
import { Command } from "commander";
|
||||
import * as ConfigManager from "../Core/ConfigManager";
|
||||
import { CLIContext } from "../Types/CLITypes";
|
||||
import { printSuccess, printError, printInfo } from "../Core/OutputFormatter";
|
||||
import Table from "cli-table3";
|
||||
import chalk from "chalk";
|
||||
|
||||
export function registerConfigCommands(program: Command): void {
|
||||
// Login command
|
||||
const loginCmd: Command = program
|
||||
.command("login")
|
||||
.description("Authenticate with a OneUptime instance")
|
||||
.argument("<api-key>", "API key for authentication")
|
||||
.argument(
|
||||
"<instance-url>",
|
||||
"OneUptime instance URL (e.g. https://oneuptime.com)",
|
||||
)
|
||||
.option("--context-name <name>", "Name for this context", "default")
|
||||
.action(
|
||||
(
|
||||
apiKey: string,
|
||||
instanceUrl: string,
|
||||
options: { contextName: string },
|
||||
) => {
|
||||
try {
|
||||
const context: CLIContext = {
|
||||
name: options.contextName,
|
||||
apiUrl: instanceUrl.replace(/\/+$/, ""),
|
||||
apiKey: apiKey,
|
||||
};
|
||||
|
||||
ConfigManager.addContext(context);
|
||||
ConfigManager.setCurrentContext(context.name);
|
||||
|
||||
printSuccess(
|
||||
`Logged in successfully. Context "${context.name}" is now active.`,
|
||||
);
|
||||
} catch (error) {
|
||||
printError(
|
||||
`Login failed: ${error instanceof Error ? error.message : String(error)}`,
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
// Suppress unused variable warning - loginCmd is used for registration
|
||||
void loginCmd;
|
||||
|
||||
// Context commands
|
||||
const contextCmd: Command = program
|
||||
.command("context")
|
||||
.description("Manage CLI contexts (environments/projects)");
|
||||
|
||||
contextCmd
|
||||
.command("list")
|
||||
.description("List all configured contexts")
|
||||
.action(() => {
|
||||
const contexts: Array<CLIContext & { isCurrent: boolean }> =
|
||||
ConfigManager.listContexts();
|
||||
|
||||
if (contexts.length === 0) {
|
||||
printInfo(
|
||||
"No contexts configured. Run `oneuptime login` to create one.",
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const noColor: boolean =
|
||||
process.env["NO_COLOR"] !== undefined ||
|
||||
process.argv.includes("--no-color");
|
||||
|
||||
const table: Table.Table = new Table({
|
||||
head: ["", "Name", "URL"].map((h: string) => {
|
||||
return noColor ? h : chalk.cyan(h);
|
||||
}),
|
||||
style: { head: [], border: [] },
|
||||
});
|
||||
|
||||
for (const ctx of contexts) {
|
||||
table.push([ctx.isCurrent ? "*" : "", ctx.name, ctx.apiUrl]);
|
||||
}
|
||||
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(table.toString());
|
||||
});
|
||||
|
||||
contextCmd
|
||||
.command("use <name>")
|
||||
.description("Switch to a different context")
|
||||
.action((name: string) => {
|
||||
try {
|
||||
ConfigManager.setCurrentContext(name);
|
||||
printSuccess(`Switched to context "${name}".`);
|
||||
} catch (error) {
|
||||
printError(error instanceof Error ? error.message : String(error));
|
||||
process.exit(1);
|
||||
}
|
||||
});
|
||||
|
||||
contextCmd
|
||||
.command("current")
|
||||
.description("Show the current active context")
|
||||
.action(() => {
|
||||
const ctx: CLIContext | null = ConfigManager.getCurrentContext();
|
||||
if (!ctx) {
|
||||
printInfo(
|
||||
"No current context set. Run `oneuptime login` to create one.",
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const maskedKey: string =
|
||||
ctx.apiKey.length > 8
|
||||
? ctx.apiKey.substring(0, 4) +
|
||||
"****" +
|
||||
ctx.apiKey.substring(ctx.apiKey.length - 4)
|
||||
: "****";
|
||||
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(`Context: ${ctx.name}`);
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(`URL: ${ctx.apiUrl}`);
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(`API Key: ${maskedKey}`);
|
||||
});
|
||||
|
||||
contextCmd
|
||||
.command("delete <name>")
|
||||
.description("Delete a context")
|
||||
.action((name: string) => {
|
||||
try {
|
||||
ConfigManager.removeContext(name);
|
||||
printSuccess(`Context "${name}" deleted.`);
|
||||
} catch (error) {
|
||||
printError(error instanceof Error ? error.message : String(error));
|
||||
process.exit(1);
|
||||
}
|
||||
});
|
||||
}
|
||||
356
CLI/Commands/ResourceCommands.ts
Normal file
356
CLI/Commands/ResourceCommands.ts
Normal file
@@ -0,0 +1,356 @@
|
||||
import { Command } from "commander";
|
||||
import DatabaseModels from "Common/Models/DatabaseModels/Index";
|
||||
import AnalyticsModels from "Common/Models/AnalyticsModels/Index";
|
||||
import BaseModel from "Common/Models/DatabaseModels/DatabaseBaseModel/DatabaseBaseModel";
|
||||
import AnalyticsBaseModel from "Common/Models/AnalyticsModels/AnalyticsBaseModel/AnalyticsBaseModel";
|
||||
import { ResourceInfo, ResolvedCredentials } from "../Types/CLITypes";
|
||||
import { executeApiRequest, ApiOperation } from "../Core/ApiClient";
|
||||
import { CLIOptions, getResolvedCredentials } from "../Core/ConfigManager";
|
||||
import { formatOutput, printSuccess } from "../Core/OutputFormatter";
|
||||
import { handleError } from "../Core/ErrorHandler";
|
||||
import { generateAllFieldsSelect } from "../Utils/SelectFieldGenerator";
|
||||
import { JSONObject, JSONValue } from "Common/Types/JSON";
|
||||
import * as fs from "fs";
|
||||
|
||||
function toKebabCase(str: string): string {
|
||||
return str
|
||||
.replace(/([a-z])([A-Z])/g, "$1-$2")
|
||||
.replace(/[\s_]+/g, "-")
|
||||
.toLowerCase();
|
||||
}
|
||||
|
||||
function parseJsonArg(value: string): JSONObject {
|
||||
try {
|
||||
return JSON.parse(value) as JSONObject;
|
||||
} catch {
|
||||
throw new Error(`Invalid JSON: ${value}`);
|
||||
}
|
||||
}
|
||||
|
||||
export function discoverResources(): ResourceInfo[] {
|
||||
const resources: ResourceInfo[] = [];
|
||||
|
||||
// Database models
|
||||
for (const ModelClass of DatabaseModels) {
|
||||
try {
|
||||
const model: BaseModel = new ModelClass();
|
||||
const tableName: string = model.tableName || ModelClass.name;
|
||||
const singularName: string = model.singularName || tableName;
|
||||
const pluralName: string = model.pluralName || `${singularName}s`;
|
||||
const apiPath: string | undefined = model.crudApiPath?.toString();
|
||||
|
||||
if (tableName && model.enableMCP && apiPath) {
|
||||
resources.push({
|
||||
name: toKebabCase(singularName),
|
||||
singularName,
|
||||
pluralName,
|
||||
apiPath,
|
||||
tableName,
|
||||
modelType: "database",
|
||||
});
|
||||
}
|
||||
} catch {
|
||||
// Skip models that fail to instantiate
|
||||
}
|
||||
}
|
||||
|
||||
// Analytics models
|
||||
for (const ModelClass of AnalyticsModels) {
|
||||
try {
|
||||
const model: AnalyticsBaseModel = new ModelClass();
|
||||
const tableName: string = model.tableName || ModelClass.name;
|
||||
const singularName: string = model.singularName || tableName;
|
||||
const pluralName: string = model.pluralName || `${singularName}s`;
|
||||
const apiPath: string | undefined = model.crudApiPath?.toString();
|
||||
|
||||
if (tableName && model.enableMCP && apiPath) {
|
||||
resources.push({
|
||||
name: toKebabCase(singularName),
|
||||
singularName,
|
||||
pluralName,
|
||||
apiPath,
|
||||
tableName,
|
||||
modelType: "analytics",
|
||||
});
|
||||
}
|
||||
} catch {
|
||||
// Skip models that fail to instantiate
|
||||
}
|
||||
}
|
||||
|
||||
return resources;
|
||||
}
|
||||
|
||||
function getParentOptions(cmd: Command): CLIOptions {
|
||||
// Walk up to root program to get global options
|
||||
let current: Command | null = cmd;
|
||||
while (current?.parent) {
|
||||
current = current.parent;
|
||||
}
|
||||
const opts: Record<string, unknown> = current?.opts() || {};
|
||||
return {
|
||||
apiKey: opts["apiKey"] as string | undefined,
|
||||
url: opts["url"] as string | undefined,
|
||||
context: opts["context"] as string | undefined,
|
||||
};
|
||||
}
|
||||
|
||||
function registerListCommand(
|
||||
resourceCmd: Command,
|
||||
resource: ResourceInfo,
|
||||
): void {
|
||||
resourceCmd
|
||||
.command("list")
|
||||
.description(`List ${resource.pluralName}`)
|
||||
.option("--query <json>", "Filter query as JSON")
|
||||
.option("--limit <n>", "Max results to return", "10")
|
||||
.option("--skip <n>", "Number of results to skip", "0")
|
||||
.option("--sort <json>", "Sort order as JSON")
|
||||
.option("-o, --output <format>", "Output format: json, table, wide")
|
||||
.action(
|
||||
async (options: {
|
||||
query?: string;
|
||||
limit: string;
|
||||
skip: string;
|
||||
sort?: string;
|
||||
output?: string;
|
||||
}) => {
|
||||
try {
|
||||
const parentOpts: CLIOptions = getParentOptions(resourceCmd);
|
||||
const creds: ResolvedCredentials = getResolvedCredentials(parentOpts);
|
||||
const select: JSONObject = generateAllFieldsSelect(
|
||||
resource.tableName,
|
||||
resource.modelType,
|
||||
);
|
||||
|
||||
const result: JSONValue = await executeApiRequest({
|
||||
apiUrl: creds.apiUrl,
|
||||
apiKey: creds.apiKey,
|
||||
apiPath: resource.apiPath,
|
||||
operation: "list" as ApiOperation,
|
||||
query: options.query ? parseJsonArg(options.query) : undefined,
|
||||
select,
|
||||
skip: parseInt(options.skip, 10),
|
||||
limit: parseInt(options.limit, 10),
|
||||
sort: options.sort ? parseJsonArg(options.sort) : undefined,
|
||||
});
|
||||
|
||||
// Extract data array from response
|
||||
const responseData: JSONValue =
|
||||
result && typeof result === "object" && !Array.isArray(result)
|
||||
? ((result as JSONObject)["data"] as JSONValue) || result
|
||||
: result;
|
||||
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(formatOutput(responseData, options.output));
|
||||
} catch (error) {
|
||||
handleError(error);
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
function registerGetCommand(
|
||||
resourceCmd: Command,
|
||||
resource: ResourceInfo,
|
||||
): void {
|
||||
resourceCmd
|
||||
.command("get <id>")
|
||||
.description(`Get a single ${resource.singularName} by ID`)
|
||||
.option("-o, --output <format>", "Output format: json, table, wide")
|
||||
.action(async (id: string, options: { output?: string }) => {
|
||||
try {
|
||||
const parentOpts: CLIOptions = getParentOptions(resourceCmd);
|
||||
const creds: ResolvedCredentials = getResolvedCredentials(parentOpts);
|
||||
const select: JSONObject = generateAllFieldsSelect(
|
||||
resource.tableName,
|
||||
resource.modelType,
|
||||
);
|
||||
|
||||
const result: JSONValue = await executeApiRequest({
|
||||
apiUrl: creds.apiUrl,
|
||||
apiKey: creds.apiKey,
|
||||
apiPath: resource.apiPath,
|
||||
operation: "read" as ApiOperation,
|
||||
id,
|
||||
select,
|
||||
});
|
||||
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(formatOutput(result, options.output));
|
||||
} catch (error) {
|
||||
handleError(error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function registerCreateCommand(
|
||||
resourceCmd: Command,
|
||||
resource: ResourceInfo,
|
||||
): void {
|
||||
resourceCmd
|
||||
.command("create")
|
||||
.description(`Create a new ${resource.singularName}`)
|
||||
.option("--data <json>", "Resource data as JSON")
|
||||
.option("--file <path>", "Read resource data from a JSON file")
|
||||
.option("-o, --output <format>", "Output format: json, table, wide")
|
||||
.action(
|
||||
async (options: { data?: string; file?: string; output?: string }) => {
|
||||
try {
|
||||
let data: JSONObject;
|
||||
|
||||
if (options.file) {
|
||||
const fileContent: string = fs.readFileSync(options.file, "utf-8");
|
||||
data = JSON.parse(fileContent) as JSONObject;
|
||||
} else if (options.data) {
|
||||
data = parseJsonArg(options.data);
|
||||
} else {
|
||||
throw new Error("Either --data or --file is required for create.");
|
||||
}
|
||||
|
||||
const parentOpts: CLIOptions = getParentOptions(resourceCmd);
|
||||
const creds: ResolvedCredentials = getResolvedCredentials(parentOpts);
|
||||
|
||||
const result: JSONValue = await executeApiRequest({
|
||||
apiUrl: creds.apiUrl,
|
||||
apiKey: creds.apiKey,
|
||||
apiPath: resource.apiPath,
|
||||
operation: "create" as ApiOperation,
|
||||
data,
|
||||
});
|
||||
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(formatOutput(result, options.output));
|
||||
} catch (error) {
|
||||
handleError(error);
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
function registerUpdateCommand(
|
||||
resourceCmd: Command,
|
||||
resource: ResourceInfo,
|
||||
): void {
|
||||
resourceCmd
|
||||
.command("update <id>")
|
||||
.description(`Update an existing ${resource.singularName}`)
|
||||
.requiredOption("--data <json>", "Fields to update as JSON")
|
||||
.option("-o, --output <format>", "Output format: json, table, wide")
|
||||
.action(async (id: string, options: { data: string; output?: string }) => {
|
||||
try {
|
||||
const data: JSONObject = parseJsonArg(options.data);
|
||||
const parentOpts: CLIOptions = getParentOptions(resourceCmd);
|
||||
const creds: ResolvedCredentials = getResolvedCredentials(parentOpts);
|
||||
|
||||
const result: JSONValue = await executeApiRequest({
|
||||
apiUrl: creds.apiUrl,
|
||||
apiKey: creds.apiKey,
|
||||
apiPath: resource.apiPath,
|
||||
operation: "update" as ApiOperation,
|
||||
id,
|
||||
data,
|
||||
});
|
||||
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(formatOutput(result, options.output));
|
||||
} catch (error) {
|
||||
handleError(error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function registerDeleteCommand(
|
||||
resourceCmd: Command,
|
||||
resource: ResourceInfo,
|
||||
): void {
|
||||
resourceCmd
|
||||
.command("delete <id>")
|
||||
.description(`Delete a ${resource.singularName}`)
|
||||
.option("--force", "Skip confirmation")
|
||||
.action(async (id: string, _options: { force?: boolean }) => {
|
||||
try {
|
||||
const parentOpts: CLIOptions = getParentOptions(resourceCmd);
|
||||
const creds: ResolvedCredentials = getResolvedCredentials(parentOpts);
|
||||
|
||||
await executeApiRequest({
|
||||
apiUrl: creds.apiUrl,
|
||||
apiKey: creds.apiKey,
|
||||
apiPath: resource.apiPath,
|
||||
operation: "delete" as ApiOperation,
|
||||
id,
|
||||
});
|
||||
|
||||
printSuccess(`${resource.singularName} ${id} deleted successfully.`);
|
||||
} catch (error) {
|
||||
handleError(error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function registerCountCommand(
|
||||
resourceCmd: Command,
|
||||
resource: ResourceInfo,
|
||||
): void {
|
||||
resourceCmd
|
||||
.command("count")
|
||||
.description(`Count ${resource.pluralName}`)
|
||||
.option("--query <json>", "Filter query as JSON")
|
||||
.action(async (options: { query?: string }) => {
|
||||
try {
|
||||
const parentOpts: CLIOptions = getParentOptions(resourceCmd);
|
||||
const creds: ResolvedCredentials = getResolvedCredentials(parentOpts);
|
||||
|
||||
const result: JSONValue = await executeApiRequest({
|
||||
apiUrl: creds.apiUrl,
|
||||
apiKey: creds.apiKey,
|
||||
apiPath: resource.apiPath,
|
||||
operation: "count" as ApiOperation,
|
||||
query: options.query ? parseJsonArg(options.query) : undefined,
|
||||
});
|
||||
|
||||
// Count response is typically { count: number }
|
||||
if (
|
||||
result &&
|
||||
typeof result === "object" &&
|
||||
!Array.isArray(result) &&
|
||||
"count" in (result as JSONObject)
|
||||
) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log((result as JSONObject)["count"]);
|
||||
} else {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(result);
|
||||
}
|
||||
} catch (error) {
|
||||
handleError(error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export function registerResourceCommands(program: Command): void {
|
||||
const resources: ResourceInfo[] = discoverResources();
|
||||
|
||||
for (const resource of resources) {
|
||||
const resourceCmd: Command = program
|
||||
.command(resource.name)
|
||||
.description(`Manage ${resource.pluralName} (${resource.modelType})`);
|
||||
|
||||
// Database models get full CRUD
|
||||
if (resource.modelType === "database") {
|
||||
registerListCommand(resourceCmd, resource);
|
||||
registerGetCommand(resourceCmd, resource);
|
||||
registerCreateCommand(resourceCmd, resource);
|
||||
registerUpdateCommand(resourceCmd, resource);
|
||||
registerDeleteCommand(resourceCmd, resource);
|
||||
registerCountCommand(resourceCmd, resource);
|
||||
}
|
||||
|
||||
// Analytics models get create, list, count
|
||||
if (resource.modelType === "analytics") {
|
||||
registerListCommand(resourceCmd, resource);
|
||||
registerCreateCommand(resourceCmd, resource);
|
||||
registerCountCommand(resourceCmd, resource);
|
||||
}
|
||||
}
|
||||
}
|
||||
129
CLI/Commands/UtilityCommands.ts
Normal file
129
CLI/Commands/UtilityCommands.ts
Normal file
@@ -0,0 +1,129 @@
|
||||
import { Command } from "commander";
|
||||
import {
|
||||
CLIContext,
|
||||
ResolvedCredentials,
|
||||
ResourceInfo,
|
||||
} from "../Types/CLITypes";
|
||||
import {
|
||||
getCurrentContext,
|
||||
CLIOptions,
|
||||
getResolvedCredentials,
|
||||
} from "../Core/ConfigManager";
|
||||
import { printInfo, printError } from "../Core/OutputFormatter";
|
||||
import { discoverResources } from "./ResourceCommands";
|
||||
import Table from "cli-table3";
|
||||
import chalk from "chalk";
|
||||
|
||||
export function registerUtilityCommands(program: Command): void {
|
||||
// Version command
|
||||
program
|
||||
.command("version")
|
||||
.description("Print CLI version")
|
||||
.action(() => {
|
||||
try {
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires, @typescript-eslint/no-require-imports
|
||||
const pkg: { version: string } = require("../package.json") as {
|
||||
version: string;
|
||||
};
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(pkg.version);
|
||||
} catch {
|
||||
// Fallback if package.json can't be loaded at runtime
|
||||
// eslint-disable-next-line no-console
|
||||
console.log("1.0.0");
|
||||
}
|
||||
});
|
||||
|
||||
// Whoami command
|
||||
program
|
||||
.command("whoami")
|
||||
.description("Show current authentication info")
|
||||
.action(() => {
|
||||
try {
|
||||
const ctx: CLIContext | null = getCurrentContext();
|
||||
const opts: Record<string, unknown> = program.opts();
|
||||
const cliOpts: CLIOptions = {
|
||||
apiKey: opts["apiKey"] as string | undefined,
|
||||
url: opts["url"] as string | undefined,
|
||||
context: opts["context"] as string | undefined,
|
||||
};
|
||||
|
||||
let creds: ResolvedCredentials;
|
||||
try {
|
||||
creds = getResolvedCredentials(cliOpts);
|
||||
} catch {
|
||||
printInfo(
|
||||
"Not authenticated. Run `oneuptime login` to authenticate.",
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const maskedKey: string =
|
||||
creds.apiKey.length > 8
|
||||
? creds.apiKey.substring(0, 4) +
|
||||
"****" +
|
||||
creds.apiKey.substring(creds.apiKey.length - 4)
|
||||
: "****";
|
||||
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(`URL: ${creds.apiUrl}`);
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(`API Key: ${maskedKey}`);
|
||||
if (ctx) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(`Context: ${ctx.name}`);
|
||||
}
|
||||
} catch (error) {
|
||||
printError(error instanceof Error ? error.message : String(error));
|
||||
process.exit(1);
|
||||
}
|
||||
});
|
||||
|
||||
// Resources command
|
||||
program
|
||||
.command("resources")
|
||||
.description("List all available resource types")
|
||||
.option("--type <type>", "Filter by model type: database, analytics")
|
||||
.action((options: { type?: string }) => {
|
||||
const resources: ResourceInfo[] = discoverResources();
|
||||
|
||||
const filtered: ResourceInfo[] = options.type
|
||||
? resources.filter((r: ResourceInfo) => {
|
||||
return r.modelType === options.type;
|
||||
})
|
||||
: resources;
|
||||
|
||||
if (filtered.length === 0) {
|
||||
printInfo("No resources found.");
|
||||
return;
|
||||
}
|
||||
|
||||
const noColor: boolean =
|
||||
process.env["NO_COLOR"] !== undefined ||
|
||||
process.argv.includes("--no-color");
|
||||
|
||||
const table: Table.Table = new Table({
|
||||
head: ["Command", "Singular", "Plural", "Type", "API Path"].map(
|
||||
(h: string) => {
|
||||
return noColor ? h : chalk.cyan(h);
|
||||
},
|
||||
),
|
||||
style: { head: [], border: [] },
|
||||
});
|
||||
|
||||
for (const r of filtered) {
|
||||
table.push([
|
||||
r.name,
|
||||
r.singularName,
|
||||
r.pluralName,
|
||||
r.modelType,
|
||||
r.apiPath,
|
||||
]);
|
||||
}
|
||||
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(table.toString());
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(`\nTotal: ${filtered.length} resources`);
|
||||
});
|
||||
}
|
||||
150
CLI/Core/ApiClient.ts
Normal file
150
CLI/Core/ApiClient.ts
Normal file
@@ -0,0 +1,150 @@
|
||||
import API from "Common/Utils/API";
|
||||
import URL from "Common/Types/API/URL";
|
||||
import Route from "Common/Types/API/Route";
|
||||
import Headers from "Common/Types/API/Headers";
|
||||
import HTTPResponse from "Common/Types/API/HTTPResponse";
|
||||
import HTTPErrorResponse from "Common/Types/API/HTTPErrorResponse";
|
||||
import Protocol from "Common/Types/API/Protocol";
|
||||
import Hostname from "Common/Types/API/Hostname";
|
||||
import { JSONObject, JSONValue } from "Common/Types/JSON";
|
||||
|
||||
export type ApiOperation =
|
||||
| "create"
|
||||
| "read"
|
||||
| "list"
|
||||
| "update"
|
||||
| "delete"
|
||||
| "count";
|
||||
|
||||
export interface ApiRequestOptions {
|
||||
apiUrl: string;
|
||||
apiKey: string;
|
||||
apiPath: string;
|
||||
operation: ApiOperation;
|
||||
id?: string | undefined;
|
||||
data?: JSONObject | undefined;
|
||||
query?: JSONObject | undefined;
|
||||
select?: JSONObject | undefined;
|
||||
skip?: number | undefined;
|
||||
limit?: number | undefined;
|
||||
sort?: JSONObject | undefined;
|
||||
}
|
||||
|
||||
function buildApiRoute(
|
||||
apiPath: string,
|
||||
operation: ApiOperation,
|
||||
id?: string,
|
||||
): Route {
|
||||
let fullPath: string = `/api${apiPath}`;
|
||||
|
||||
switch (operation) {
|
||||
case "read":
|
||||
if (id) {
|
||||
fullPath = `/api${apiPath}/${id}/get-item`;
|
||||
}
|
||||
break;
|
||||
case "update":
|
||||
case "delete":
|
||||
if (id) {
|
||||
fullPath = `/api${apiPath}/${id}/`;
|
||||
}
|
||||
break;
|
||||
case "count":
|
||||
fullPath = `/api${apiPath}/count`;
|
||||
break;
|
||||
case "list":
|
||||
fullPath = `/api${apiPath}/get-list`;
|
||||
break;
|
||||
case "create":
|
||||
default:
|
||||
fullPath = `/api${apiPath}`;
|
||||
break;
|
||||
}
|
||||
|
||||
return new Route(fullPath);
|
||||
}
|
||||
|
||||
function buildHeaders(apiKey: string): Headers {
|
||||
return {
|
||||
"Content-Type": "application/json",
|
||||
Accept: "application/json",
|
||||
APIKey: apiKey,
|
||||
};
|
||||
}
|
||||
|
||||
function buildRequestData(options: ApiRequestOptions): JSONObject | undefined {
|
||||
switch (options.operation) {
|
||||
case "create":
|
||||
return { data: options.data || {} } as JSONObject;
|
||||
case "update":
|
||||
return { data: options.data || {} } as JSONObject;
|
||||
case "list":
|
||||
case "count":
|
||||
return {
|
||||
query: options.query || {},
|
||||
select: options.select || {},
|
||||
skip: options.skip || 0,
|
||||
limit: options.limit || 10,
|
||||
sort: options.sort || {},
|
||||
} as JSONObject;
|
||||
case "read":
|
||||
return {
|
||||
select: options.select || {},
|
||||
} as JSONObject;
|
||||
case "delete":
|
||||
default:
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
export async function executeApiRequest(
|
||||
options: ApiRequestOptions,
|
||||
): Promise<JSONValue> {
|
||||
const url: URL = URL.fromString(options.apiUrl);
|
||||
const protocol: Protocol = url.protocol;
|
||||
const hostname: Hostname = url.hostname;
|
||||
|
||||
const api: API = new API(protocol, hostname, new Route("/"));
|
||||
const route: Route = buildApiRoute(
|
||||
options.apiPath,
|
||||
options.operation,
|
||||
options.id,
|
||||
);
|
||||
const headers: Headers = buildHeaders(options.apiKey);
|
||||
const data: JSONObject | undefined = buildRequestData(options);
|
||||
|
||||
const requestUrl: URL = new URL(api.protocol, api.hostname, route);
|
||||
const baseOptions: { url: URL; headers: Headers } = {
|
||||
url: requestUrl,
|
||||
headers,
|
||||
};
|
||||
|
||||
let response: HTTPResponse<JSONObject> | HTTPErrorResponse;
|
||||
|
||||
switch (options.operation) {
|
||||
case "create":
|
||||
case "count":
|
||||
case "list":
|
||||
case "read":
|
||||
response = await API.post(data ? { ...baseOptions, data } : baseOptions);
|
||||
break;
|
||||
case "update":
|
||||
response = await API.put(data ? { ...baseOptions, data } : baseOptions);
|
||||
break;
|
||||
case "delete":
|
||||
response = await API.delete(
|
||||
data ? { ...baseOptions, data } : baseOptions,
|
||||
);
|
||||
break;
|
||||
default:
|
||||
throw new Error(`Unsupported operation: ${options.operation}`);
|
||||
}
|
||||
|
||||
if (response instanceof HTTPErrorResponse) {
|
||||
throw new Error(
|
||||
`API error (${response.statusCode}): ${response.message || "Unknown error"}`,
|
||||
);
|
||||
}
|
||||
|
||||
return response.data;
|
||||
}
|
||||
141
CLI/Core/ConfigManager.ts
Normal file
141
CLI/Core/ConfigManager.ts
Normal file
@@ -0,0 +1,141 @@
|
||||
import * as fs from "fs";
|
||||
import * as path from "path";
|
||||
import * as os from "os";
|
||||
import { CLIConfig, CLIContext, ResolvedCredentials } from "../Types/CLITypes";
|
||||
|
||||
const CONFIG_DIR: string = path.join(os.homedir(), ".oneuptime");
|
||||
const CONFIG_FILE: string = path.join(CONFIG_DIR, "config.json");
|
||||
|
||||
function getDefaultConfig(): CLIConfig {
|
||||
return {
|
||||
currentContext: "",
|
||||
contexts: {},
|
||||
defaults: {
|
||||
output: "table",
|
||||
limit: 10,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export function load(): CLIConfig {
|
||||
try {
|
||||
if (!fs.existsSync(CONFIG_FILE)) {
|
||||
return getDefaultConfig();
|
||||
}
|
||||
const raw: string = fs.readFileSync(CONFIG_FILE, "utf-8");
|
||||
return JSON.parse(raw) as CLIConfig;
|
||||
} catch {
|
||||
return getDefaultConfig();
|
||||
}
|
||||
}
|
||||
|
||||
export function save(config: CLIConfig): void {
|
||||
if (!fs.existsSync(CONFIG_DIR)) {
|
||||
fs.mkdirSync(CONFIG_DIR, { recursive: true });
|
||||
}
|
||||
fs.writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2), {
|
||||
mode: 0o600,
|
||||
});
|
||||
}
|
||||
|
||||
export function getCurrentContext(): CLIContext | null {
|
||||
const config: CLIConfig = load();
|
||||
if (!config.currentContext) {
|
||||
return null;
|
||||
}
|
||||
return config.contexts[config.currentContext] || null;
|
||||
}
|
||||
|
||||
export function setCurrentContext(name: string): void {
|
||||
const config: CLIConfig = load();
|
||||
if (!config.contexts[name]) {
|
||||
throw new Error(`Context "${name}" does not exist.`);
|
||||
}
|
||||
config.currentContext = name;
|
||||
save(config);
|
||||
}
|
||||
|
||||
export function addContext(context: CLIContext): void {
|
||||
const config: CLIConfig = load();
|
||||
config.contexts[context.name] = context;
|
||||
if (!config.currentContext) {
|
||||
config.currentContext = context.name;
|
||||
}
|
||||
save(config);
|
||||
}
|
||||
|
||||
export function removeContext(name: string): void {
|
||||
const config: CLIConfig = load();
|
||||
if (!config.contexts[name]) {
|
||||
throw new Error(`Context "${name}" does not exist.`);
|
||||
}
|
||||
delete config.contexts[name];
|
||||
if (config.currentContext === name) {
|
||||
const remaining: string[] = Object.keys(config.contexts);
|
||||
config.currentContext = remaining[0] || "";
|
||||
}
|
||||
save(config);
|
||||
}
|
||||
|
||||
export function listContexts(): Array<CLIContext & { isCurrent: boolean }> {
|
||||
const config: CLIConfig = load();
|
||||
return Object.values(config.contexts).map(
|
||||
(ctx: CLIContext): CLIContext & { isCurrent: boolean } => {
|
||||
return {
|
||||
...ctx,
|
||||
isCurrent: ctx.name === config.currentContext,
|
||||
};
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
export interface CLIOptions {
|
||||
apiKey?: string | undefined;
|
||||
url?: string | undefined;
|
||||
context?: string | undefined;
|
||||
}
|
||||
|
||||
export function getResolvedCredentials(
|
||||
cliOptions: CLIOptions,
|
||||
): ResolvedCredentials {
|
||||
// Priority 1: CLI flags
|
||||
if (cliOptions.apiKey && cliOptions.url) {
|
||||
return { apiKey: cliOptions.apiKey, apiUrl: cliOptions.url };
|
||||
}
|
||||
|
||||
// Priority 2: Environment variables
|
||||
const envApiKey: string | undefined = process.env["ONEUPTIME_API_KEY"];
|
||||
const envUrl: string | undefined = process.env["ONEUPTIME_URL"];
|
||||
if (envApiKey && envUrl) {
|
||||
return { apiKey: envApiKey, apiUrl: envUrl };
|
||||
}
|
||||
|
||||
// Priority 3: Specific context if specified via --context flag
|
||||
if (cliOptions.context) {
|
||||
const config: CLIConfig = load();
|
||||
const ctx: CLIContext | undefined = config.contexts[cliOptions.context];
|
||||
if (ctx) {
|
||||
return { apiKey: ctx.apiKey, apiUrl: ctx.apiUrl };
|
||||
}
|
||||
throw new Error(`Context "${cliOptions.context}" does not exist.`);
|
||||
}
|
||||
|
||||
// Priority 4: Current context in config file
|
||||
const currentCtx: CLIContext | null = getCurrentContext();
|
||||
if (currentCtx) {
|
||||
return { apiKey: currentCtx.apiKey, apiUrl: currentCtx.apiUrl };
|
||||
}
|
||||
|
||||
// Partial env vars + partial context
|
||||
if (envApiKey || envUrl) {
|
||||
const ctx: CLIContext | null = getCurrentContext();
|
||||
return {
|
||||
apiKey: envApiKey || ctx?.apiKey || "",
|
||||
apiUrl: envUrl || ctx?.apiUrl || "",
|
||||
};
|
||||
}
|
||||
|
||||
throw new Error(
|
||||
"No credentials found. Run `oneuptime login` or set ONEUPTIME_API_KEY and ONEUPTIME_URL environment variables.",
|
||||
);
|
||||
}
|
||||
43
CLI/Core/ErrorHandler.ts
Normal file
43
CLI/Core/ErrorHandler.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
import { printError } from "./OutputFormatter";
|
||||
|
||||
export enum ExitCode {
|
||||
Success = 0,
|
||||
GeneralError = 1,
|
||||
AuthError = 2,
|
||||
NotFound = 3,
|
||||
}
|
||||
|
||||
export function handleError(error: unknown): never {
|
||||
if (error instanceof Error) {
|
||||
const message: string = error.message;
|
||||
|
||||
// Check for auth-related errors
|
||||
if (
|
||||
message.includes("API key") ||
|
||||
message.includes("credentials") ||
|
||||
message.includes("Unauthorized") ||
|
||||
message.includes("401")
|
||||
) {
|
||||
printError(`Authentication error: ${message}`);
|
||||
process.exit(ExitCode.AuthError);
|
||||
}
|
||||
|
||||
// Check for not found errors
|
||||
if (message.includes("404") || message.includes("not found")) {
|
||||
printError(`Not found: ${message}`);
|
||||
process.exit(ExitCode.NotFound);
|
||||
}
|
||||
|
||||
// General API errors
|
||||
if (message.includes("API error")) {
|
||||
printError(message);
|
||||
process.exit(ExitCode.GeneralError);
|
||||
}
|
||||
|
||||
printError(`Error: ${message}`);
|
||||
} else {
|
||||
printError(`Error: ${String(error)}`);
|
||||
}
|
||||
|
||||
process.exit(ExitCode.GeneralError);
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user