mirror of
https://github.com/fosrl/pangolin.git
synced 2026-02-28 15:56:39 +00:00
Compare commits
1143 Commits
1.11.0-s.2
...
1.13.0-rc.
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b63a8fd3ed | ||
|
|
ada3c6f2ef | ||
|
|
aafca7694d | ||
|
|
4345669793 | ||
|
|
66cae9802d | ||
|
|
2325e30f26 | ||
|
|
d1c98cf650 | ||
|
|
d06cd9b5be | ||
|
|
2eb440d019 | ||
|
|
4084c85c00 | ||
|
|
4fee65e5a4 | ||
|
|
17ee51249c | ||
|
|
f239c4370e | ||
|
|
c2a32a50cd | ||
|
|
7229bfa51b | ||
|
|
080e2f0a3a | ||
|
|
64e5cc172d | ||
|
|
c51a1c9c4d | ||
|
|
79958be380 | ||
|
|
05daedc6ad | ||
|
|
0234234108 | ||
|
|
f9b15b9156 | ||
|
|
37830d211d | ||
|
|
24cdac95cd | ||
|
|
e10f7efcbe | ||
|
|
1d7f4322e3 | ||
|
|
e8f10b049e | ||
|
|
a3ba4fff54 | ||
|
|
eecfcd640c | ||
|
|
40c38fa070 | ||
|
|
042c88ccb8 | ||
|
|
5a60f66ae0 | ||
|
|
4d665e8596 | ||
|
|
9221bcf889 | ||
|
|
2418813902 | ||
|
|
f66a9bdd33 | ||
|
|
bc7a1f4673 | ||
|
|
9010803046 | ||
|
|
311233b9f7 | ||
|
|
38203a0e7c | ||
|
|
5e9d660e26 | ||
|
|
110e950476 | ||
|
|
4e7843c1f3 | ||
|
|
502d15b9dc | ||
|
|
71db29c09c | ||
|
|
8cced5011b | ||
|
|
a812dde026 | ||
|
|
58374f77c9 | ||
|
|
8df3fa0ac0 | ||
|
|
840e9914cb | ||
|
|
f30a4f3cfd | ||
|
|
27004f9d0c | ||
|
|
427638ed3d | ||
|
|
350379b0c7 | ||
|
|
cf80c9d45c | ||
|
|
2d801b8ea5 | ||
|
|
f82d01d39b | ||
|
|
e959ce1698 | ||
|
|
25e176e8d5 | ||
|
|
8df01eb13a | ||
|
|
8d87f31bec | ||
|
|
2b3594a5ea | ||
|
|
72b7c8de0c | ||
|
|
b329dbb585 | ||
|
|
56d30ad6bd | ||
|
|
e24a13fb11 | ||
|
|
d7e06161a8 | ||
|
|
8a8c0edad3 | ||
|
|
66fc8529c2 | ||
|
|
0beaadf512 | ||
|
|
58177f4a02 | ||
|
|
28725dd164 | ||
|
|
1714140ee7 | ||
|
|
6329c3d140 | ||
|
|
44113ad93a | ||
|
|
ee1af459cc | ||
|
|
69561caa74 | ||
|
|
6f03d099b8 | ||
|
|
1581b5cb74 | ||
|
|
e09ec56fad | ||
|
|
8bcad76eb5 | ||
|
|
ff4a6b1d3f | ||
|
|
07b04b2603 | ||
|
|
54471c703c | ||
|
|
8a160ec0fe | ||
|
|
15da2f130b | ||
|
|
d64d2d6916 | ||
|
|
68928843a5 | ||
|
|
1228fddb01 | ||
|
|
3fa0b01c41 | ||
|
|
a4884f90a9 | ||
|
|
d7311ad947 | ||
|
|
1aa155a0af | ||
|
|
4f1c207083 | ||
|
|
dc6ee70eba | ||
|
|
0f9f4dfaeb | ||
|
|
22941c0653 | ||
|
|
d714f7d52c | ||
|
|
4f2dd92e81 | ||
|
|
090706c816 | ||
|
|
f449fdc7ec | ||
|
|
394d1503dd | ||
|
|
60380b70ed | ||
|
|
cece7a59bf | ||
|
|
00174be8c0 | ||
|
|
1d303feca2 | ||
|
|
3f4fae8f09 | ||
|
|
dab795e94a | ||
|
|
bd2165c553 | ||
|
|
646497cda0 | ||
|
|
dbc046397b | ||
|
|
fbafb48562 | ||
|
|
ccb17cdbbf | ||
|
|
c56512dc7d | ||
|
|
a92edf519e | ||
|
|
6cd3f2df1b | ||
|
|
b9c0089fac | ||
|
|
b2f78c9149 | ||
|
|
2a361b010f | ||
|
|
7bfa732a90 | ||
|
|
c554364001 | ||
|
|
5e52c48e77 | ||
|
|
c233fc564e | ||
|
|
151cd3e6de | ||
|
|
97489b9564 | ||
|
|
d263d282ee | ||
|
|
d1c7832e40 | ||
|
|
313d3c72da | ||
|
|
c8ec94c307 | ||
|
|
4809b64f7d | ||
|
|
26e49ca39d | ||
|
|
bb1472d25c | ||
|
|
8ea7b2ce02 | ||
|
|
1ee70e04ed | ||
|
|
d90f3bb6be | ||
|
|
149f4c1332 | ||
|
|
8e3b5688d5 | ||
|
|
bfd1293847 | ||
|
|
f4701f3da5 | ||
|
|
93af09ee97 | ||
|
|
897ddbec01 | ||
|
|
889b381e96 | ||
|
|
54c05c8345 | ||
|
|
a3b852ef45 | ||
|
|
53bb4efbb2 | ||
|
|
96dbec9352 | ||
|
|
2d3fbb9704 | ||
|
|
d3be1fbf4c | ||
|
|
89ee57cdf9 | ||
|
|
bdfc7fbcdb | ||
|
|
8726a7f931 | ||
|
|
1cae815be5 | ||
|
|
8d62fb3865 | ||
|
|
c5befee134 | ||
|
|
9cf2dbc2cc | ||
|
|
6217086cd5 | ||
|
|
6fbe25e91f | ||
|
|
57b3f49819 | ||
|
|
35f9c67cfe | ||
|
|
6707b3c7fe | ||
|
|
dfb85f2c89 | ||
|
|
17dec6cf0b | ||
|
|
8ee4ee7baf | ||
|
|
b1b0702886 | ||
|
|
92aed108cd | ||
|
|
2dcc94cd14 | ||
|
|
a7185ff913 | ||
|
|
04e73515b8 | ||
|
|
2bad9daaea | ||
|
|
54670e150d | ||
|
|
761ed1de9a | ||
|
|
078692c818 | ||
|
|
53ab51691a | ||
|
|
54e2d95b55 | ||
|
|
6e6fa77625 | ||
|
|
5c0c12cabe | ||
|
|
b3ed7c0129 | ||
|
|
10a00ff225 | ||
|
|
ba09479827 | ||
|
|
1c5c36fc12 | ||
|
|
d37ff6e15b | ||
|
|
9288575341 | ||
|
|
0ceed4c812 | ||
|
|
4b61a38501 | ||
|
|
ca9273c9ea | ||
|
|
810704e190 | ||
|
|
f33be1434b | ||
|
|
82a9f2b24f | ||
|
|
7204b5f0de | ||
|
|
9b372780bd | ||
|
|
9065385b87 | ||
|
|
77306e8c97 | ||
|
|
a746ef36a8 | ||
|
|
6e565f1331 | ||
|
|
84c608c2cf | ||
|
|
6da7f58ced | ||
|
|
351097b04d | ||
|
|
bd3d339905 | ||
|
|
c6ad36d78e | ||
|
|
eaeb65e9b4 | ||
|
|
4176bdbc81 | ||
|
|
a2cdd8484c | ||
|
|
23ab76ae08 | ||
|
|
8eec122114 | ||
|
|
79ccbc8e92 | ||
|
|
d70da2aa70 | ||
|
|
c695f50122 | ||
|
|
1b09e5b9f9 | ||
|
|
7efc947e26 | ||
|
|
4b580105cd | ||
|
|
a61c82570a | ||
|
|
6734003d85 | ||
|
|
e49d796b06 | ||
|
|
4ab4029625 | ||
|
|
5afff3c662 | ||
|
|
9be5a01173 | ||
|
|
357f297a3e | ||
|
|
e1edbe6067 | ||
|
|
5a859aad29 | ||
|
|
a28b15a81d | ||
|
|
e62186f395 | ||
|
|
11c1efc19c | ||
|
|
8b0491eb52 | ||
|
|
0032634004 | ||
|
|
4af10c8108 | ||
|
|
56cb685813 | ||
|
|
ccfe1f7d0a | ||
|
|
bf987d867c | ||
|
|
3870ced635 | ||
|
|
cb3861a5c8 | ||
|
|
f5bfddd262 | ||
|
|
f060063f53 | ||
|
|
6eb6b44f41 | ||
|
|
c93ab34021 | ||
|
|
06a31bb716 | ||
|
|
152fb47ca4 | ||
|
|
3d400b2321 | ||
|
|
2cdc23d63e | ||
|
|
45a82f3ecc | ||
|
|
342bedc012 | ||
|
|
18db4a11c8 | ||
|
|
a7e32d4013 | ||
|
|
beea28daf3 | ||
|
|
b5e94d44ae | ||
|
|
a623604e96 | ||
|
|
8c62dfa706 | ||
|
|
610e46f2d5 | ||
|
|
92125611e9 | ||
|
|
096da391e5 | ||
|
|
dd6b1d88d3 | ||
|
|
79f0d60533 | ||
|
|
67665864c2 | ||
|
|
336d31ce39 | ||
|
|
8df62e8b6a | ||
|
|
3eab3b0827 | ||
|
|
fbbab60956 | ||
|
|
c4de617751 | ||
|
|
19e3c5045e | ||
|
|
9f63d8bb5b | ||
|
|
49348c6ab7 | ||
|
|
0961ac1da1 | ||
|
|
6a79436516 | ||
|
|
85b46392e1 | ||
|
|
f721c983aa | ||
|
|
ff0b30fc2e | ||
|
|
18070a37a8 | ||
|
|
5bd31f87f0 | ||
|
|
de83cf9d8c | ||
|
|
ceae787cf5 | ||
|
|
ce6afd0019 | ||
|
|
d977d57b2a | ||
|
|
7bcd6adf01 | ||
|
|
ac68dbd545 | ||
|
|
d450e2c3ab | ||
|
|
9440a4f879 | ||
|
|
73b0411e1c | ||
|
|
a8d11d78fc | ||
|
|
e16aa6e90b | ||
|
|
6368b9d837 | ||
|
|
1b643fb4b6 | ||
|
|
d118c6b666 | ||
|
|
380e062d25 | ||
|
|
261f0333b8 | ||
|
|
24adca6108 | ||
|
|
3f440f0f7a | ||
|
|
ba6defa87c | ||
|
|
887a0ef574 | ||
|
|
200743747d | ||
|
|
2082c5eed2 | ||
|
|
a42d012788 | ||
|
|
82cc51424b | ||
|
|
7924f195aa | ||
|
|
d41bd3023f | ||
|
|
87a0dd2d12 | ||
|
|
5fd64596eb | ||
|
|
d23f61d995 | ||
|
|
7ac27b3883 | ||
|
|
9420b41e39 | ||
|
|
2cfb0e05cf | ||
|
|
5b9386b18a | ||
|
|
f5c3dff43c | ||
|
|
eeb82c8cfe | ||
|
|
3750c36aa7 | ||
|
|
be4d697dfe | ||
|
|
94b34c489c | ||
|
|
3801354ae6 | ||
|
|
266fbb1762 | ||
|
|
5d1f81a92c | ||
|
|
d6e8eb5307 | ||
|
|
2bc82f49ed | ||
|
|
487985558d | ||
|
|
dc237b8052 | ||
|
|
4ed4515262 | ||
|
|
cd76fa0139 | ||
|
|
af4b9e83f7 | ||
|
|
fa5facdf33 | ||
|
|
937b36e756 | ||
|
|
e90bdf8f97 | ||
|
|
56491cc17b | ||
|
|
6da531e99b | ||
|
|
01b5158b73 | ||
|
|
8f9b665bef | ||
|
|
806949879a | ||
|
|
e72e2b53aa | ||
|
|
10f42fe2e6 | ||
|
|
51b438117a | ||
|
|
d73825dd24 | ||
|
|
b5c6191c67 | ||
|
|
97c707248e | ||
|
|
02fbc279b5 | ||
|
|
447b706909 | ||
|
|
80a68507cd | ||
|
|
dbb1e37033 | ||
|
|
364b84359e | ||
|
|
93d4a40977 | ||
|
|
97312343e4 | ||
|
|
1736ad486a | ||
|
|
a07ad843a2 | ||
|
|
fef9101058 | ||
|
|
2890ff2605 | ||
|
|
026ad2ccb9 | ||
|
|
a82969b778 | ||
|
|
612b04c26f | ||
|
|
2162f5f76f | ||
|
|
710f16ce68 | ||
|
|
61a4f468ba | ||
|
|
b00fea5656 | ||
|
|
269ff630aa | ||
|
|
986f7121bd | ||
|
|
21f0501bc6 | ||
|
|
2b31dd955c | ||
|
|
e7aeb4ff89 | ||
|
|
9dd1192033 | ||
|
|
e61da0958f | ||
|
|
fce588057e | ||
|
|
33331fd3c8 | ||
|
|
1261ad3a00 | ||
|
|
7dcf4d5192 | ||
|
|
dc87df5d38 | ||
|
|
5d2f65daa9 | ||
|
|
58cf471bc4 | ||
|
|
7db99a7dd5 | ||
|
|
000904eb31 | ||
|
|
6d1713b6b9 | ||
|
|
de8262d7b9 | ||
|
|
4f026acad8 | ||
|
|
5b31bbce8d | ||
|
|
e6e80f6fc7 | ||
|
|
bde4492d49 | ||
|
|
7c728c144c | ||
|
|
8ad7bcc0d6 | ||
|
|
e62806d6fb | ||
|
|
4e0a2e441b | ||
|
|
aabe39137b | ||
|
|
d9564ed6fe | ||
|
|
0798a0c6c2 | ||
|
|
c9786946b7 | ||
|
|
9344ab3546 | ||
|
|
1a4078b8a1 | ||
|
|
ca66637270 | ||
|
|
8674ca931b | ||
|
|
08c82e072e | ||
|
|
23c9827e4c | ||
|
|
864b587b89 | ||
|
|
ca89aa7ce8 | ||
|
|
63a1ecfb86 | ||
|
|
fbce392137 | ||
|
|
c004e969cb | ||
|
|
c6611471b1 | ||
|
|
bdf1625976 | ||
|
|
0a5dc17800 | ||
|
|
fa7aa508ea | ||
|
|
2973b61676 | ||
|
|
2428413442 | ||
|
|
5602d8ee64 | ||
|
|
a70799c8c0 | ||
|
|
d38b321f85 | ||
|
|
b0ff50a76f | ||
|
|
37acdc2796 | ||
|
|
f3d31cb6de | ||
|
|
a336955066 | ||
|
|
a229fc1c61 | ||
|
|
7995fd364e | ||
|
|
5e0d822d45 | ||
|
|
4fddaa8f11 | ||
|
|
4a87cecf89 | ||
|
|
ac5ee5c7ca | ||
|
|
8a8c357563 | ||
|
|
263fd80c18 | ||
|
|
7bdf05bdf5 | ||
|
|
d00f12967d | ||
|
|
d9991a18e2 | ||
|
|
a51c21cdd2 | ||
|
|
265cab5b64 | ||
|
|
da15e5e77b | ||
|
|
a717ca2675 | ||
|
|
693c9fbe0f | ||
|
|
564b290244 | ||
|
|
84d78df67e | ||
|
|
107053a98f | ||
|
|
6422a78e6f | ||
|
|
10f8298161 | ||
|
|
5f11630e27 | ||
|
|
a776b2ea94 | ||
|
|
b83ec1b503 | ||
|
|
83bd5957cd | ||
|
|
f98b4baa73 | ||
|
|
0af51cebbe | ||
|
|
abc5f8ec68 | ||
|
|
ddc14d164e | ||
|
|
aeda85fcfb | ||
|
|
66124f09c4 | ||
|
|
ac5fe1486a | ||
|
|
50ac52d316 | ||
|
|
f85d9f8b6e | ||
|
|
feb0bd58c8 | ||
|
|
32949127d2 | ||
|
|
84d24d9bf5 | ||
|
|
8e1bb6a6fd | ||
|
|
66c14c2d09 | ||
|
|
cad4d97fb3 | ||
|
|
de53cfb912 | ||
|
|
55fd276773 | ||
|
|
7125b49024 | ||
|
|
fb9ed8f592 | ||
|
|
020cb2d794 | ||
|
|
9b2c0d0b67 | ||
|
|
3993e5b705 | ||
|
|
47bcadb329 | ||
|
|
00df2c876f | ||
|
|
b4535f3dc4 | ||
|
|
e51fca1f61 | ||
|
|
0e7f5b1aef | ||
|
|
579a4e1021 | ||
|
|
c813202f92 | ||
|
|
94e1c534ca | ||
|
|
41e21acf42 | ||
|
|
b6e98632b5 | ||
|
|
51db267a4a | ||
|
|
8a5f59cb9f | ||
|
|
669817818a | ||
|
|
b84453bfbe | ||
|
|
15d561f59f | ||
|
|
0745734273 | ||
|
|
aa3f07f1ba | ||
|
|
2b8204fdc8 | ||
|
|
90e72c6aca | ||
|
|
62e2b7ca9e | ||
|
|
f7e7993fd4 | ||
|
|
18cdf070c7 | ||
|
|
563a5b3e7e | ||
|
|
3756aaecda | ||
|
|
58a13de0ff | ||
|
|
d32505a833 | ||
|
|
42091e88cb | ||
|
|
c2f607bb9a | ||
|
|
3f38080b46 | ||
|
|
9f9aa07c2d | ||
|
|
76d54b2d0f | ||
|
|
bdb564823d | ||
|
|
b3a616c9f3 | ||
|
|
ec1f94791a | ||
|
|
bea1c65076 | ||
|
|
2274a3525b | ||
|
|
749cea5a4d | ||
|
|
999fb2fff1 | ||
|
|
2a7529c39e | ||
|
|
f27ae210ed | ||
|
|
ea744f8d28 | ||
|
|
0b70cbb1a3 | ||
|
|
fce887436d | ||
|
|
f928708156 | ||
|
|
fae899a8f1 | ||
|
|
3489107a49 | ||
|
|
45fb0a4156 | ||
|
|
a62299c387 | ||
|
|
18757d7eb3 | ||
|
|
296b220bf3 | ||
|
|
0a9f37c44d | ||
|
|
776c33d79d | ||
|
|
9fd6af3a31 | ||
|
|
4ade878320 | ||
|
|
9e2477587c | ||
|
|
c7787352c8 | ||
|
|
85892c30b2 | ||
|
|
7a2dd31019 | ||
|
|
096ca379ce | ||
|
|
41601010f4 | ||
|
|
64b87e203a | ||
|
|
c64b102aaa | ||
|
|
f371c7df81 | ||
|
|
030f90db2e | ||
|
|
e51b6b545e | ||
|
|
ef5d72663f | ||
|
|
6ddfc9b8fe | ||
|
|
301654b63e | ||
|
|
c73f8c88f7 | ||
|
|
2274404324 | ||
|
|
6d349693a7 | ||
|
|
b9ce316574 | ||
|
|
a247ef7564 | ||
|
|
18566c09dc | ||
|
|
1090dca634 | ||
|
|
44f419d4f7 | ||
|
|
162c6d567c | ||
|
|
2f1abfbef8 | ||
|
|
a26a441d56 | ||
|
|
f628a76223 | ||
|
|
8088e30e06 | ||
|
|
801cdec7f3 | ||
|
|
3fd3f9871d | ||
|
|
959a562e7c | ||
|
|
3b12a77cf0 | ||
|
|
03e0e8d9c2 | ||
|
|
7cd31313d8 | ||
|
|
52a311bf36 | ||
|
|
9822deb4bf | ||
|
|
83e0282212 | ||
|
|
8942cb7aa7 | ||
|
|
f0f219f293 | ||
|
|
dc75d72522 | ||
|
|
6da81b3817 | ||
|
|
847479b639 | ||
|
|
0790f37f5e | ||
|
|
9dd472c59b | ||
|
|
5746d69f98 | ||
|
|
8356c5933f | ||
|
|
2c488baa80 | ||
|
|
d30743a428 | ||
|
|
009d84a3c6 | ||
|
|
e888b76747 | ||
|
|
6174599754 | ||
|
|
8ba04aeb74 | ||
|
|
43590896e9 | ||
|
|
3547c4832b | ||
|
|
1cd098252e | ||
|
|
4adbc31dae | ||
|
|
99031feb35 | ||
|
|
d363b06d0e | ||
|
|
2af100cc86 | ||
|
|
3e90211108 | ||
|
|
6dd161fe17 | ||
|
|
558bd040c6 | ||
|
|
f2c48975f6 | ||
|
|
fc43a56bb3 | ||
|
|
ca7f557a3c | ||
|
|
7477713eef | ||
|
|
c16e762fa4 | ||
|
|
41592133a6 | ||
|
|
54f7525f1b | ||
|
|
ad6bb3da9f | ||
|
|
49bc2dc5da | ||
|
|
cdf77087cd | ||
|
|
8e5dde887c | ||
|
|
f21188000e | ||
|
|
1b3eb32bf4 | ||
|
|
eec3f183e6 | ||
|
|
31b66cd911 | ||
|
|
ad425e8d9e | ||
|
|
da0196a308 | ||
|
|
e585972b7b | ||
|
|
cc62cd4add | ||
|
|
25225a452c | ||
|
|
678644c7fb | ||
|
|
32f20ed984 | ||
|
|
4eb5bf08d5 | ||
|
|
35c93f38e0 | ||
|
|
f60c2f4fb9 | ||
|
|
b2cf152b9e | ||
|
|
444928dffd | ||
|
|
4d7e2d5840 | ||
|
|
318046ce1d | ||
|
|
808ad1e272 | ||
|
|
05a1195661 | ||
|
|
c46322c6a6 | ||
|
|
80d5efc41f | ||
|
|
0409ab7dc1 | ||
|
|
63f079ec76 | ||
|
|
5988f1e8da | ||
|
|
ed0c0edeba | ||
|
|
34b4841f4d | ||
|
|
ff47c5a8ad | ||
|
|
9430a53c0c | ||
|
|
03334e3f0f | ||
|
|
6f2ecf9d0d | ||
|
|
6f803c3b4b | ||
|
|
15d400c842 | ||
|
|
3ddf150661 | ||
|
|
5b519afee4 | ||
|
|
15ea9f3dcc | ||
|
|
d5e2536f8d | ||
|
|
d7e9083e06 | ||
|
|
e0cc338c3a | ||
|
|
624c5741e2 | ||
|
|
558507dd71 | ||
|
|
565340bd53 | ||
|
|
756745487a | ||
|
|
d2ece4d370 | ||
|
|
d5f5d1da1e | ||
|
|
dfaf1a72cc | ||
|
|
ff8e5b871c | ||
|
|
927dda4e53 | ||
|
|
0e51bac307 | ||
|
|
7a50af14f3 | ||
|
|
396477c2e2 | ||
|
|
8765874d9a | ||
|
|
49dffe086d | ||
|
|
77ddadcded | ||
|
|
05b297ddec | ||
|
|
feb0de9a08 | ||
|
|
f4f2361d22 | ||
|
|
cae6a9f51c | ||
|
|
2872f5c018 | ||
|
|
0512c21ad7 | ||
|
|
922a69feed | ||
|
|
24192c79d4 | ||
|
|
17c22a635f | ||
|
|
bcbcf417b5 | ||
|
|
acf7596368 | ||
|
|
34c7d925ca | ||
|
|
c10730ebb9 | ||
|
|
e50743b922 | ||
|
|
75b0745e42 | ||
|
|
ebd99f95a3 | ||
|
|
0e649883cb | ||
|
|
3d376c8d14 | ||
|
|
adedb0e391 | ||
|
|
521935786c | ||
|
|
885b9d186b | ||
|
|
356f023539 | ||
|
|
de8d3f45da | ||
|
|
72c9956190 | ||
|
|
6dc4cbe448 | ||
|
|
77364488c2 | ||
|
|
5a61040027 | ||
|
|
c6f7be40df | ||
|
|
c36fb63f8c | ||
|
|
48aebea6cf | ||
|
|
55082d2ef8 | ||
|
|
cc03b97234 | ||
|
|
5542873368 | ||
|
|
1db5d76ef1 | ||
|
|
ca6c45087b | ||
|
|
3333eb95f9 | ||
|
|
d681725fc3 | ||
|
|
f5eadc9e1e | ||
|
|
219e213c1e | ||
|
|
af654e663b | ||
|
|
39b3b4ef9d | ||
|
|
6c62a0900f | ||
|
|
ddd772eb43 | ||
|
|
69458ab649 | ||
|
|
c7df70143e | ||
|
|
a81ea7cc8f | ||
|
|
02330a0756 | ||
|
|
db49b599b5 | ||
|
|
bb0bfd440a | ||
|
|
10ce732b8d | ||
|
|
4c567cf2d7 | ||
|
|
2783d2989d | ||
|
|
c3d6510231 | ||
|
|
3bb948991f | ||
|
|
4b9ce22f06 | ||
|
|
772bda69f9 | ||
|
|
8b4722b1c9 | ||
|
|
9e5c9d9c34 | ||
|
|
ee533df38f | ||
|
|
52dc8e011c | ||
|
|
bd5cc790d6 | ||
|
|
7d6d5a7787 | ||
|
|
ba6e7dd06a | ||
|
|
6270fb3237 | ||
|
|
16ec50a6ee | ||
|
|
3d2021c8a1 | ||
|
|
15d63ddffa | ||
|
|
7ce6fadb3d | ||
|
|
6b18a24f9b | ||
|
|
a38cb961c7 | ||
|
|
3c5fe21078 | ||
|
|
b44305694f | ||
|
|
be217e2b6f | ||
|
|
6ce04c2aa1 | ||
|
|
85e4b649db | ||
|
|
73a3335148 | ||
|
|
32845c5a3d | ||
|
|
05a878ac34 | ||
|
|
847d015243 | ||
|
|
51cde2681c | ||
|
|
9c0606942c | ||
|
|
646d476bdb | ||
|
|
31261681a0 | ||
|
|
f6fae820c4 | ||
|
|
b3cbf925aa | ||
|
|
aa1ae3ee42 | ||
|
|
80f6c8b74e | ||
|
|
79d8e8d59d | ||
|
|
9193375586 | ||
|
|
240bcb8759 | ||
|
|
a5dcafb84c | ||
|
|
192207a857 | ||
|
|
d18fafb0ef | ||
|
|
380c86898c | ||
|
|
b59a6b82ef | ||
|
|
77ba568c36 | ||
|
|
a0f05cc77b | ||
|
|
80f43a9774 | ||
|
|
c04d9eda6b | ||
|
|
cabf3e9695 | ||
|
|
ff7b4386d6 | ||
|
|
4dbbe159ee | ||
|
|
eeab92719a | ||
|
|
43e6b7de07 | ||
|
|
4cfd1b1ff5 | ||
|
|
09ba018493 | ||
|
|
7acf7dd0eb | ||
|
|
592d085de6 | ||
|
|
2cf2c64651 | ||
|
|
560974f7d2 | ||
|
|
85270f497a | ||
|
|
9fbea4a380 | ||
|
|
cbf9c5361e | ||
|
|
44316731c0 | ||
|
|
60513af8ed | ||
|
|
24cfe02979 | ||
|
|
8f3324560a | ||
|
|
2041edcf30 | ||
|
|
1227b3c11a | ||
|
|
8973726f63 | ||
|
|
5559fef1bc | ||
|
|
9cb3c3821a | ||
|
|
c85e367ded | ||
|
|
5e20487216 | ||
|
|
bc6b9eb905 | ||
|
|
5940bbd498 | ||
|
|
f4a0f6a2e6 | ||
|
|
0df7d45678 | ||
|
|
a05ee2483b | ||
|
|
f5dbc18c05 | ||
|
|
dd052fa1af | ||
|
|
2cc4ad9c30 | ||
|
|
4dd741cc3f | ||
|
|
9ce81b34c9 | ||
|
|
460df46abc | ||
|
|
1e70e4289b | ||
|
|
5fa0ac5927 | ||
|
|
4b40e7b8d6 | ||
|
|
29cd035a05 | ||
|
|
39d6b93d42 | ||
|
|
629f17294a | ||
|
|
10a5af67aa | ||
|
|
b542d82553 | ||
|
|
2a644c3f88 | ||
|
|
f6de61968d | ||
|
|
68f0c4df3a | ||
|
|
0743daf56a | ||
|
|
58b6ab2601 | ||
|
|
038f8829c2 | ||
|
|
ddcf77a62d | ||
|
|
adefbdbeb3 | ||
|
|
921285e5b1 | ||
|
|
264bf46798 | ||
|
|
5a7b5d65a4 | ||
|
|
23b13f0a0e | ||
|
|
90ddffce0e | ||
|
|
e30fde5237 | ||
|
|
ac683c3ff7 | ||
|
|
b5a931c96e | ||
|
|
5b61742075 | ||
|
|
4e4a38f7e9 | ||
|
|
c1bb029a1c | ||
|
|
eae2c37388 | ||
|
|
7193fea068 | ||
|
|
9b85deebf8 | ||
|
|
0211f75cb6 | ||
|
|
fa6b7ca3ed | ||
|
|
007d03e7f6 | ||
|
|
a534301eb7 | ||
|
|
1baa987016 | ||
|
|
a5b48ab392 | ||
|
|
7f981f05fb | ||
|
|
259cea1c42 | ||
|
|
9024b2a974 | ||
|
|
f2c31d3ca6 | ||
|
|
6f8b5dd909 | ||
|
|
6521b66b7c | ||
|
|
202d2075a6 | ||
|
|
e575fae73b | ||
|
|
d84ee3d03d | ||
|
|
ba745588e9 | ||
|
|
84731bdc19 | ||
|
|
f748c5dbe4 | ||
|
|
fdd4d5244f | ||
|
|
9301477262 | ||
|
|
9a787e6ef8 | ||
|
|
5b8cdf7884 | ||
|
|
5fd104bb30 | ||
|
|
9ba42a8fa3 | ||
|
|
fe8fd2e3a8 | ||
|
|
9ebce35d2b | ||
|
|
654145be84 | ||
|
|
3662d42374 | ||
|
|
d392fb371e | ||
|
|
1142d6ac48 | ||
|
|
bdc3b2425b | ||
|
|
9a64f45815 | ||
|
|
3633e02ff7 | ||
|
|
2c502ec764 | ||
|
|
b17d7f0e27 | ||
|
|
65364d6b0f | ||
|
|
6fd6c77ce6 | ||
|
|
e447549de1 | ||
|
|
6b0dd00aa5 | ||
|
|
461866836e | ||
|
|
3ae42f054f | ||
|
|
5a571f19e1 | ||
|
|
70aeaf7b5d | ||
|
|
7a6838f5a5 | ||
|
|
07f5e8f215 | ||
|
|
2b05bc1f5f | ||
|
|
edf64ae7b5 | ||
|
|
7370448be9 | ||
|
|
51af293d66 | ||
|
|
d37e28215e | ||
|
|
2c01849f2e | ||
|
|
c29ba9bb5f | ||
|
|
8fdf120ec2 | ||
|
|
a9b9161c40 | ||
|
|
43f907ebec | ||
|
|
ae670e1eb5 | ||
|
|
f102718901 | ||
|
|
9d452efc7d | ||
|
|
156fe529b5 | ||
|
|
df24525105 | ||
|
|
d938345deb | ||
|
|
d6681733dd | ||
|
|
2f1aec02f0 | ||
|
|
d30e0a3c51 | ||
|
|
3f3e9cf1bb | ||
|
|
e77909d498 | ||
|
|
d10830f892 | ||
|
|
18d8f72da2 | ||
|
|
4a59823e58 | ||
|
|
f3149e46cd | ||
|
|
60379a7b4e | ||
|
|
605b3cccee | ||
|
|
843799f4f6 | ||
|
|
a69cda5c13 | ||
|
|
dbaa3dbd52 | ||
|
|
58197c6fb2 | ||
|
|
7813093452 | ||
|
|
3f2c3dc987 | ||
|
|
08ddba25d0 | ||
|
|
d47fa7e64f | ||
|
|
c87aa2e537 | ||
|
|
bc430546bc | ||
|
|
9428e065eb | ||
|
|
10408c5717 | ||
|
|
ae902da913 | ||
|
|
0be5a91eff | ||
|
|
7dcf46ce98 | ||
|
|
33e6e4b411 | ||
|
|
bab6e4eb0d | ||
|
|
6a7c7521d8 | ||
|
|
d070244ea7 | ||
|
|
9219bb7d6e | ||
|
|
54e83f35e5 | ||
|
|
eb138d6526 | ||
|
|
edd0c3099b | ||
|
|
04455d40cf | ||
|
|
221af94d15 | ||
|
|
48ac3bb7af | ||
|
|
07273b8b7f | ||
|
|
bfb5b2864d | ||
|
|
07330e84fb | ||
|
|
0e39704b3a | ||
|
|
f25e794e7c | ||
|
|
df46ce8bdc | ||
|
|
4d83f537dc | ||
|
|
58443ef53f | ||
|
|
1ee52ad86b | ||
|
|
bc941239ec | ||
|
|
9a52d5387d | ||
|
|
1f50bc3752 | ||
|
|
0819df0910 | ||
|
|
663787c15b | ||
|
|
2c39d07261 | ||
|
|
dce84b9b09 | ||
|
|
a5bab6bb80 | ||
|
|
7536c03f63 | ||
|
|
ada5d2ef0e | ||
|
|
b8bead0590 | ||
|
|
68f852d6d1 | ||
|
|
d9fe5a8819 | ||
|
|
346183a23f | ||
|
|
dcfd7f5443 | ||
|
|
e59cd6672b | ||
|
|
7c8c440f67 | ||
|
|
f258c41f15 | ||
|
|
ae4a24f4aa | ||
|
|
476cdcfe86 | ||
|
|
f869df2f65 | ||
|
|
03cfabacd9 | ||
|
|
47ac5875f3 | ||
|
|
f67327358e | ||
|
|
4901823f15 | ||
|
|
5407e3c821 | ||
|
|
1d5cdad8b7 | ||
|
|
cd2424cb77 | ||
|
|
c17efde6bf | ||
|
|
40cd8cdec7 | ||
|
|
6768672a44 | ||
|
|
240c5b005b | ||
|
|
8dde170a35 | ||
|
|
c07abf8ff9 | ||
|
|
e5a436593f | ||
|
|
bb6e093ac6 | ||
|
|
59a334ce24 | ||
|
|
d241dcfb27 | ||
|
|
af263e7913 | ||
|
|
6610e7d405 | ||
|
|
c476e65cf2 | ||
|
|
b69b2eeeb3 | ||
|
|
89dab0917b | ||
|
|
73efdb95ae | ||
|
|
1bcca88614 | ||
|
|
3af1e0ef56 | ||
|
|
8387571c1d | ||
|
|
1d017f60b4 | ||
|
|
81effda9e8 | ||
|
|
9343906ab1 | ||
|
|
08b7d6735c | ||
|
|
a91ebd1e91 | ||
|
|
312e03b4eb | ||
|
|
e8a57e432c | ||
|
|
bca2eef2e8 | ||
|
|
ec7211a15d | ||
|
|
46807c6477 | ||
|
|
b578786e62 | ||
|
|
2e0ad8d262 | ||
|
|
003f0cfa6d | ||
|
|
ee3df081ef | ||
|
|
08eeb12519 | ||
|
|
e66c6b2505 | ||
|
|
d2a880d9c8 | ||
|
|
edc0b86470 | ||
|
|
aebe6b80b7 | ||
|
|
4d87333b43 | ||
|
|
ef32f3ed5a | ||
|
|
216ded3034 | ||
|
|
cb59fe2cee | ||
|
|
7776f6d09c | ||
|
|
c50392c947 | ||
|
|
ceee978fcd | ||
|
|
c5a73dc87e | ||
|
|
7198ef2774 | ||
|
|
7e9a066797 | ||
|
|
ba96332313 | ||
|
|
e2d0338b0b | ||
|
|
59ecab5738 | ||
|
|
721bf3403d | ||
|
|
3b8ba47377 | ||
|
|
e752929f69 | ||
|
|
e41c3e6f54 | ||
|
|
9dedd1a8de | ||
|
|
c4a5fae28f | ||
|
|
5f95a3233f | ||
|
|
d3174d0196 | ||
|
|
3710d71974 | ||
|
|
f62e88eb67 | ||
|
|
904b302fb6 | ||
|
|
5fc096f2d5 | ||
|
|
87668c492f | ||
|
|
6d7a8b97ad | ||
|
|
282d444933 | ||
|
|
f3d7d97fb9 | ||
|
|
de857a7c4e | ||
|
|
20a0ebfc9d | ||
|
|
ba8166bdeb | ||
|
|
2b634fc6c5 | ||
|
|
5429bc03ab | ||
|
|
a558b34608 | ||
|
|
1850d56977 | ||
|
|
61b4c62824 | ||
|
|
10e5ccfe86 | ||
|
|
9f5d475e80 | ||
|
|
9bb9a3acbe | ||
|
|
0923b7e3c5 | ||
|
|
ccd81f6fe2 | ||
|
|
0f74107e86 | ||
|
|
8377434c08 | ||
|
|
1fbf2bfb8d | ||
|
|
42facf8e12 | ||
|
|
4bb3d85c25 | ||
|
|
c0039190bd | ||
|
|
a8d00a47cd | ||
|
|
57bcbf6c48 | ||
|
|
c57db1479e | ||
|
|
cd8062ada3 | ||
|
|
244d05adb1 | ||
|
|
812bd64325 | ||
|
|
276d1361ac | ||
|
|
881eac4722 | ||
|
|
2a2a550a6a | ||
|
|
e75001080a | ||
|
|
6fbba38a76 | ||
|
|
902b413881 | ||
|
|
8b2f8ad3ef | ||
|
|
377cb77307 | ||
|
|
733bf0b169 | ||
|
|
8faff3e075 | ||
|
|
48af91c976 | ||
|
|
6664efaa13 | ||
|
|
e5ee96cf52 | ||
|
|
38faf1f905 | ||
|
|
2cff142266 | ||
|
|
2c99cfacc0 | ||
|
|
0c63ea1f50 | ||
|
|
f50df66e3a | ||
|
|
4b93491160 | ||
|
|
19210cbf7d | ||
|
|
9af206b69a | ||
|
|
b6b9c71c5e | ||
|
|
c000c4502f | ||
|
|
b6c1d9a592 | ||
|
|
7a75fe0cad | ||
|
|
a83e660902 | ||
|
|
65eb3e4b95 | ||
|
|
093fb419f3 | ||
|
|
026e56aead | ||
|
|
fa9bc59f62 | ||
|
|
06ec80db42 | ||
|
|
24d564b79b | ||
|
|
2f5e6248cd | ||
|
|
c0cc81ed96 | ||
|
|
b33a54a449 | ||
|
|
94137e587c | ||
|
|
a6086d3724 | ||
|
|
0a377150e3 | ||
|
|
d20e0a228a | ||
|
|
ca146a1b57 | ||
|
|
c7c3e3ee73 | ||
|
|
cd27f6459c | ||
|
|
b1e212721e | ||
|
|
ccd2773331 | ||
|
|
cfa82b51fb | ||
|
|
9c91a8db46 | ||
|
|
b160eee8d2 | ||
|
|
37ceabdf5d | ||
|
|
e7828a43fa | ||
|
|
ccb1f04ad8 | ||
|
|
4c14ccbb63 | ||
|
|
25c24ca9cf | ||
|
|
787869fe21 | ||
|
|
b51c27a823 | ||
|
|
5917881b47 | ||
|
|
c7a40d59b7 | ||
|
|
a50c0d84e9 | ||
|
|
f17a957058 | ||
|
|
2c63851130 | ||
|
|
6b125bba7c | ||
|
|
d92b87b7c8 | ||
|
|
f64a477c3d | ||
|
|
b6f8ed1e4a | ||
|
|
bad88e4741 | ||
|
|
01db519691 | ||
|
|
e601038c0f | ||
|
|
e0996a17ef | ||
|
|
526307e192 | ||
|
|
1b01c4f053 | ||
|
|
a184e23f16 | ||
|
|
06156e0ca6 | ||
|
|
02b1de3266 | ||
|
|
c5b3d92466 | ||
|
|
186a78b064 | ||
|
|
9a808dc139 | ||
|
|
977404b8c3 | ||
|
|
b00143ce9b | ||
|
|
4435d9a248 | ||
|
|
7d0303e2be | ||
|
|
a0da9c1129 | ||
|
|
5e73690570 | ||
|
|
b0409b7d52 | ||
|
|
fe474b3989 | ||
|
|
5154d5d3ee | ||
|
|
62df92f63a | ||
|
|
e2534af40e | ||
|
|
b627e391ac | ||
|
|
40a3eac704 | ||
|
|
2ee3f10e02 | ||
|
|
5a3bf2f758 | ||
|
|
e121dd0d1d | ||
|
|
2c46a37a53 | ||
|
|
23f05d7f4e | ||
|
|
6105eea7a9 | ||
|
|
850e9a734a | ||
|
|
2d30b155f2 | ||
|
|
1333e21553 | ||
|
|
4c412528f5 | ||
|
|
a8fce47ba0 | ||
|
|
cb7c57fd03 | ||
|
|
494d0f7c14 | ||
|
|
a4e480e02b | ||
|
|
cd285cc019 | ||
|
|
9e8e00d4bb | ||
|
|
389834f735 | ||
|
|
b14ddc07fb | ||
|
|
4447fb8202 | ||
|
|
1c9c4b1802 | ||
|
|
19e15f4ef5 | ||
|
|
c2c29e2cd2 | ||
|
|
7b33dc591d | ||
|
|
a95f2e76f4 | ||
|
|
979860a951 | ||
|
|
9e9a81d9e8 | ||
|
|
8f09561114 | ||
|
|
e4c0a157e3 | ||
|
|
22477b7e81 | ||
|
|
b6c76a2164 | ||
|
|
043834274d | ||
|
|
1e4ca69c89 | ||
|
|
ff2bcfb0e7 | ||
|
|
b47fc9f901 | ||
|
|
ee8952de10 |
@@ -29,4 +29,6 @@ CONTRIBUTING.md
|
|||||||
dist
|
dist
|
||||||
.git
|
.git
|
||||||
migrations/
|
migrations/
|
||||||
config/
|
config/
|
||||||
|
build.ts
|
||||||
|
tsconfig.json
|
||||||
127
.github/workflows/cicd.yml
vendored
127
.github/workflows/cicd.yml
vendored
@@ -1,34 +1,62 @@
|
|||||||
name: CI/CD Pipeline
|
name: CI/CD Pipeline
|
||||||
|
|
||||||
|
# CI/CD workflow for building, publishing, mirroring, signing container images and building release binaries.
|
||||||
|
# Actions are pinned to specific SHAs to reduce supply-chain risk. This workflow triggers on tag push events.
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
packages: write # for GHCR push
|
||||||
|
id-token: write # for Cosign Keyless (OIDC) Signing
|
||||||
|
|
||||||
|
# Required secrets:
|
||||||
|
# - DOCKER_HUB_USERNAME / DOCKER_HUB_ACCESS_TOKEN: push to Docker Hub
|
||||||
|
# - GITHUB_TOKEN: used for GHCR login and OIDC keyless signing
|
||||||
|
# - COSIGN_PRIVATE_KEY / COSIGN_PASSWORD / COSIGN_PUBLIC_KEY: for key-based signing
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
tags:
|
tags:
|
||||||
- "*"
|
- "[0-9]+.[0-9]+.[0-9]+"
|
||||||
|
- "[0-9]+.[0-9]+.[0-9]+.rc.[0-9]+"
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: ${{ github.ref }}
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
release:
|
release:
|
||||||
name: Build and Release
|
name: Build and Release
|
||||||
runs-on: ubuntu-latest
|
runs-on: [self-hosted, linux, x64]
|
||||||
|
# Job-level timeout to avoid runaway or stuck runs
|
||||||
|
timeout-minutes: 120
|
||||||
|
env:
|
||||||
|
# Target images
|
||||||
|
DOCKERHUB_IMAGE: docker.io/fosrl/${{ github.event.repository.name }}
|
||||||
|
GHCR_IMAGE: ghcr.io/${{ github.repository_owner }}/${{ github.event.repository.name }}
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@v5
|
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
|
||||||
|
|
||||||
|
- name: Set up QEMU
|
||||||
|
uses: docker/setup-qemu-action@c7c53464625b32c7a7e944ae62b3e17d2b600130 # v3.7.0
|
||||||
|
|
||||||
- name: Set up Docker Buildx
|
- name: Set up Docker Buildx
|
||||||
uses: docker/setup-buildx-action@v3
|
uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # v3.11.1
|
||||||
|
|
||||||
- name: Log in to Docker Hub
|
- name: Log in to Docker Hub
|
||||||
uses: docker/login-action@v3
|
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
|
||||||
with:
|
with:
|
||||||
|
registry: docker.io
|
||||||
username: ${{ secrets.DOCKER_HUB_USERNAME }}
|
username: ${{ secrets.DOCKER_HUB_USERNAME }}
|
||||||
password: ${{ secrets.DOCKER_HUB_ACCESS_TOKEN }}
|
password: ${{ secrets.DOCKER_HUB_ACCESS_TOKEN }}
|
||||||
|
|
||||||
- name: Extract tag name
|
- name: Extract tag name
|
||||||
id: get-tag
|
id: get-tag
|
||||||
run: echo "TAG=${GITHUB_REF#refs/tags/}" >> $GITHUB_ENV
|
run: echo "TAG=${GITHUB_REF#refs/tags/}" >> $GITHUB_ENV
|
||||||
|
shell: bash
|
||||||
|
|
||||||
- name: Install Go
|
- name: Install Go
|
||||||
uses: actions/setup-go@v6
|
uses: actions/setup-go@4dc6199c7b1a012772edbd06daecab0f50c9053c # v6.1.0
|
||||||
with:
|
with:
|
||||||
go-version: 1.24
|
go-version: 1.24
|
||||||
|
|
||||||
@@ -37,18 +65,21 @@ jobs:
|
|||||||
TAG=${{ env.TAG }}
|
TAG=${{ env.TAG }}
|
||||||
sed -i "s/export const APP_VERSION = \".*\";/export const APP_VERSION = \"$TAG\";/" server/lib/consts.ts
|
sed -i "s/export const APP_VERSION = \".*\";/export const APP_VERSION = \"$TAG\";/" server/lib/consts.ts
|
||||||
cat server/lib/consts.ts
|
cat server/lib/consts.ts
|
||||||
|
shell: bash
|
||||||
|
|
||||||
- name: Pull latest Gerbil version
|
- name: Pull latest Gerbil version
|
||||||
id: get-gerbil-tag
|
id: get-gerbil-tag
|
||||||
run: |
|
run: |
|
||||||
LATEST_TAG=$(curl -s https://api.github.com/repos/fosrl/gerbil/tags | jq -r '.[0].name')
|
LATEST_TAG=$(curl -s https://api.github.com/repos/fosrl/gerbil/tags | jq -r '.[0].name')
|
||||||
echo "LATEST_GERBIL_TAG=$LATEST_TAG" >> $GITHUB_ENV
|
echo "LATEST_GERBIL_TAG=$LATEST_TAG" >> $GITHUB_ENV
|
||||||
|
shell: bash
|
||||||
|
|
||||||
- name: Pull latest Badger version
|
- name: Pull latest Badger version
|
||||||
id: get-badger-tag
|
id: get-badger-tag
|
||||||
run: |
|
run: |
|
||||||
LATEST_TAG=$(curl -s https://api.github.com/repos/fosrl/badger/tags | jq -r '.[0].name')
|
LATEST_TAG=$(curl -s https://api.github.com/repos/fosrl/badger/tags | jq -r '.[0].name')
|
||||||
echo "LATEST_BADGER_TAG=$LATEST_TAG" >> $GITHUB_ENV
|
echo "LATEST_BADGER_TAG=$LATEST_TAG" >> $GITHUB_ENV
|
||||||
|
shell: bash
|
||||||
|
|
||||||
- name: Update install/main.go
|
- name: Update install/main.go
|
||||||
run: |
|
run: |
|
||||||
@@ -60,6 +91,7 @@ jobs:
|
|||||||
sed -i "s/config.BadgerVersion = \".*\"/config.BadgerVersion = \"$BADGER_VERSION\"/" install/main.go
|
sed -i "s/config.BadgerVersion = \".*\"/config.BadgerVersion = \"$BADGER_VERSION\"/" install/main.go
|
||||||
echo "Updated install/main.go with Pangolin version $PANGOLIN_VERSION, Gerbil version $GERBIL_VERSION, and Badger version $BADGER_VERSION"
|
echo "Updated install/main.go with Pangolin version $PANGOLIN_VERSION, Gerbil version $GERBIL_VERSION, and Badger version $BADGER_VERSION"
|
||||||
cat install/main.go
|
cat install/main.go
|
||||||
|
shell: bash
|
||||||
|
|
||||||
- name: Build installer
|
- name: Build installer
|
||||||
working-directory: install
|
working-directory: install
|
||||||
@@ -67,12 +99,89 @@ jobs:
|
|||||||
make go-build-release
|
make go-build-release
|
||||||
|
|
||||||
- name: Upload artifacts from /install/bin
|
- name: Upload artifacts from /install/bin
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
|
||||||
with:
|
with:
|
||||||
name: install-bin
|
name: install-bin
|
||||||
path: install/bin/
|
path: install/bin/
|
||||||
|
|
||||||
- name: Build and push Docker images
|
- name: Build and push Docker images (Docker Hub)
|
||||||
run: |
|
run: |
|
||||||
TAG=${{ env.TAG }}
|
TAG=${{ env.TAG }}
|
||||||
make build-release tag=$TAG
|
make build-release tag=$TAG
|
||||||
|
echo "Built & pushed to: ${{ env.DOCKERHUB_IMAGE }}:${TAG}"
|
||||||
|
shell: bash
|
||||||
|
|
||||||
|
- name: Install skopeo + jq
|
||||||
|
# skopeo: copy/inspect images between registries
|
||||||
|
# jq: JSON parsing tool used to extract digest values
|
||||||
|
run: |
|
||||||
|
sudo apt-get update -y
|
||||||
|
sudo apt-get install -y skopeo jq
|
||||||
|
skopeo --version
|
||||||
|
shell: bash
|
||||||
|
|
||||||
|
- name: Login to GHCR
|
||||||
|
run: |
|
||||||
|
skopeo login ghcr.io -u "${{ github.actor }}" -p "${{ secrets.GITHUB_TOKEN }}"
|
||||||
|
shell: bash
|
||||||
|
|
||||||
|
- name: Copy tag from Docker Hub to GHCR
|
||||||
|
# Mirror the already-built image (all architectures) to GHCR so we can sign it
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
TAG=${{ env.TAG }}
|
||||||
|
echo "Copying ${{ env.DOCKERHUB_IMAGE }}:${TAG} -> ${{ env.GHCR_IMAGE }}:${TAG}"
|
||||||
|
skopeo copy --all --retry-times 3 \
|
||||||
|
docker://$DOCKERHUB_IMAGE:$TAG \
|
||||||
|
docker://$GHCR_IMAGE:$TAG
|
||||||
|
shell: bash
|
||||||
|
|
||||||
|
- name: Login to GitHub Container Registry (for cosign)
|
||||||
|
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
|
||||||
|
with:
|
||||||
|
registry: ghcr.io
|
||||||
|
username: ${{ github.actor }}
|
||||||
|
password: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
|
- name: Install cosign
|
||||||
|
# cosign is used to sign and verify container images (key and keyless)
|
||||||
|
uses: sigstore/cosign-installer@faadad0cce49287aee09b3a48701e75088a2c6ad # v4.0.0
|
||||||
|
|
||||||
|
- name: Dual-sign and verify (GHCR & Docker Hub)
|
||||||
|
# Sign each image by digest using keyless (OIDC) and key-based signing,
|
||||||
|
# then verify both the public key signature and the keyless OIDC signature.
|
||||||
|
env:
|
||||||
|
TAG: ${{ env.TAG }}
|
||||||
|
COSIGN_PRIVATE_KEY: ${{ secrets.COSIGN_PRIVATE_KEY }}
|
||||||
|
COSIGN_PASSWORD: ${{ secrets.COSIGN_PASSWORD }}
|
||||||
|
COSIGN_PUBLIC_KEY: ${{ secrets.COSIGN_PUBLIC_KEY }}
|
||||||
|
COSIGN_YES: "true"
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
issuer="https://token.actions.githubusercontent.com"
|
||||||
|
id_regex="^https://github.com/${{ github.repository }}/.+" # accept this repo (all workflows/refs)
|
||||||
|
|
||||||
|
for IMAGE in "${GHCR_IMAGE}" "${DOCKERHUB_IMAGE}"; do
|
||||||
|
echo "Processing ${IMAGE}:${TAG}"
|
||||||
|
|
||||||
|
DIGEST="$(skopeo inspect --retry-times 3 docker://${IMAGE}:${TAG} | jq -r '.Digest')"
|
||||||
|
REF="${IMAGE}@${DIGEST}"
|
||||||
|
echo "Resolved digest: ${REF}"
|
||||||
|
|
||||||
|
echo "==> cosign sign (keyless) --recursive ${REF}"
|
||||||
|
cosign sign --recursive "${REF}"
|
||||||
|
|
||||||
|
echo "==> cosign sign (key) --recursive ${REF}"
|
||||||
|
cosign sign --key env://COSIGN_PRIVATE_KEY --recursive "${REF}"
|
||||||
|
|
||||||
|
echo "==> cosign verify (public key) ${REF}"
|
||||||
|
cosign verify --key env://COSIGN_PUBLIC_KEY "${REF}" -o text
|
||||||
|
|
||||||
|
echo "==> cosign verify (keyless policy) ${REF}"
|
||||||
|
cosign verify \
|
||||||
|
--certificate-oidc-issuer "${issuer}" \
|
||||||
|
--certificate-identity-regexp "${id_regex}" \
|
||||||
|
"${REF}" -o text
|
||||||
|
done
|
||||||
|
shell: bash
|
||||||
|
|||||||
9
.github/workflows/linting.yml
vendored
9
.github/workflows/linting.yml
vendored
@@ -1,5 +1,8 @@
|
|||||||
name: ESLint
|
name: ESLint
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
on:
|
on:
|
||||||
pull_request:
|
pull_request:
|
||||||
paths:
|
paths:
|
||||||
@@ -18,10 +21,10 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@v5
|
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
|
||||||
|
|
||||||
- name: Set up Node.js
|
- name: Set up Node.js
|
||||||
uses: actions/setup-node@v5
|
uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
|
||||||
with:
|
with:
|
||||||
node-version: '22'
|
node-version: '22'
|
||||||
|
|
||||||
@@ -32,4 +35,4 @@ jobs:
|
|||||||
run: npm run set:oss
|
run: npm run set:oss
|
||||||
|
|
||||||
- name: Run ESLint
|
- name: Run ESLint
|
||||||
run: npx eslint . --ext .js,.jsx,.ts,.tsx
|
run: npx eslint . --ext .js,.jsx,.ts,.tsx
|
||||||
|
|||||||
132
.github/workflows/mirror.yaml
vendored
Normal file
132
.github/workflows/mirror.yaml
vendored
Normal file
@@ -0,0 +1,132 @@
|
|||||||
|
name: Mirror & Sign (Docker Hub to GHCR)
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_dispatch: {}
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
packages: write
|
||||||
|
id-token: write # for keyless OIDC
|
||||||
|
|
||||||
|
env:
|
||||||
|
SOURCE_IMAGE: docker.io/fosrl/pangolin
|
||||||
|
DEST_IMAGE: ghcr.io/${{ github.repository_owner }}/${{ github.event.repository.name }}
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
mirror-and-dual-sign:
|
||||||
|
runs-on: amd64-runner
|
||||||
|
steps:
|
||||||
|
- name: Install skopeo + jq
|
||||||
|
run: |
|
||||||
|
sudo apt-get update -y
|
||||||
|
sudo apt-get install -y skopeo jq
|
||||||
|
skopeo --version
|
||||||
|
|
||||||
|
- name: Install cosign
|
||||||
|
uses: sigstore/cosign-installer@faadad0cce49287aee09b3a48701e75088a2c6ad # v4.0.0
|
||||||
|
|
||||||
|
- name: Input check
|
||||||
|
run: |
|
||||||
|
test -n "${SOURCE_IMAGE}" || (echo "SOURCE_IMAGE is empty" && exit 1)
|
||||||
|
echo "Source : ${SOURCE_IMAGE}"
|
||||||
|
echo "Target : ${DEST_IMAGE}"
|
||||||
|
|
||||||
|
# Auth for skopeo (containers-auth)
|
||||||
|
- name: Skopeo login to GHCR
|
||||||
|
run: |
|
||||||
|
skopeo login ghcr.io -u "${{ github.actor }}" -p "${{ secrets.GITHUB_TOKEN }}"
|
||||||
|
|
||||||
|
# Auth for cosign (docker-config)
|
||||||
|
- name: Docker login to GHCR (for cosign)
|
||||||
|
run: |
|
||||||
|
echo "${{ secrets.GITHUB_TOKEN }}" | docker login ghcr.io -u "${{ github.actor }}" --password-stdin
|
||||||
|
|
||||||
|
- name: List source tags
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
skopeo list-tags --retry-times 3 docker://"${SOURCE_IMAGE}" \
|
||||||
|
| jq -r '.Tags[]' | sort -u > src-tags.txt
|
||||||
|
echo "Found source tags: $(wc -l < src-tags.txt)"
|
||||||
|
head -n 20 src-tags.txt || true
|
||||||
|
|
||||||
|
- name: List destination tags (skip existing)
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
if skopeo list-tags --retry-times 3 docker://"${DEST_IMAGE}" >/tmp/dst.json 2>/dev/null; then
|
||||||
|
jq -r '.Tags[]' /tmp/dst.json | sort -u > dst-tags.txt
|
||||||
|
else
|
||||||
|
: > dst-tags.txt
|
||||||
|
fi
|
||||||
|
echo "Existing destination tags: $(wc -l < dst-tags.txt)"
|
||||||
|
|
||||||
|
- name: Mirror, dual-sign, and verify
|
||||||
|
env:
|
||||||
|
# keyless
|
||||||
|
COSIGN_YES: "true"
|
||||||
|
# key-based
|
||||||
|
COSIGN_PRIVATE_KEY: ${{ secrets.COSIGN_PRIVATE_KEY }}
|
||||||
|
COSIGN_PASSWORD: ${{ secrets.COSIGN_PASSWORD }}
|
||||||
|
# verify
|
||||||
|
COSIGN_PUBLIC_KEY: ${{ secrets.COSIGN_PUBLIC_KEY }}
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
copied=0; skipped=0; v_ok=0; errs=0
|
||||||
|
|
||||||
|
issuer="https://token.actions.githubusercontent.com"
|
||||||
|
id_regex="^https://github.com/${{ github.repository }}/.+"
|
||||||
|
|
||||||
|
while read -r tag; do
|
||||||
|
[ -z "$tag" ] && continue
|
||||||
|
|
||||||
|
if grep -Fxq "$tag" dst-tags.txt; then
|
||||||
|
echo "::notice ::Skip (exists) ${DEST_IMAGE}:${tag}"
|
||||||
|
skipped=$((skipped+1))
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "==> Copy ${SOURCE_IMAGE}:${tag} → ${DEST_IMAGE}:${tag}"
|
||||||
|
if ! skopeo copy --all --retry-times 3 \
|
||||||
|
docker://"${SOURCE_IMAGE}:${tag}" docker://"${DEST_IMAGE}:${tag}"; then
|
||||||
|
echo "::warning title=Copy failed::${SOURCE_IMAGE}:${tag}"
|
||||||
|
errs=$((errs+1)); continue
|
||||||
|
fi
|
||||||
|
copied=$((copied+1))
|
||||||
|
|
||||||
|
digest="$(skopeo inspect --retry-times 3 docker://"${DEST_IMAGE}:${tag}" | jq -r '.Digest')"
|
||||||
|
ref="${DEST_IMAGE}@${digest}"
|
||||||
|
|
||||||
|
echo "==> cosign sign (keyless) --recursive ${ref}"
|
||||||
|
if ! cosign sign --recursive "${ref}"; then
|
||||||
|
echo "::warning title=Keyless sign failed::${ref}"
|
||||||
|
errs=$((errs+1))
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "==> cosign sign (key) --recursive ${ref}"
|
||||||
|
if ! cosign sign --key env://COSIGN_PRIVATE_KEY --recursive "${ref}"; then
|
||||||
|
echo "::warning title=Key sign failed::${ref}"
|
||||||
|
errs=$((errs+1))
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "==> cosign verify (public key) ${ref}"
|
||||||
|
if ! cosign verify --key env://COSIGN_PUBLIC_KEY "${ref}" -o text; then
|
||||||
|
echo "::warning title=Verify(pubkey) failed::${ref}"
|
||||||
|
errs=$((errs+1))
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "==> cosign verify (keyless policy) ${ref}"
|
||||||
|
if ! cosign verify \
|
||||||
|
--certificate-oidc-issuer "${issuer}" \
|
||||||
|
--certificate-identity-regexp "${id_regex}" \
|
||||||
|
"${ref}" -o text; then
|
||||||
|
echo "::warning title=Verify(keyless) failed::${ref}"
|
||||||
|
errs=$((errs+1))
|
||||||
|
else
|
||||||
|
v_ok=$((v_ok+1))
|
||||||
|
fi
|
||||||
|
done < src-tags.txt
|
||||||
|
|
||||||
|
echo "---- Summary ----"
|
||||||
|
echo "Copied : $copied"
|
||||||
|
echo "Skipped : $skipped"
|
||||||
|
echo "Verified OK : $v_ok"
|
||||||
|
echo "Errors : $errs"
|
||||||
4
.github/workflows/stale-bot.yml
vendored
4
.github/workflows/stale-bot.yml
vendored
@@ -14,7 +14,7 @@ jobs:
|
|||||||
stale:
|
stale:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/stale@v10
|
- uses: actions/stale@5f858e3efba33a5ca4407a664cc011ad407f2008 # v10.1.0
|
||||||
with:
|
with:
|
||||||
days-before-stale: 14
|
days-before-stale: 14
|
||||||
days-before-close: 14
|
days-before-close: 14
|
||||||
@@ -34,4 +34,4 @@ jobs:
|
|||||||
operations-per-run: 100
|
operations-per-run: 100
|
||||||
remove-stale-when-updated: true
|
remove-stale-when-updated: true
|
||||||
delete-branch: false
|
delete-branch: false
|
||||||
enable-statistics: true
|
enable-statistics: true
|
||||||
|
|||||||
10
.github/workflows/test.yml
vendored
10
.github/workflows/test.yml
vendored
@@ -1,5 +1,8 @@
|
|||||||
name: Run Tests
|
name: Run Tests
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
on:
|
on:
|
||||||
pull_request:
|
pull_request:
|
||||||
branches:
|
branches:
|
||||||
@@ -11,9 +14,9 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v5
|
- uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
|
||||||
|
|
||||||
- uses: actions/setup-node@v5
|
- uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
|
||||||
with:
|
with:
|
||||||
node-version: '22'
|
node-version: '22'
|
||||||
|
|
||||||
@@ -35,6 +38,9 @@ jobs:
|
|||||||
- name: Apply database migrations
|
- name: Apply database migrations
|
||||||
run: npm run db:sqlite:push
|
run: npm run db:sqlite:push
|
||||||
|
|
||||||
|
- name: Test with tsc
|
||||||
|
run: npx tsc --noEmit
|
||||||
|
|
||||||
- name: Start app in background
|
- name: Start app in background
|
||||||
run: nohup npm run dev &
|
run: nohup npm run dev &
|
||||||
|
|
||||||
|
|||||||
5
.gitignore
vendored
5
.gitignore
vendored
@@ -47,4 +47,7 @@ server/db/index.ts
|
|||||||
server/build.ts
|
server/build.ts
|
||||||
postgres/
|
postgres/
|
||||||
dynamic/
|
dynamic/
|
||||||
*.mmdb
|
*.mmdb
|
||||||
|
scratch/
|
||||||
|
tsconfig.json
|
||||||
|
hydrateSaas.ts
|
||||||
@@ -4,7 +4,7 @@ Contributions are welcome!
|
|||||||
|
|
||||||
Please see the contribution and local development guide on the docs page before getting started:
|
Please see the contribution and local development guide on the docs page before getting started:
|
||||||
|
|
||||||
https://docs.digpangolin.com/development/contributing
|
https://docs.pangolin.net/development/contributing
|
||||||
|
|
||||||
### Licensing Considerations
|
### Licensing Considerations
|
||||||
|
|
||||||
|
|||||||
36
Dockerfile
36
Dockerfile
@@ -1,10 +1,12 @@
|
|||||||
FROM node:22-alpine AS builder
|
FROM node:24-alpine AS builder
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
ARG BUILD=oss
|
ARG BUILD=oss
|
||||||
ARG DATABASE=sqlite
|
ARG DATABASE=sqlite
|
||||||
|
|
||||||
|
RUN apk add --no-cache curl tzdata python3 make g++
|
||||||
|
|
||||||
# COPY package.json package-lock.json ./
|
# COPY package.json package-lock.json ./
|
||||||
COPY package*.json ./
|
COPY package*.json ./
|
||||||
RUN npm ci
|
RUN npm ci
|
||||||
@@ -12,20 +14,42 @@ RUN npm ci
|
|||||||
COPY . .
|
COPY . .
|
||||||
|
|
||||||
RUN echo "export * from \"./$DATABASE\";" > server/db/index.ts
|
RUN echo "export * from \"./$DATABASE\";" > server/db/index.ts
|
||||||
|
RUN echo "export const driver: \"pg\" | \"sqlite\" = \"$DATABASE\";" >> server/db/index.ts
|
||||||
|
|
||||||
RUN echo "export const build = \"$BUILD\" as any;" > server/build.ts
|
RUN echo "export const build = \"$BUILD\" as \"saas\" | \"enterprise\" | \"oss\";" > server/build.ts
|
||||||
|
|
||||||
RUN if [ "$DATABASE" = "pg" ]; then npx drizzle-kit generate --dialect postgresql --schema ./server/db/pg/schema.ts --out init; else npx drizzle-kit generate --dialect $DATABASE --schema ./server/db/$DATABASE/schema.ts --out init; fi
|
# Copy the appropriate TypeScript configuration based on build type
|
||||||
|
RUN if [ "$BUILD" = "oss" ]; then cp tsconfig.oss.json tsconfig.json; \
|
||||||
|
elif [ "$BUILD" = "saas" ]; then cp tsconfig.saas.json tsconfig.json; \
|
||||||
|
elif [ "$BUILD" = "enterprise" ]; then cp tsconfig.enterprise.json tsconfig.json; \
|
||||||
|
fi
|
||||||
|
|
||||||
|
# if the build is oss then remove the server/private directory
|
||||||
|
RUN if [ "$BUILD" = "oss" ]; then rm -rf server/private; fi
|
||||||
|
|
||||||
|
RUN if [ "$DATABASE" = "pg" ]; then npx drizzle-kit generate --dialect postgresql --schema ./server/db/pg/schema --out init; else npx drizzle-kit generate --dialect $DATABASE --schema ./server/db/$DATABASE/schema --out init; fi
|
||||||
|
|
||||||
|
RUN mkdir -p dist
|
||||||
|
RUN npm run next:build
|
||||||
|
RUN node esbuild.mjs -e server/index.ts -o dist/server.mjs -b $BUILD
|
||||||
|
RUN if [ "$DATABASE" = "pg" ]; then \
|
||||||
|
node esbuild.mjs -e server/setup/migrationsPg.ts -o dist/migrations.mjs; \
|
||||||
|
else \
|
||||||
|
node esbuild.mjs -e server/setup/migrationsSqlite.ts -o dist/migrations.mjs; \
|
||||||
|
fi
|
||||||
|
|
||||||
|
# test to make sure the build output is there and error if not
|
||||||
|
RUN test -f dist/server.mjs
|
||||||
|
|
||||||
RUN npm run build:$DATABASE
|
|
||||||
RUN npm run build:cli
|
RUN npm run build:cli
|
||||||
|
|
||||||
FROM node:22-alpine AS runner
|
FROM node:24-alpine AS runner
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
# Curl used for the health checks
|
# Curl used for the health checks
|
||||||
RUN apk add --no-cache curl tzdata
|
# Python and build tools needed for better-sqlite3 native compilation
|
||||||
|
RUN apk add --no-cache curl tzdata python3 make g++
|
||||||
|
|
||||||
# COPY package.json package-lock.json ./
|
# COPY package.json package-lock.json ./
|
||||||
COPY package*.json ./
|
COPY package*.json ./
|
||||||
|
|||||||
50
Makefile
50
Makefile
@@ -8,6 +8,7 @@ build-release:
|
|||||||
exit 1; \
|
exit 1; \
|
||||||
fi
|
fi
|
||||||
docker buildx build \
|
docker buildx build \
|
||||||
|
--build-arg BUILD=oss \
|
||||||
--build-arg DATABASE=sqlite \
|
--build-arg DATABASE=sqlite \
|
||||||
--platform linux/arm64,linux/amd64 \
|
--platform linux/arm64,linux/amd64 \
|
||||||
--tag fosrl/pangolin:latest \
|
--tag fosrl/pangolin:latest \
|
||||||
@@ -16,6 +17,7 @@ build-release:
|
|||||||
--tag fosrl/pangolin:$(tag) \
|
--tag fosrl/pangolin:$(tag) \
|
||||||
--push .
|
--push .
|
||||||
docker buildx build \
|
docker buildx build \
|
||||||
|
--build-arg BUILD=oss \
|
||||||
--build-arg DATABASE=pg \
|
--build-arg DATABASE=pg \
|
||||||
--platform linux/arm64,linux/amd64 \
|
--platform linux/arm64,linux/amd64 \
|
||||||
--tag fosrl/pangolin:postgresql-latest \
|
--tag fosrl/pangolin:postgresql-latest \
|
||||||
@@ -23,6 +25,54 @@ build-release:
|
|||||||
--tag fosrl/pangolin:postgresql-$(minor_tag) \
|
--tag fosrl/pangolin:postgresql-$(minor_tag) \
|
||||||
--tag fosrl/pangolin:postgresql-$(tag) \
|
--tag fosrl/pangolin:postgresql-$(tag) \
|
||||||
--push .
|
--push .
|
||||||
|
docker buildx build \
|
||||||
|
--build-arg BUILD=enterprise \
|
||||||
|
--build-arg DATABASE=sqlite \
|
||||||
|
--platform linux/arm64,linux/amd64 \
|
||||||
|
--tag fosrl/pangolin:ee-latest \
|
||||||
|
--tag fosrl/pangolin:ee-$(major_tag) \
|
||||||
|
--tag fosrl/pangolin:ee-$(minor_tag) \
|
||||||
|
--tag fosrl/pangolin:ee-$(tag) \
|
||||||
|
--push .
|
||||||
|
docker buildx build \
|
||||||
|
--build-arg BUILD=enterprise \
|
||||||
|
--build-arg DATABASE=pg \
|
||||||
|
--platform linux/arm64,linux/amd64 \
|
||||||
|
--tag fosrl/pangolin:ee-postgresql-latest \
|
||||||
|
--tag fosrl/pangolin:ee-postgresql-$(major_tag) \
|
||||||
|
--tag fosrl/pangolin:ee-postgresql-$(minor_tag) \
|
||||||
|
--tag fosrl/pangolin:ee-postgresql-$(tag) \
|
||||||
|
--push .
|
||||||
|
|
||||||
|
build-rc:
|
||||||
|
@if [ -z "$(tag)" ]; then \
|
||||||
|
echo "Error: tag is required. Usage: make build-release tag=<tag>"; \
|
||||||
|
exit 1; \
|
||||||
|
fi
|
||||||
|
docker buildx build \
|
||||||
|
--build-arg BUILD=oss \
|
||||||
|
--build-arg DATABASE=sqlite \
|
||||||
|
--platform linux/arm64,linux/amd64 \
|
||||||
|
--tag fosrl/pangolin:$(tag) \
|
||||||
|
--push .
|
||||||
|
docker buildx build \
|
||||||
|
--build-arg BUILD=oss \
|
||||||
|
--build-arg DATABASE=pg \
|
||||||
|
--platform linux/arm64,linux/amd64 \
|
||||||
|
--tag fosrl/pangolin:postgresql-$(tag) \
|
||||||
|
--push .
|
||||||
|
docker buildx build \
|
||||||
|
--build-arg BUILD=enterprise \
|
||||||
|
--build-arg DATABASE=sqlite \
|
||||||
|
--platform linux/arm64,linux/amd64 \
|
||||||
|
--tag fosrl/pangolin:ee-$(tag) \
|
||||||
|
--push .
|
||||||
|
docker buildx build \
|
||||||
|
--build-arg BUILD=enterprise \
|
||||||
|
--build-arg DATABASE=pg \
|
||||||
|
--platform linux/arm64,linux/amd64 \
|
||||||
|
--tag fosrl/pangolin:ee-postgresql-$(tag) \
|
||||||
|
--push .
|
||||||
|
|
||||||
build-arm:
|
build-arm:
|
||||||
docker buildx build --platform linux/arm64 -t fosrl/pangolin:latest .
|
docker buildx build --platform linux/arm64 -t fosrl/pangolin:latest .
|
||||||
|
|||||||
150
README.md
150
README.md
@@ -1,157 +1,95 @@
|
|||||||
<div align="center">
|
<div align="center">
|
||||||
<h2>
|
<h2>
|
||||||
<picture>
|
<a href="https://pangolin.net/">
|
||||||
<source media="(prefers-color-scheme: dark)" srcset="public/logo/word_mark_white.png">
|
<picture>
|
||||||
<img alt="Pangolin Logo" src="public/logo/word_mark_black.png" width="250">
|
<source media="(prefers-color-scheme: dark)" srcset="public/logo/word_mark_white.png">
|
||||||
|
<img alt="Pangolin Logo" src="public/logo/word_mark_black.png" width="350">
|
||||||
</picture>
|
</picture>
|
||||||
|
</a>
|
||||||
</h2>
|
</h2>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<h4 align="center">Secure gateway to your private networks</h4>
|
|
||||||
<div align="center">
|
|
||||||
|
|
||||||
_Pangolin tunnels your services to the internet so you can access anything from anywhere._
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div align="center">
|
<div align="center">
|
||||||
<h5>
|
<h5>
|
||||||
<a href="https://digpangolin.com">
|
<a href="https://pangolin.net/">
|
||||||
Website
|
Website
|
||||||
</a>
|
</a>
|
||||||
<span> | </span>
|
<span> | </span>
|
||||||
<a href="https://docs.digpangolin.com/self-host/quick-install-managed">
|
<a href="https://docs.pangolin.net/">
|
||||||
Quick Install Guide
|
Documentation
|
||||||
</a>
|
</a>
|
||||||
<span> | </span>
|
<span> | </span>
|
||||||
<a href="mailto:contact@fossorial.io">
|
<a href="mailto:contact@pangolin.net">
|
||||||
Contact Us
|
Contact Us
|
||||||
</a>
|
</a>
|
||||||
<span> | </span>
|
|
||||||
<a href="https://digpangolin.com/slack">
|
|
||||||
Slack
|
|
||||||
</a>
|
|
||||||
<span> | </span>
|
|
||||||
<a href="https://discord.gg/HCJR8Xhme4">
|
|
||||||
Discord
|
|
||||||
</a>
|
|
||||||
</h5>
|
</h5>
|
||||||
|
</div>
|
||||||
|
|
||||||
[](https://digpangolin.com/slack)
|
<div align="center">
|
||||||
|
|
||||||
|
[](https://discord.gg/HCJR8Xhme4)
|
||||||
|
[](https://pangolin.net/slack)
|
||||||
[](https://hub.docker.com/r/fosrl/pangolin)
|
[](https://hub.docker.com/r/fosrl/pangolin)
|
||||||

|

|
||||||
[](https://discord.gg/HCJR8Xhme4)
|
|
||||||
[](https://www.youtube.com/@fossorial-app)
|
[](https://www.youtube.com/@fossorial-app)
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<p align="center">
|
<p align="center">
|
||||||
<strong>
|
<strong>
|
||||||
Start testing Pangolin at <a href="https://pangolin.fossorial.io/auth/signup">pangolin.fossorial.io</a>
|
Start testing Pangolin at <a href="https://app.pangolin.net/auth/signup">app.pangolin.net</a>
|
||||||
</strong>
|
</strong>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
Pangolin is a self-hosted tunneled reverse proxy server with identity and access control, designed to securely expose private resources on distributed networks. Acting as a central hub, it connects isolated networks — even those behind restrictive firewalls — through encrypted tunnels, enabling easy access to remote services without opening ports.
|
Pangolin is a self-hosted tunneled reverse proxy server with identity and context aware access control, designed to easily expose and protect applications running anywhere. Pangolin acts as a central hub and connects isolated networks — even those behind restrictive firewalls — through encrypted tunnels, enabling easy access to remote services without opening ports or requiring a VPN.
|
||||||
|
|
||||||
<img src="public/screenshots/hero.png" alt="Preview"/>
|
## Installation
|
||||||
|
|
||||||

|
- Check out the [quick install guide](https://docs.pangolin.net/self-host/quick-install) for how to install and set up Pangolin.
|
||||||
|
- Install from the [DigitalOcean marketplace](https://marketplace.digitalocean.com/apps/pangolin-ce-1?refcode=edf0480eeb81) for a one-click pre-configured installer.
|
||||||
|
|
||||||
## Key Features
|
<img src="public/screenshots/hero.png" />
|
||||||
|
|
||||||
### Reverse Proxy Through WireGuard Tunnel
|
|
||||||
|
|
||||||
- Expose private resources on your network **without opening ports** (firewall punching).
|
|
||||||
- Secure and easy to configure private connectivity via a custom **user space WireGuard client**, [Newt](https://github.com/fosrl/newt).
|
|
||||||
- Built-in support for any WireGuard client.
|
|
||||||
- Automated **SSL certificates** (https) via [LetsEncrypt](https://letsencrypt.org/).
|
|
||||||
- Support for HTTP/HTTPS and **raw TCP/UDP services**.
|
|
||||||
- Load balancing.
|
|
||||||
- Extend functionality with existing [Traefik](https://github.com/traefik/traefik) plugins, such as [CrowdSec](https://plugins.traefik.io/plugins/6335346ca4caa9ddeffda116/crowdsec-bouncer-traefik-plugin) and [Geoblock](https://github.com/PascalMinder/geoblock).
|
|
||||||
- **Automatically install and configure Crowdsec via Pangolin's installer script.**
|
|
||||||
- Attach as many sites to the central server as you wish.
|
|
||||||
|
|
||||||
### Identity & Access Management
|
|
||||||
|
|
||||||
- Centralized authentication system using platform SSO. **Users will only have to manage one login.**
|
|
||||||
- **Define access control rules for IPs, IP ranges, and URL paths per resource.**
|
|
||||||
- TOTP with backup codes for two-factor authentication.
|
|
||||||
- Create organizations, each with multiple sites, users, and roles.
|
|
||||||
- **Role-based access control** to manage resource access permissions.
|
|
||||||
- Additional authentication options include:
|
|
||||||
- Email whitelisting with **one-time passcodes.**
|
|
||||||
- **Temporary, self-destructing share links.**
|
|
||||||
- Resource specific pin codes.
|
|
||||||
- Resource specific passwords.
|
|
||||||
- Passkeys
|
|
||||||
- External identity provider (IdP) support with OAuth2/OIDC, such as Authentik, Keycloak, Okta, and others.
|
|
||||||
- Auto-provision users and roles from your IdP.
|
|
||||||
|
|
||||||
<img src="public/auth-diagram1.png" alt="Auth and diagram"/>
|
|
||||||
|
|
||||||
## Use Cases
|
|
||||||
|
|
||||||
### Manage Access to Internal Apps
|
|
||||||
|
|
||||||
- Grant users access to your apps from anywhere using just a web browser. No client software required.
|
|
||||||
|
|
||||||
### Developers and DevOps
|
|
||||||
|
|
||||||
- Expose and test internal tools and dashboards like **Grafana**. Bring localhost or private IPs online for easy access.
|
|
||||||
|
|
||||||
### Secure API Gateway
|
|
||||||
|
|
||||||
- One application load balancer across multiple clouds and on-premises.
|
|
||||||
|
|
||||||
### IoT and Edge Devices
|
|
||||||
|
|
||||||
- Easily expose **IoT devices**, **edge servers**, or **Raspberry Pi** to the internet for field equipment monitoring.
|
|
||||||
|
|
||||||
<img src="public/screenshots/sites.png" alt="Sites"/>
|
|
||||||
|
|
||||||
## Deployment Options
|
## Deployment Options
|
||||||
|
|
||||||
### Fully Self Hosted
|
| <img width=500 /> | Description |
|
||||||
|
|-----------------|--------------|
|
||||||
|
| **Self-Host: Community Edition** | Free, open source, and licensed under AGPL-3. |
|
||||||
|
| **Self-Host: Enterprise Edition** | Licensed under Fossorial Commercial License. Free for personal and hobbyist use, and for businesses earning under \$100K USD annually. |
|
||||||
|
| **Pangolin Cloud** | Fully managed service with instant setup and pay-as-you-go pricing — no infrastructure required. Or, self-host your own [remote node](https://docs.pangolin.net/manage/remote-node/nodes) and connect to our control plane. |
|
||||||
|
|
||||||
Host the full application on your own server or on the cloud with a VPS. Take a look at the [documentation](https://docs.digpangolin.com/self-host/quick-install) to get started.
|
## Key Features
|
||||||
|
|
||||||
> Many of our users have had a great experience with [RackNerd](https://my.racknerd.com/aff.php?aff=13788). Depending on promotions, you can get a [**VPS with 1 vCPU, 1GB RAM, and ~20GB SSD for just around $12/year**](https://my.racknerd.com/aff.php?aff=13788&pid=912). That's a great deal!
|
Pangolin packages everything you need for seamless application access and exposure into one cohesive platform.
|
||||||
|
|
||||||
### Pangolin Cloud
|
| <img width=500 /> | <img width=500 /> |
|
||||||
|
|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|--------------------------------------------------------------------|
|
||||||
|
| **Manage applications in one place**<br /><br /> Pangolin provides a unified dashboard where you can monitor, configure, and secure all of your services regardless of where they are hosted. | <img src="public/screenshots/hero.png" width=500 /><tr></tr> |
|
||||||
|
| **Reverse proxy across networks anywhere**<br /><br />Route traffic via tunnels to any private network. Pangolin works like a reverse proxy that spans multiple networks and handles routing, load balancing, health checking, and more to the right services on the other end. | <img src="public/screenshots/sites.png" width=500 /><tr></tr> |
|
||||||
|
| **Enforce identity and context aware rules**<br /><br />Protect your applications with identity and context aware rules such as SSO, OIDC, PIN, password, temporary share links, geolocation, IP, and more. | <img src="public/auth-diagram1.png" width=500 /><tr></tr> |
|
||||||
|
| **Quickly connect Pangolin sites**<br /><br />Pangolin's lightweight [Newt](https://github.com/fosrl/newt) client runs in userspace and can run anywhere. Use it as a site connector to route traffic to backends across all of your environments. | <img src="public/clip.gif" width=500 /><tr></tr> |
|
||||||
|
|
||||||
Easy to use with simple [pay as you go pricing](https://digpangolin.com/pricing). [Check it out here](https://pangolin.fossorial.io/auth/signup).
|
## Get Started
|
||||||
|
|
||||||
- Everything you get with self hosted Pangolin, but fully managed for you.
|
### Check out the docs
|
||||||
|
|
||||||
### Managed & High Availability
|
We encourage everyone to read the full documentation first, which is
|
||||||
|
available at [docs.pangolin.net](https://docs.pangolin.net). This README provides only a very brief subset of
|
||||||
|
the docs to illustrate some basic ideas.
|
||||||
|
|
||||||
Managed control plane, your infrastructure
|
### Sign up and try now
|
||||||
|
|
||||||
- We manage database and control plane.
|
For Pangolin's managed service, you will first need to create an account at
|
||||||
- You self-host lightweight exit-node.
|
[app.pangolin.net](https://app.pangolin.net). We have a generous free tier to get started.
|
||||||
- Traffic flows through your infra.
|
|
||||||
- We coordinate failover between your nodes or to Cloud when things go bad.
|
|
||||||
|
|
||||||
Try it out using [Pangolin Cloud](https://pangolin.fossorial.io)
|
|
||||||
|
|
||||||
### Full Enterprise On-Premises
|
|
||||||
|
|
||||||
[Contact us](mailto:numbat@fossorial.io) for a full distributed and enterprise deployments on your infrastructure controlled by your team.
|
|
||||||
|
|
||||||
## Project Development / Roadmap
|
|
||||||
|
|
||||||
We want to hear your feature requests! Add them to the [discussion board](https://github.com/orgs/fosrl/discussions/categories/feature-requests).
|
|
||||||
|
|
||||||
## Licensing
|
## Licensing
|
||||||
|
|
||||||
Pangolin is dual licensed under the AGPL-3 and the Fossorial Commercial license. For inquiries about commercial licensing, please contact us at [numbat@fossorial.io](mailto:numbat@fossorial.io).
|
Pangolin is dual licensed under the AGPL-3 and the [Fossorial Commercial License](https://pangolin.net/fcl.html). For inquiries about commercial licensing, please contact us at [contact@pangolin.net](mailto:contact@pangolin.net).
|
||||||
|
|
||||||
## Contributions
|
## Contributions
|
||||||
|
|
||||||
Looking for something to contribute? Take a look at issues marked with [help wanted](https://github.com/fosrl/pangolin/issues?q=is%3Aissue%20state%3Aopen%20label%3A%22help%20wanted%22). Also take a look through the freature requests in Discussions - any are available and some are marked as a good first issue.
|
|
||||||
|
|
||||||
Please see [CONTRIBUTING](./CONTRIBUTING.md) in the repository for guidelines and best practices.
|
Please see [CONTRIBUTING](./CONTRIBUTING.md) in the repository for guidelines and best practices.
|
||||||
|
|
||||||
Please post bug reports and other functional issues in the [Issues](https://github.com/fosrl/pangolin/issues) section of the repository.
|
---
|
||||||
|
|
||||||
If you are looking to help with translations, please contribute [on Crowdin](https://crowdin.com/project/fossorial-pangolin) or open a PR with changes to the translations files found in `messages/`.
|
WireGuard® is a registered trademark of Jason A. Donenfeld.
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
If you discover a security vulnerability, please follow the steps below to responsibly disclose it to us:
|
If you discover a security vulnerability, please follow the steps below to responsibly disclose it to us:
|
||||||
|
|
||||||
1. **Do not create a public GitHub issue or discussion post.** This could put the security of other users at risk.
|
1. **Do not create a public GitHub issue or discussion post.** This could put the security of other users at risk.
|
||||||
2. Send a detailed report to [security@fossorial.io](mailto:security@fossorial.io) or send a **private** message to a maintainer on [Discord](https://discord.gg/HCJR8Xhme4). Include:
|
2. Send a detailed report to [security@pangolin.net](mailto:security@pangolin.net) or send a **private** message to a maintainer on [Discord](https://discord.gg/HCJR8Xhme4). Include:
|
||||||
|
|
||||||
- Description and location of the vulnerability.
|
- Description and location of the vulnerability.
|
||||||
- Potential impact of the vulnerability.
|
- Potential impact of the vulnerability.
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import base64
|
|||||||
YAML_FILE_PATH = 'blueprint.yaml'
|
YAML_FILE_PATH = 'blueprint.yaml'
|
||||||
|
|
||||||
# The API endpoint and headers from the curl request
|
# The API endpoint and headers from the curl request
|
||||||
API_URL = 'http://api.pangolin.fossorial.io/v1/org/test/blueprint'
|
API_URL = 'http://api.pangolin.net/v1/org/test/blueprint'
|
||||||
HEADERS = {
|
HEADERS = {
|
||||||
'accept': '*/*',
|
'accept': '*/*',
|
||||||
'Authorization': 'Bearer <your_token_here>',
|
'Authorization': 'Bearer <your_token_here>',
|
||||||
|
|||||||
@@ -28,9 +28,10 @@ proxy-resources:
|
|||||||
# sso-roles:
|
# sso-roles:
|
||||||
# - Member
|
# - Member
|
||||||
# sso-users:
|
# sso-users:
|
||||||
# - owen@fossorial.io
|
# - owen@pangolin.net
|
||||||
# whitelist-users:
|
# whitelist-users:
|
||||||
# - owen@fossorial.io
|
# - owen@pangolin.net
|
||||||
|
# auto-login-idp: 1
|
||||||
headers:
|
headers:
|
||||||
- name: X-Example-Header
|
- name: X-Example-Header
|
||||||
value: example-value
|
value: example-value
|
||||||
|
|||||||
@@ -5,14 +5,14 @@ meta {
|
|||||||
}
|
}
|
||||||
|
|
||||||
post {
|
post {
|
||||||
url: http://localhost:4000/api/v1/auth/login
|
url: http://localhost:3000/api/v1/auth/login
|
||||||
body: json
|
body: json
|
||||||
auth: none
|
auth: none
|
||||||
}
|
}
|
||||||
|
|
||||||
body:json {
|
body:json {
|
||||||
{
|
{
|
||||||
"email": "owen@fossorial.io",
|
"email": "admin@fosrl.io",
|
||||||
"password": "Password123!"
|
"password": "Password123!"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,6 +12,6 @@ post {
|
|||||||
|
|
||||||
body:json {
|
body:json {
|
||||||
{
|
{
|
||||||
"email": "milo@fossorial.io"
|
"email": "milo@pangolin.net"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ put {
|
|||||||
|
|
||||||
body:json {
|
body:json {
|
||||||
{
|
{
|
||||||
"email": "numbat@fossorial.io",
|
"email": "numbat@pangolin.net",
|
||||||
"password": "Password123!"
|
"password": "Password123!"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
15
bruno/Olm/createOlm.bru
Normal file
15
bruno/Olm/createOlm.bru
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
meta {
|
||||||
|
name: createOlm
|
||||||
|
type: http
|
||||||
|
seq: 1
|
||||||
|
}
|
||||||
|
|
||||||
|
put {
|
||||||
|
url: http://localhost:3000/api/v1/olm
|
||||||
|
body: none
|
||||||
|
auth: inherit
|
||||||
|
}
|
||||||
|
|
||||||
|
settings {
|
||||||
|
encodeUrl: true
|
||||||
|
}
|
||||||
8
bruno/Olm/folder.bru
Normal file
8
bruno/Olm/folder.bru
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
meta {
|
||||||
|
name: Olm
|
||||||
|
seq: 15
|
||||||
|
}
|
||||||
|
|
||||||
|
auth {
|
||||||
|
mode: inherit
|
||||||
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"version": "1",
|
"version": "1",
|
||||||
"name": "Pangolin Saas",
|
"name": "Pangolin",
|
||||||
"type": "collection",
|
"type": "collection",
|
||||||
"ignore": [
|
"ignore": [
|
||||||
"node_modules",
|
"node_modules",
|
||||||
|
|||||||
@@ -90,7 +90,8 @@ export const setAdminCredentials: CommandModule<{}, SetAdminCredentialsArgs> = {
|
|||||||
passwordHash,
|
passwordHash,
|
||||||
dateCreated: moment().toISOString(),
|
dateCreated: moment().toISOString(),
|
||||||
serverAdmin: true,
|
serverAdmin: true,
|
||||||
emailVerified: true
|
emailVerified: true,
|
||||||
|
lastPasswordChange: new Date().getTime()
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log("Server admin created");
|
console.log("Server admin created");
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
# To see all available options, please visit the docs:
|
# To see all available options, please visit the docs:
|
||||||
# https://docs.digpangolin.com/self-host/advanced/config-file
|
# https://docs.pangolin.net/self-host/advanced/config-file
|
||||||
|
|
||||||
app:
|
app:
|
||||||
dashboard_url: http://localhost:3002
|
dashboard_url: http://localhost:3002
|
||||||
@@ -25,4 +25,3 @@ flags:
|
|||||||
disable_user_create_org: true
|
disable_user_create_org: true
|
||||||
allow_raw_resources: true
|
allow_raw_resources: true
|
||||||
enable_integration_api: true
|
enable_integration_api: true
|
||||||
enable_clients: true
|
|
||||||
|
|||||||
15
docker-compose.drizzle.yml
Normal file
15
docker-compose.drizzle.yml
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
services:
|
||||||
|
drizzle-gateway:
|
||||||
|
image: ghcr.io/drizzle-team/gateway:latest
|
||||||
|
ports:
|
||||||
|
- "4984:4983"
|
||||||
|
depends_on:
|
||||||
|
- db
|
||||||
|
environment:
|
||||||
|
- STORE_PATH=/app
|
||||||
|
- DATABASE_URL=postgresql://postgres:password@db:5432/postgres
|
||||||
|
volumes:
|
||||||
|
- drizzle-gateway-data:/app
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
drizzle-gateway-data:
|
||||||
@@ -20,7 +20,7 @@ services:
|
|||||||
pangolin:
|
pangolin:
|
||||||
condition: service_healthy
|
condition: service_healthy
|
||||||
command:
|
command:
|
||||||
- --reachableAt=http://gerbil:3003
|
- --reachableAt=http://gerbil:3004
|
||||||
- --generateAndSaveKeyTo=/var/config/key
|
- --generateAndSaveKeyTo=/var/config/key
|
||||||
- --remoteConfig=http://pangolin:3001/api/v1/
|
- --remoteConfig=http://pangolin:3001/api/v1/
|
||||||
volumes:
|
volumes:
|
||||||
@@ -35,7 +35,7 @@ services:
|
|||||||
- 80:80 # Port for traefik because of the network_mode
|
- 80:80 # Port for traefik because of the network_mode
|
||||||
|
|
||||||
traefik:
|
traefik:
|
||||||
image: traefik:v3.5
|
image: traefik:v3.6
|
||||||
container_name: traefik
|
container_name: traefik
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
network_mode: service:gerbil # Ports appear on the gerbil service
|
network_mode: service:gerbil # Ports appear on the gerbil service
|
||||||
@@ -52,4 +52,4 @@ networks:
|
|||||||
default:
|
default:
|
||||||
driver: bridge
|
driver: bridge
|
||||||
name: pangolin
|
name: pangolin
|
||||||
enable_ipv6: true
|
enable_ipv6: true
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ services:
|
|||||||
- ./config/postgres:/var/lib/postgresql/data
|
- ./config/postgres:/var/lib/postgresql/data
|
||||||
ports:
|
ports:
|
||||||
- "5432:5432" # Map host port 5432 to container port 5432
|
- "5432:5432" # Map host port 5432 to container port 5432
|
||||||
restart: no
|
restart: no
|
||||||
|
|
||||||
redis:
|
redis:
|
||||||
image: redis:latest # Use the latest Redis image
|
image: redis:latest # Use the latest Redis image
|
||||||
|
|||||||
@@ -1,16 +1,9 @@
|
|||||||
import { defineConfig } from "drizzle-kit";
|
import { defineConfig } from "drizzle-kit";
|
||||||
import path from "path";
|
import path from "path";
|
||||||
import { build } from "@server/build";
|
|
||||||
|
|
||||||
let schema;
|
const schema = [
|
||||||
if (build === "oss") {
|
path.join("server", "db", "pg", "schema"),
|
||||||
schema = [path.join("server", "db", "pg", "schema.ts")];
|
];
|
||||||
} else {
|
|
||||||
schema = [
|
|
||||||
path.join("server", "db", "pg", "schema.ts"),
|
|
||||||
path.join("server", "db", "pg", "privateSchema.ts")
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
dialect: "postgresql",
|
dialect: "postgresql",
|
||||||
|
|||||||
@@ -1,17 +1,10 @@
|
|||||||
import { build } from "@server/build";
|
|
||||||
import { APP_PATH } from "@server/lib/consts";
|
import { APP_PATH } from "@server/lib/consts";
|
||||||
import { defineConfig } from "drizzle-kit";
|
import { defineConfig } from "drizzle-kit";
|
||||||
import path from "path";
|
import path from "path";
|
||||||
|
|
||||||
let schema;
|
const schema = [
|
||||||
if (build === "oss") {
|
path.join("server", "db", "sqlite", "schema"),
|
||||||
schema = [path.join("server", "db", "sqlite", "schema.ts")];
|
];
|
||||||
} else {
|
|
||||||
schema = [
|
|
||||||
path.join("server", "db", "sqlite", "schema.ts"),
|
|
||||||
path.join("server", "db", "sqlite", "privateSchema.ts")
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
dialect: "sqlite",
|
dialect: "sqlite",
|
||||||
|
|||||||
210
esbuild.mjs
210
esbuild.mjs
@@ -2,8 +2,9 @@ import esbuild from "esbuild";
|
|||||||
import yargs from "yargs";
|
import yargs from "yargs";
|
||||||
import { hideBin } from "yargs/helpers";
|
import { hideBin } from "yargs/helpers";
|
||||||
import { nodeExternalsPlugin } from "esbuild-node-externals";
|
import { nodeExternalsPlugin } from "esbuild-node-externals";
|
||||||
|
import path from "path";
|
||||||
|
import fs from "fs";
|
||||||
// import { glob } from "glob";
|
// import { glob } from "glob";
|
||||||
// import path from "path";
|
|
||||||
|
|
||||||
const banner = `
|
const banner = `
|
||||||
// patch __dirname
|
// patch __dirname
|
||||||
@@ -18,7 +19,7 @@ const require = topLevelCreateRequire(import.meta.url);
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
const argv = yargs(hideBin(process.argv))
|
const argv = yargs(hideBin(process.argv))
|
||||||
.usage("Usage: $0 -entry [string] -out [string]")
|
.usage("Usage: $0 -entry [string] -out [string] -build [string]")
|
||||||
.option("entry", {
|
.option("entry", {
|
||||||
alias: "e",
|
alias: "e",
|
||||||
describe: "Entry point file",
|
describe: "Entry point file",
|
||||||
@@ -31,6 +32,13 @@ const argv = yargs(hideBin(process.argv))
|
|||||||
type: "string",
|
type: "string",
|
||||||
demandOption: true,
|
demandOption: true,
|
||||||
})
|
})
|
||||||
|
.option("build", {
|
||||||
|
alias: "b",
|
||||||
|
describe: "Build type (oss, saas, enterprise)",
|
||||||
|
type: "string",
|
||||||
|
choices: ["oss", "saas", "enterprise"],
|
||||||
|
default: "oss",
|
||||||
|
})
|
||||||
.help()
|
.help()
|
||||||
.alias("help", "h").argv;
|
.alias("help", "h").argv;
|
||||||
|
|
||||||
@@ -46,6 +54,179 @@ function getPackagePaths() {
|
|||||||
return ["package.json"];
|
return ["package.json"];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Plugin to guard against bad imports from #private
|
||||||
|
function privateImportGuardPlugin() {
|
||||||
|
return {
|
||||||
|
name: "private-import-guard",
|
||||||
|
setup(build) {
|
||||||
|
const violations = [];
|
||||||
|
|
||||||
|
build.onResolve({ filter: /^#private\// }, (args) => {
|
||||||
|
const importingFile = args.importer;
|
||||||
|
|
||||||
|
// Check if the importing file is NOT in server/private
|
||||||
|
const normalizedImporter = path.normalize(importingFile);
|
||||||
|
const isInServerPrivate = normalizedImporter.includes(path.normalize("server/private"));
|
||||||
|
|
||||||
|
if (!isInServerPrivate) {
|
||||||
|
const violation = {
|
||||||
|
file: importingFile,
|
||||||
|
importPath: args.path,
|
||||||
|
resolveDir: args.resolveDir
|
||||||
|
};
|
||||||
|
violations.push(violation);
|
||||||
|
|
||||||
|
console.log(`PRIVATE IMPORT VIOLATION:`);
|
||||||
|
console.log(` File: ${importingFile}`);
|
||||||
|
console.log(` Import: ${args.path}`);
|
||||||
|
console.log(` Resolve dir: ${args.resolveDir || 'N/A'}`);
|
||||||
|
console.log('');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return null to let the default resolver handle it
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
|
||||||
|
build.onEnd((result) => {
|
||||||
|
if (violations.length > 0) {
|
||||||
|
console.log(`\nSUMMARY: Found ${violations.length} private import violation(s):`);
|
||||||
|
violations.forEach((v, i) => {
|
||||||
|
console.log(` ${i + 1}. ${path.relative(process.cwd(), v.file)} imports ${v.importPath}`);
|
||||||
|
});
|
||||||
|
console.log('');
|
||||||
|
|
||||||
|
result.errors.push({
|
||||||
|
text: `Private import violations detected: ${violations.length} violation(s) found`,
|
||||||
|
location: null,
|
||||||
|
notes: violations.map(v => ({
|
||||||
|
text: `${path.relative(process.cwd(), v.file)} imports ${v.importPath}`,
|
||||||
|
location: null
|
||||||
|
}))
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Plugin to guard against bad imports from #private
|
||||||
|
function dynamicImportGuardPlugin() {
|
||||||
|
return {
|
||||||
|
name: "dynamic-import-guard",
|
||||||
|
setup(build) {
|
||||||
|
const violations = [];
|
||||||
|
|
||||||
|
build.onResolve({ filter: /^#dynamic\// }, (args) => {
|
||||||
|
const importingFile = args.importer;
|
||||||
|
|
||||||
|
// Check if the importing file is NOT in server/private
|
||||||
|
const normalizedImporter = path.normalize(importingFile);
|
||||||
|
const isInServerPrivate = normalizedImporter.includes(path.normalize("server/private"));
|
||||||
|
|
||||||
|
if (isInServerPrivate) {
|
||||||
|
const violation = {
|
||||||
|
file: importingFile,
|
||||||
|
importPath: args.path,
|
||||||
|
resolveDir: args.resolveDir
|
||||||
|
};
|
||||||
|
violations.push(violation);
|
||||||
|
|
||||||
|
console.log(`DYNAMIC IMPORT VIOLATION:`);
|
||||||
|
console.log(` File: ${importingFile}`);
|
||||||
|
console.log(` Import: ${args.path}`);
|
||||||
|
console.log(` Resolve dir: ${args.resolveDir || 'N/A'}`);
|
||||||
|
console.log('');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return null to let the default resolver handle it
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
|
||||||
|
build.onEnd((result) => {
|
||||||
|
if (violations.length > 0) {
|
||||||
|
console.log(`\nSUMMARY: Found ${violations.length} dynamic import violation(s):`);
|
||||||
|
violations.forEach((v, i) => {
|
||||||
|
console.log(` ${i + 1}. ${path.relative(process.cwd(), v.file)} imports ${v.importPath}`);
|
||||||
|
});
|
||||||
|
console.log('');
|
||||||
|
|
||||||
|
result.errors.push({
|
||||||
|
text: `Dynamic import violations detected: ${violations.length} violation(s) found`,
|
||||||
|
location: null,
|
||||||
|
notes: violations.map(v => ({
|
||||||
|
text: `${path.relative(process.cwd(), v.file)} imports ${v.importPath}`,
|
||||||
|
location: null
|
||||||
|
}))
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Plugin to dynamically switch imports based on build type
|
||||||
|
function dynamicImportSwitcherPlugin(buildValue) {
|
||||||
|
return {
|
||||||
|
name: "dynamic-import-switcher",
|
||||||
|
setup(build) {
|
||||||
|
const switches = [];
|
||||||
|
|
||||||
|
build.onStart(() => {
|
||||||
|
console.log(`Dynamic import switcher using build type: ${buildValue}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
build.onResolve({ filter: /^#dynamic\// }, (args) => {
|
||||||
|
// Extract the path after #dynamic/
|
||||||
|
const dynamicPath = args.path.replace(/^#dynamic\//, '');
|
||||||
|
|
||||||
|
// Determine the replacement based on build type
|
||||||
|
let replacement;
|
||||||
|
if (buildValue === "oss") {
|
||||||
|
replacement = `#open/${dynamicPath}`;
|
||||||
|
} else if (buildValue === "saas" || buildValue === "enterprise") {
|
||||||
|
replacement = `#closed/${dynamicPath}`; // We use #closed here so that the route guards dont complain after its been changed but this is the same as #private
|
||||||
|
} else {
|
||||||
|
console.warn(`Unknown build type '${buildValue}', defaulting to #open/`);
|
||||||
|
replacement = `#open/${dynamicPath}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const switchInfo = {
|
||||||
|
file: args.importer,
|
||||||
|
originalPath: args.path,
|
||||||
|
replacementPath: replacement,
|
||||||
|
buildType: buildValue
|
||||||
|
};
|
||||||
|
switches.push(switchInfo);
|
||||||
|
|
||||||
|
console.log(`DYNAMIC IMPORT SWITCH:`);
|
||||||
|
console.log(` File: ${args.importer}`);
|
||||||
|
console.log(` Original: ${args.path}`);
|
||||||
|
console.log(` Switched to: ${replacement} (build: ${buildValue})`);
|
||||||
|
console.log('');
|
||||||
|
|
||||||
|
// Rewrite the import path and let the normal resolution continue
|
||||||
|
return build.resolve(replacement, {
|
||||||
|
importer: args.importer,
|
||||||
|
namespace: args.namespace,
|
||||||
|
resolveDir: args.resolveDir,
|
||||||
|
kind: args.kind
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
build.onEnd((result) => {
|
||||||
|
if (switches.length > 0) {
|
||||||
|
console.log(`\nDYNAMIC IMPORT SUMMARY: Switched ${switches.length} import(s) for build type '${buildValue}':`);
|
||||||
|
switches.forEach((s, i) => {
|
||||||
|
console.log(` ${i + 1}. ${path.relative(process.cwd(), s.file)}`);
|
||||||
|
console.log(` ${s.originalPath} → ${s.replacementPath}`);
|
||||||
|
});
|
||||||
|
console.log('');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
esbuild
|
esbuild
|
||||||
.build({
|
.build({
|
||||||
entryPoints: [argv.entry],
|
entryPoints: [argv.entry],
|
||||||
@@ -59,6 +240,9 @@ esbuild
|
|||||||
platform: "node",
|
platform: "node",
|
||||||
external: ["body-parser"],
|
external: ["body-parser"],
|
||||||
plugins: [
|
plugins: [
|
||||||
|
privateImportGuardPlugin(),
|
||||||
|
dynamicImportGuardPlugin(),
|
||||||
|
dynamicImportSwitcherPlugin(argv.build),
|
||||||
nodeExternalsPlugin({
|
nodeExternalsPlugin({
|
||||||
packagePath: getPackagePaths(),
|
packagePath: getPackagePaths(),
|
||||||
}),
|
}),
|
||||||
@@ -66,7 +250,27 @@ esbuild
|
|||||||
sourcemap: "inline",
|
sourcemap: "inline",
|
||||||
target: "node22",
|
target: "node22",
|
||||||
})
|
})
|
||||||
.then(() => {
|
.then((result) => {
|
||||||
|
// Check if there were any errors in the build result
|
||||||
|
if (result.errors && result.errors.length > 0) {
|
||||||
|
console.error(`Build failed with ${result.errors.length} error(s):`);
|
||||||
|
result.errors.forEach((error, i) => {
|
||||||
|
console.error(`${i + 1}. ${error.text}`);
|
||||||
|
if (error.notes) {
|
||||||
|
error.notes.forEach(note => {
|
||||||
|
console.error(` - ${note.text}`);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// remove the output file if it was created
|
||||||
|
if (fs.existsSync(argv.out)) {
|
||||||
|
fs.unlinkSync(argv.out);
|
||||||
|
}
|
||||||
|
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
console.log("Build completed successfully");
|
console.log("Build completed successfully");
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
|
|||||||
@@ -18,7 +18,11 @@ put-back:
|
|||||||
mv main.go.bak main.go
|
mv main.go.bak main.go
|
||||||
|
|
||||||
dev-update-versions:
|
dev-update-versions:
|
||||||
PANGOLIN_VERSION=$$(curl -s https://api.github.com/repos/fosrl/pangolin/tags | jq -r '.[0].name') && \
|
if [ -z "$(tag)" ]; then \
|
||||||
|
PANGOLIN_VERSION=$$(curl -s https://api.github.com/repos/fosrl/pangolin/tags | jq -r '.[0].name'); \
|
||||||
|
else \
|
||||||
|
PANGOLIN_VERSION=$(tag); \
|
||||||
|
fi && \
|
||||||
GERBIL_VERSION=$$(curl -s https://api.github.com/repos/fosrl/gerbil/tags | jq -r '.[0].name') && \
|
GERBIL_VERSION=$$(curl -s https://api.github.com/repos/fosrl/gerbil/tags | jq -r '.[0].name') && \
|
||||||
BADGER_VERSION=$$(curl -s https://api.github.com/repos/fosrl/badger/tags | jq -r '.[0].name') && \
|
BADGER_VERSION=$$(curl -s https://api.github.com/repos/fosrl/badger/tags | jq -r '.[0].name') && \
|
||||||
echo "Latest versions - Pangolin: $$PANGOLIN_VERSION, Gerbil: $$GERBIL_VERSION, Badger: $$BADGER_VERSION" && \
|
echo "Latest versions - Pangolin: $$PANGOLIN_VERSION, Gerbil: $$GERBIL_VERSION, Badger: $$BADGER_VERSION" && \
|
||||||
|
|||||||
@@ -1,15 +1,10 @@
|
|||||||
# To see all available options, please visit the docs:
|
# To see all available options, please visit the docs:
|
||||||
# https://docs.digpangolin.com/self-host/advanced/config-file
|
# https://docs.pangolin.net/
|
||||||
|
|
||||||
gerbil:
|
gerbil:
|
||||||
start_port: 51820
|
start_port: 51820
|
||||||
base_endpoint: "{{.DashboardDomain}}"
|
base_endpoint: "{{.DashboardDomain}}"
|
||||||
{{if .HybridMode}}
|
|
||||||
managed:
|
|
||||||
id: "{{.HybridId}}"
|
|
||||||
secret: "{{.HybridSecret}}"
|
|
||||||
|
|
||||||
{{else}}
|
|
||||||
app:
|
app:
|
||||||
dashboard_url: "https://{{.DashboardDomain}}"
|
dashboard_url: "https://{{.DashboardDomain}}"
|
||||||
log_level: "info"
|
log_level: "info"
|
||||||
@@ -19,7 +14,6 @@ app:
|
|||||||
domains:
|
domains:
|
||||||
domain1:
|
domain1:
|
||||||
base_domain: "{{.BaseDomain}}"
|
base_domain: "{{.BaseDomain}}"
|
||||||
cert_resolver: "letsencrypt"
|
|
||||||
|
|
||||||
server:
|
server:
|
||||||
secret: "{{.Secret}}"
|
secret: "{{.Secret}}"
|
||||||
@@ -28,6 +22,7 @@ server:
|
|||||||
methods: ["GET", "POST", "PUT", "DELETE", "PATCH"]
|
methods: ["GET", "POST", "PUT", "DELETE", "PATCH"]
|
||||||
allowed_headers: ["X-CSRF-Token", "Content-Type"]
|
allowed_headers: ["X-CSRF-Token", "Content-Type"]
|
||||||
credentials: false
|
credentials: false
|
||||||
|
{{if .EnableGeoblocking}}maxmind_db_path: "./config/GeoLite2-Country.mmdb"{{end}}
|
||||||
{{if .EnableEmail}}
|
{{if .EnableEmail}}
|
||||||
email:
|
email:
|
||||||
smtp_host: "{{.EmailSMTPHost}}"
|
smtp_host: "{{.EmailSMTPHost}}"
|
||||||
@@ -41,4 +36,3 @@ flags:
|
|||||||
disable_signup_without_invite: true
|
disable_signup_without_invite: true
|
||||||
disable_user_create_org: false
|
disable_user_create_org: false
|
||||||
allow_raw_resources: true
|
allow_raw_resources: true
|
||||||
{{end}}
|
|
||||||
@@ -6,8 +6,6 @@ services:
|
|||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
volumes:
|
volumes:
|
||||||
- ./config:/app/config
|
- ./config:/app/config
|
||||||
- pangolin-data:/var/certificates
|
|
||||||
- pangolin-data:/var/dynamic
|
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: ["CMD", "curl", "-f", "http://localhost:3001/api/v1/"]
|
test: ["CMD", "curl", "-f", "http://localhost:3001/api/v1/"]
|
||||||
interval: "10s"
|
interval: "10s"
|
||||||
@@ -22,7 +20,7 @@ services:
|
|||||||
pangolin:
|
pangolin:
|
||||||
condition: service_healthy
|
condition: service_healthy
|
||||||
command:
|
command:
|
||||||
- --reachableAt=http://gerbil:3003
|
- --reachableAt=http://gerbil:3004
|
||||||
- --generateAndSaveKeyTo=/var/config/key
|
- --generateAndSaveKeyTo=/var/config/key
|
||||||
- --remoteConfig=http://pangolin:3001/api/v1/
|
- --remoteConfig=http://pangolin:3001/api/v1/
|
||||||
volumes:
|
volumes:
|
||||||
@@ -33,11 +31,11 @@ services:
|
|||||||
ports:
|
ports:
|
||||||
- 51820:51820/udp
|
- 51820:51820/udp
|
||||||
- 21820:21820/udp
|
- 21820:21820/udp
|
||||||
- 443:{{if .HybridMode}}8443{{else}}443{{end}}
|
- 443:443
|
||||||
- 80:80
|
- 80:80
|
||||||
{{end}}
|
{{end}}
|
||||||
traefik:
|
traefik:
|
||||||
image: docker.io/traefik:v3.5
|
image: docker.io/traefik:v3.6
|
||||||
container_name: traefik
|
container_name: traefik
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
{{if .InstallGerbil}}
|
{{if .InstallGerbil}}
|
||||||
@@ -56,15 +54,9 @@ services:
|
|||||||
- ./config/traefik:/etc/traefik:ro # Volume to store the Traefik configuration
|
- ./config/traefik:/etc/traefik:ro # Volume to store the Traefik configuration
|
||||||
- ./config/letsencrypt:/letsencrypt # Volume to store the Let's Encrypt certificates
|
- ./config/letsencrypt:/letsencrypt # Volume to store the Let's Encrypt certificates
|
||||||
- ./config/traefik/logs:/var/log/traefik # Volume to store Traefik logs
|
- ./config/traefik/logs:/var/log/traefik # Volume to store Traefik logs
|
||||||
# Shared volume for certificates and dynamic config in file mode
|
|
||||||
- pangolin-data:/var/certificates:ro
|
|
||||||
- pangolin-data:/var/dynamic:ro
|
|
||||||
|
|
||||||
networks:
|
networks:
|
||||||
default:
|
default:
|
||||||
driver: bridge
|
driver: bridge
|
||||||
name: pangolin
|
name: pangolin
|
||||||
{{if .EnableIPv6}} enable_ipv6: true{{end}}
|
{{if .EnableIPv6}} enable_ipv6: true{{end}}
|
||||||
|
|
||||||
volumes:
|
|
||||||
pangolin-data:
|
|
||||||
|
|||||||
@@ -51,3 +51,12 @@ http:
|
|||||||
loadBalancer:
|
loadBalancer:
|
||||||
servers:
|
servers:
|
||||||
- url: "http://pangolin:3000" # API/WebSocket server
|
- url: "http://pangolin:3000" # API/WebSocket server
|
||||||
|
|
||||||
|
tcp:
|
||||||
|
serversTransports:
|
||||||
|
pp-transport-v1:
|
||||||
|
proxyProtocol:
|
||||||
|
version: 1
|
||||||
|
pp-transport-v2:
|
||||||
|
proxyProtocol:
|
||||||
|
version: 2
|
||||||
@@ -3,17 +3,12 @@ api:
|
|||||||
dashboard: true
|
dashboard: true
|
||||||
|
|
||||||
providers:
|
providers:
|
||||||
{{if not .HybridMode}}
|
|
||||||
http:
|
http:
|
||||||
endpoint: "http://pangolin:3001/api/v1/traefik-config"
|
endpoint: "http://pangolin:3001/api/v1/traefik-config"
|
||||||
pollInterval: "5s"
|
pollInterval: "5s"
|
||||||
file:
|
file:
|
||||||
filename: "/etc/traefik/dynamic_config.yml"
|
filename: "/etc/traefik/dynamic_config.yml"
|
||||||
{{else}}
|
|
||||||
file:
|
|
||||||
directory: "/var/dynamic"
|
|
||||||
watch: true
|
|
||||||
{{end}}
|
|
||||||
experimental:
|
experimental:
|
||||||
plugins:
|
plugins:
|
||||||
badger:
|
badger:
|
||||||
@@ -27,7 +22,7 @@ log:
|
|||||||
maxBackups: 3
|
maxBackups: 3
|
||||||
maxAge: 3
|
maxAge: 3
|
||||||
compress: true
|
compress: true
|
||||||
{{if not .HybridMode}}
|
|
||||||
certificatesResolvers:
|
certificatesResolvers:
|
||||||
letsencrypt:
|
letsencrypt:
|
||||||
acme:
|
acme:
|
||||||
@@ -36,22 +31,18 @@ certificatesResolvers:
|
|||||||
email: "{{.LetsEncryptEmail}}"
|
email: "{{.LetsEncryptEmail}}"
|
||||||
storage: "/letsencrypt/acme.json"
|
storage: "/letsencrypt/acme.json"
|
||||||
caServer: "https://acme-v02.api.letsencrypt.org/directory"
|
caServer: "https://acme-v02.api.letsencrypt.org/directory"
|
||||||
{{end}}
|
|
||||||
entryPoints:
|
entryPoints:
|
||||||
web:
|
web:
|
||||||
address: ":80"
|
address: ":80"
|
||||||
websecure:
|
websecure:
|
||||||
address: ":443"
|
address: ":443"
|
||||||
{{if .HybridMode}} proxyProtocol:
|
|
||||||
trustedIPs:
|
|
||||||
- 0.0.0.0/0
|
|
||||||
- ::1/128{{end}}
|
|
||||||
transport:
|
transport:
|
||||||
respondingTimeouts:
|
respondingTimeouts:
|
||||||
readTimeout: "30m"
|
readTimeout: "30m"
|
||||||
{{if not .HybridMode}} http:
|
http:
|
||||||
tls:
|
tls:
|
||||||
certResolver: "letsencrypt"{{end}}
|
certResolver: "letsencrypt"
|
||||||
|
|
||||||
serversTransport:
|
serversTransport:
|
||||||
insecureSkipVerify: true
|
insecureSkipVerify: true
|
||||||
|
|||||||
@@ -73,7 +73,7 @@ func installDocker() error {
|
|||||||
case strings.Contains(osRelease, "ID=ubuntu"):
|
case strings.Contains(osRelease, "ID=ubuntu"):
|
||||||
installCmd = exec.Command("bash", "-c", fmt.Sprintf(`
|
installCmd = exec.Command("bash", "-c", fmt.Sprintf(`
|
||||||
apt-get update &&
|
apt-get update &&
|
||||||
apt-get install -y apt-transport-https ca-certificates curl software-properties-common &&
|
apt-get install -y apt-transport-https ca-certificates curl &&
|
||||||
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | gpg --dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg &&
|
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | gpg --dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg &&
|
||||||
echo "deb [arch=%s signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable" > /etc/apt/sources.list.d/docker.list &&
|
echo "deb [arch=%s signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable" > /etc/apt/sources.list.d/docker.list &&
|
||||||
apt-get update &&
|
apt-get update &&
|
||||||
@@ -82,7 +82,7 @@ func installDocker() error {
|
|||||||
case strings.Contains(osRelease, "ID=debian"):
|
case strings.Contains(osRelease, "ID=debian"):
|
||||||
installCmd = exec.Command("bash", "-c", fmt.Sprintf(`
|
installCmd = exec.Command("bash", "-c", fmt.Sprintf(`
|
||||||
apt-get update &&
|
apt-get update &&
|
||||||
apt-get install -y apt-transport-https ca-certificates curl software-properties-common &&
|
apt-get install -y apt-transport-https ca-certificates curl &&
|
||||||
curl -fsSL https://download.docker.com/linux/debian/gpg | gpg --dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg &&
|
curl -fsSL https://download.docker.com/linux/debian/gpg | gpg --dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg &&
|
||||||
echo "deb [arch=%s signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] https://download.docker.com/linux/debian $(lsb_release -cs) stable" > /etc/apt/sources.list.d/docker.list &&
|
echo "deb [arch=%s signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] https://download.docker.com/linux/debian $(lsb_release -cs) stable" > /etc/apt/sources.list.d/docker.list &&
|
||||||
apt-get update &&
|
apt-get update &&
|
||||||
|
|||||||
180
install/get-installer.sh
Normal file
180
install/get-installer.sh
Normal file
@@ -0,0 +1,180 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# Get installer - Cross-platform installation script
|
||||||
|
# Usage: curl -fsSL https://raw.githubusercontent.com/fosrl/installer/refs/heads/main/get-installer.sh | bash
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
# Colors for output
|
||||||
|
RED='\033[0;31m'
|
||||||
|
GREEN='\033[0;32m'
|
||||||
|
YELLOW='\033[1;33m'
|
||||||
|
NC='\033[0m' # No Color
|
||||||
|
|
||||||
|
# GitHub repository info
|
||||||
|
REPO="fosrl/pangolin"
|
||||||
|
GITHUB_API_URL="https://api.github.com/repos/${REPO}/releases/latest"
|
||||||
|
|
||||||
|
# Function to print colored output
|
||||||
|
print_status() {
|
||||||
|
echo -e "${GREEN}[INFO]${NC} $1"
|
||||||
|
}
|
||||||
|
|
||||||
|
print_warning() {
|
||||||
|
echo -e "${YELLOW}[WARN]${NC} $1"
|
||||||
|
}
|
||||||
|
|
||||||
|
print_error() {
|
||||||
|
echo -e "${RED}[ERROR]${NC} $1"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Function to get latest version from GitHub API
|
||||||
|
get_latest_version() {
|
||||||
|
local latest_info
|
||||||
|
|
||||||
|
if command -v curl >/dev/null 2>&1; then
|
||||||
|
latest_info=$(curl -fsSL "$GITHUB_API_URL" 2>/dev/null)
|
||||||
|
elif command -v wget >/dev/null 2>&1; then
|
||||||
|
latest_info=$(wget -qO- "$GITHUB_API_URL" 2>/dev/null)
|
||||||
|
else
|
||||||
|
print_error "Neither curl nor wget is available. Please install one of them." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -z "$latest_info" ]; then
|
||||||
|
print_error "Failed to fetch latest version information" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Extract version from JSON response (works without jq)
|
||||||
|
local version=$(echo "$latest_info" | grep '"tag_name"' | head -1 | sed 's/.*"tag_name": *"\([^"]*\)".*/\1/')
|
||||||
|
|
||||||
|
if [ -z "$version" ]; then
|
||||||
|
print_error "Could not parse version from GitHub API response" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Remove 'v' prefix if present
|
||||||
|
version=$(echo "$version" | sed 's/^v//')
|
||||||
|
|
||||||
|
echo "$version"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Detect OS and architecture
|
||||||
|
detect_platform() {
|
||||||
|
local os arch
|
||||||
|
|
||||||
|
# Detect OS - only support Linux
|
||||||
|
case "$(uname -s)" in
|
||||||
|
Linux*) os="linux" ;;
|
||||||
|
*)
|
||||||
|
print_error "Unsupported operating system: $(uname -s). Only Linux is supported."
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
# Detect architecture - only support amd64 and arm64
|
||||||
|
case "$(uname -m)" in
|
||||||
|
x86_64|amd64) arch="amd64" ;;
|
||||||
|
arm64|aarch64) arch="arm64" ;;
|
||||||
|
*)
|
||||||
|
print_error "Unsupported architecture: $(uname -m). Only amd64 and arm64 are supported on Linux."
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
echo "${os}_${arch}"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Get installation directory
|
||||||
|
get_install_dir() {
|
||||||
|
# Install to the current directory
|
||||||
|
local install_dir="$(pwd)"
|
||||||
|
if [ ! -d "$install_dir" ]; then
|
||||||
|
print_error "Installation directory does not exist: $install_dir"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
echo "$install_dir"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Download and install installer
|
||||||
|
install_installer() {
|
||||||
|
local platform="$1"
|
||||||
|
local install_dir="$2"
|
||||||
|
local binary_name="installer_${platform}"
|
||||||
|
|
||||||
|
local download_url="${BASE_URL}/${binary_name}"
|
||||||
|
local temp_file="/tmp/installer"
|
||||||
|
local final_path="${install_dir}/installer"
|
||||||
|
|
||||||
|
print_status "Downloading installer from ${download_url}"
|
||||||
|
|
||||||
|
# Download the binary
|
||||||
|
if command -v curl >/dev/null 2>&1; then
|
||||||
|
curl -fsSL "$download_url" -o "$temp_file"
|
||||||
|
elif command -v wget >/dev/null 2>&1; then
|
||||||
|
wget -q "$download_url" -O "$temp_file"
|
||||||
|
else
|
||||||
|
print_error "Neither curl nor wget is available. Please install one of them."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Create install directory if it doesn't exist
|
||||||
|
mkdir -p "$install_dir"
|
||||||
|
|
||||||
|
# Move binary to install directory
|
||||||
|
mv "$temp_file" "$final_path"
|
||||||
|
|
||||||
|
# Make executable
|
||||||
|
chmod +x "$final_path"
|
||||||
|
|
||||||
|
print_status "Installer downloaded to ${final_path}"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Verify installation
|
||||||
|
verify_installation() {
|
||||||
|
local install_dir="$1"
|
||||||
|
local installer_path="${install_dir}/installer"
|
||||||
|
|
||||||
|
if [ -f "$installer_path" ] && [ -x "$installer_path" ]; then
|
||||||
|
print_status "Installation successful!"
|
||||||
|
return 0
|
||||||
|
else
|
||||||
|
print_error "Installation failed. Binary not found or not executable."
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Main installation process
|
||||||
|
main() {
|
||||||
|
print_status "Installing latest version of installer..."
|
||||||
|
|
||||||
|
# Get latest version
|
||||||
|
print_status "Fetching latest version from GitHub..."
|
||||||
|
VERSION=$(get_latest_version)
|
||||||
|
print_status "Latest version: v${VERSION}"
|
||||||
|
|
||||||
|
# Set base URL with the fetched version
|
||||||
|
BASE_URL="https://github.com/${REPO}/releases/download/${VERSION}"
|
||||||
|
|
||||||
|
# Detect platform
|
||||||
|
PLATFORM=$(detect_platform)
|
||||||
|
print_status "Detected platform: ${PLATFORM}"
|
||||||
|
|
||||||
|
# Get install directory
|
||||||
|
INSTALL_DIR=$(get_install_dir)
|
||||||
|
print_status "Install directory: ${INSTALL_DIR}"
|
||||||
|
|
||||||
|
# Install installer
|
||||||
|
install_installer "$PLATFORM" "$INSTALL_DIR"
|
||||||
|
|
||||||
|
# Verify installation
|
||||||
|
if verify_installation "$INSTALL_DIR"; then
|
||||||
|
print_status "Installer is ready to use!"
|
||||||
|
else
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Run main function
|
||||||
|
main "$@"
|
||||||
@@ -3,8 +3,8 @@ module installer
|
|||||||
go 1.24.0
|
go 1.24.0
|
||||||
|
|
||||||
require (
|
require (
|
||||||
golang.org/x/term v0.35.0
|
golang.org/x/term v0.37.0
|
||||||
gopkg.in/yaml.v3 v3.0.1
|
gopkg.in/yaml.v3 v3.0.1
|
||||||
)
|
)
|
||||||
|
|
||||||
require golang.org/x/sys v0.36.0 // indirect
|
require golang.org/x/sys v0.38.0 // indirect
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
golang.org/x/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k=
|
golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc=
|
||||||
golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||||
golang.org/x/term v0.35.0 h1:bZBVKBudEyhRcajGcNc3jIfWPqV4y/Kt2XcoigOWtDQ=
|
golang.org/x/term v0.37.0 h1:8EGAD0qCmHYZg6J17DvsMy9/wJ7/D/4pV/wfnld5lTU=
|
||||||
golang.org/x/term v0.35.0/go.mod h1:TPGtkTLesOwf2DE8CgVYiZinHAOuy5AYUYT1lENIZnA=
|
golang.org/x/term v0.37.0/go.mod h1:5pB4lxRNYYVZuTLmy8oR2BH8dflOR+IbTYFD8fi3254=
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
|
|||||||
204
install/main.go
204
install/main.go
@@ -2,7 +2,6 @@ package main
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bufio"
|
"bufio"
|
||||||
"bytes"
|
|
||||||
"embed"
|
"embed"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
@@ -48,17 +47,15 @@ type Config struct {
|
|||||||
InstallGerbil bool
|
InstallGerbil bool
|
||||||
TraefikBouncerKey string
|
TraefikBouncerKey string
|
||||||
DoCrowdsecInstall bool
|
DoCrowdsecInstall bool
|
||||||
|
EnableGeoblocking bool
|
||||||
Secret string
|
Secret string
|
||||||
HybridMode bool
|
|
||||||
HybridId string
|
|
||||||
HybridSecret string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type SupportedContainer string
|
type SupportedContainer string
|
||||||
|
|
||||||
const (
|
const (
|
||||||
Docker SupportedContainer = "docker"
|
Docker SupportedContainer = "docker"
|
||||||
Podman SupportedContainer = "podman"
|
Podman SupportedContainer = "podman"
|
||||||
Undefined SupportedContainer = "undefined"
|
Undefined SupportedContainer = "undefined"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -98,24 +95,6 @@ func main() {
|
|||||||
|
|
||||||
fmt.Println("\n=== Generating Configuration Files ===")
|
fmt.Println("\n=== Generating Configuration Files ===")
|
||||||
|
|
||||||
// If the secret and id are not generated then generate them
|
|
||||||
if config.HybridMode && (config.HybridId == "" || config.HybridSecret == "") {
|
|
||||||
// fmt.Println("Requesting hybrid credentials from cloud...")
|
|
||||||
credentials, err := requestHybridCredentials()
|
|
||||||
if err != nil {
|
|
||||||
fmt.Printf("Error requesting hybrid credentials: %v\n", err)
|
|
||||||
fmt.Println("Please obtain credentials manually from the dashboard and run the installer again.")
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
config.HybridId = credentials.RemoteExitNodeId
|
|
||||||
config.HybridSecret = credentials.Secret
|
|
||||||
fmt.Printf("Your managed credentials have been obtained successfully.\n")
|
|
||||||
fmt.Printf(" ID: %s\n", config.HybridId)
|
|
||||||
fmt.Printf(" Secret: %s\n", config.HybridSecret)
|
|
||||||
fmt.Println("Take these to the Pangolin dashboard https://pangolin.fossorial.io to adopt your node.")
|
|
||||||
readBool(reader, "Have you adopted your node?", true)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := createConfigFiles(config); err != nil {
|
if err := createConfigFiles(config); err != nil {
|
||||||
fmt.Printf("Error creating config files: %v\n", err)
|
fmt.Printf("Error creating config files: %v\n", err)
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
@@ -125,6 +104,15 @@ func main() {
|
|||||||
|
|
||||||
fmt.Println("\nConfiguration files created successfully!")
|
fmt.Println("\nConfiguration files created successfully!")
|
||||||
|
|
||||||
|
// Download MaxMind database if requested
|
||||||
|
if config.EnableGeoblocking {
|
||||||
|
fmt.Println("\n=== Downloading MaxMind Database ===")
|
||||||
|
if err := downloadMaxMindDatabase(); err != nil {
|
||||||
|
fmt.Printf("Error downloading MaxMind database: %v\n", err)
|
||||||
|
fmt.Println("You can download it manually later if needed.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fmt.Println("\n=== Starting installation ===")
|
fmt.Println("\n=== Starting installation ===")
|
||||||
|
|
||||||
if readBool(reader, "Would you like to install and start the containers?", true) {
|
if readBool(reader, "Would you like to install and start the containers?", true) {
|
||||||
@@ -172,9 +160,34 @@ func main() {
|
|||||||
} else {
|
} else {
|
||||||
alreadyInstalled = true
|
alreadyInstalled = true
|
||||||
fmt.Println("Looks like you already installed Pangolin!")
|
fmt.Println("Looks like you already installed Pangolin!")
|
||||||
|
|
||||||
|
// Check if MaxMind database exists and offer to update it
|
||||||
|
fmt.Println("\n=== MaxMind Database Update ===")
|
||||||
|
if _, err := os.Stat("config/GeoLite2-Country.mmdb"); err == nil {
|
||||||
|
fmt.Println("MaxMind GeoLite2 Country database found.")
|
||||||
|
if readBool(reader, "Would you like to update the MaxMind database to the latest version?", false) {
|
||||||
|
if err := downloadMaxMindDatabase(); err != nil {
|
||||||
|
fmt.Printf("Error updating MaxMind database: %v\n", err)
|
||||||
|
fmt.Println("You can try updating it manually later if needed.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
fmt.Println("MaxMind GeoLite2 Country database not found.")
|
||||||
|
if readBool(reader, "Would you like to download the MaxMind GeoLite2 database for geoblocking functionality?", false) {
|
||||||
|
if err := downloadMaxMindDatabase(); err != nil {
|
||||||
|
fmt.Printf("Error downloading MaxMind database: %v\n", err)
|
||||||
|
fmt.Println("You can try downloading it manually later if needed.")
|
||||||
|
}
|
||||||
|
// Now you need to update your config file accordingly to enable geoblocking
|
||||||
|
fmt.Println("Please remember to update your config/config.yml file to enable geoblocking! \n")
|
||||||
|
// add maxmind_db_path: "./config/GeoLite2-Country.mmdb" under server
|
||||||
|
fmt.Println("Add the following line under the 'server' section:")
|
||||||
|
fmt.Println(" maxmind_db_path: \"./config/GeoLite2-Country.mmdb\"")
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if !checkIsCrowdsecInstalledInCompose() && !checkIsPangolinInstalledWithHybrid() {
|
if !checkIsCrowdsecInstalledInCompose() {
|
||||||
fmt.Println("\n=== CrowdSec Install ===")
|
fmt.Println("\n=== CrowdSec Install ===")
|
||||||
// check if crowdsec is installed
|
// check if crowdsec is installed
|
||||||
if readBool(reader, "Would you like to install CrowdSec?", false) {
|
if readBool(reader, "Would you like to install CrowdSec?", false) {
|
||||||
@@ -196,8 +209,8 @@ func main() {
|
|||||||
|
|
||||||
parsedURL, err := url.Parse(appConfig.DashboardURL)
|
parsedURL, err := url.Parse(appConfig.DashboardURL)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Printf("Error parsing URL: %v\n", err)
|
fmt.Printf("Error parsing URL: %v\n", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
config.DashboardDomain = parsedURL.Hostname()
|
config.DashboardDomain = parsedURL.Hostname()
|
||||||
@@ -225,12 +238,11 @@ func main() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fmt.Println("CrowdSec installed successfully!")
|
fmt.Println("CrowdSec installed successfully!")
|
||||||
return
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if !config.HybridMode && !alreadyInstalled {
|
if !alreadyInstalled || config.DoCrowdsecInstall {
|
||||||
// Setup Token Section
|
// Setup Token Section
|
||||||
fmt.Println("\n=== Setup Token ===")
|
fmt.Println("\n=== Setup Token ===")
|
||||||
|
|
||||||
@@ -251,9 +263,7 @@ func main() {
|
|||||||
|
|
||||||
fmt.Println("\nInstallation complete!")
|
fmt.Println("\nInstallation complete!")
|
||||||
|
|
||||||
if !config.HybridMode && !checkIsPangolinInstalledWithHybrid() {
|
fmt.Printf("\nTo complete the initial setup, please visit:\nhttps://%s/auth/initial-setup\n", config.DashboardDomain)
|
||||||
fmt.Printf("\nTo complete the initial setup, please visit:\nhttps://%s/auth/initial-setup\n", config.DashboardDomain)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func podmanOrDocker(reader *bufio.Reader) SupportedContainer {
|
func podmanOrDocker(reader *bufio.Reader) SupportedContainer {
|
||||||
@@ -328,66 +338,42 @@ func collectUserInput(reader *bufio.Reader) Config {
|
|||||||
|
|
||||||
// Basic configuration
|
// Basic configuration
|
||||||
fmt.Println("\n=== Basic Configuration ===")
|
fmt.Println("\n=== Basic Configuration ===")
|
||||||
for {
|
|
||||||
response := readString(reader, "Do you want to install Pangolin as a cloud-managed (beta) node? (yes/no)", "")
|
config.BaseDomain = readString(reader, "Enter your base domain (no subdomain e.g. example.com)", "")
|
||||||
if strings.EqualFold(response, "yes") || strings.EqualFold(response, "y") {
|
|
||||||
config.HybridMode = true
|
// Set default dashboard domain after base domain is collected
|
||||||
break
|
defaultDashboardDomain := ""
|
||||||
} else if strings.EqualFold(response, "no") || strings.EqualFold(response, "n") {
|
if config.BaseDomain != "" {
|
||||||
config.HybridMode = false
|
defaultDashboardDomain = "pangolin." + config.BaseDomain
|
||||||
break
|
}
|
||||||
}
|
config.DashboardDomain = readString(reader, "Enter the domain for the Pangolin dashboard", defaultDashboardDomain)
|
||||||
fmt.Println("Please answer 'yes' or 'no'")
|
config.LetsEncryptEmail = readString(reader, "Enter email for Let's Encrypt certificates", "")
|
||||||
|
config.InstallGerbil = readBool(reader, "Do you want to use Gerbil to allow tunneled connections", true)
|
||||||
|
|
||||||
|
// Email configuration
|
||||||
|
fmt.Println("\n=== Email Configuration ===")
|
||||||
|
config.EnableEmail = readBool(reader, "Enable email functionality (SMTP)", false)
|
||||||
|
|
||||||
|
if config.EnableEmail {
|
||||||
|
config.EmailSMTPHost = readString(reader, "Enter SMTP host", "")
|
||||||
|
config.EmailSMTPPort = readInt(reader, "Enter SMTP port (default 587)", 587)
|
||||||
|
config.EmailSMTPUser = readString(reader, "Enter SMTP username", "")
|
||||||
|
config.EmailSMTPPass = readString(reader, "Enter SMTP password", "") // Should this be readPassword?
|
||||||
|
config.EmailNoReply = readString(reader, "Enter no-reply email address (often the same as SMTP username)", "")
|
||||||
}
|
}
|
||||||
|
|
||||||
if config.HybridMode {
|
// Validate required fields
|
||||||
alreadyHaveCreds := readBool(reader, "Do you already have credentials from the dashboard? If not, we will create them later", false)
|
if config.BaseDomain == "" {
|
||||||
|
fmt.Println("Error: Domain name is required")
|
||||||
if alreadyHaveCreds {
|
os.Exit(1)
|
||||||
config.HybridId = readString(reader, "Enter your ID", "")
|
}
|
||||||
config.HybridSecret = readString(reader, "Enter your secret", "")
|
if config.LetsEncryptEmail == "" {
|
||||||
}
|
fmt.Println("Error: Let's Encrypt email is required")
|
||||||
|
os.Exit(1)
|
||||||
// Try to get public IP as default
|
}
|
||||||
publicIP := getPublicIP()
|
if config.EnableEmail && config.EmailNoReply == "" {
|
||||||
if publicIP != "" {
|
fmt.Println("Error: No-reply email address is required when email is enabled")
|
||||||
fmt.Printf("Detected public IP: %s\n", publicIP)
|
os.Exit(1)
|
||||||
}
|
|
||||||
config.DashboardDomain = readString(reader, "The public addressable IP address for this node or a domain pointing to it", publicIP)
|
|
||||||
config.InstallGerbil = true
|
|
||||||
} else {
|
|
||||||
config.BaseDomain = readString(reader, "Enter your base domain (no subdomain e.g. example.com)", "")
|
|
||||||
|
|
||||||
// Set default dashboard domain after base domain is collected
|
|
||||||
defaultDashboardDomain := ""
|
|
||||||
if config.BaseDomain != "" {
|
|
||||||
defaultDashboardDomain = "pangolin." + config.BaseDomain
|
|
||||||
}
|
|
||||||
config.DashboardDomain = readString(reader, "Enter the domain for the Pangolin dashboard", defaultDashboardDomain)
|
|
||||||
config.LetsEncryptEmail = readString(reader, "Enter email for Let's Encrypt certificates", "")
|
|
||||||
config.InstallGerbil = readBool(reader, "Do you want to use Gerbil to allow tunneled connections", true)
|
|
||||||
|
|
||||||
// Email configuration
|
|
||||||
fmt.Println("\n=== Email Configuration ===")
|
|
||||||
config.EnableEmail = readBool(reader, "Enable email functionality (SMTP)", false)
|
|
||||||
|
|
||||||
if config.EnableEmail {
|
|
||||||
config.EmailSMTPHost = readString(reader, "Enter SMTP host", "")
|
|
||||||
config.EmailSMTPPort = readInt(reader, "Enter SMTP port (default 587)", 587)
|
|
||||||
config.EmailSMTPUser = readString(reader, "Enter SMTP username", "")
|
|
||||||
config.EmailSMTPPass = readString(reader, "Enter SMTP password", "") // Should this be readPassword?
|
|
||||||
config.EmailNoReply = readString(reader, "Enter no-reply email address", "")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Validate required fields
|
|
||||||
if config.BaseDomain == "" {
|
|
||||||
fmt.Println("Error: Domain name is required")
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
if config.LetsEncryptEmail == "" {
|
|
||||||
fmt.Println("Error: Let's Encrypt email is required")
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Advanced configuration
|
// Advanced configuration
|
||||||
@@ -395,6 +381,7 @@ func collectUserInput(reader *bufio.Reader) Config {
|
|||||||
fmt.Println("\n=== Advanced Configuration ===")
|
fmt.Println("\n=== Advanced Configuration ===")
|
||||||
|
|
||||||
config.EnableIPv6 = readBool(reader, "Is your server IPv6 capable?", true)
|
config.EnableIPv6 = readBool(reader, "Is your server IPv6 capable?", true)
|
||||||
|
config.EnableGeoblocking = readBool(reader, "Do you want to download the MaxMind GeoLite2 database for geoblocking functionality?", true)
|
||||||
|
|
||||||
if config.DashboardDomain == "" {
|
if config.DashboardDomain == "" {
|
||||||
fmt.Println("Error: Dashboard Domain name is required")
|
fmt.Println("Error: Dashboard Domain name is required")
|
||||||
@@ -429,11 +416,6 @@ func createConfigFiles(config Config) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// the hybrid does not need the dynamic config
|
|
||||||
if config.HybridMode && strings.Contains(path, "dynamic_config.yml") {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// skip .DS_Store
|
// skip .DS_Store
|
||||||
if strings.Contains(path, ".DS_Store") {
|
if strings.Contains(path, ".DS_Store") {
|
||||||
return nil
|
return nil
|
||||||
@@ -663,18 +645,30 @@ func checkPortsAvailable(port int) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func checkIsPangolinInstalledWithHybrid() bool {
|
func downloadMaxMindDatabase() error {
|
||||||
// Check if config/config.yml exists and contains hybrid section
|
fmt.Println("Downloading MaxMind GeoLite2 Country database...")
|
||||||
if _, err := os.Stat("config/config.yml"); err != nil {
|
|
||||||
return false
|
// Download the GeoLite2 Country database
|
||||||
|
if err := run("curl", "-L", "-o", "GeoLite2-Country.tar.gz",
|
||||||
|
"https://github.com/GitSquared/node-geolite2-redist/raw/refs/heads/master/redist/GeoLite2-Country.tar.gz"); err != nil {
|
||||||
|
return fmt.Errorf("failed to download GeoLite2 database: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Read config file to check for hybrid section
|
// Extract the database
|
||||||
content, err := os.ReadFile("config/config.yml")
|
if err := run("tar", "-xzf", "GeoLite2-Country.tar.gz"); err != nil {
|
||||||
if err != nil {
|
return fmt.Errorf("failed to extract GeoLite2 database: %v", err)
|
||||||
return false
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check for hybrid section
|
// Find the .mmdb file and move it to the config directory
|
||||||
return bytes.Contains(content, []byte("managed:"))
|
if err := run("bash", "-c", "mv GeoLite2-Country_*/GeoLite2-Country.mmdb config/"); err != nil {
|
||||||
|
return fmt.Errorf("failed to move GeoLite2 database to config directory: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clean up the downloaded files
|
||||||
|
if err := run("rm", "-rf", "GeoLite2-Country.tar.gz", "GeoLite2-Country_*"); err != nil {
|
||||||
|
fmt.Printf("Warning: failed to clean up temporary files: %v\n", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Println("MaxMind GeoLite2 Country database downloaded successfully!")
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,110 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"encoding/base64"
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"net/http"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
FRONTEND_SECRET_KEY = "af4e4785-7e09-11f0-b93a-74563c4e2a7e"
|
|
||||||
// CLOUD_API_URL = "https://pangolin.fossorial.io/api/v1/remote-exit-node/quick-start"
|
|
||||||
CLOUD_API_URL = "https://pangolin.fossorial.io/api/v1/remote-exit-node/quick-start"
|
|
||||||
)
|
|
||||||
|
|
||||||
// HybridCredentials represents the response from the cloud API
|
|
||||||
type HybridCredentials struct {
|
|
||||||
RemoteExitNodeId string `json:"remoteExitNodeId"`
|
|
||||||
Secret string `json:"secret"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// APIResponse represents the full response structure from the cloud API
|
|
||||||
type APIResponse struct {
|
|
||||||
Data HybridCredentials `json:"data"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// RequestPayload represents the request body structure
|
|
||||||
type RequestPayload struct {
|
|
||||||
Token string `json:"token"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func generateValidationToken() string {
|
|
||||||
timestamp := time.Now().UnixMilli()
|
|
||||||
data := fmt.Sprintf("%s|%d", FRONTEND_SECRET_KEY, timestamp)
|
|
||||||
obfuscated := make([]byte, len(data))
|
|
||||||
for i, char := range []byte(data) {
|
|
||||||
obfuscated[i] = char + 5
|
|
||||||
}
|
|
||||||
return base64.StdEncoding.EncodeToString(obfuscated)
|
|
||||||
}
|
|
||||||
|
|
||||||
// requestHybridCredentials makes an HTTP POST request to the cloud API
|
|
||||||
// to get hybrid credentials (ID and secret)
|
|
||||||
func requestHybridCredentials() (*HybridCredentials, error) {
|
|
||||||
// Generate validation token
|
|
||||||
token := generateValidationToken()
|
|
||||||
|
|
||||||
// Create request payload
|
|
||||||
payload := RequestPayload{
|
|
||||||
Token: token,
|
|
||||||
}
|
|
||||||
|
|
||||||
// Marshal payload to JSON
|
|
||||||
jsonData, err := json.Marshal(payload)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to marshal request payload: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create HTTP request
|
|
||||||
req, err := http.NewRequest("POST", CLOUD_API_URL, bytes.NewBuffer(jsonData))
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to create HTTP request: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set headers
|
|
||||||
req.Header.Set("Content-Type", "application/json")
|
|
||||||
req.Header.Set("X-CSRF-Token", "x-csrf-protection")
|
|
||||||
|
|
||||||
// Create HTTP client with timeout
|
|
||||||
client := &http.Client{
|
|
||||||
Timeout: 30 * time.Second,
|
|
||||||
}
|
|
||||||
|
|
||||||
// Make the request
|
|
||||||
resp, err := client.Do(req)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to make HTTP request: %v", err)
|
|
||||||
}
|
|
||||||
defer resp.Body.Close()
|
|
||||||
|
|
||||||
// Check response status
|
|
||||||
if resp.StatusCode != http.StatusOK {
|
|
||||||
return nil, fmt.Errorf("API request failed with status code: %d", resp.StatusCode)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Read response body for debugging
|
|
||||||
body, err := io.ReadAll(resp.Body)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to read response body: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Print the raw JSON response for debugging
|
|
||||||
// fmt.Printf("Raw JSON response: %s\n", string(body))
|
|
||||||
|
|
||||||
// Parse response
|
|
||||||
var apiResponse APIResponse
|
|
||||||
if err := json.Unmarshal(body, &apiResponse); err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to decode API response: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Validate response data
|
|
||||||
if apiResponse.Data.RemoteExitNodeId == "" || apiResponse.Data.Secret == "" {
|
|
||||||
return nil, fmt.Errorf("invalid response: missing remoteExitNodeId or secret")
|
|
||||||
}
|
|
||||||
|
|
||||||
return &apiResponse.Data, nil
|
|
||||||
}
|
|
||||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
1027
messages/fr-FR.json
1027
messages/fr-FR.json
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
2099
messages/zh-TW.json
Normal file
2099
messages/zh-TW.json
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,12 +1,15 @@
|
|||||||
|
import type { NextConfig } from "next";
|
||||||
import createNextIntlPlugin from "next-intl/plugin";
|
import createNextIntlPlugin from "next-intl/plugin";
|
||||||
|
|
||||||
const withNextIntl = createNextIntlPlugin();
|
const withNextIntl = createNextIntlPlugin();
|
||||||
|
|
||||||
/** @type {import("next").NextConfig} */
|
const nextConfig: NextConfig = {
|
||||||
const nextConfig = {
|
|
||||||
eslint: {
|
eslint: {
|
||||||
ignoreDuringBuilds: true
|
ignoreDuringBuilds: true
|
||||||
},
|
},
|
||||||
|
experimental: {
|
||||||
|
reactCompiler: true
|
||||||
|
},
|
||||||
output: "standalone"
|
output: "standalone"
|
||||||
};
|
};
|
||||||
|
|
||||||
11428
package-lock.json
generated
11428
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
146
package.json
146
package.json
@@ -19,54 +19,58 @@
|
|||||||
"db:sqlite:studio": "drizzle-kit studio --config=./drizzle.sqlite.config.ts",
|
"db:sqlite:studio": "drizzle-kit studio --config=./drizzle.sqlite.config.ts",
|
||||||
"db:pg:studio": "drizzle-kit studio --config=./drizzle.pg.config.ts",
|
"db:pg:studio": "drizzle-kit studio --config=./drizzle.pg.config.ts",
|
||||||
"db:clear-migrations": "rm -rf server/migrations",
|
"db:clear-migrations": "rm -rf server/migrations",
|
||||||
"set:oss": "echo 'export const build = \"oss\" as any;' > server/build.ts",
|
"set:oss": "echo 'export const build = \"oss\" as any;' > server/build.ts && cp tsconfig.oss.json tsconfig.json",
|
||||||
"set:saas": "echo 'export const build = \"saas\" as any;' > server/build.ts",
|
"set:saas": "echo 'export const build = \"saas\" as any;' > server/build.ts && cp tsconfig.saas.json tsconfig.json",
|
||||||
"set:enterprise": "echo 'export const build = \"enterprise\" as any;' > server/build.ts",
|
"set:enterprise": "echo 'export const build = \"enterprise\" as any;' > server/build.ts && cp tsconfig.enterprise.json tsconfig.json",
|
||||||
"set:sqlite": "echo 'export * from \"./sqlite\";' > server/db/index.ts",
|
"set:sqlite": "echo 'export * from \"./sqlite\";\nexport const driver: \"pg\" | \"sqlite\" = \"sqlite\";' > server/db/index.ts",
|
||||||
"set:pg": "echo 'export * from \"./pg\";' > server/db/index.ts",
|
"set:pg": "echo 'export * from \"./pg\";\nexport const driver: \"pg\" | \"sqlite\" = \"pg\";' > server/db/index.ts",
|
||||||
|
"next:build": "next build",
|
||||||
"build:sqlite": "mkdir -p dist && next build && node esbuild.mjs -e server/index.ts -o dist/server.mjs && node esbuild.mjs -e server/setup/migrationsSqlite.ts -o dist/migrations.mjs",
|
"build:sqlite": "mkdir -p dist && next build && node esbuild.mjs -e server/index.ts -o dist/server.mjs && node esbuild.mjs -e server/setup/migrationsSqlite.ts -o dist/migrations.mjs",
|
||||||
"build:pg": "mkdir -p dist && next build && node esbuild.mjs -e server/index.ts -o dist/server.mjs && node esbuild.mjs -e server/setup/migrationsPg.ts -o dist/migrations.mjs",
|
"build:pg": "mkdir -p dist && next build && node esbuild.mjs -e server/index.ts -o dist/server.mjs && node esbuild.mjs -e server/setup/migrationsPg.ts -o dist/migrations.mjs",
|
||||||
"start": "ENVIRONMENT=prod node dist/migrations.mjs && ENVIRONMENT=prod NODE_ENV=development node --enable-source-maps dist/server.mjs",
|
"start": "ENVIRONMENT=prod node dist/migrations.mjs && ENVIRONMENT=prod NODE_ENV=development node --enable-source-maps dist/server.mjs",
|
||||||
"email": "email dev --dir server/emails/templates --port 3005",
|
"email": "email dev --dir server/emails/templates --port 3005",
|
||||||
"build:cli": "node esbuild.mjs -e cli/index.ts -o dist/cli.mjs",
|
"build:cli": "node esbuild.mjs -e cli/index.ts -o dist/cli.mjs"
|
||||||
"db:sqlite:seed-exit-node": "sqlite3 config/db/db.sqlite \"INSERT INTO exitNodes (exitNodeId, name, address, endpoint, publicKey, listenPort, reachableAt, maxConnections, online, lastPing, type, region) VALUES (null, 'test', '10.0.0.1/24', 'localhost', 'MJ44MpnWGxMZURgxW/fWXDFsejhabnEFYDo60LQwK3A=', 1234, 'http://localhost:3003', 123, 1, null, 'gerbil', null);\""
|
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@asteasolutions/zod-to-openapi": "^7.3.4",
|
"@asteasolutions/zod-to-openapi": "8.1.0",
|
||||||
"@aws-sdk/client-s3": "3.837.0",
|
"@faker-js/faker": "^10.1.0",
|
||||||
|
"@headlessui/react": "^2.2.9",
|
||||||
|
"@aws-sdk/client-s3": "3.943.0",
|
||||||
"@hookform/resolvers": "5.2.2",
|
"@hookform/resolvers": "5.2.2",
|
||||||
|
"@monaco-editor/react": "^4.7.0",
|
||||||
"@node-rs/argon2": "^2.0.2",
|
"@node-rs/argon2": "^2.0.2",
|
||||||
"@oslojs/crypto": "1.0.1",
|
"@oslojs/crypto": "1.0.1",
|
||||||
"@oslojs/encoding": "1.1.0",
|
"@oslojs/encoding": "1.1.0",
|
||||||
"@radix-ui/react-avatar": "1.1.10",
|
"@radix-ui/react-avatar": "1.1.11",
|
||||||
"@radix-ui/react-checkbox": "1.3.3",
|
"@radix-ui/react-checkbox": "1.3.3",
|
||||||
"@radix-ui/react-collapsible": "1.1.12",
|
"@radix-ui/react-collapsible": "1.1.12",
|
||||||
"@radix-ui/react-dialog": "1.1.15",
|
"@radix-ui/react-dialog": "1.1.15",
|
||||||
"@radix-ui/react-dropdown-menu": "2.1.16",
|
"@radix-ui/react-dropdown-menu": "2.1.16",
|
||||||
"@radix-ui/react-icons": "1.3.2",
|
"@radix-ui/react-icons": "1.3.2",
|
||||||
"@radix-ui/react-label": "2.1.7",
|
"@radix-ui/react-label": "2.1.8",
|
||||||
"@radix-ui/react-popover": "1.1.15",
|
"@radix-ui/react-popover": "1.1.15",
|
||||||
"@radix-ui/react-progress": "^1.1.7",
|
"@radix-ui/react-progress": "^1.1.8",
|
||||||
"@radix-ui/react-radio-group": "1.3.8",
|
"@radix-ui/react-radio-group": "1.3.8",
|
||||||
"@radix-ui/react-scroll-area": "^1.2.10",
|
"@radix-ui/react-scroll-area": "^1.2.10",
|
||||||
"@radix-ui/react-select": "2.2.6",
|
"@radix-ui/react-select": "2.2.6",
|
||||||
"@radix-ui/react-separator": "1.1.7",
|
"@radix-ui/react-separator": "1.1.8",
|
||||||
"@radix-ui/react-slot": "1.2.3",
|
"@radix-ui/react-slot": "1.2.4",
|
||||||
"@radix-ui/react-switch": "1.2.6",
|
"@radix-ui/react-switch": "1.2.6",
|
||||||
"@radix-ui/react-tabs": "1.1.13",
|
"@radix-ui/react-tabs": "1.1.13",
|
||||||
"@radix-ui/react-toast": "1.2.15",
|
"@radix-ui/react-toast": "1.2.15",
|
||||||
"@radix-ui/react-tooltip": "^1.2.8",
|
"@radix-ui/react-tooltip": "^1.2.8",
|
||||||
"@react-email/components": "0.5.5",
|
"@react-email/components": "0.5.7",
|
||||||
"@react-email/render": "^1.2.0",
|
"@react-email/render": "^1.3.2",
|
||||||
"@react-email/tailwind": "1.2.2",
|
"@react-email/tailwind": "1.2.2",
|
||||||
"@simplewebauthn/browser": "^13.2.0",
|
"@simplewebauthn/browser": "^13.2.2",
|
||||||
"@simplewebauthn/server": "^13.2.1",
|
"@simplewebauthn/server": "^13.2.2",
|
||||||
"@tailwindcss/forms": "^0.5.10",
|
"@tailwindcss/forms": "^0.5.10",
|
||||||
|
"@tanstack/react-query": "^5.90.6",
|
||||||
"@tanstack/react-table": "8.21.3",
|
"@tanstack/react-table": "8.21.3",
|
||||||
"arctic": "^3.7.0",
|
"arctic": "^3.7.0",
|
||||||
"axios": "^1.12.2",
|
"axios": "^1.13.2",
|
||||||
"better-sqlite3": "11.7.0",
|
"better-sqlite3": "11.7.0",
|
||||||
"canvas-confetti": "1.9.3",
|
"canvas-confetti": "1.9.4",
|
||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
"clsx": "2.1.1",
|
"clsx": "2.1.1",
|
||||||
"cmdk": "1.1.1",
|
"cmdk": "1.1.1",
|
||||||
@@ -75,89 +79,103 @@
|
|||||||
"cookies": "^0.9.1",
|
"cookies": "^0.9.1",
|
||||||
"cors": "2.8.5",
|
"cors": "2.8.5",
|
||||||
"crypto-js": "^4.2.0",
|
"crypto-js": "^4.2.0",
|
||||||
"drizzle-orm": "0.44.6",
|
"d3": "^7.9.0",
|
||||||
"eslint": "9.35.0",
|
"date-fns": "4.1.0",
|
||||||
"eslint-config-next": "15.5.4",
|
"drizzle-orm": "0.45.0",
|
||||||
"express": "5.1.0",
|
"eslint": "9.39.1",
|
||||||
"express-rate-limit": "8.1.0",
|
"eslint-config-next": "16.0.7",
|
||||||
"glob": "11.0.3",
|
"express": "5.2.1",
|
||||||
|
"express-rate-limit": "8.2.1",
|
||||||
|
"glob": "11.1.0",
|
||||||
"helmet": "8.1.0",
|
"helmet": "8.1.0",
|
||||||
"http-errors": "2.0.0",
|
"http-errors": "2.0.1",
|
||||||
"i": "^0.3.7",
|
"i": "^0.3.7",
|
||||||
"input-otp": "1.4.2",
|
"input-otp": "1.4.2",
|
||||||
"ioredis": "5.6.1",
|
"ioredis": "5.8.2",
|
||||||
"jmespath": "^0.16.0",
|
"jmespath": "^0.16.0",
|
||||||
"js-yaml": "4.1.0",
|
"js-yaml": "4.1.1",
|
||||||
"jsonwebtoken": "^9.0.2",
|
"jsonwebtoken": "^9.0.2",
|
||||||
"lucide-react": "^0.544.0",
|
"lucide-react": "^0.556.0",
|
||||||
"maxmind": "5.0.0",
|
"maxmind": "5.0.1",
|
||||||
"moment": "2.30.1",
|
"moment": "2.30.1",
|
||||||
"next": "15.5.4",
|
"next": "15.5.7",
|
||||||
"next-intl": "^4.3.9",
|
"next-intl": "^4.4.0",
|
||||||
"next-themes": "0.4.6",
|
"next-themes": "0.4.6",
|
||||||
|
"nextjs-toploader": "^3.9.17",
|
||||||
"node-cache": "5.1.2",
|
"node-cache": "5.1.2",
|
||||||
"node-fetch": "3.3.2",
|
"node-fetch": "3.3.2",
|
||||||
"nodemailer": "7.0.6",
|
"nodemailer": "7.0.11",
|
||||||
"npm": "^11.6.1",
|
"npm": "^11.6.4",
|
||||||
|
"nprogress": "^0.2.0",
|
||||||
"oslo": "1.2.1",
|
"oslo": "1.2.1",
|
||||||
"pg": "^8.16.2",
|
"pg": "^8.16.2",
|
||||||
"posthog-node": "^5.8.4",
|
"posthog-node": "^5.11.2",
|
||||||
"qrcode.react": "4.2.0",
|
"qrcode.react": "4.2.0",
|
||||||
"react": "19.1.1",
|
"react": "19.2.1",
|
||||||
"react-dom": "19.1.1",
|
"react-day-picker": "9.11.3",
|
||||||
"react-easy-sort": "^1.7.0",
|
"react-dom": "19.2.1",
|
||||||
"react-hook-form": "7.62.0",
|
"react-easy-sort": "^1.8.0",
|
||||||
|
"react-hook-form": "7.68.0",
|
||||||
"react-icons": "^5.5.0",
|
"react-icons": "^5.5.0",
|
||||||
"rebuild": "0.1.2",
|
"rebuild": "0.1.2",
|
||||||
|
"recharts": "^2.15.4",
|
||||||
"reodotdev": "^1.0.0",
|
"reodotdev": "^1.0.0",
|
||||||
"resend": "^6.1.1",
|
"resend": "^6.4.2",
|
||||||
"semver": "^7.7.2",
|
"semver": "^7.7.3",
|
||||||
"stripe": "18.2.1",
|
"stripe": "18.2.1",
|
||||||
"swagger-ui-express": "^5.0.1",
|
"swagger-ui-express": "^5.0.1",
|
||||||
"tailwind-merge": "3.3.1",
|
"topojson-client": "^3.1.0",
|
||||||
|
"tailwind-merge": "3.4.0",
|
||||||
"tw-animate-css": "^1.3.8",
|
"tw-animate-css": "^1.3.8",
|
||||||
"uuid": "^13.0.0",
|
"uuid": "^13.0.0",
|
||||||
"vaul": "1.1.2",
|
"vaul": "1.1.2",
|
||||||
"winston": "3.17.0",
|
"visionscarto-world-atlas": "^1.0.0",
|
||||||
|
"winston": "3.18.3",
|
||||||
"winston-daily-rotate-file": "5.0.0",
|
"winston-daily-rotate-file": "5.0.0",
|
||||||
"ws": "8.18.3",
|
"ws": "8.18.3",
|
||||||
|
"yaml": "^2.8.1",
|
||||||
"yargs": "18.0.0",
|
"yargs": "18.0.0",
|
||||||
"zod": "3.25.76",
|
"zod": "4.1.12",
|
||||||
"zod-validation-error": "3.5.2"
|
"zod-validation-error": "5.0.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@dotenvx/dotenvx": "1.51.0",
|
"@dotenvx/dotenvx": "1.51.1",
|
||||||
"@esbuild-plugins/tsconfig-paths": "0.1.2",
|
"@esbuild-plugins/tsconfig-paths": "0.1.2",
|
||||||
"@react-email/preview-server": "4.1.0",
|
"@react-email/preview-server": "4.3.2",
|
||||||
"@tailwindcss/postcss": "^4.1.14",
|
"@tailwindcss/postcss": "^4.1.17",
|
||||||
|
"@tanstack/react-query-devtools": "^5.90.2",
|
||||||
"@types/better-sqlite3": "7.6.12",
|
"@types/better-sqlite3": "7.6.12",
|
||||||
"@types/cookie-parser": "1.4.9",
|
"@types/cookie-parser": "1.4.10",
|
||||||
"@types/cors": "2.8.19",
|
"@types/cors": "2.8.19",
|
||||||
"@types/crypto-js": "^4.2.2",
|
"@types/crypto-js": "^4.2.2",
|
||||||
"@types/express": "5.0.3",
|
"@types/d3": "^7.4.3",
|
||||||
|
"@types/express": "5.0.6",
|
||||||
"@types/express-session": "^1.18.2",
|
"@types/express-session": "^1.18.2",
|
||||||
"@types/jmespath": "^0.15.2",
|
"@types/jmespath": "^0.15.2",
|
||||||
"@types/js-yaml": "4.0.9",
|
"@types/js-yaml": "4.0.9",
|
||||||
"@types/jsonwebtoken": "^9.0.10",
|
"@types/jsonwebtoken": "^9.0.10",
|
||||||
"@types/node": "24.6.1",
|
"@types/node": "24.10.1",
|
||||||
"@types/nodemailer": "7.0.2",
|
"@types/nprogress": "^0.2.3",
|
||||||
"@types/pg": "8.15.5",
|
"@types/nodemailer": "7.0.4",
|
||||||
"@types/react": "19.1.16",
|
"@types/pg": "8.15.6",
|
||||||
"@types/react-dom": "19.1.9",
|
"@types/react": "19.2.7",
|
||||||
|
"@types/react-dom": "19.2.3",
|
||||||
"@types/semver": "^7.7.1",
|
"@types/semver": "^7.7.1",
|
||||||
"@types/swagger-ui-express": "^4.1.8",
|
"@types/swagger-ui-express": "^4.1.8",
|
||||||
|
"@types/topojson-client": "^3.1.5",
|
||||||
"@types/ws": "8.18.1",
|
"@types/ws": "8.18.1",
|
||||||
"@types/yargs": "17.0.33",
|
"babel-plugin-react-compiler": "^1.0.0",
|
||||||
"drizzle-kit": "0.31.5",
|
"@types/yargs": "17.0.35",
|
||||||
"esbuild": "0.25.10",
|
"drizzle-kit": "0.31.8",
|
||||||
"esbuild-node-externals": "1.18.0",
|
"esbuild": "0.27.1",
|
||||||
|
"esbuild-node-externals": "1.20.1",
|
||||||
"postcss": "^8",
|
"postcss": "^8",
|
||||||
"react-email": "4.2.12",
|
"react-email": "4.3.2",
|
||||||
"tailwindcss": "^4.1.4",
|
"tailwindcss": "^4.1.4",
|
||||||
"tsc-alias": "1.8.16",
|
"tsc-alias": "1.8.16",
|
||||||
"tsx": "4.20.6",
|
"tsx": "4.21.0",
|
||||||
"typescript": "^5",
|
"typescript": "^5",
|
||||||
"typescript-eslint": "^8.45.0"
|
"typescript-eslint": "^8.46.3"
|
||||||
},
|
},
|
||||||
"overrides": {
|
"overrides": {
|
||||||
"emblor": {
|
"emblor": {
|
||||||
@@ -165,4 +183,4 @@
|
|||||||
"react-dom": "19.0.0"
|
"react-dom": "19.0.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 15 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 34 KiB After Width: | Height: | Size: 13 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 33 KiB After Width: | Height: | Size: 13 KiB |
@@ -7,21 +7,21 @@ import {
|
|||||||
errorHandlerMiddleware,
|
errorHandlerMiddleware,
|
||||||
notFoundMiddleware
|
notFoundMiddleware
|
||||||
} from "@server/middlewares";
|
} from "@server/middlewares";
|
||||||
import { corsWithLoginPageSupport } from "@server/middlewares/private/corsWithLoginPage";
|
import { authenticated, unauthenticated } from "#dynamic/routers/external";
|
||||||
import { authenticated, unauthenticated } from "@server/routers/external";
|
import { router as wsRouter, handleWSUpgrade } from "#dynamic/routers/ws";
|
||||||
import { router as wsRouter, handleWSUpgrade } from "@server/routers/ws";
|
|
||||||
import { logIncomingMiddleware } from "./middlewares/logIncoming";
|
import { logIncomingMiddleware } from "./middlewares/logIncoming";
|
||||||
import { csrfProtectionMiddleware } from "./middlewares/csrfProtection";
|
import { csrfProtectionMiddleware } from "./middlewares/csrfProtection";
|
||||||
import helmet from "helmet";
|
import helmet from "helmet";
|
||||||
import { stripeWebhookHandler } from "@server/routers/private/billing/webhooks";
|
|
||||||
import { build } from "./build";
|
import { build } from "./build";
|
||||||
import rateLimit, { ipKeyGenerator } from "express-rate-limit";
|
import rateLimit, { ipKeyGenerator } from "express-rate-limit";
|
||||||
import createHttpError from "http-errors";
|
import createHttpError from "http-errors";
|
||||||
import HttpCode from "./types/HttpCode";
|
import HttpCode from "./types/HttpCode";
|
||||||
import requestTimeoutMiddleware from "./middlewares/requestTimeout";
|
import requestTimeoutMiddleware from "./middlewares/requestTimeout";
|
||||||
import { createStore } from "@server/lib/private/rateLimitStore";
|
import { createStore } from "#dynamic/lib/rateLimitStore";
|
||||||
import hybridRouter from "@server/routers/private/hybrid";
|
|
||||||
import { stripDuplicateSesions } from "./middlewares/stripDuplicateSessions";
|
import { stripDuplicateSesions } from "./middlewares/stripDuplicateSessions";
|
||||||
|
import { corsWithLoginPageSupport } from "@server/lib/corsWithLoginPage";
|
||||||
|
import { hybridRouter } from "#dynamic/routers/hybrid";
|
||||||
|
import { billingWebhookHandler } from "#dynamic/routers/billing/webhooks";
|
||||||
|
|
||||||
const dev = config.isDev;
|
const dev = config.isDev;
|
||||||
const externalPort = config.getRawConfig().server.external_port;
|
const externalPort = config.getRawConfig().server.external_port;
|
||||||
@@ -39,32 +39,30 @@ export function createApiServer() {
|
|||||||
apiServer.post(
|
apiServer.post(
|
||||||
`${prefix}/billing/webhooks`,
|
`${prefix}/billing/webhooks`,
|
||||||
express.raw({ type: "application/json" }),
|
express.raw({ type: "application/json" }),
|
||||||
stripeWebhookHandler
|
billingWebhookHandler
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const corsConfig = config.getRawConfig().server.cors;
|
const corsConfig = config.getRawConfig().server.cors;
|
||||||
|
const options = {
|
||||||
|
...(corsConfig?.origins
|
||||||
|
? { origin: corsConfig.origins }
|
||||||
|
: {
|
||||||
|
origin: (origin: any, callback: any) => {
|
||||||
|
callback(null, true);
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
...(corsConfig?.methods && { methods: corsConfig.methods }),
|
||||||
|
...(corsConfig?.allowed_headers && {
|
||||||
|
allowedHeaders: corsConfig.allowed_headers
|
||||||
|
}),
|
||||||
|
credentials: !(corsConfig?.credentials === false)
|
||||||
|
};
|
||||||
|
|
||||||
if (build == "oss") {
|
if (build == "oss" || !corsConfig) {
|
||||||
const options = {
|
|
||||||
...(corsConfig?.origins
|
|
||||||
? { origin: corsConfig.origins }
|
|
||||||
: {
|
|
||||||
origin: (origin: any, callback: any) => {
|
|
||||||
callback(null, true);
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
...(corsConfig?.methods && { methods: corsConfig.methods }),
|
|
||||||
...(corsConfig?.allowed_headers && {
|
|
||||||
allowedHeaders: corsConfig.allowed_headers
|
|
||||||
}),
|
|
||||||
credentials: !(corsConfig?.credentials === false)
|
|
||||||
};
|
|
||||||
|
|
||||||
logger.debug("Using CORS options", options);
|
logger.debug("Using CORS options", options);
|
||||||
|
|
||||||
apiServer.use(cors(options));
|
apiServer.use(cors(options));
|
||||||
} else {
|
} else if (corsConfig) {
|
||||||
// Use the custom CORS middleware with loginPage support
|
// Use the custom CORS middleware with loginPage support
|
||||||
apiServer.use(corsWithLoginPageSupport(corsConfig));
|
apiServer.use(corsWithLoginPageSupport(corsConfig));
|
||||||
}
|
}
|
||||||
@@ -81,6 +79,12 @@ export function createApiServer() {
|
|||||||
// Add request timeout middleware
|
// Add request timeout middleware
|
||||||
apiServer.use(requestTimeoutMiddleware(60000)); // 60 second timeout
|
apiServer.use(requestTimeoutMiddleware(60000)); // 60 second timeout
|
||||||
|
|
||||||
|
apiServer.use(logIncomingMiddleware);
|
||||||
|
|
||||||
|
if (build !== "oss") {
|
||||||
|
apiServer.use(`${prefix}/hybrid`, hybridRouter); // put before rate limiting because we will rate limit there separately because some of the routes are heavily used
|
||||||
|
}
|
||||||
|
|
||||||
if (!dev) {
|
if (!dev) {
|
||||||
apiServer.use(
|
apiServer.use(
|
||||||
rateLimit({
|
rateLimit({
|
||||||
@@ -103,11 +107,7 @@ export function createApiServer() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// API routes
|
// API routes
|
||||||
apiServer.use(logIncomingMiddleware);
|
|
||||||
apiServer.use(prefix, unauthenticated);
|
apiServer.use(prefix, unauthenticated);
|
||||||
if (build !== "oss") {
|
|
||||||
apiServer.use(`${prefix}/hybrid`, hybridRouter);
|
|
||||||
}
|
|
||||||
apiServer.use(prefix, authenticated);
|
apiServer.use(prefix, authenticated);
|
||||||
|
|
||||||
// WebSocket routes
|
// WebSocket routes
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import { userActions, roleActions, userOrgs } from "@server/db";
|
|||||||
import { and, eq } from "drizzle-orm";
|
import { and, eq } from "drizzle-orm";
|
||||||
import createHttpError from "http-errors";
|
import createHttpError from "http-errors";
|
||||||
import HttpCode from "@server/types/HttpCode";
|
import HttpCode from "@server/types/HttpCode";
|
||||||
import { sendUsageNotification } from "@server/routers/org";
|
|
||||||
|
|
||||||
export enum ActionsEnum {
|
export enum ActionsEnum {
|
||||||
createOrgUser = "createOrgUser",
|
createOrgUser = "createOrgUser",
|
||||||
@@ -20,6 +19,7 @@ export enum ActionsEnum {
|
|||||||
getSite = "getSite",
|
getSite = "getSite",
|
||||||
listSites = "listSites",
|
listSites = "listSites",
|
||||||
updateSite = "updateSite",
|
updateSite = "updateSite",
|
||||||
|
reGenerateSecret = "reGenerateSecret",
|
||||||
createResource = "createResource",
|
createResource = "createResource",
|
||||||
deleteResource = "deleteResource",
|
deleteResource = "deleteResource",
|
||||||
getResource = "getResource",
|
getResource = "getResource",
|
||||||
@@ -61,6 +61,7 @@ export enum ActionsEnum {
|
|||||||
getUser = "getUser",
|
getUser = "getUser",
|
||||||
setResourcePassword = "setResourcePassword",
|
setResourcePassword = "setResourcePassword",
|
||||||
setResourcePincode = "setResourcePincode",
|
setResourcePincode = "setResourcePincode",
|
||||||
|
setResourceHeaderAuth = "setResourceHeaderAuth",
|
||||||
setResourceWhitelist = "setResourceWhitelist",
|
setResourceWhitelist = "setResourceWhitelist",
|
||||||
getResourceWhitelist = "getResourceWhitelist",
|
getResourceWhitelist = "getResourceWhitelist",
|
||||||
generateAccessToken = "generateAccessToken",
|
generateAccessToken = "generateAccessToken",
|
||||||
@@ -81,7 +82,11 @@ export enum ActionsEnum {
|
|||||||
listClients = "listClients",
|
listClients = "listClients",
|
||||||
getClient = "getClient",
|
getClient = "getClient",
|
||||||
listOrgDomains = "listOrgDomains",
|
listOrgDomains = "listOrgDomains",
|
||||||
|
getDomain = "getDomain",
|
||||||
|
updateOrgDomain = "updateOrgDomain",
|
||||||
|
getDNSRecords = "getDNSRecords",
|
||||||
createNewt = "createNewt",
|
createNewt = "createNewt",
|
||||||
|
createOlm = "createOlm",
|
||||||
createIdp = "createIdp",
|
createIdp = "createIdp",
|
||||||
updateIdp = "updateIdp",
|
updateIdp = "updateIdp",
|
||||||
deleteIdp = "deleteIdp",
|
deleteIdp = "deleteIdp",
|
||||||
@@ -116,7 +121,11 @@ export enum ActionsEnum {
|
|||||||
updateLoginPage = "updateLoginPage",
|
updateLoginPage = "updateLoginPage",
|
||||||
getLoginPage = "getLoginPage",
|
getLoginPage = "getLoginPage",
|
||||||
deleteLoginPage = "deleteLoginPage",
|
deleteLoginPage = "deleteLoginPage",
|
||||||
applyBlueprint = "applyBlueprint"
|
listBlueprints = "listBlueprints",
|
||||||
|
getBlueprint = "getBlueprint",
|
||||||
|
applyBlueprint = "applyBlueprint",
|
||||||
|
viewLogs = "viewLogs",
|
||||||
|
exportLogs = "exportLogs"
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function checkUserActionPermission(
|
export async function checkUserActionPermission(
|
||||||
@@ -193,8 +202,6 @@ export async function checkUserActionPermission(
|
|||||||
.limit(1);
|
.limit(1);
|
||||||
|
|
||||||
return roleActionPermission.length > 0;
|
return roleActionPermission.length > 0;
|
||||||
|
|
||||||
return false;
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error checking user action permission:", error);
|
console.error("Error checking user action permission:", error);
|
||||||
throw createHttpError(
|
throw createHttpError(
|
||||||
|
|||||||
@@ -36,12 +36,15 @@ export async function createSession(
|
|||||||
const sessionId = encodeHexLowerCase(
|
const sessionId = encodeHexLowerCase(
|
||||||
sha256(new TextEncoder().encode(token))
|
sha256(new TextEncoder().encode(token))
|
||||||
);
|
);
|
||||||
const session: Session = {
|
const [session] = await db
|
||||||
sessionId: sessionId,
|
.insert(sessions)
|
||||||
userId,
|
.values({
|
||||||
expiresAt: new Date(Date.now() + SESSION_COOKIE_EXPIRES).getTime()
|
sessionId: sessionId,
|
||||||
};
|
userId,
|
||||||
await db.insert(sessions).values(session);
|
expiresAt: new Date(Date.now() + SESSION_COOKIE_EXPIRES).getTime(),
|
||||||
|
issuedAt: new Date().getTime()
|
||||||
|
})
|
||||||
|
.returning();
|
||||||
return session;
|
return session;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,9 +4,6 @@ import { resourceSessions, ResourceSession } from "@server/db";
|
|||||||
import { db } from "@server/db";
|
import { db } from "@server/db";
|
||||||
import { eq, and } from "drizzle-orm";
|
import { eq, and } from "drizzle-orm";
|
||||||
import config from "@server/lib/config";
|
import config from "@server/lib/config";
|
||||||
import axios from "axios";
|
|
||||||
import logger from "@server/logger";
|
|
||||||
import { tokenManager } from "@server/lib/tokenManager";
|
|
||||||
|
|
||||||
export const SESSION_COOKIE_NAME =
|
export const SESSION_COOKIE_NAME =
|
||||||
config.getRawConfig().server.session_cookie_name;
|
config.getRawConfig().server.session_cookie_name;
|
||||||
@@ -53,7 +50,8 @@ export async function createResourceSession(opts: {
|
|||||||
doNotExtend: opts.doNotExtend || false,
|
doNotExtend: opts.doNotExtend || false,
|
||||||
accessTokenId: opts.accessTokenId || null,
|
accessTokenId: opts.accessTokenId || null,
|
||||||
isRequestToken: opts.isRequestToken || false,
|
isRequestToken: opts.isRequestToken || false,
|
||||||
userSessionId: opts.userSessionId || null
|
userSessionId: opts.userSessionId || null,
|
||||||
|
issuedAt: new Date().getTime()
|
||||||
};
|
};
|
||||||
|
|
||||||
await db.insert(resourceSessions).values(session);
|
await db.insert(resourceSessions).values(session);
|
||||||
@@ -65,29 +63,6 @@ export async function validateResourceSessionToken(
|
|||||||
token: string,
|
token: string,
|
||||||
resourceId: number
|
resourceId: number
|
||||||
): Promise<ResourceSessionValidationResult> {
|
): Promise<ResourceSessionValidationResult> {
|
||||||
if (config.isManagedMode()) {
|
|
||||||
try {
|
|
||||||
const response = await axios.post(`${config.getRawConfig().managed?.endpoint}/api/v1/hybrid/resource/${resourceId}/session/validate`, {
|
|
||||||
token: token
|
|
||||||
}, await tokenManager.getAuthHeader());
|
|
||||||
return response.data.data;
|
|
||||||
} catch (error) {
|
|
||||||
if (axios.isAxiosError(error)) {
|
|
||||||
logger.error("Error validating resource session token in hybrid mode:", {
|
|
||||||
message: error.message,
|
|
||||||
code: error.code,
|
|
||||||
status: error.response?.status,
|
|
||||||
statusText: error.response?.statusText,
|
|
||||||
url: error.config?.url,
|
|
||||||
method: error.config?.method
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
logger.error("Error validating resource session token in hybrid mode:", error);
|
|
||||||
}
|
|
||||||
return { resourceSession: null };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const sessionId = encodeHexLowerCase(
|
const sessionId = encodeHexLowerCase(
|
||||||
sha256(new TextEncoder().encode(token))
|
sha256(new TextEncoder().encode(token))
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,9 +1,43 @@
|
|||||||
import { Request } from "express";
|
import { Request } from "express";
|
||||||
import { validateSessionToken, SESSION_COOKIE_NAME } from "@server/auth/sessions/app";
|
import {
|
||||||
|
validateSessionToken,
|
||||||
|
SESSION_COOKIE_NAME
|
||||||
|
} from "@server/auth/sessions/app";
|
||||||
|
|
||||||
export async function verifySession(req: Request) {
|
export async function verifySession(req: Request, forceLogin?: boolean) {
|
||||||
const res = await validateSessionToken(
|
const res = await validateSessionToken(
|
||||||
req.cookies[SESSION_COOKIE_NAME] ?? "",
|
req.cookies[SESSION_COOKIE_NAME] ?? ""
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (!forceLogin) {
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
if (!res.session || !res.user) {
|
||||||
|
return {
|
||||||
|
session: null,
|
||||||
|
user: null
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (res.session.deviceAuthUsed) {
|
||||||
|
return {
|
||||||
|
session: null,
|
||||||
|
user: null
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (!res.session.issuedAt) {
|
||||||
|
return {
|
||||||
|
session: null,
|
||||||
|
user: null
|
||||||
|
};
|
||||||
|
}
|
||||||
|
const mins = 5 * 60 * 1000;
|
||||||
|
const now = new Date().getTime();
|
||||||
|
if (now - res.session.issuedAt > mins) {
|
||||||
|
return {
|
||||||
|
session: null,
|
||||||
|
user: null
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
return res;
|
return res;
|
||||||
}
|
}
|
||||||
|
|||||||
13
server/cleanup.ts
Normal file
13
server/cleanup.ts
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
import { cleanup as wsCleanup } from "#dynamic/routers/ws";
|
||||||
|
|
||||||
|
async function cleanup() {
|
||||||
|
await wsCleanup();
|
||||||
|
|
||||||
|
process.exit(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function initCleanup() {
|
||||||
|
// Handle process termination
|
||||||
|
process.on("SIGTERM", () => cleanup());
|
||||||
|
process.on("SIGINT", () => cleanup());
|
||||||
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
import { join } from "path";
|
import { join } from "path";
|
||||||
import { readFileSync } from "fs";
|
import { readFileSync } from "fs";
|
||||||
import { db, resources, siteResources } from "@server/db";
|
import { clients, db, resources, siteResources } from "@server/db";
|
||||||
|
import { randomInt } from "crypto";
|
||||||
import { exitNodes, sites } from "@server/db";
|
import { exitNodes, sites } from "@server/db";
|
||||||
import { eq, and } from "drizzle-orm";
|
import { eq, and } from "drizzle-orm";
|
||||||
import { __DIRNAME } from "@server/lib/consts";
|
import { __DIRNAME } from "@server/lib/consts";
|
||||||
@@ -15,6 +16,25 @@ if (!dev) {
|
|||||||
}
|
}
|
||||||
export const names = JSON.parse(readFileSync(file, "utf-8"));
|
export const names = JSON.parse(readFileSync(file, "utf-8"));
|
||||||
|
|
||||||
|
export async function getUniqueClientName(orgId: string): Promise<string> {
|
||||||
|
let loops = 0;
|
||||||
|
while (true) {
|
||||||
|
if (loops > 100) {
|
||||||
|
throw new Error("Could not generate a unique name");
|
||||||
|
}
|
||||||
|
|
||||||
|
const name = generateName();
|
||||||
|
const count = await db
|
||||||
|
.select({ niceId: clients.niceId, orgId: clients.orgId })
|
||||||
|
.from(clients)
|
||||||
|
.where(and(eq(clients.niceId, name), eq(clients.orgId, orgId)));
|
||||||
|
if (count.length === 0) {
|
||||||
|
return name;
|
||||||
|
}
|
||||||
|
loops++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export async function getUniqueSiteName(orgId: string): Promise<string> {
|
export async function getUniqueSiteName(orgId: string): Promise<string> {
|
||||||
let loops = 0;
|
let loops = 0;
|
||||||
while (true) {
|
while (true) {
|
||||||
@@ -42,18 +62,36 @@ export async function getUniqueResourceName(orgId: string): Promise<string> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const name = generateName();
|
const name = generateName();
|
||||||
const count = await db
|
const [resourceCount, siteResourceCount] = await Promise.all([
|
||||||
.select({ niceId: resources.niceId, orgId: resources.orgId })
|
db
|
||||||
.from(resources)
|
.select({ niceId: resources.niceId, orgId: resources.orgId })
|
||||||
.where(and(eq(resources.niceId, name), eq(resources.orgId, orgId)));
|
.from(resources)
|
||||||
if (count.length === 0) {
|
.where(
|
||||||
|
and(eq(resources.niceId, name), eq(resources.orgId, orgId))
|
||||||
|
),
|
||||||
|
db
|
||||||
|
.select({
|
||||||
|
niceId: siteResources.niceId,
|
||||||
|
orgId: siteResources.orgId
|
||||||
|
})
|
||||||
|
.from(siteResources)
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
eq(siteResources.niceId, name),
|
||||||
|
eq(siteResources.orgId, orgId)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
]);
|
||||||
|
if (resourceCount.length === 0 && siteResourceCount.length === 0) {
|
||||||
return name;
|
return name;
|
||||||
}
|
}
|
||||||
loops++;
|
loops++;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getUniqueSiteResourceName(orgId: string): Promise<string> {
|
export async function getUniqueSiteResourceName(
|
||||||
|
orgId: string
|
||||||
|
): Promise<string> {
|
||||||
let loops = 0;
|
let loops = 0;
|
||||||
while (true) {
|
while (true) {
|
||||||
if (loops > 100) {
|
if (loops > 100) {
|
||||||
@@ -61,11 +99,27 @@ export async function getUniqueSiteResourceName(orgId: string): Promise<string>
|
|||||||
}
|
}
|
||||||
|
|
||||||
const name = generateName();
|
const name = generateName();
|
||||||
const count = await db
|
const [resourceCount, siteResourceCount] = await Promise.all([
|
||||||
.select({ niceId: siteResources.niceId, orgId: siteResources.orgId })
|
db
|
||||||
.from(siteResources)
|
.select({ niceId: resources.niceId, orgId: resources.orgId })
|
||||||
.where(and(eq(siteResources.niceId, name), eq(siteResources.orgId, orgId)));
|
.from(resources)
|
||||||
if (count.length === 0) {
|
.where(
|
||||||
|
and(eq(resources.niceId, name), eq(resources.orgId, orgId))
|
||||||
|
),
|
||||||
|
db
|
||||||
|
.select({
|
||||||
|
niceId: siteResources.niceId,
|
||||||
|
orgId: siteResources.orgId
|
||||||
|
})
|
||||||
|
.from(siteResources)
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
eq(siteResources.niceId, name),
|
||||||
|
eq(siteResources.orgId, orgId)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
]);
|
||||||
|
if (resourceCount.length === 0 && siteResourceCount.length === 0) {
|
||||||
return name;
|
return name;
|
||||||
}
|
}
|
||||||
loops++;
|
loops++;
|
||||||
@@ -74,9 +128,7 @@ export async function getUniqueSiteResourceName(orgId: string): Promise<string>
|
|||||||
|
|
||||||
export async function getUniqueExitNodeEndpointName(): Promise<string> {
|
export async function getUniqueExitNodeEndpointName(): Promise<string> {
|
||||||
let loops = 0;
|
let loops = 0;
|
||||||
const count = await db
|
const count = await db.select().from(exitNodes);
|
||||||
.select()
|
|
||||||
.from(exitNodes);
|
|
||||||
while (true) {
|
while (true) {
|
||||||
if (loops > 100) {
|
if (loops > 100) {
|
||||||
throw new Error("Could not generate a unique name");
|
throw new Error("Could not generate a unique name");
|
||||||
@@ -95,14 +147,11 @@ export async function getUniqueExitNodeEndpointName(): Promise<string> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
export function generateName(): string {
|
export function generateName(): string {
|
||||||
const name = (
|
const name = (
|
||||||
names.descriptors[
|
names.descriptors[randomInt(names.descriptors.length)] +
|
||||||
Math.floor(Math.random() * names.descriptors.length)
|
|
||||||
] +
|
|
||||||
"-" +
|
"-" +
|
||||||
names.animals[Math.floor(Math.random() * names.animals.length)]
|
names.animals[randomInt(names.animals.length)]
|
||||||
)
|
)
|
||||||
.toLowerCase()
|
.toLowerCase()
|
||||||
.replace(/\s/g, "-");
|
.replace(/\s/g, "-");
|
||||||
|
|||||||
@@ -13,9 +13,12 @@ function createDb() {
|
|||||||
connection_string: process.env.POSTGRES_CONNECTION_STRING
|
connection_string: process.env.POSTGRES_CONNECTION_STRING
|
||||||
};
|
};
|
||||||
if (process.env.POSTGRES_REPLICA_CONNECTION_STRINGS) {
|
if (process.env.POSTGRES_REPLICA_CONNECTION_STRINGS) {
|
||||||
const replicas = process.env.POSTGRES_REPLICA_CONNECTION_STRINGS.split(",").map((conn) => ({
|
const replicas =
|
||||||
connection_string: conn.trim()
|
process.env.POSTGRES_REPLICA_CONNECTION_STRINGS.split(
|
||||||
}));
|
","
|
||||||
|
).map((conn) => ({
|
||||||
|
connection_string: conn.trim()
|
||||||
|
}));
|
||||||
config.postgres.replicas = replicas;
|
config.postgres.replicas = replicas;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -35,32 +38,49 @@ function createDb() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Create connection pools instead of individual connections
|
// Create connection pools instead of individual connections
|
||||||
|
const poolConfig = config.postgres.pool;
|
||||||
const primaryPool = new Pool({
|
const primaryPool = new Pool({
|
||||||
connectionString,
|
connectionString,
|
||||||
max: 20,
|
max: poolConfig?.max_connections || 20,
|
||||||
idleTimeoutMillis: 30000,
|
idleTimeoutMillis: poolConfig?.idle_timeout_ms || 30000,
|
||||||
connectionTimeoutMillis: 5000,
|
connectionTimeoutMillis: poolConfig?.connection_timeout_ms || 5000
|
||||||
});
|
});
|
||||||
|
|
||||||
const replicas = [];
|
const replicas = [];
|
||||||
|
|
||||||
if (!replicaConnections.length) {
|
if (!replicaConnections.length) {
|
||||||
replicas.push(DrizzlePostgres(primaryPool));
|
replicas.push(
|
||||||
|
DrizzlePostgres(primaryPool, {
|
||||||
|
logger: process.env.QUERY_LOGGING == "true"
|
||||||
|
})
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
for (const conn of replicaConnections) {
|
for (const conn of replicaConnections) {
|
||||||
const replicaPool = new Pool({
|
const replicaPool = new Pool({
|
||||||
connectionString: conn.connection_string,
|
connectionString: conn.connection_string,
|
||||||
max: 10,
|
max: poolConfig?.max_replica_connections || 20,
|
||||||
idleTimeoutMillis: 30000,
|
idleTimeoutMillis: poolConfig?.idle_timeout_ms || 30000,
|
||||||
connectionTimeoutMillis: 5000,
|
connectionTimeoutMillis:
|
||||||
|
poolConfig?.connection_timeout_ms || 5000
|
||||||
});
|
});
|
||||||
replicas.push(DrizzlePostgres(replicaPool));
|
replicas.push(
|
||||||
|
DrizzlePostgres(replicaPool, {
|
||||||
|
logger: process.env.QUERY_LOGGING == "true"
|
||||||
|
})
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return withReplicas(DrizzlePostgres(primaryPool), replicas as any);
|
return withReplicas(
|
||||||
|
DrizzlePostgres(primaryPool, {
|
||||||
|
logger: process.env.QUERY_LOGGING == "true"
|
||||||
|
}),
|
||||||
|
replicas as any
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export const db = createDb();
|
export const db = createDb();
|
||||||
export default db;
|
export default db;
|
||||||
export type Transaction = Parameters<Parameters<typeof db["transaction"]>[0]>[0];
|
export type Transaction = Parameters<
|
||||||
|
Parameters<(typeof db)["transaction"]>[0]
|
||||||
|
>[0];
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
export * from "./driver";
|
export * from "./driver";
|
||||||
export * from "./schema";
|
export * from "./schema/schema";
|
||||||
export * from "./privateSchema";
|
export * from "./schema/privateSchema";
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ const runMigrations = async () => {
|
|||||||
migrationsFolder: migrationsFolder
|
migrationsFolder: migrationsFolder
|
||||||
});
|
});
|
||||||
console.log("Migrations completed successfully.");
|
console.log("Migrations completed successfully.");
|
||||||
|
process.exit(0);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error running migrations:", error);
|
console.error("Error running migrations:", error);
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
|
|||||||
@@ -1,16 +1,3 @@
|
|||||||
/*
|
|
||||||
* This file is part of a proprietary work.
|
|
||||||
*
|
|
||||||
* Copyright (c) 2025 Fossorial, Inc.
|
|
||||||
* All rights reserved.
|
|
||||||
*
|
|
||||||
* This file is licensed under the Fossorial Commercial License.
|
|
||||||
* You may not use this file except in compliance with the License.
|
|
||||||
* Unauthorized use, copying, modification, or distribution is strictly prohibited.
|
|
||||||
*
|
|
||||||
* This file is not licensed under the AGPLv3.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import {
|
import {
|
||||||
pgTable,
|
pgTable,
|
||||||
serial,
|
serial,
|
||||||
@@ -19,7 +6,8 @@ import {
|
|||||||
integer,
|
integer,
|
||||||
bigint,
|
bigint,
|
||||||
real,
|
real,
|
||||||
text
|
text,
|
||||||
|
index
|
||||||
} from "drizzle-orm/pg-core";
|
} from "drizzle-orm/pg-core";
|
||||||
import { InferSelectModel } from "drizzle-orm";
|
import { InferSelectModel } from "drizzle-orm";
|
||||||
import { domains, orgs, targets, users, exitNodes, sessions } from "./schema";
|
import { domains, orgs, targets, users, exitNodes, sessions } from "./schema";
|
||||||
@@ -179,6 +167,7 @@ export const remoteExitNodes = pgTable("remoteExitNode", {
|
|||||||
secretHash: varchar("secretHash").notNull(),
|
secretHash: varchar("secretHash").notNull(),
|
||||||
dateCreated: varchar("dateCreated").notNull(),
|
dateCreated: varchar("dateCreated").notNull(),
|
||||||
version: varchar("version"),
|
version: varchar("version"),
|
||||||
|
secondaryVersion: varchar("secondaryVersion"), // This is to detect the new nodes after the transition to pangolin-node
|
||||||
exitNodeId: integer("exitNodeId").references(() => exitNodes.exitNodeId, {
|
exitNodeId: integer("exitNodeId").references(() => exitNodes.exitNodeId, {
|
||||||
onDelete: "cascade"
|
onDelete: "cascade"
|
||||||
})
|
})
|
||||||
@@ -226,6 +215,43 @@ export const sessionTransferToken = pgTable("sessionTransferToken", {
|
|||||||
expiresAt: bigint("expiresAt", { mode: "number" }).notNull()
|
expiresAt: bigint("expiresAt", { mode: "number" }).notNull()
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const actionAuditLog = pgTable("actionAuditLog", {
|
||||||
|
id: serial("id").primaryKey(),
|
||||||
|
timestamp: bigint("timestamp", { mode: "number" }).notNull(), // this is EPOCH time in seconds
|
||||||
|
orgId: varchar("orgId")
|
||||||
|
.notNull()
|
||||||
|
.references(() => orgs.orgId, { onDelete: "cascade" }),
|
||||||
|
actorType: varchar("actorType", { length: 50 }).notNull(),
|
||||||
|
actor: varchar("actor", { length: 255 }).notNull(),
|
||||||
|
actorId: varchar("actorId", { length: 255 }).notNull(),
|
||||||
|
action: varchar("action", { length: 100 }).notNull(),
|
||||||
|
metadata: text("metadata")
|
||||||
|
}, (table) => ([
|
||||||
|
index("idx_actionAuditLog_timestamp").on(table.timestamp),
|
||||||
|
index("idx_actionAuditLog_org_timestamp").on(table.orgId, table.timestamp)
|
||||||
|
]));
|
||||||
|
|
||||||
|
export const accessAuditLog = pgTable("accessAuditLog", {
|
||||||
|
id: serial("id").primaryKey(),
|
||||||
|
timestamp: bigint("timestamp", { mode: "number" }).notNull(), // this is EPOCH time in seconds
|
||||||
|
orgId: varchar("orgId")
|
||||||
|
.notNull()
|
||||||
|
.references(() => orgs.orgId, { onDelete: "cascade" }),
|
||||||
|
actorType: varchar("actorType", { length: 50 }),
|
||||||
|
actor: varchar("actor", { length: 255 }),
|
||||||
|
actorId: varchar("actorId", { length: 255 }),
|
||||||
|
resourceId: integer("resourceId"),
|
||||||
|
ip: varchar("ip", { length: 45 }),
|
||||||
|
type: varchar("type", { length: 100 }).notNull(),
|
||||||
|
action: boolean("action").notNull(),
|
||||||
|
location: text("location"),
|
||||||
|
userAgent: text("userAgent"),
|
||||||
|
metadata: text("metadata")
|
||||||
|
}, (table) => ([
|
||||||
|
index("idx_identityAuditLog_timestamp").on(table.timestamp),
|
||||||
|
index("idx_identityAuditLog_org_timestamp").on(table.orgId, table.timestamp)
|
||||||
|
]));
|
||||||
|
|
||||||
export type Limit = InferSelectModel<typeof limits>;
|
export type Limit = InferSelectModel<typeof limits>;
|
||||||
export type Account = InferSelectModel<typeof account>;
|
export type Account = InferSelectModel<typeof account>;
|
||||||
export type Certificate = InferSelectModel<typeof certificates>;
|
export type Certificate = InferSelectModel<typeof certificates>;
|
||||||
@@ -243,3 +269,5 @@ export type RemoteExitNodeSession = InferSelectModel<
|
|||||||
>;
|
>;
|
||||||
export type ExitNodeOrg = InferSelectModel<typeof exitNodeOrgs>;
|
export type ExitNodeOrg = InferSelectModel<typeof exitNodeOrgs>;
|
||||||
export type LoginPage = InferSelectModel<typeof loginPage>;
|
export type LoginPage = InferSelectModel<typeof loginPage>;
|
||||||
|
export type ActionAuditLog = InferSelectModel<typeof actionAuditLog>;
|
||||||
|
export type AccessAuditLog = InferSelectModel<typeof accessAuditLog>;
|
||||||
@@ -6,10 +6,12 @@ import {
|
|||||||
integer,
|
integer,
|
||||||
bigint,
|
bigint,
|
||||||
real,
|
real,
|
||||||
text
|
text,
|
||||||
|
index
|
||||||
} from "drizzle-orm/pg-core";
|
} from "drizzle-orm/pg-core";
|
||||||
import { InferSelectModel } from "drizzle-orm";
|
import { InferSelectModel } from "drizzle-orm";
|
||||||
import { randomUUID } from "crypto";
|
import { randomUUID } from "crypto";
|
||||||
|
import { alias } from "yargs";
|
||||||
|
|
||||||
export const domains = pgTable("domains", {
|
export const domains = pgTable("domains", {
|
||||||
domainId: varchar("domainId").primaryKey(),
|
domainId: varchar("domainId").primaryKey(),
|
||||||
@@ -18,15 +20,41 @@ export const domains = pgTable("domains", {
|
|||||||
type: varchar("type"), // "ns", "cname", "wildcard"
|
type: varchar("type"), // "ns", "cname", "wildcard"
|
||||||
verified: boolean("verified").notNull().default(false),
|
verified: boolean("verified").notNull().default(false),
|
||||||
failed: boolean("failed").notNull().default(false),
|
failed: boolean("failed").notNull().default(false),
|
||||||
tries: integer("tries").notNull().default(0)
|
tries: integer("tries").notNull().default(0),
|
||||||
|
certResolver: varchar("certResolver"),
|
||||||
|
customCertResolver: varchar("customCertResolver"),
|
||||||
|
preferWildcardCert: boolean("preferWildcardCert")
|
||||||
|
});
|
||||||
|
|
||||||
|
export const dnsRecords = pgTable("dnsRecords", {
|
||||||
|
id: serial("id").primaryKey(),
|
||||||
|
domainId: varchar("domainId")
|
||||||
|
.notNull()
|
||||||
|
.references(() => domains.domainId, { onDelete: "cascade" }),
|
||||||
|
recordType: varchar("recordType").notNull(), // "NS" | "CNAME" | "A" | "TXT"
|
||||||
|
baseDomain: varchar("baseDomain"),
|
||||||
|
value: varchar("value").notNull(),
|
||||||
|
verified: boolean("verified").notNull().default(false)
|
||||||
});
|
});
|
||||||
|
|
||||||
export const orgs = pgTable("orgs", {
|
export const orgs = pgTable("orgs", {
|
||||||
orgId: varchar("orgId").primaryKey(),
|
orgId: varchar("orgId").primaryKey(),
|
||||||
name: varchar("name").notNull(),
|
name: varchar("name").notNull(),
|
||||||
subnet: varchar("subnet"),
|
subnet: varchar("subnet"),
|
||||||
|
utilitySubnet: varchar("utilitySubnet"), // this is the subnet for utility addresses
|
||||||
createdAt: text("createdAt"),
|
createdAt: text("createdAt"),
|
||||||
settings: text("settings") // JSON blob of org-specific settings
|
requireTwoFactor: boolean("requireTwoFactor"),
|
||||||
|
maxSessionLengthHours: integer("maxSessionLengthHours"),
|
||||||
|
passwordExpiryDays: integer("passwordExpiryDays"),
|
||||||
|
settingsLogRetentionDaysRequest: integer("settingsLogRetentionDaysRequest") // where 0 = dont keep logs and -1 = keep forever, and 9001 = end of the following year
|
||||||
|
.notNull()
|
||||||
|
.default(7),
|
||||||
|
settingsLogRetentionDaysAccess: integer("settingsLogRetentionDaysAccess") // where 0 = dont keep logs and -1 = keep forever and 9001 = end of the following year
|
||||||
|
.notNull()
|
||||||
|
.default(0),
|
||||||
|
settingsLogRetentionDaysAction: integer("settingsLogRetentionDaysAction") // where 0 = dont keep logs and -1 = keep forever and 9001 = end of the following year
|
||||||
|
.notNull()
|
||||||
|
.default(0)
|
||||||
});
|
});
|
||||||
|
|
||||||
export const orgDomains = pgTable("orgDomains", {
|
export const orgDomains = pgTable("orgDomains", {
|
||||||
@@ -62,8 +90,7 @@ export const sites = pgTable("sites", {
|
|||||||
publicKey: varchar("publicKey"),
|
publicKey: varchar("publicKey"),
|
||||||
lastHolePunch: bigint("lastHolePunch", { mode: "number" }),
|
lastHolePunch: bigint("lastHolePunch", { mode: "number" }),
|
||||||
listenPort: integer("listenPort"),
|
listenPort: integer("listenPort"),
|
||||||
dockerSocketEnabled: boolean("dockerSocketEnabled").notNull().default(true),
|
dockerSocketEnabled: boolean("dockerSocketEnabled").notNull().default(true)
|
||||||
remoteSubnets: text("remoteSubnets") // comma-separated list of subnets that this site can access
|
|
||||||
});
|
});
|
||||||
|
|
||||||
export const resources = pgTable("resources", {
|
export const resources = pgTable("resources", {
|
||||||
@@ -100,9 +127,11 @@ export const resources = pgTable("resources", {
|
|||||||
setHostHeader: varchar("setHostHeader"),
|
setHostHeader: varchar("setHostHeader"),
|
||||||
enableProxy: boolean("enableProxy").default(true),
|
enableProxy: boolean("enableProxy").default(true),
|
||||||
skipToIdpId: integer("skipToIdpId").references(() => idp.idpId, {
|
skipToIdpId: integer("skipToIdpId").references(() => idp.idpId, {
|
||||||
onDelete: "cascade"
|
onDelete: "set null"
|
||||||
}),
|
}),
|
||||||
headers: text("headers") // comma-separated list of headers to add to the request
|
headers: text("headers"), // comma-separated list of headers to add to the request
|
||||||
|
proxyProtocol: boolean("proxyProtocol").notNull().default(false),
|
||||||
|
proxyProtocolVersion: integer("proxyProtocolVersion").default(1)
|
||||||
});
|
});
|
||||||
|
|
||||||
export const targets = pgTable("targets", {
|
export const targets = pgTable("targets", {
|
||||||
@@ -125,7 +154,8 @@ export const targets = pgTable("targets", {
|
|||||||
path: text("path"),
|
path: text("path"),
|
||||||
pathMatchType: text("pathMatchType"), // exact, prefix, regex
|
pathMatchType: text("pathMatchType"), // exact, prefix, regex
|
||||||
rewritePath: text("rewritePath"), // if set, rewrites the path to this value before sending to the target
|
rewritePath: text("rewritePath"), // if set, rewrites the path to this value before sending to the target
|
||||||
rewritePathType: text("rewritePathType") // exact, prefix, regex, stripPrefix
|
rewritePathType: text("rewritePathType"), // exact, prefix, regex, stripPrefix
|
||||||
|
priority: integer("priority").notNull().default(100)
|
||||||
});
|
});
|
||||||
|
|
||||||
export const targetHealthCheck = pgTable("targetHealthCheck", {
|
export const targetHealthCheck = pgTable("targetHealthCheck", {
|
||||||
@@ -146,7 +176,8 @@ export const targetHealthCheck = pgTable("targetHealthCheck", {
|
|||||||
hcFollowRedirects: boolean("hcFollowRedirects").default(true),
|
hcFollowRedirects: boolean("hcFollowRedirects").default(true),
|
||||||
hcMethod: varchar("hcMethod").default("GET"),
|
hcMethod: varchar("hcMethod").default("GET"),
|
||||||
hcStatus: integer("hcStatus"), // http code
|
hcStatus: integer("hcStatus"), // http code
|
||||||
hcHealth: text("hcHealth").default("unknown") // "unknown", "healthy", "unhealthy"
|
hcHealth: text("hcHealth").default("unknown"), // "unknown", "healthy", "unhealthy"
|
||||||
|
hcTlsServerName: text("hcTlsServerName"),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const exitNodes = pgTable("exitNodes", {
|
export const exitNodes = pgTable("exitNodes", {
|
||||||
@@ -175,11 +206,41 @@ export const siteResources = pgTable("siteResources", {
|
|||||||
.references(() => orgs.orgId, { onDelete: "cascade" }),
|
.references(() => orgs.orgId, { onDelete: "cascade" }),
|
||||||
niceId: varchar("niceId").notNull(),
|
niceId: varchar("niceId").notNull(),
|
||||||
name: varchar("name").notNull(),
|
name: varchar("name").notNull(),
|
||||||
protocol: varchar("protocol").notNull(),
|
mode: varchar("mode").notNull(), // "host" | "cidr" | "port"
|
||||||
proxyPort: integer("proxyPort").notNull(),
|
protocol: varchar("protocol"), // only for port mode
|
||||||
destinationPort: integer("destinationPort").notNull(),
|
proxyPort: integer("proxyPort"), // only for port mode
|
||||||
destinationIp: varchar("destinationIp").notNull(),
|
destinationPort: integer("destinationPort"), // only for port mode
|
||||||
enabled: boolean("enabled").notNull().default(true)
|
destination: varchar("destination").notNull(), // ip, cidr, hostname; validate against the mode
|
||||||
|
enabled: boolean("enabled").notNull().default(true),
|
||||||
|
alias: varchar("alias"),
|
||||||
|
aliasAddress: varchar("aliasAddress")
|
||||||
|
});
|
||||||
|
|
||||||
|
export const clientSiteResources = pgTable("clientSiteResources", {
|
||||||
|
clientId: integer("clientId")
|
||||||
|
.notNull()
|
||||||
|
.references(() => clients.clientId, { onDelete: "cascade" }),
|
||||||
|
siteResourceId: integer("siteResourceId")
|
||||||
|
.notNull()
|
||||||
|
.references(() => siteResources.siteResourceId, { onDelete: "cascade" })
|
||||||
|
});
|
||||||
|
|
||||||
|
export const roleSiteResources = pgTable("roleSiteResources", {
|
||||||
|
roleId: integer("roleId")
|
||||||
|
.notNull()
|
||||||
|
.references(() => roles.roleId, { onDelete: "cascade" }),
|
||||||
|
siteResourceId: integer("siteResourceId")
|
||||||
|
.notNull()
|
||||||
|
.references(() => siteResources.siteResourceId, { onDelete: "cascade" })
|
||||||
|
});
|
||||||
|
|
||||||
|
export const userSiteResources = pgTable("userSiteResources", {
|
||||||
|
userId: varchar("userId")
|
||||||
|
.notNull()
|
||||||
|
.references(() => users.userId, { onDelete: "cascade" }),
|
||||||
|
siteResourceId: integer("siteResourceId")
|
||||||
|
.notNull()
|
||||||
|
.references(() => siteResources.siteResourceId, { onDelete: "cascade" })
|
||||||
});
|
});
|
||||||
|
|
||||||
export const users = pgTable("user", {
|
export const users = pgTable("user", {
|
||||||
@@ -199,7 +260,8 @@ export const users = pgTable("user", {
|
|||||||
dateCreated: varchar("dateCreated").notNull(),
|
dateCreated: varchar("dateCreated").notNull(),
|
||||||
termsAcceptedTimestamp: varchar("termsAcceptedTimestamp"),
|
termsAcceptedTimestamp: varchar("termsAcceptedTimestamp"),
|
||||||
termsVersion: varchar("termsVersion"),
|
termsVersion: varchar("termsVersion"),
|
||||||
serverAdmin: boolean("serverAdmin").notNull().default(false)
|
serverAdmin: boolean("serverAdmin").notNull().default(false),
|
||||||
|
lastPasswordChange: bigint("lastPasswordChange", { mode: "number" })
|
||||||
});
|
});
|
||||||
|
|
||||||
export const newts = pgTable("newt", {
|
export const newts = pgTable("newt", {
|
||||||
@@ -225,7 +287,9 @@ export const sessions = pgTable("session", {
|
|||||||
userId: varchar("userId")
|
userId: varchar("userId")
|
||||||
.notNull()
|
.notNull()
|
||||||
.references(() => users.userId, { onDelete: "cascade" }),
|
.references(() => users.userId, { onDelete: "cascade" }),
|
||||||
expiresAt: bigint("expiresAt", { mode: "number" }).notNull()
|
expiresAt: bigint("expiresAt", { mode: "number" }).notNull(),
|
||||||
|
issuedAt: bigint("issuedAt", { mode: "number" }),
|
||||||
|
deviceAuthUsed: boolean("deviceAuthUsed").notNull().default(false)
|
||||||
});
|
});
|
||||||
|
|
||||||
export const newtSessions = pgTable("newtSession", {
|
export const newtSessions = pgTable("newtSession", {
|
||||||
@@ -380,6 +444,14 @@ export const resourcePassword = pgTable("resourcePassword", {
|
|||||||
passwordHash: varchar("passwordHash").notNull()
|
passwordHash: varchar("passwordHash").notNull()
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const resourceHeaderAuth = pgTable("resourceHeaderAuth", {
|
||||||
|
headerAuthId: serial("headerAuthId").primaryKey(),
|
||||||
|
resourceId: integer("resourceId")
|
||||||
|
.notNull()
|
||||||
|
.references(() => resources.resourceId, { onDelete: "cascade" }),
|
||||||
|
headerAuthHash: varchar("headerAuthHash").notNull()
|
||||||
|
});
|
||||||
|
|
||||||
export const resourceAccessToken = pgTable("resourceAccessToken", {
|
export const resourceAccessToken = pgTable("resourceAccessToken", {
|
||||||
accessTokenId: varchar("accessTokenId").primaryKey(),
|
accessTokenId: varchar("accessTokenId").primaryKey(),
|
||||||
orgId: varchar("orgId")
|
orgId: varchar("orgId")
|
||||||
@@ -434,7 +506,8 @@ export const resourceSessions = pgTable("resourceSessions", {
|
|||||||
{
|
{
|
||||||
onDelete: "cascade"
|
onDelete: "cascade"
|
||||||
}
|
}
|
||||||
)
|
),
|
||||||
|
issuedAt: bigint("issuedAt", { mode: "number" })
|
||||||
});
|
});
|
||||||
|
|
||||||
export const resourceWhitelist = pgTable("resourceWhitelist", {
|
export const resourceWhitelist = pgTable("resourceWhitelist", {
|
||||||
@@ -558,7 +631,7 @@ export const idpOrg = pgTable("idpOrg", {
|
|||||||
});
|
});
|
||||||
|
|
||||||
export const clients = pgTable("clients", {
|
export const clients = pgTable("clients", {
|
||||||
clientId: serial("id").primaryKey(),
|
clientId: serial("clientId").primaryKey(),
|
||||||
orgId: varchar("orgId")
|
orgId: varchar("orgId")
|
||||||
.references(() => orgs.orgId, {
|
.references(() => orgs.orgId, {
|
||||||
onDelete: "cascade"
|
onDelete: "cascade"
|
||||||
@@ -567,6 +640,12 @@ export const clients = pgTable("clients", {
|
|||||||
exitNodeId: integer("exitNode").references(() => exitNodes.exitNodeId, {
|
exitNodeId: integer("exitNode").references(() => exitNodes.exitNodeId, {
|
||||||
onDelete: "set null"
|
onDelete: "set null"
|
||||||
}),
|
}),
|
||||||
|
userId: text("userId").references(() => users.userId, {
|
||||||
|
// optionally tied to a user and in this case delete when the user deletes
|
||||||
|
onDelete: "cascade"
|
||||||
|
}),
|
||||||
|
niceId: varchar("niceId").notNull(),
|
||||||
|
olmId: text("olmId"), // to lock it to a specific olm optionally
|
||||||
name: varchar("name").notNull(),
|
name: varchar("name").notNull(),
|
||||||
pubKey: varchar("pubKey"),
|
pubKey: varchar("pubKey"),
|
||||||
subnet: varchar("subnet").notNull(),
|
subnet: varchar("subnet").notNull(),
|
||||||
@@ -581,23 +660,40 @@ export const clients = pgTable("clients", {
|
|||||||
maxConnections: integer("maxConnections")
|
maxConnections: integer("maxConnections")
|
||||||
});
|
});
|
||||||
|
|
||||||
export const clientSites = pgTable("clientSites", {
|
export const clientSitesAssociationsCache = pgTable(
|
||||||
clientId: integer("clientId")
|
"clientSitesAssociationsCache",
|
||||||
.notNull()
|
{
|
||||||
.references(() => clients.clientId, { onDelete: "cascade" }),
|
clientId: integer("clientId") // not a foreign key here so after its deleted the rebuild function can delete it and send the message
|
||||||
siteId: integer("siteId")
|
.notNull(),
|
||||||
.notNull()
|
siteId: integer("siteId").notNull(),
|
||||||
.references(() => sites.siteId, { onDelete: "cascade" }),
|
isRelayed: boolean("isRelayed").notNull().default(false),
|
||||||
isRelayed: boolean("isRelayed").notNull().default(false),
|
endpoint: varchar("endpoint"),
|
||||||
endpoint: varchar("endpoint")
|
publicKey: varchar("publicKey") // this will act as the session's public key for hole punching so we can track when it changes
|
||||||
});
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
export const clientSiteResourcesAssociationsCache = pgTable(
|
||||||
|
"clientSiteResourcesAssociationsCache",
|
||||||
|
{
|
||||||
|
clientId: integer("clientId") // not a foreign key here so after its deleted the rebuild function can delete it and send the message
|
||||||
|
.notNull(),
|
||||||
|
siteResourceId: integer("siteResourceId").notNull()
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
export const olms = pgTable("olms", {
|
export const olms = pgTable("olms", {
|
||||||
olmId: varchar("id").primaryKey(),
|
olmId: varchar("id").primaryKey(),
|
||||||
secretHash: varchar("secretHash").notNull(),
|
secretHash: varchar("secretHash").notNull(),
|
||||||
dateCreated: varchar("dateCreated").notNull(),
|
dateCreated: varchar("dateCreated").notNull(),
|
||||||
version: text("version"),
|
version: text("version"),
|
||||||
|
agent: text("agent"),
|
||||||
|
name: varchar("name"),
|
||||||
clientId: integer("clientId").references(() => clients.clientId, {
|
clientId: integer("clientId").references(() => clients.clientId, {
|
||||||
|
// we will switch this depending on the current org it wants to connect to
|
||||||
|
onDelete: "set null"
|
||||||
|
}),
|
||||||
|
userId: text("userId").references(() => users.userId, {
|
||||||
|
// optionally tied to a user and in this case delete when the user deletes
|
||||||
onDelete: "cascade"
|
onDelete: "cascade"
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
@@ -662,6 +758,72 @@ export const setupTokens = pgTable("setupTokens", {
|
|||||||
dateUsed: varchar("dateUsed")
|
dateUsed: varchar("dateUsed")
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Blueprint runs
|
||||||
|
export const blueprints = pgTable("blueprints", {
|
||||||
|
blueprintId: serial("blueprintId").primaryKey(),
|
||||||
|
orgId: text("orgId")
|
||||||
|
.references(() => orgs.orgId, {
|
||||||
|
onDelete: "cascade"
|
||||||
|
})
|
||||||
|
.notNull(),
|
||||||
|
name: varchar("name").notNull(),
|
||||||
|
source: varchar("source").notNull(),
|
||||||
|
createdAt: integer("createdAt").notNull(),
|
||||||
|
succeeded: boolean("succeeded").notNull(),
|
||||||
|
contents: text("contents").notNull(),
|
||||||
|
message: text("message")
|
||||||
|
});
|
||||||
|
export const requestAuditLog = pgTable(
|
||||||
|
"requestAuditLog",
|
||||||
|
{
|
||||||
|
id: serial("id").primaryKey(),
|
||||||
|
timestamp: integer("timestamp").notNull(), // this is EPOCH time in seconds
|
||||||
|
orgId: text("orgId").references(() => orgs.orgId, {
|
||||||
|
onDelete: "cascade"
|
||||||
|
}),
|
||||||
|
action: boolean("action").notNull(),
|
||||||
|
reason: integer("reason").notNull(),
|
||||||
|
actorType: text("actorType"),
|
||||||
|
actor: text("actor"),
|
||||||
|
actorId: text("actorId"),
|
||||||
|
resourceId: integer("resourceId"),
|
||||||
|
ip: text("ip"),
|
||||||
|
location: text("location"),
|
||||||
|
userAgent: text("userAgent"),
|
||||||
|
metadata: text("metadata"),
|
||||||
|
headers: text("headers"), // JSON blob
|
||||||
|
query: text("query"), // JSON blob
|
||||||
|
originalRequestURL: text("originalRequestURL"),
|
||||||
|
scheme: text("scheme"),
|
||||||
|
host: text("host"),
|
||||||
|
path: text("path"),
|
||||||
|
method: text("method"),
|
||||||
|
tls: boolean("tls")
|
||||||
|
},
|
||||||
|
(table) => [
|
||||||
|
index("idx_requestAuditLog_timestamp").on(table.timestamp),
|
||||||
|
index("idx_requestAuditLog_org_timestamp").on(
|
||||||
|
table.orgId,
|
||||||
|
table.timestamp
|
||||||
|
)
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
export const deviceWebAuthCodes = pgTable("deviceWebAuthCodes", {
|
||||||
|
codeId: serial("codeId").primaryKey(),
|
||||||
|
code: text("code").notNull().unique(),
|
||||||
|
ip: text("ip"),
|
||||||
|
city: text("city"),
|
||||||
|
deviceName: text("deviceName"),
|
||||||
|
applicationName: text("applicationName").notNull(),
|
||||||
|
expiresAt: bigint("expiresAt", { mode: "number" }).notNull(),
|
||||||
|
createdAt: bigint("createdAt", { mode: "number" }).notNull(),
|
||||||
|
verified: boolean("verified").notNull().default(false),
|
||||||
|
userId: varchar("userId").references(() => users.userId, {
|
||||||
|
onDelete: "cascade"
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
export type Org = InferSelectModel<typeof orgs>;
|
export type Org = InferSelectModel<typeof orgs>;
|
||||||
export type User = InferSelectModel<typeof users>;
|
export type User = InferSelectModel<typeof users>;
|
||||||
export type Site = InferSelectModel<typeof sites>;
|
export type Site = InferSelectModel<typeof sites>;
|
||||||
@@ -689,6 +851,7 @@ export type UserOrg = InferSelectModel<typeof userOrgs>;
|
|||||||
export type ResourceSession = InferSelectModel<typeof resourceSessions>;
|
export type ResourceSession = InferSelectModel<typeof resourceSessions>;
|
||||||
export type ResourcePincode = InferSelectModel<typeof resourcePincode>;
|
export type ResourcePincode = InferSelectModel<typeof resourcePincode>;
|
||||||
export type ResourcePassword = InferSelectModel<typeof resourcePassword>;
|
export type ResourcePassword = InferSelectModel<typeof resourcePassword>;
|
||||||
|
export type ResourceHeaderAuth = InferSelectModel<typeof resourceHeaderAuth>;
|
||||||
export type ResourceOtp = InferSelectModel<typeof resourceOtp>;
|
export type ResourceOtp = InferSelectModel<typeof resourceOtp>;
|
||||||
export type ResourceAccessToken = InferSelectModel<typeof resourceAccessToken>;
|
export type ResourceAccessToken = InferSelectModel<typeof resourceAccessToken>;
|
||||||
export type ResourceWhitelist = InferSelectModel<typeof resourceWhitelist>;
|
export type ResourceWhitelist = InferSelectModel<typeof resourceWhitelist>;
|
||||||
@@ -701,7 +864,7 @@ export type ApiKey = InferSelectModel<typeof apiKeys>;
|
|||||||
export type ApiKeyAction = InferSelectModel<typeof apiKeyActions>;
|
export type ApiKeyAction = InferSelectModel<typeof apiKeyActions>;
|
||||||
export type ApiKeyOrg = InferSelectModel<typeof apiKeyOrg>;
|
export type ApiKeyOrg = InferSelectModel<typeof apiKeyOrg>;
|
||||||
export type Client = InferSelectModel<typeof clients>;
|
export type Client = InferSelectModel<typeof clients>;
|
||||||
export type ClientSite = InferSelectModel<typeof clientSites>;
|
export type ClientSite = InferSelectModel<typeof clientSitesAssociationsCache>;
|
||||||
export type Olm = InferSelectModel<typeof olms>;
|
export type Olm = InferSelectModel<typeof olms>;
|
||||||
export type OlmSession = InferSelectModel<typeof olmSessions>;
|
export type OlmSession = InferSelectModel<typeof olmSessions>;
|
||||||
export type UserClient = InferSelectModel<typeof userClients>;
|
export type UserClient = InferSelectModel<typeof userClients>;
|
||||||
@@ -710,4 +873,11 @@ export type OrgDomains = InferSelectModel<typeof orgDomains>;
|
|||||||
export type SiteResource = InferSelectModel<typeof siteResources>;
|
export type SiteResource = InferSelectModel<typeof siteResources>;
|
||||||
export type SetupToken = InferSelectModel<typeof setupTokens>;
|
export type SetupToken = InferSelectModel<typeof setupTokens>;
|
||||||
export type HostMeta = InferSelectModel<typeof hostMeta>;
|
export type HostMeta = InferSelectModel<typeof hostMeta>;
|
||||||
export type TargetHealthCheck = InferSelectModel<typeof targetHealthCheck>;
|
export type TargetHealthCheck = InferSelectModel<typeof targetHealthCheck>;
|
||||||
|
export type IdpOidcConfig = InferSelectModel<typeof idpOidcConfig>;
|
||||||
|
export type Blueprint = InferSelectModel<typeof blueprints>;
|
||||||
|
export type LicenseKey = InferSelectModel<typeof licenseKey>;
|
||||||
|
export type SecurityKey = InferSelectModel<typeof securityKeys>;
|
||||||
|
export type WebauthnChallenge = InferSelectModel<typeof webauthnChallenge>;
|
||||||
|
export type DeviceWebAuthCode = InferSelectModel<typeof deviceWebAuthCodes>;
|
||||||
|
export type RequestAuditLog = InferSelectModel<typeof requestAuditLog>;
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import { db, loginPage, LoginPage, loginPageOrg } from "@server/db";
|
import { db, loginPage, LoginPage, loginPageOrg, Org, orgs } from "@server/db";
|
||||||
import {
|
import {
|
||||||
Resource,
|
Resource,
|
||||||
ResourcePassword,
|
ResourcePassword,
|
||||||
@@ -6,6 +6,8 @@ import {
|
|||||||
ResourceRule,
|
ResourceRule,
|
||||||
resourcePassword,
|
resourcePassword,
|
||||||
resourcePincode,
|
resourcePincode,
|
||||||
|
resourceHeaderAuth,
|
||||||
|
ResourceHeaderAuth,
|
||||||
resourceRules,
|
resourceRules,
|
||||||
resources,
|
resources,
|
||||||
roleResources,
|
roleResources,
|
||||||
@@ -15,15 +17,13 @@ import {
|
|||||||
users
|
users
|
||||||
} from "@server/db";
|
} from "@server/db";
|
||||||
import { and, eq } from "drizzle-orm";
|
import { and, eq } from "drizzle-orm";
|
||||||
import axios from "axios";
|
|
||||||
import config from "@server/lib/config";
|
|
||||||
import logger from "@server/logger";
|
|
||||||
import { tokenManager } from "@server/lib/tokenManager";
|
|
||||||
|
|
||||||
export type ResourceWithAuth = {
|
export type ResourceWithAuth = {
|
||||||
resource: Resource | null;
|
resource: Resource | null;
|
||||||
pincode: ResourcePincode | null;
|
pincode: ResourcePincode | null;
|
||||||
password: ResourcePassword | null;
|
password: ResourcePassword | null;
|
||||||
|
headerAuth: ResourceHeaderAuth | null;
|
||||||
|
org: Org;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type UserSessionWithUser = {
|
export type UserSessionWithUser = {
|
||||||
@@ -37,30 +37,6 @@ export type UserSessionWithUser = {
|
|||||||
export async function getResourceByDomain(
|
export async function getResourceByDomain(
|
||||||
domain: string
|
domain: string
|
||||||
): Promise<ResourceWithAuth | null> {
|
): Promise<ResourceWithAuth | null> {
|
||||||
if (config.isManagedMode()) {
|
|
||||||
try {
|
|
||||||
const response = await axios.get(
|
|
||||||
`${config.getRawConfig().managed?.endpoint}/api/v1/hybrid/resource/domain/${domain}`,
|
|
||||||
await tokenManager.getAuthHeader()
|
|
||||||
);
|
|
||||||
return response.data.data;
|
|
||||||
} catch (error) {
|
|
||||||
if (axios.isAxiosError(error)) {
|
|
||||||
logger.error("Error fetching config in verify session:", {
|
|
||||||
message: error.message,
|
|
||||||
code: error.code,
|
|
||||||
status: error.response?.status,
|
|
||||||
statusText: error.response?.statusText,
|
|
||||||
url: error.config?.url,
|
|
||||||
method: error.config?.method
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
logger.error("Error fetching config in verify session:", error);
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const [result] = await db
|
const [result] = await db
|
||||||
.select()
|
.select()
|
||||||
.from(resources)
|
.from(resources)
|
||||||
@@ -72,6 +48,14 @@ export async function getResourceByDomain(
|
|||||||
resourcePassword,
|
resourcePassword,
|
||||||
eq(resourcePassword.resourceId, resources.resourceId)
|
eq(resourcePassword.resourceId, resources.resourceId)
|
||||||
)
|
)
|
||||||
|
.leftJoin(
|
||||||
|
resourceHeaderAuth,
|
||||||
|
eq(resourceHeaderAuth.resourceId, resources.resourceId)
|
||||||
|
)
|
||||||
|
.innerJoin(
|
||||||
|
orgs,
|
||||||
|
eq(orgs.orgId, resources.orgId)
|
||||||
|
)
|
||||||
.where(eq(resources.fullDomain, domain))
|
.where(eq(resources.fullDomain, domain))
|
||||||
.limit(1);
|
.limit(1);
|
||||||
|
|
||||||
@@ -82,7 +66,9 @@ export async function getResourceByDomain(
|
|||||||
return {
|
return {
|
||||||
resource: result.resources,
|
resource: result.resources,
|
||||||
pincode: result.resourcePincode,
|
pincode: result.resourcePincode,
|
||||||
password: result.resourcePassword
|
password: result.resourcePassword,
|
||||||
|
headerAuth: result.resourceHeaderAuth,
|
||||||
|
org: result.orgs
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -92,30 +78,6 @@ export async function getResourceByDomain(
|
|||||||
export async function getUserSessionWithUser(
|
export async function getUserSessionWithUser(
|
||||||
userSessionId: string
|
userSessionId: string
|
||||||
): Promise<UserSessionWithUser | null> {
|
): Promise<UserSessionWithUser | null> {
|
||||||
if (config.isManagedMode()) {
|
|
||||||
try {
|
|
||||||
const response = await axios.get(
|
|
||||||
`${config.getRawConfig().managed?.endpoint}/api/v1/hybrid/session/${userSessionId}`,
|
|
||||||
await tokenManager.getAuthHeader()
|
|
||||||
);
|
|
||||||
return response.data.data;
|
|
||||||
} catch (error) {
|
|
||||||
if (axios.isAxiosError(error)) {
|
|
||||||
logger.error("Error fetching config in verify session:", {
|
|
||||||
message: error.message,
|
|
||||||
code: error.code,
|
|
||||||
status: error.response?.status,
|
|
||||||
statusText: error.response?.statusText,
|
|
||||||
url: error.config?.url,
|
|
||||||
method: error.config?.method
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
logger.error("Error fetching config in verify session:", error);
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const [res] = await db
|
const [res] = await db
|
||||||
.select()
|
.select()
|
||||||
.from(sessions)
|
.from(sessions)
|
||||||
@@ -136,30 +98,6 @@ export async function getUserSessionWithUser(
|
|||||||
* Get user organization role
|
* Get user organization role
|
||||||
*/
|
*/
|
||||||
export async function getUserOrgRole(userId: string, orgId: string) {
|
export async function getUserOrgRole(userId: string, orgId: string) {
|
||||||
if (config.isManagedMode()) {
|
|
||||||
try {
|
|
||||||
const response = await axios.get(
|
|
||||||
`${config.getRawConfig().managed?.endpoint}/api/v1/hybrid/user/${userId}/org/${orgId}/role`,
|
|
||||||
await tokenManager.getAuthHeader()
|
|
||||||
);
|
|
||||||
return response.data.data;
|
|
||||||
} catch (error) {
|
|
||||||
if (axios.isAxiosError(error)) {
|
|
||||||
logger.error("Error fetching config in verify session:", {
|
|
||||||
message: error.message,
|
|
||||||
code: error.code,
|
|
||||||
status: error.response?.status,
|
|
||||||
statusText: error.response?.statusText,
|
|
||||||
url: error.config?.url,
|
|
||||||
method: error.config?.method
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
logger.error("Error fetching config in verify session:", error);
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const userOrgRole = await db
|
const userOrgRole = await db
|
||||||
.select()
|
.select()
|
||||||
.from(userOrgs)
|
.from(userOrgs)
|
||||||
@@ -176,30 +114,6 @@ export async function getRoleResourceAccess(
|
|||||||
resourceId: number,
|
resourceId: number,
|
||||||
roleId: number
|
roleId: number
|
||||||
) {
|
) {
|
||||||
if (config.isManagedMode()) {
|
|
||||||
try {
|
|
||||||
const response = await axios.get(
|
|
||||||
`${config.getRawConfig().managed?.endpoint}/api/v1/hybrid/role/${roleId}/resource/${resourceId}/access`,
|
|
||||||
await tokenManager.getAuthHeader()
|
|
||||||
);
|
|
||||||
return response.data.data;
|
|
||||||
} catch (error) {
|
|
||||||
if (axios.isAxiosError(error)) {
|
|
||||||
logger.error("Error fetching config in verify session:", {
|
|
||||||
message: error.message,
|
|
||||||
code: error.code,
|
|
||||||
status: error.response?.status,
|
|
||||||
statusText: error.response?.statusText,
|
|
||||||
url: error.config?.url,
|
|
||||||
method: error.config?.method
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
logger.error("Error fetching config in verify session:", error);
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const roleResourceAccess = await db
|
const roleResourceAccess = await db
|
||||||
.select()
|
.select()
|
||||||
.from(roleResources)
|
.from(roleResources)
|
||||||
@@ -221,30 +135,6 @@ export async function getUserResourceAccess(
|
|||||||
userId: string,
|
userId: string,
|
||||||
resourceId: number
|
resourceId: number
|
||||||
) {
|
) {
|
||||||
if (config.isManagedMode()) {
|
|
||||||
try {
|
|
||||||
const response = await axios.get(
|
|
||||||
`${config.getRawConfig().managed?.endpoint}/api/v1/hybrid/user/${userId}/resource/${resourceId}/access`,
|
|
||||||
await tokenManager.getAuthHeader()
|
|
||||||
);
|
|
||||||
return response.data.data;
|
|
||||||
} catch (error) {
|
|
||||||
if (axios.isAxiosError(error)) {
|
|
||||||
logger.error("Error fetching config in verify session:", {
|
|
||||||
message: error.message,
|
|
||||||
code: error.code,
|
|
||||||
status: error.response?.status,
|
|
||||||
statusText: error.response?.statusText,
|
|
||||||
url: error.config?.url,
|
|
||||||
method: error.config?.method
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
logger.error("Error fetching config in verify session:", error);
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const userResourceAccess = await db
|
const userResourceAccess = await db
|
||||||
.select()
|
.select()
|
||||||
.from(userResources)
|
.from(userResources)
|
||||||
@@ -265,30 +155,6 @@ export async function getUserResourceAccess(
|
|||||||
export async function getResourceRules(
|
export async function getResourceRules(
|
||||||
resourceId: number
|
resourceId: number
|
||||||
): Promise<ResourceRule[]> {
|
): Promise<ResourceRule[]> {
|
||||||
if (config.isManagedMode()) {
|
|
||||||
try {
|
|
||||||
const response = await axios.get(
|
|
||||||
`${config.getRawConfig().managed?.endpoint}/api/v1/hybrid/resource/${resourceId}/rules`,
|
|
||||||
await tokenManager.getAuthHeader()
|
|
||||||
);
|
|
||||||
return response.data.data;
|
|
||||||
} catch (error) {
|
|
||||||
if (axios.isAxiosError(error)) {
|
|
||||||
logger.error("Error fetching config in verify session:", {
|
|
||||||
message: error.message,
|
|
||||||
code: error.code,
|
|
||||||
status: error.response?.status,
|
|
||||||
statusText: error.response?.statusText,
|
|
||||||
url: error.config?.url,
|
|
||||||
method: error.config?.method
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
logger.error("Error fetching config in verify session:", error);
|
|
||||||
}
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const rules = await db
|
const rules = await db
|
||||||
.select()
|
.select()
|
||||||
.from(resourceRules)
|
.from(resourceRules)
|
||||||
@@ -303,30 +169,6 @@ export async function getResourceRules(
|
|||||||
export async function getOrgLoginPage(
|
export async function getOrgLoginPage(
|
||||||
orgId: string
|
orgId: string
|
||||||
): Promise<LoginPage | null> {
|
): Promise<LoginPage | null> {
|
||||||
if (config.isManagedMode()) {
|
|
||||||
try {
|
|
||||||
const response = await axios.get(
|
|
||||||
`${config.getRawConfig().managed?.endpoint}/api/v1/hybrid/org/${orgId}/login-page`,
|
|
||||||
await tokenManager.getAuthHeader()
|
|
||||||
);
|
|
||||||
return response.data.data;
|
|
||||||
} catch (error) {
|
|
||||||
if (axios.isAxiosError(error)) {
|
|
||||||
logger.error("Error fetching config in verify session:", {
|
|
||||||
message: error.message,
|
|
||||||
code: error.code,
|
|
||||||
status: error.response?.status,
|
|
||||||
statusText: error.response?.statusText,
|
|
||||||
url: error.config?.url,
|
|
||||||
method: error.config?.method
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
logger.error("Error fetching config in verify session:", error);
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const [result] = await db
|
const [result] = await db
|
||||||
.select()
|
.select()
|
||||||
.from(loginPageOrg)
|
.from(loginPageOrg)
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { drizzle as DrizzleSqlite } from "drizzle-orm/better-sqlite3";
|
import { drizzle as DrizzleSqlite } from "drizzle-orm/better-sqlite3";
|
||||||
import Database from "better-sqlite3";
|
import Database from "better-sqlite3";
|
||||||
import * as schema from "./schema";
|
import * as schema from "./schema/schema";
|
||||||
import path from "path";
|
import path from "path";
|
||||||
import fs from "fs";
|
import fs from "fs";
|
||||||
import { APP_PATH } from "@server/lib/consts";
|
import { APP_PATH } from "@server/lib/consts";
|
||||||
@@ -13,12 +13,16 @@ bootstrapVolume();
|
|||||||
|
|
||||||
function createDb() {
|
function createDb() {
|
||||||
const sqlite = new Database(location);
|
const sqlite = new Database(location);
|
||||||
return DrizzleSqlite(sqlite, { schema });
|
return DrizzleSqlite(sqlite, {
|
||||||
|
schema
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export const db = createDb();
|
export const db = createDb();
|
||||||
export default db;
|
export default db;
|
||||||
export type Transaction = Parameters<Parameters<typeof db["transaction"]>[0]>[0];
|
export type Transaction = Parameters<
|
||||||
|
Parameters<(typeof db)["transaction"]>[0]
|
||||||
|
>[0];
|
||||||
|
|
||||||
function checkFileExists(filePath: string): boolean {
|
function checkFileExists(filePath: string): boolean {
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
export * from "./driver";
|
export * from "./driver";
|
||||||
export * from "./schema";
|
export * from "./schema/schema";
|
||||||
export * from "./privateSchema";
|
export * from "./schema/privateSchema";
|
||||||
|
|||||||
@@ -1,24 +1,13 @@
|
|||||||
/*
|
|
||||||
* This file is part of a proprietary work.
|
|
||||||
*
|
|
||||||
* Copyright (c) 2025 Fossorial, Inc.
|
|
||||||
* All rights reserved.
|
|
||||||
*
|
|
||||||
* This file is licensed under the Fossorial Commercial License.
|
|
||||||
* You may not use this file except in compliance with the License.
|
|
||||||
* Unauthorized use, copying, modification, or distribution is strictly prohibited.
|
|
||||||
*
|
|
||||||
* This file is not licensed under the AGPLv3.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import {
|
import {
|
||||||
sqliteTable,
|
sqliteTable,
|
||||||
integer,
|
integer,
|
||||||
text,
|
text,
|
||||||
real
|
real,
|
||||||
|
index
|
||||||
} from "drizzle-orm/sqlite-core";
|
} from "drizzle-orm/sqlite-core";
|
||||||
import { InferSelectModel } from "drizzle-orm";
|
import { InferSelectModel } from "drizzle-orm";
|
||||||
import { domains, orgs, targets, users, exitNodes, sessions } from "./schema";
|
import { domains, orgs, targets, users, exitNodes, sessions } from "./schema";
|
||||||
|
import { metadata } from "@app/app/[orgId]/settings/layout";
|
||||||
|
|
||||||
export const certificates = sqliteTable("certificates", {
|
export const certificates = sqliteTable("certificates", {
|
||||||
certId: integer("certId").primaryKey({ autoIncrement: true }),
|
certId: integer("certId").primaryKey({ autoIncrement: true }),
|
||||||
@@ -173,6 +162,7 @@ export const remoteExitNodes = sqliteTable("remoteExitNode", {
|
|||||||
secretHash: text("secretHash").notNull(),
|
secretHash: text("secretHash").notNull(),
|
||||||
dateCreated: text("dateCreated").notNull(),
|
dateCreated: text("dateCreated").notNull(),
|
||||||
version: text("version"),
|
version: text("version"),
|
||||||
|
secondaryVersion: text("secondaryVersion"), // This is to detect the new nodes after the transition to pangolin-node
|
||||||
exitNodeId: integer("exitNodeId").references(() => exitNodes.exitNodeId, {
|
exitNodeId: integer("exitNodeId").references(() => exitNodes.exitNodeId, {
|
||||||
onDelete: "cascade"
|
onDelete: "cascade"
|
||||||
})
|
})
|
||||||
@@ -220,6 +210,43 @@ export const sessionTransferToken = sqliteTable("sessionTransferToken", {
|
|||||||
expiresAt: integer("expiresAt").notNull()
|
expiresAt: integer("expiresAt").notNull()
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const actionAuditLog = sqliteTable("actionAuditLog", {
|
||||||
|
id: integer("id").primaryKey({ autoIncrement: true }),
|
||||||
|
timestamp: integer("timestamp").notNull(), // this is EPOCH time in seconds
|
||||||
|
orgId: text("orgId")
|
||||||
|
.notNull()
|
||||||
|
.references(() => orgs.orgId, { onDelete: "cascade" }),
|
||||||
|
actorType: text("actorType").notNull(),
|
||||||
|
actor: text("actor").notNull(),
|
||||||
|
actorId: text("actorId").notNull(),
|
||||||
|
action: text("action").notNull(),
|
||||||
|
metadata: text("metadata")
|
||||||
|
}, (table) => ([
|
||||||
|
index("idx_actionAuditLog_timestamp").on(table.timestamp),
|
||||||
|
index("idx_actionAuditLog_org_timestamp").on(table.orgId, table.timestamp)
|
||||||
|
]));
|
||||||
|
|
||||||
|
export const accessAuditLog = sqliteTable("accessAuditLog", {
|
||||||
|
id: integer("id").primaryKey({ autoIncrement: true }),
|
||||||
|
timestamp: integer("timestamp").notNull(), // this is EPOCH time in seconds
|
||||||
|
orgId: text("orgId")
|
||||||
|
.notNull()
|
||||||
|
.references(() => orgs.orgId, { onDelete: "cascade" }),
|
||||||
|
actorType: text("actorType"),
|
||||||
|
actor: text("actor"),
|
||||||
|
actorId: text("actorId"),
|
||||||
|
resourceId: integer("resourceId"),
|
||||||
|
ip: text("ip"),
|
||||||
|
location: text("location"),
|
||||||
|
type: text("type").notNull(),
|
||||||
|
action: integer("action", { mode: "boolean" }).notNull(),
|
||||||
|
userAgent: text("userAgent"),
|
||||||
|
metadata: text("metadata")
|
||||||
|
}, (table) => ([
|
||||||
|
index("idx_identityAuditLog_timestamp").on(table.timestamp),
|
||||||
|
index("idx_identityAuditLog_org_timestamp").on(table.orgId, table.timestamp)
|
||||||
|
]));
|
||||||
|
|
||||||
export type Limit = InferSelectModel<typeof limits>;
|
export type Limit = InferSelectModel<typeof limits>;
|
||||||
export type Account = InferSelectModel<typeof account>;
|
export type Account = InferSelectModel<typeof account>;
|
||||||
export type Certificate = InferSelectModel<typeof certificates>;
|
export type Certificate = InferSelectModel<typeof certificates>;
|
||||||
@@ -237,3 +264,5 @@ export type RemoteExitNodeSession = InferSelectModel<
|
|||||||
>;
|
>;
|
||||||
export type ExitNodeOrg = InferSelectModel<typeof exitNodeOrgs>;
|
export type ExitNodeOrg = InferSelectModel<typeof exitNodeOrgs>;
|
||||||
export type LoginPage = InferSelectModel<typeof loginPage>;
|
export type LoginPage = InferSelectModel<typeof loginPage>;
|
||||||
|
export type ActionAuditLog = InferSelectModel<typeof actionAuditLog>;
|
||||||
|
export type AccessAuditLog = InferSelectModel<typeof accessAuditLog>;
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
import { randomUUID } from "crypto";
|
import { randomUUID } from "crypto";
|
||||||
import { InferSelectModel } from "drizzle-orm";
|
import { InferSelectModel } from "drizzle-orm";
|
||||||
import { sqliteTable, text, integer } from "drizzle-orm/sqlite-core";
|
import { sqliteTable, text, integer, index } from "drizzle-orm/sqlite-core";
|
||||||
|
import { no } from "zod/v4/locales";
|
||||||
|
|
||||||
export const domains = sqliteTable("domains", {
|
export const domains = sqliteTable("domains", {
|
||||||
domainId: text("domainId").primaryKey(),
|
domainId: text("domainId").primaryKey(),
|
||||||
@@ -11,15 +12,41 @@ export const domains = sqliteTable("domains", {
|
|||||||
type: text("type"), // "ns", "cname", "wildcard"
|
type: text("type"), // "ns", "cname", "wildcard"
|
||||||
verified: integer("verified", { mode: "boolean" }).notNull().default(false),
|
verified: integer("verified", { mode: "boolean" }).notNull().default(false),
|
||||||
failed: integer("failed", { mode: "boolean" }).notNull().default(false),
|
failed: integer("failed", { mode: "boolean" }).notNull().default(false),
|
||||||
tries: integer("tries").notNull().default(0)
|
tries: integer("tries").notNull().default(0),
|
||||||
|
certResolver: text("certResolver"),
|
||||||
|
preferWildcardCert: integer("preferWildcardCert", { mode: "boolean" })
|
||||||
|
});
|
||||||
|
|
||||||
|
export const dnsRecords = sqliteTable("dnsRecords", {
|
||||||
|
id: integer("id").primaryKey({ autoIncrement: true }),
|
||||||
|
domainId: text("domainId")
|
||||||
|
.notNull()
|
||||||
|
.references(() => domains.domainId, { onDelete: "cascade" }),
|
||||||
|
|
||||||
|
recordType: text("recordType").notNull(), // "NS" | "CNAME" | "A" | "TXT"
|
||||||
|
baseDomain: text("baseDomain"),
|
||||||
|
value: text("value").notNull(),
|
||||||
|
verified: integer("verified", { mode: "boolean" }).notNull().default(false)
|
||||||
});
|
});
|
||||||
|
|
||||||
export const orgs = sqliteTable("orgs", {
|
export const orgs = sqliteTable("orgs", {
|
||||||
orgId: text("orgId").primaryKey(),
|
orgId: text("orgId").primaryKey(),
|
||||||
name: text("name").notNull(),
|
name: text("name").notNull(),
|
||||||
subnet: text("subnet"),
|
subnet: text("subnet"),
|
||||||
|
utilitySubnet: text("utilitySubnet"), // this is the subnet for utility addresses
|
||||||
createdAt: text("createdAt"),
|
createdAt: text("createdAt"),
|
||||||
settings: text("settings") // JSON blob of org-specific settings
|
requireTwoFactor: integer("requireTwoFactor", { mode: "boolean" }),
|
||||||
|
maxSessionLengthHours: integer("maxSessionLengthHours"), // hours
|
||||||
|
passwordExpiryDays: integer("passwordExpiryDays"), // days
|
||||||
|
settingsLogRetentionDaysRequest: integer("settingsLogRetentionDaysRequest") // where 0 = dont keep logs and -1 = keep forever and 9001 = end of the following year
|
||||||
|
.notNull()
|
||||||
|
.default(7),
|
||||||
|
settingsLogRetentionDaysAccess: integer("settingsLogRetentionDaysAccess") // where 0 = dont keep logs and -1 = keep forever and 9001 = end of the following year
|
||||||
|
.notNull()
|
||||||
|
.default(0),
|
||||||
|
settingsLogRetentionDaysAction: integer("settingsLogRetentionDaysAction") // where 0 = dont keep logs and -1 = keep forever and 9001 = end of the following year
|
||||||
|
.notNull()
|
||||||
|
.default(0)
|
||||||
});
|
});
|
||||||
|
|
||||||
export const userDomains = sqliteTable("userDomains", {
|
export const userDomains = sqliteTable("userDomains", {
|
||||||
@@ -68,8 +95,7 @@ export const sites = sqliteTable("sites", {
|
|||||||
listenPort: integer("listenPort"),
|
listenPort: integer("listenPort"),
|
||||||
dockerSocketEnabled: integer("dockerSocketEnabled", { mode: "boolean" })
|
dockerSocketEnabled: integer("dockerSocketEnabled", { mode: "boolean" })
|
||||||
.notNull()
|
.notNull()
|
||||||
.default(true),
|
.default(true)
|
||||||
remoteSubnets: text("remoteSubnets") // comma-separated list of subnets that this site can access
|
|
||||||
});
|
});
|
||||||
|
|
||||||
export const resources = sqliteTable("resources", {
|
export const resources = sqliteTable("resources", {
|
||||||
@@ -112,9 +138,13 @@ export const resources = sqliteTable("resources", {
|
|||||||
setHostHeader: text("setHostHeader"),
|
setHostHeader: text("setHostHeader"),
|
||||||
enableProxy: integer("enableProxy", { mode: "boolean" }).default(true),
|
enableProxy: integer("enableProxy", { mode: "boolean" }).default(true),
|
||||||
skipToIdpId: integer("skipToIdpId").references(() => idp.idpId, {
|
skipToIdpId: integer("skipToIdpId").references(() => idp.idpId, {
|
||||||
onDelete: "cascade"
|
onDelete: "set null"
|
||||||
}),
|
}),
|
||||||
headers: text("headers") // comma-separated list of headers to add to the request
|
headers: text("headers"), // comma-separated list of headers to add to the request
|
||||||
|
proxyProtocol: integer("proxyProtocol", { mode: "boolean" })
|
||||||
|
.notNull()
|
||||||
|
.default(false),
|
||||||
|
proxyProtocolVersion: integer("proxyProtocolVersion").default(1)
|
||||||
});
|
});
|
||||||
|
|
||||||
export const targets = sqliteTable("targets", {
|
export const targets = sqliteTable("targets", {
|
||||||
@@ -137,15 +167,20 @@ export const targets = sqliteTable("targets", {
|
|||||||
path: text("path"),
|
path: text("path"),
|
||||||
pathMatchType: text("pathMatchType"), // exact, prefix, regex
|
pathMatchType: text("pathMatchType"), // exact, prefix, regex
|
||||||
rewritePath: text("rewritePath"), // if set, rewrites the path to this value before sending to the target
|
rewritePath: text("rewritePath"), // if set, rewrites the path to this value before sending to the target
|
||||||
rewritePathType: text("rewritePathType") // exact, prefix, regex, stripPrefix
|
rewritePathType: text("rewritePathType"), // exact, prefix, regex, stripPrefix
|
||||||
|
priority: integer("priority").notNull().default(100)
|
||||||
});
|
});
|
||||||
|
|
||||||
export const targetHealthCheck = sqliteTable("targetHealthCheck", {
|
export const targetHealthCheck = sqliteTable("targetHealthCheck", {
|
||||||
targetHealthCheckId: integer("targetHealthCheckId").primaryKey({ autoIncrement: true }),
|
targetHealthCheckId: integer("targetHealthCheckId").primaryKey({
|
||||||
|
autoIncrement: true
|
||||||
|
}),
|
||||||
targetId: integer("targetId")
|
targetId: integer("targetId")
|
||||||
.notNull()
|
.notNull()
|
||||||
.references(() => targets.targetId, { onDelete: "cascade" }),
|
.references(() => targets.targetId, { onDelete: "cascade" }),
|
||||||
hcEnabled: integer("hcEnabled", { mode: "boolean" }).notNull().default(false),
|
hcEnabled: integer("hcEnabled", { mode: "boolean" })
|
||||||
|
.notNull()
|
||||||
|
.default(false),
|
||||||
hcPath: text("hcPath"),
|
hcPath: text("hcPath"),
|
||||||
hcScheme: text("hcScheme"),
|
hcScheme: text("hcScheme"),
|
||||||
hcMode: text("hcMode").default("http"),
|
hcMode: text("hcMode").default("http"),
|
||||||
@@ -155,10 +190,13 @@ export const targetHealthCheck = sqliteTable("targetHealthCheck", {
|
|||||||
hcUnhealthyInterval: integer("hcUnhealthyInterval").default(30), // in seconds
|
hcUnhealthyInterval: integer("hcUnhealthyInterval").default(30), // in seconds
|
||||||
hcTimeout: integer("hcTimeout").default(5), // in seconds
|
hcTimeout: integer("hcTimeout").default(5), // in seconds
|
||||||
hcHeaders: text("hcHeaders"),
|
hcHeaders: text("hcHeaders"),
|
||||||
hcFollowRedirects: integer("hcFollowRedirects", { mode: "boolean" }).default(true),
|
hcFollowRedirects: integer("hcFollowRedirects", {
|
||||||
|
mode: "boolean"
|
||||||
|
}).default(true),
|
||||||
hcMethod: text("hcMethod").default("GET"),
|
hcMethod: text("hcMethod").default("GET"),
|
||||||
hcStatus: integer("hcStatus"), // http code
|
hcStatus: integer("hcStatus"), // http code
|
||||||
hcHealth: text("hcHealth").default("unknown") // "unknown", "healthy", "unhealthy"
|
hcHealth: text("hcHealth").default("unknown"), // "unknown", "healthy", "unhealthy"
|
||||||
|
hcTlsServerName: text("hcTlsServerName")
|
||||||
});
|
});
|
||||||
|
|
||||||
export const exitNodes = sqliteTable("exitNodes", {
|
export const exitNodes = sqliteTable("exitNodes", {
|
||||||
@@ -189,11 +227,41 @@ export const siteResources = sqliteTable("siteResources", {
|
|||||||
.references(() => orgs.orgId, { onDelete: "cascade" }),
|
.references(() => orgs.orgId, { onDelete: "cascade" }),
|
||||||
niceId: text("niceId").notNull(),
|
niceId: text("niceId").notNull(),
|
||||||
name: text("name").notNull(),
|
name: text("name").notNull(),
|
||||||
protocol: text("protocol").notNull(),
|
mode: text("mode").notNull(), // "host" | "cidr" | "port"
|
||||||
proxyPort: integer("proxyPort").notNull(),
|
protocol: text("protocol"), // only for port mode
|
||||||
destinationPort: integer("destinationPort").notNull(),
|
proxyPort: integer("proxyPort"), // only for port mode
|
||||||
destinationIp: text("destinationIp").notNull(),
|
destinationPort: integer("destinationPort"), // only for port mode
|
||||||
enabled: integer("enabled", { mode: "boolean" }).notNull().default(true)
|
destination: text("destination").notNull(), // ip, cidr, hostname
|
||||||
|
enabled: integer("enabled", { mode: "boolean" }).notNull().default(true),
|
||||||
|
alias: text("alias"),
|
||||||
|
aliasAddress: text("aliasAddress")
|
||||||
|
});
|
||||||
|
|
||||||
|
export const clientSiteResources = sqliteTable("clientSiteResources", {
|
||||||
|
clientId: integer("clientId")
|
||||||
|
.notNull()
|
||||||
|
.references(() => clients.clientId, { onDelete: "cascade" }),
|
||||||
|
siteResourceId: integer("siteResourceId")
|
||||||
|
.notNull()
|
||||||
|
.references(() => siteResources.siteResourceId, { onDelete: "cascade" })
|
||||||
|
});
|
||||||
|
|
||||||
|
export const roleSiteResources = sqliteTable("roleSiteResources", {
|
||||||
|
roleId: integer("roleId")
|
||||||
|
.notNull()
|
||||||
|
.references(() => roles.roleId, { onDelete: "cascade" }),
|
||||||
|
siteResourceId: integer("siteResourceId")
|
||||||
|
.notNull()
|
||||||
|
.references(() => siteResources.siteResourceId, { onDelete: "cascade" })
|
||||||
|
});
|
||||||
|
|
||||||
|
export const userSiteResources = sqliteTable("userSiteResources", {
|
||||||
|
userId: text("userId")
|
||||||
|
.notNull()
|
||||||
|
.references(() => users.userId, { onDelete: "cascade" }),
|
||||||
|
siteResourceId: integer("siteResourceId")
|
||||||
|
.notNull()
|
||||||
|
.references(() => siteResources.siteResourceId, { onDelete: "cascade" })
|
||||||
});
|
});
|
||||||
|
|
||||||
export const users = sqliteTable("user", {
|
export const users = sqliteTable("user", {
|
||||||
@@ -221,7 +289,8 @@ export const users = sqliteTable("user", {
|
|||||||
termsVersion: text("termsVersion"),
|
termsVersion: text("termsVersion"),
|
||||||
serverAdmin: integer("serverAdmin", { mode: "boolean" })
|
serverAdmin: integer("serverAdmin", { mode: "boolean" })
|
||||||
.notNull()
|
.notNull()
|
||||||
.default(false)
|
.default(false),
|
||||||
|
lastPasswordChange: integer("lastPasswordChange")
|
||||||
});
|
});
|
||||||
|
|
||||||
export const securityKeys = sqliteTable("webauthnCredentials", {
|
export const securityKeys = sqliteTable("webauthnCredentials", {
|
||||||
@@ -268,7 +337,7 @@ export const newts = sqliteTable("newt", {
|
|||||||
});
|
});
|
||||||
|
|
||||||
export const clients = sqliteTable("clients", {
|
export const clients = sqliteTable("clients", {
|
||||||
clientId: integer("id").primaryKey({ autoIncrement: true }),
|
clientId: integer("clientId").primaryKey({ autoIncrement: true }),
|
||||||
orgId: text("orgId")
|
orgId: text("orgId")
|
||||||
.references(() => orgs.orgId, {
|
.references(() => orgs.orgId, {
|
||||||
onDelete: "cascade"
|
onDelete: "cascade"
|
||||||
@@ -277,8 +346,14 @@ export const clients = sqliteTable("clients", {
|
|||||||
exitNodeId: integer("exitNode").references(() => exitNodes.exitNodeId, {
|
exitNodeId: integer("exitNode").references(() => exitNodes.exitNodeId, {
|
||||||
onDelete: "set null"
|
onDelete: "set null"
|
||||||
}),
|
}),
|
||||||
|
userId: text("userId").references(() => users.userId, {
|
||||||
|
// optionally tied to a user and in this case delete when the user deletes
|
||||||
|
onDelete: "cascade"
|
||||||
|
}),
|
||||||
|
niceId: text("niceId").notNull(),
|
||||||
name: text("name").notNull(),
|
name: text("name").notNull(),
|
||||||
pubKey: text("pubKey"),
|
pubKey: text("pubKey"),
|
||||||
|
olmId: text("olmId"), // to lock it to a specific olm optionally
|
||||||
subnet: text("subnet").notNull(),
|
subnet: text("subnet").notNull(),
|
||||||
megabytesIn: integer("bytesIn"),
|
megabytesIn: integer("bytesIn"),
|
||||||
megabytesOut: integer("bytesOut"),
|
megabytesOut: integer("bytesOut"),
|
||||||
@@ -290,25 +365,42 @@ export const clients = sqliteTable("clients", {
|
|||||||
lastHolePunch: integer("lastHolePunch")
|
lastHolePunch: integer("lastHolePunch")
|
||||||
});
|
});
|
||||||
|
|
||||||
export const clientSites = sqliteTable("clientSites", {
|
export const clientSitesAssociationsCache = sqliteTable(
|
||||||
clientId: integer("clientId")
|
"clientSitesAssociationsCache",
|
||||||
.notNull()
|
{
|
||||||
.references(() => clients.clientId, { onDelete: "cascade" }),
|
clientId: integer("clientId") // not a foreign key here so after its deleted the rebuild function can delete it and send the message
|
||||||
siteId: integer("siteId")
|
.notNull(),
|
||||||
.notNull()
|
siteId: integer("siteId").notNull(),
|
||||||
.references(() => sites.siteId, { onDelete: "cascade" }),
|
isRelayed: integer("isRelayed", { mode: "boolean" })
|
||||||
isRelayed: integer("isRelayed", { mode: "boolean" })
|
.notNull()
|
||||||
.notNull()
|
.default(false),
|
||||||
.default(false),
|
endpoint: text("endpoint"),
|
||||||
endpoint: text("endpoint")
|
publicKey: text("publicKey") // this will act as the session's public key for hole punching so we can track when it changes
|
||||||
});
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
export const clientSiteResourcesAssociationsCache = sqliteTable(
|
||||||
|
"clientSiteResourcesAssociationsCache",
|
||||||
|
{
|
||||||
|
clientId: integer("clientId") // not a foreign key here so after its deleted the rebuild function can delete it and send the message
|
||||||
|
.notNull(),
|
||||||
|
siteResourceId: integer("siteResourceId").notNull()
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
export const olms = sqliteTable("olms", {
|
export const olms = sqliteTable("olms", {
|
||||||
olmId: text("id").primaryKey(),
|
olmId: text("id").primaryKey(),
|
||||||
secretHash: text("secretHash").notNull(),
|
secretHash: text("secretHash").notNull(),
|
||||||
dateCreated: text("dateCreated").notNull(),
|
dateCreated: text("dateCreated").notNull(),
|
||||||
version: text("version"),
|
version: text("version"),
|
||||||
|
agent: text("agent"),
|
||||||
|
name: text("name"),
|
||||||
clientId: integer("clientId").references(() => clients.clientId, {
|
clientId: integer("clientId").references(() => clients.clientId, {
|
||||||
|
// we will switch this depending on the current org it wants to connect to
|
||||||
|
onDelete: "set null"
|
||||||
|
}),
|
||||||
|
userId: text("userId").references(() => users.userId, {
|
||||||
|
// optionally tied to a user and in this case delete when the user deletes
|
||||||
onDelete: "cascade"
|
onDelete: "cascade"
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
@@ -326,7 +418,11 @@ export const sessions = sqliteTable("session", {
|
|||||||
userId: text("userId")
|
userId: text("userId")
|
||||||
.notNull()
|
.notNull()
|
||||||
.references(() => users.userId, { onDelete: "cascade" }),
|
.references(() => users.userId, { onDelete: "cascade" }),
|
||||||
expiresAt: integer("expiresAt").notNull()
|
expiresAt: integer("expiresAt").notNull(),
|
||||||
|
issuedAt: integer("issuedAt"),
|
||||||
|
deviceAuthUsed: integer("deviceAuthUsed", { mode: "boolean" })
|
||||||
|
.notNull()
|
||||||
|
.default(false)
|
||||||
});
|
});
|
||||||
|
|
||||||
export const newtSessions = sqliteTable("newtSession", {
|
export const newtSessions = sqliteTable("newtSession", {
|
||||||
@@ -513,6 +609,16 @@ export const resourcePassword = sqliteTable("resourcePassword", {
|
|||||||
passwordHash: text("passwordHash").notNull()
|
passwordHash: text("passwordHash").notNull()
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const resourceHeaderAuth = sqliteTable("resourceHeaderAuth", {
|
||||||
|
headerAuthId: integer("headerAuthId").primaryKey({
|
||||||
|
autoIncrement: true
|
||||||
|
}),
|
||||||
|
resourceId: integer("resourceId")
|
||||||
|
.notNull()
|
||||||
|
.references(() => resources.resourceId, { onDelete: "cascade" }),
|
||||||
|
headerAuthHash: text("headerAuthHash").notNull()
|
||||||
|
});
|
||||||
|
|
||||||
export const resourceAccessToken = sqliteTable("resourceAccessToken", {
|
export const resourceAccessToken = sqliteTable("resourceAccessToken", {
|
||||||
accessTokenId: text("accessTokenId").primaryKey(),
|
accessTokenId: text("accessTokenId").primaryKey(),
|
||||||
orgId: text("orgId")
|
orgId: text("orgId")
|
||||||
@@ -566,7 +672,8 @@ export const resourceSessions = sqliteTable("resourceSessions", {
|
|||||||
{
|
{
|
||||||
onDelete: "cascade"
|
onDelete: "cascade"
|
||||||
}
|
}
|
||||||
)
|
),
|
||||||
|
issuedAt: integer("issuedAt")
|
||||||
});
|
});
|
||||||
|
|
||||||
export const resourceWhitelist = sqliteTable("resourceWhitelist", {
|
export const resourceWhitelist = sqliteTable("resourceWhitelist", {
|
||||||
@@ -699,6 +806,74 @@ export const idpOrg = sqliteTable("idpOrg", {
|
|||||||
orgMapping: text("orgMapping")
|
orgMapping: text("orgMapping")
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Blueprint runs
|
||||||
|
export const blueprints = sqliteTable("blueprints", {
|
||||||
|
blueprintId: integer("blueprintId").primaryKey({
|
||||||
|
autoIncrement: true
|
||||||
|
}),
|
||||||
|
orgId: text("orgId")
|
||||||
|
.references(() => orgs.orgId, {
|
||||||
|
onDelete: "cascade"
|
||||||
|
})
|
||||||
|
.notNull(),
|
||||||
|
name: text("name").notNull(),
|
||||||
|
source: text("source").notNull(),
|
||||||
|
createdAt: integer("createdAt").notNull(),
|
||||||
|
succeeded: integer("succeeded", { mode: "boolean" }).notNull(),
|
||||||
|
contents: text("contents").notNull(),
|
||||||
|
message: text("message")
|
||||||
|
});
|
||||||
|
export const requestAuditLog = sqliteTable(
|
||||||
|
"requestAuditLog",
|
||||||
|
{
|
||||||
|
id: integer("id").primaryKey({ autoIncrement: true }),
|
||||||
|
timestamp: integer("timestamp").notNull(), // this is EPOCH time in seconds
|
||||||
|
orgId: text("orgId").references(() => orgs.orgId, {
|
||||||
|
onDelete: "cascade"
|
||||||
|
}),
|
||||||
|
action: integer("action", { mode: "boolean" }).notNull(),
|
||||||
|
reason: integer("reason").notNull(),
|
||||||
|
actorType: text("actorType"),
|
||||||
|
actor: text("actor"),
|
||||||
|
actorId: text("actorId"),
|
||||||
|
resourceId: integer("resourceId"),
|
||||||
|
ip: text("ip"),
|
||||||
|
location: text("location"),
|
||||||
|
userAgent: text("userAgent"),
|
||||||
|
metadata: text("metadata"),
|
||||||
|
headers: text("headers"), // JSON blob
|
||||||
|
query: text("query"), // JSON blob
|
||||||
|
originalRequestURL: text("originalRequestURL"),
|
||||||
|
scheme: text("scheme"),
|
||||||
|
host: text("host"),
|
||||||
|
path: text("path"),
|
||||||
|
method: text("method"),
|
||||||
|
tls: integer("tls", { mode: "boolean" })
|
||||||
|
},
|
||||||
|
(table) => [
|
||||||
|
index("idx_requestAuditLog_timestamp").on(table.timestamp),
|
||||||
|
index("idx_requestAuditLog_org_timestamp").on(
|
||||||
|
table.orgId,
|
||||||
|
table.timestamp
|
||||||
|
)
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
export const deviceWebAuthCodes = sqliteTable("deviceWebAuthCodes", {
|
||||||
|
codeId: integer("codeId").primaryKey({ autoIncrement: true }),
|
||||||
|
code: text("code").notNull().unique(),
|
||||||
|
ip: text("ip"),
|
||||||
|
city: text("city"),
|
||||||
|
deviceName: text("deviceName"),
|
||||||
|
applicationName: text("applicationName").notNull(),
|
||||||
|
expiresAt: integer("expiresAt").notNull(),
|
||||||
|
createdAt: integer("createdAt").notNull(),
|
||||||
|
verified: integer("verified", { mode: "boolean" }).notNull().default(false),
|
||||||
|
userId: text("userId").references(() => users.userId, {
|
||||||
|
onDelete: "cascade"
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
export type Org = InferSelectModel<typeof orgs>;
|
export type Org = InferSelectModel<typeof orgs>;
|
||||||
export type User = InferSelectModel<typeof users>;
|
export type User = InferSelectModel<typeof users>;
|
||||||
export type Site = InferSelectModel<typeof sites>;
|
export type Site = InferSelectModel<typeof sites>;
|
||||||
@@ -728,14 +903,16 @@ export type UserOrg = InferSelectModel<typeof userOrgs>;
|
|||||||
export type ResourceSession = InferSelectModel<typeof resourceSessions>;
|
export type ResourceSession = InferSelectModel<typeof resourceSessions>;
|
||||||
export type ResourcePincode = InferSelectModel<typeof resourcePincode>;
|
export type ResourcePincode = InferSelectModel<typeof resourcePincode>;
|
||||||
export type ResourcePassword = InferSelectModel<typeof resourcePassword>;
|
export type ResourcePassword = InferSelectModel<typeof resourcePassword>;
|
||||||
|
export type ResourceHeaderAuth = InferSelectModel<typeof resourceHeaderAuth>;
|
||||||
export type ResourceOtp = InferSelectModel<typeof resourceOtp>;
|
export type ResourceOtp = InferSelectModel<typeof resourceOtp>;
|
||||||
export type ResourceAccessToken = InferSelectModel<typeof resourceAccessToken>;
|
export type ResourceAccessToken = InferSelectModel<typeof resourceAccessToken>;
|
||||||
export type ResourceWhitelist = InferSelectModel<typeof resourceWhitelist>;
|
export type ResourceWhitelist = InferSelectModel<typeof resourceWhitelist>;
|
||||||
export type VersionMigration = InferSelectModel<typeof versionMigrations>;
|
export type VersionMigration = InferSelectModel<typeof versionMigrations>;
|
||||||
export type ResourceRule = InferSelectModel<typeof resourceRules>;
|
export type ResourceRule = InferSelectModel<typeof resourceRules>;
|
||||||
export type Domain = InferSelectModel<typeof domains>;
|
export type Domain = InferSelectModel<typeof domains>;
|
||||||
|
export type DnsRecord = InferSelectModel<typeof dnsRecords>;
|
||||||
export type Client = InferSelectModel<typeof clients>;
|
export type Client = InferSelectModel<typeof clients>;
|
||||||
export type ClientSite = InferSelectModel<typeof clientSites>;
|
export type ClientSite = InferSelectModel<typeof clientSitesAssociationsCache>;
|
||||||
export type RoleClient = InferSelectModel<typeof roleClients>;
|
export type RoleClient = InferSelectModel<typeof roleClients>;
|
||||||
export type UserClient = InferSelectModel<typeof userClients>;
|
export type UserClient = InferSelectModel<typeof userClients>;
|
||||||
export type SupporterKey = InferSelectModel<typeof supporterKey>;
|
export type SupporterKey = InferSelectModel<typeof supporterKey>;
|
||||||
@@ -747,4 +924,11 @@ export type SiteResource = InferSelectModel<typeof siteResources>;
|
|||||||
export type OrgDomains = InferSelectModel<typeof orgDomains>;
|
export type OrgDomains = InferSelectModel<typeof orgDomains>;
|
||||||
export type SetupToken = InferSelectModel<typeof setupTokens>;
|
export type SetupToken = InferSelectModel<typeof setupTokens>;
|
||||||
export type HostMeta = InferSelectModel<typeof hostMeta>;
|
export type HostMeta = InferSelectModel<typeof hostMeta>;
|
||||||
export type TargetHealthCheck = InferSelectModel<typeof targetHealthCheck>;
|
export type TargetHealthCheck = InferSelectModel<typeof targetHealthCheck>;
|
||||||
|
export type IdpOidcConfig = InferSelectModel<typeof idpOidcConfig>;
|
||||||
|
export type Blueprint = InferSelectModel<typeof blueprints>;
|
||||||
|
export type LicenseKey = InferSelectModel<typeof licenseKey>;
|
||||||
|
export type SecurityKey = InferSelectModel<typeof securityKeys>;
|
||||||
|
export type WebauthnChallenge = InferSelectModel<typeof webauthnChallenge>;
|
||||||
|
export type RequestAuditLog = InferSelectModel<typeof requestAuditLog>;
|
||||||
|
export type DeviceWebAuthCode = InferSelectModel<typeof deviceWebAuthCodes>;
|
||||||
@@ -6,11 +6,6 @@ import logger from "@server/logger";
|
|||||||
import SMTPTransport from "nodemailer/lib/smtp-transport";
|
import SMTPTransport from "nodemailer/lib/smtp-transport";
|
||||||
|
|
||||||
function createEmailClient() {
|
function createEmailClient() {
|
||||||
if (config.isManagedMode()) {
|
|
||||||
// LETS NOT WORRY ABOUT EMAILS IN HYBRID
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const emailConfig = config.getRawConfig().email;
|
const emailConfig = config.getRawConfig().email;
|
||||||
if (!emailConfig) {
|
if (!emailConfig) {
|
||||||
logger.warn(
|
logger.warn(
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ import { render } from "@react-email/render";
|
|||||||
import { ReactElement } from "react";
|
import { ReactElement } from "react";
|
||||||
import emailClient from "@server/emails";
|
import emailClient from "@server/emails";
|
||||||
import logger from "@server/logger";
|
import logger from "@server/logger";
|
||||||
import config from "@server/lib/config";
|
|
||||||
|
|
||||||
export async function sendEmail(
|
export async function sendEmail(
|
||||||
template: ReactElement,
|
template: ReactElement,
|
||||||
@@ -25,7 +24,7 @@ export async function sendEmail(
|
|||||||
|
|
||||||
const emailHtml = await render(template);
|
const emailHtml = await render(template);
|
||||||
|
|
||||||
const appName = config.getRawPrivateConfig().branding?.app_name || "Pangolin";
|
const appName = process.env.BRANDING_APP_NAME || "Pangolin"; // From the private config loading into env vars to seperate away the private config
|
||||||
|
|
||||||
await emailClient.sendMail({
|
await emailClient.sendMail({
|
||||||
from: {
|
from: {
|
||||||
|
|||||||
@@ -1,16 +1,3 @@
|
|||||||
/*
|
|
||||||
* This file is part of a proprietary work.
|
|
||||||
*
|
|
||||||
* Copyright (c) 2025 Fossorial, Inc.
|
|
||||||
* All rights reserved.
|
|
||||||
*
|
|
||||||
* This file is licensed under the Fossorial Commercial License.
|
|
||||||
* You may not use this file except in compliance with the License.
|
|
||||||
* Unauthorized use, copying, modification, or distribution is strictly prohibited.
|
|
||||||
*
|
|
||||||
* This file is not licensed under the AGPLv3.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { Body, Head, Html, Preview, Tailwind } from "@react-email/components";
|
import { Body, Head, Html, Preview, Tailwind } from "@react-email/components";
|
||||||
import { themeColors } from "./lib/theme";
|
import { themeColors } from "./lib/theme";
|
||||||
@@ -1,16 +1,3 @@
|
|||||||
/*
|
|
||||||
* This file is part of a proprietary work.
|
|
||||||
*
|
|
||||||
* Copyright (c) 2025 Fossorial, Inc.
|
|
||||||
* All rights reserved.
|
|
||||||
*
|
|
||||||
* This file is licensed under the Fossorial Commercial License.
|
|
||||||
* You may not use this file except in compliance with the License.
|
|
||||||
* Unauthorized use, copying, modification, or distribution is strictly prohibited.
|
|
||||||
*
|
|
||||||
* This file is not licensed under the AGPLv3.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { Body, Head, Html, Preview, Tailwind } from "@react-email/components";
|
import { Body, Head, Html, Preview, Tailwind } from "@react-email/components";
|
||||||
import { themeColors } from "./lib/theme";
|
import { themeColors } from "./lib/theme";
|
||||||
56
server/emails/templates/SupportEmail.tsx
Normal file
56
server/emails/templates/SupportEmail.tsx
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
import React from "react";
|
||||||
|
import { Body, Head, Html, Preview, Tailwind } from "@react-email/components";
|
||||||
|
import { themeColors } from "./lib/theme";
|
||||||
|
import {
|
||||||
|
EmailContainer,
|
||||||
|
EmailGreeting,
|
||||||
|
EmailLetterHead,
|
||||||
|
EmailText
|
||||||
|
} from "./components/Email";
|
||||||
|
|
||||||
|
interface SupportEmailProps {
|
||||||
|
email: string;
|
||||||
|
username: string;
|
||||||
|
subject: string;
|
||||||
|
body: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const SupportEmail = ({
|
||||||
|
username,
|
||||||
|
email,
|
||||||
|
body,
|
||||||
|
subject
|
||||||
|
}: SupportEmailProps) => {
|
||||||
|
const previewText = subject;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Html>
|
||||||
|
<Head />
|
||||||
|
<Preview>{previewText}</Preview>
|
||||||
|
<Tailwind config={themeColors}>
|
||||||
|
<Body className="font-sans bg-gray-50">
|
||||||
|
<EmailContainer>
|
||||||
|
<EmailLetterHead />
|
||||||
|
|
||||||
|
<EmailGreeting>Hi support,</EmailGreeting>
|
||||||
|
|
||||||
|
<EmailText>
|
||||||
|
You have received a new support request from{" "}
|
||||||
|
<strong>{username}</strong> ({email}).
|
||||||
|
</EmailText>
|
||||||
|
|
||||||
|
<EmailText>
|
||||||
|
<strong>Subject:</strong> {subject}
|
||||||
|
</EmailText>
|
||||||
|
|
||||||
|
<EmailText>
|
||||||
|
<strong>Message:</strong> {body}
|
||||||
|
</EmailText>
|
||||||
|
</EmailContainer>
|
||||||
|
</Body>
|
||||||
|
</Tailwind>
|
||||||
|
</Html>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default SupportEmail;
|
||||||
@@ -88,7 +88,7 @@ export const WelcomeQuickStart = ({
|
|||||||
To learn how to use Newt, including more
|
To learn how to use Newt, including more
|
||||||
installation methods, visit the{" "}
|
installation methods, visit the{" "}
|
||||||
<a
|
<a
|
||||||
href="https://docs.digpangolin.com/manage/sites/install-site"
|
href="https://docs.pangolin.net/manage/sites/install-site"
|
||||||
className="underline"
|
className="underline"
|
||||||
>
|
>
|
||||||
docs
|
docs
|
||||||
|
|||||||
@@ -89,7 +89,7 @@ export function EmailFooter({ children }: { children: React.ReactNode }) {
|
|||||||
<p className="text-xs text-gray-400 mt-4">
|
<p className="text-xs text-gray-400 mt-4">
|
||||||
For any questions or support, please contact us at:
|
For any questions or support, please contact us at:
|
||||||
<br />
|
<br />
|
||||||
support@fossorial.io
|
support@pangolin.net
|
||||||
</p>
|
</p>
|
||||||
<p className="text-xs text-gray-300 text-center mt-4">
|
<p className="text-xs text-gray-300 text-center mt-4">
|
||||||
© {new Date().getFullYear()} Fossorial, Inc. All
|
© {new Date().getFullYear()} Fossorial, Inc. All
|
||||||
|
|||||||
@@ -1,151 +0,0 @@
|
|||||||
import logger from "@server/logger";
|
|
||||||
import config from "@server/lib/config";
|
|
||||||
import { createWebSocketClient } from "./routers/ws/client";
|
|
||||||
import { addPeer, deletePeer } from "./routers/gerbil/peers";
|
|
||||||
import { db, exitNodes } from "./db";
|
|
||||||
import { TraefikConfigManager } from "./lib/traefik/TraefikConfigManager";
|
|
||||||
import { tokenManager } from "./lib/tokenManager";
|
|
||||||
import { APP_VERSION } from "./lib/consts";
|
|
||||||
import axios from "axios";
|
|
||||||
|
|
||||||
export async function createHybridClientServer() {
|
|
||||||
logger.info("Starting hybrid client server...");
|
|
||||||
|
|
||||||
// Start the token manager
|
|
||||||
await tokenManager.start();
|
|
||||||
|
|
||||||
const token = await tokenManager.getToken();
|
|
||||||
|
|
||||||
const monitor = new TraefikConfigManager();
|
|
||||||
|
|
||||||
await monitor.start();
|
|
||||||
|
|
||||||
// Create client
|
|
||||||
const client = createWebSocketClient(
|
|
||||||
token,
|
|
||||||
config.getRawConfig().managed!.endpoint!,
|
|
||||||
{
|
|
||||||
reconnectInterval: 5000,
|
|
||||||
pingInterval: 30000,
|
|
||||||
pingTimeout: 10000
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
// Register message handlers
|
|
||||||
client.registerHandler("remoteExitNode/peers/add", async (message) => {
|
|
||||||
const { publicKey, allowedIps } = message.data;
|
|
||||||
|
|
||||||
// TODO: we are getting the exit node twice here
|
|
||||||
// NOTE: there should only be one gerbil registered so...
|
|
||||||
const [exitNode] = await db.select().from(exitNodes).limit(1);
|
|
||||||
await addPeer(exitNode.exitNodeId, {
|
|
||||||
publicKey: publicKey,
|
|
||||||
allowedIps: allowedIps || []
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
client.registerHandler("remoteExitNode/peers/remove", async (message) => {
|
|
||||||
const { publicKey } = message.data;
|
|
||||||
|
|
||||||
// TODO: we are getting the exit node twice here
|
|
||||||
// NOTE: there should only be one gerbil registered so...
|
|
||||||
const [exitNode] = await db.select().from(exitNodes).limit(1);
|
|
||||||
await deletePeer(exitNode.exitNodeId, publicKey);
|
|
||||||
});
|
|
||||||
|
|
||||||
// /update-proxy-mapping
|
|
||||||
client.registerHandler("remoteExitNode/update-proxy-mapping", async (message) => {
|
|
||||||
try {
|
|
||||||
const [exitNode] = await db.select().from(exitNodes).limit(1);
|
|
||||||
if (!exitNode) {
|
|
||||||
logger.error("No exit node found for proxy mapping update");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const response = await axios.post(`${exitNode.endpoint}/update-proxy-mapping`, message.data);
|
|
||||||
logger.info(`Successfully updated proxy mapping: ${response.status}`);
|
|
||||||
} catch (error) {
|
|
||||||
// pull data out of the axios error to log
|
|
||||||
if (axios.isAxiosError(error)) {
|
|
||||||
logger.error("Error updating proxy mapping:", {
|
|
||||||
message: error.message,
|
|
||||||
code: error.code,
|
|
||||||
status: error.response?.status,
|
|
||||||
statusText: error.response?.statusText,
|
|
||||||
url: error.config?.url,
|
|
||||||
method: error.config?.method
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
logger.error("Error updating proxy mapping:", error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// /update-destinations
|
|
||||||
client.registerHandler("remoteExitNode/update-destinations", async (message) => {
|
|
||||||
try {
|
|
||||||
const [exitNode] = await db.select().from(exitNodes).limit(1);
|
|
||||||
if (!exitNode) {
|
|
||||||
logger.error("No exit node found for destinations update");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const response = await axios.post(`${exitNode.endpoint}/update-destinations`, message.data);
|
|
||||||
logger.info(`Successfully updated destinations: ${response.status}`);
|
|
||||||
} catch (error) {
|
|
||||||
// pull data out of the axios error to log
|
|
||||||
if (axios.isAxiosError(error)) {
|
|
||||||
logger.error("Error updating destinations:", {
|
|
||||||
message: error.message,
|
|
||||||
code: error.code,
|
|
||||||
status: error.response?.status,
|
|
||||||
statusText: error.response?.statusText,
|
|
||||||
url: error.config?.url,
|
|
||||||
method: error.config?.method
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
logger.error("Error updating destinations:", error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
client.registerHandler("remoteExitNode/traefik/reload", async (message) => {
|
|
||||||
await monitor.HandleTraefikConfig();
|
|
||||||
});
|
|
||||||
|
|
||||||
// Listen to connection events
|
|
||||||
client.on("connect", () => {
|
|
||||||
logger.info("Connected to WebSocket server");
|
|
||||||
client.sendMessage("remoteExitNode/register", {
|
|
||||||
remoteExitNodeVersion: APP_VERSION
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
client.on("disconnect", () => {
|
|
||||||
logger.info("Disconnected from WebSocket server");
|
|
||||||
});
|
|
||||||
|
|
||||||
client.on("message", (message) => {
|
|
||||||
logger.info(
|
|
||||||
`Received message: ${message.type} ${JSON.stringify(message.data)}`
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Connect to the server
|
|
||||||
try {
|
|
||||||
await client.connect();
|
|
||||||
logger.info("Connection initiated");
|
|
||||||
} catch (error) {
|
|
||||||
logger.error("Failed to connect:", error);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Store the ping interval stop function for cleanup if needed
|
|
||||||
const stopPingInterval = client.sendMessageInterval(
|
|
||||||
"remoteExitNode/ping",
|
|
||||||
{ timestamp: Date.now() / 1000 },
|
|
||||||
60000
|
|
||||||
); // send every minute
|
|
||||||
|
|
||||||
// Return client and cleanup function for potential use
|
|
||||||
return { client, stopPingInterval };
|
|
||||||
}
|
|
||||||
@@ -5,36 +5,49 @@ import { runSetupFunctions } from "./setup";
|
|||||||
import { createApiServer } from "./apiServer";
|
import { createApiServer } from "./apiServer";
|
||||||
import { createNextServer } from "./nextServer";
|
import { createNextServer } from "./nextServer";
|
||||||
import { createInternalServer } from "./internalServer";
|
import { createInternalServer } from "./internalServer";
|
||||||
import { ApiKey, ApiKeyOrg, RemoteExitNode, Session, User, UserOrg } from "@server/db";
|
|
||||||
import { createIntegrationApiServer } from "./integrationApiServer";
|
import { createIntegrationApiServer } from "./integrationApiServer";
|
||||||
import { createHybridClientServer } from "./hybridServer";
|
import {
|
||||||
|
ApiKey,
|
||||||
|
ApiKeyOrg,
|
||||||
|
RemoteExitNode,
|
||||||
|
Session,
|
||||||
|
SiteResource,
|
||||||
|
User,
|
||||||
|
UserOrg
|
||||||
|
} from "@server/db";
|
||||||
import config from "@server/lib/config";
|
import config from "@server/lib/config";
|
||||||
import { setHostMeta } from "@server/lib/hostMeta";
|
import { setHostMeta } from "@server/lib/hostMeta";
|
||||||
import { initTelemetryClient } from "./lib/telemetry.js";
|
import { initTelemetryClient } from "@server/lib/telemetry";
|
||||||
import { TraefikConfigManager } from "./lib/traefik/TraefikConfigManager.js";
|
import { TraefikConfigManager } from "@server/lib/traefik/TraefikConfigManager";
|
||||||
|
import { initCleanup } from "#dynamic/cleanup";
|
||||||
|
import license from "#dynamic/license/license";
|
||||||
|
import { initLogCleanupInterval } from "@server/lib/cleanupLogs";
|
||||||
|
import { fetchServerIp } from "@server/lib/serverIpService";
|
||||||
|
|
||||||
async function startServers() {
|
async function startServers() {
|
||||||
await setHostMeta();
|
await setHostMeta();
|
||||||
|
|
||||||
await config.initServer();
|
await config.initServer();
|
||||||
|
|
||||||
|
license.setServerSecret(config.getRawConfig().server.secret!);
|
||||||
|
await license.check();
|
||||||
|
|
||||||
await runSetupFunctions();
|
await runSetupFunctions();
|
||||||
|
|
||||||
|
await fetchServerIp();
|
||||||
|
|
||||||
initTelemetryClient();
|
initTelemetryClient();
|
||||||
|
|
||||||
|
initLogCleanupInterval();
|
||||||
|
|
||||||
// Start all servers
|
// Start all servers
|
||||||
const apiServer = createApiServer();
|
const apiServer = createApiServer();
|
||||||
const internalServer = createInternalServer();
|
const internalServer = createInternalServer();
|
||||||
|
|
||||||
let hybridClientServer;
|
const nextServer = await createNextServer();
|
||||||
let nextServer;
|
if (config.getRawConfig().traefik.file_mode) {
|
||||||
if (config.isManagedMode()) {
|
const monitor = new TraefikConfigManager();
|
||||||
hybridClientServer = await createHybridClientServer();
|
await monitor.start();
|
||||||
} else {
|
|
||||||
nextServer = await createNextServer();
|
|
||||||
if (config.getRawConfig().traefik.file_mode) {
|
|
||||||
const monitor = new TraefikConfigManager();
|
|
||||||
await monitor.start();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let integrationServer;
|
let integrationServer;
|
||||||
@@ -42,12 +55,13 @@ async function startServers() {
|
|||||||
integrationServer = createIntegrationApiServer();
|
integrationServer = createIntegrationApiServer();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await initCleanup();
|
||||||
|
|
||||||
return {
|
return {
|
||||||
apiServer,
|
apiServer,
|
||||||
nextServer,
|
nextServer,
|
||||||
internalServer,
|
internalServer,
|
||||||
integrationServer,
|
integrationServer
|
||||||
hybridClientServer
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -64,6 +78,8 @@ declare global {
|
|||||||
userOrgId?: string;
|
userOrgId?: string;
|
||||||
userOrgIds?: string[];
|
userOrgIds?: string[];
|
||||||
remoteExitNode?: RemoteExitNode;
|
remoteExitNode?: RemoteExitNode;
|
||||||
|
siteResource?: SiteResource;
|
||||||
|
orgPolicyAllowed?: boolean;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import {
|
|||||||
errorHandlerMiddleware,
|
errorHandlerMiddleware,
|
||||||
notFoundMiddleware,
|
notFoundMiddleware,
|
||||||
} from "@server/middlewares";
|
} from "@server/middlewares";
|
||||||
import { authenticated, unauthenticated } from "@server/routers/integration";
|
import { authenticated, unauthenticated } from "#dynamic/routers/integration";
|
||||||
import { logIncomingMiddleware } from "./middlewares/logIncoming";
|
import { logIncomingMiddleware } from "./middlewares/logIncoming";
|
||||||
import helmet from "helmet";
|
import helmet from "helmet";
|
||||||
import swaggerUi from "swagger-ui-express";
|
import swaggerUi from "swagger-ui-express";
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import {
|
|||||||
errorHandlerMiddleware,
|
errorHandlerMiddleware,
|
||||||
notFoundMiddleware
|
notFoundMiddleware
|
||||||
} from "@server/middlewares";
|
} from "@server/middlewares";
|
||||||
import internal from "@server/routers/internal";
|
import { internalRouter } from "#dynamic/routers/internal";
|
||||||
import { stripDuplicateSesions } from "./middlewares/stripDuplicateSessions";
|
import { stripDuplicateSesions } from "./middlewares/stripDuplicateSessions";
|
||||||
|
|
||||||
const internalPort = config.getRawConfig().server.internal_port;
|
const internalPort = config.getRawConfig().server.internal_port;
|
||||||
@@ -23,7 +23,7 @@ export function createInternalServer() {
|
|||||||
internalServer.use(express.json());
|
internalServer.use(express.json());
|
||||||
|
|
||||||
const prefix = `/api/v1`;
|
const prefix = `/api/v1`;
|
||||||
internalServer.use(prefix, internal);
|
internalServer.use(prefix, internalRouter);
|
||||||
|
|
||||||
internalServer.use(notFoundMiddleware);
|
internalServer.use(notFoundMiddleware);
|
||||||
internalServer.use(errorHandlerMiddleware);
|
internalServer.use(errorHandlerMiddleware);
|
||||||
|
|||||||
6
server/lib/billing/createCustomer.ts
Normal file
6
server/lib/billing/createCustomer.ts
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
export async function createCustomer(
|
||||||
|
orgId: string,
|
||||||
|
email: string | null | undefined
|
||||||
|
): Promise<string | undefined> {
|
||||||
|
return;
|
||||||
|
}
|
||||||
@@ -1,16 +1,3 @@
|
|||||||
/*
|
|
||||||
* This file is part of a proprietary work.
|
|
||||||
*
|
|
||||||
* Copyright (c) 2025 Fossorial, Inc.
|
|
||||||
* All rights reserved.
|
|
||||||
*
|
|
||||||
* This file is licensed under the Fossorial Commercial License.
|
|
||||||
* You may not use this file except in compliance with the License.
|
|
||||||
* Unauthorized use, copying, modification, or distribution is strictly prohibited.
|
|
||||||
*
|
|
||||||
* This file is not licensed under the AGPLv3.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import Stripe from "stripe";
|
import Stripe from "stripe";
|
||||||
|
|
||||||
export enum FeatureId {
|
export enum FeatureId {
|
||||||
8
server/lib/billing/getOrgTierData.ts
Normal file
8
server/lib/billing/getOrgTierData.ts
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
export async function getOrgTierData(
|
||||||
|
orgId: string
|
||||||
|
): Promise<{ tier: string | null; active: boolean }> {
|
||||||
|
const tier = null;
|
||||||
|
const active = false;
|
||||||
|
|
||||||
|
return { tier, active };
|
||||||
|
}
|
||||||
5
server/lib/billing/index.ts
Normal file
5
server/lib/billing/index.ts
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
export * from "./limitSet";
|
||||||
|
export * from "./features";
|
||||||
|
export * from "./limitsService";
|
||||||
|
export * from "./getOrgTierData";
|
||||||
|
export * from "./createCustomer";
|
||||||
@@ -1,16 +1,3 @@
|
|||||||
/*
|
|
||||||
* This file is part of a proprietary work.
|
|
||||||
*
|
|
||||||
* Copyright (c) 2025 Fossorial, Inc.
|
|
||||||
* All rights reserved.
|
|
||||||
*
|
|
||||||
* This file is licensed under the Fossorial Commercial License.
|
|
||||||
* You may not use this file except in compliance with the License.
|
|
||||||
* Unauthorized use, copying, modification, or distribution is strictly prohibited.
|
|
||||||
*
|
|
||||||
* This file is not licensed under the AGPLv3.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { FeatureId } from "./features";
|
import { FeatureId } from "./features";
|
||||||
|
|
||||||
export type LimitSet = {
|
export type LimitSet = {
|
||||||
@@ -1,16 +1,3 @@
|
|||||||
/*
|
|
||||||
* This file is part of a proprietary work.
|
|
||||||
*
|
|
||||||
* Copyright (c) 2025 Fossorial, Inc.
|
|
||||||
* All rights reserved.
|
|
||||||
*
|
|
||||||
* This file is licensed under the Fossorial Commercial License.
|
|
||||||
* You may not use this file except in compliance with the License.
|
|
||||||
* Unauthorized use, copying, modification, or distribution is strictly prohibited.
|
|
||||||
*
|
|
||||||
* This file is not licensed under the AGPLv3.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { db, limits } from "@server/db";
|
import { db, limits } from "@server/db";
|
||||||
import { and, eq } from "drizzle-orm";
|
import { and, eq } from "drizzle-orm";
|
||||||
import { LimitSet } from "./limitSet";
|
import { LimitSet } from "./limitSet";
|
||||||
@@ -1,16 +1,3 @@
|
|||||||
/*
|
|
||||||
* This file is part of a proprietary work.
|
|
||||||
*
|
|
||||||
* Copyright (c) 2025 Fossorial, Inc.
|
|
||||||
* All rights reserved.
|
|
||||||
*
|
|
||||||
* This file is licensed under the Fossorial Commercial License.
|
|
||||||
* You may not use this file except in compliance with the License.
|
|
||||||
* Unauthorized use, copying, modification, or distribution is strictly prohibited.
|
|
||||||
*
|
|
||||||
* This file is not licensed under the AGPLv3.
|
|
||||||
*/
|
|
||||||
|
|
||||||
export enum TierId {
|
export enum TierId {
|
||||||
STANDARD = "standard",
|
STANDARD = "standard",
|
||||||
}
|
}
|
||||||
@@ -1,21 +1,6 @@
|
|||||||
/*
|
|
||||||
* This file is part of a proprietary work.
|
|
||||||
*
|
|
||||||
* Copyright (c) 2025 Fossorial, Inc.
|
|
||||||
* All rights reserved.
|
|
||||||
*
|
|
||||||
* This file is licensed under the Fossorial Commercial License.
|
|
||||||
* You may not use this file except in compliance with the License.
|
|
||||||
* Unauthorized use, copying, modification, or distribution is strictly prohibited.
|
|
||||||
*
|
|
||||||
* This file is not licensed under the AGPLv3.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { eq, sql, and } from "drizzle-orm";
|
import { eq, sql, and } from "drizzle-orm";
|
||||||
import NodeCache from "node-cache";
|
|
||||||
import { v4 as uuidv4 } from "uuid";
|
import { v4 as uuidv4 } from "uuid";
|
||||||
import { PutObjectCommand } from "@aws-sdk/client-s3";
|
import { PutObjectCommand } from "@aws-sdk/client-s3";
|
||||||
import { s3Client } from "../s3";
|
|
||||||
import * as fs from "fs/promises";
|
import * as fs from "fs/promises";
|
||||||
import * as path from "path";
|
import * as path from "path";
|
||||||
import {
|
import {
|
||||||
@@ -30,10 +15,11 @@ import {
|
|||||||
Transaction
|
Transaction
|
||||||
} from "@server/db";
|
} from "@server/db";
|
||||||
import { FeatureId, getFeatureMeterId } from "./features";
|
import { FeatureId, getFeatureMeterId } from "./features";
|
||||||
import config from "@server/lib/config";
|
|
||||||
import logger from "@server/logger";
|
import logger from "@server/logger";
|
||||||
import { sendToClient } from "@server/routers/ws";
|
import { sendToClient } from "#dynamic/routers/ws";
|
||||||
import { build } from "@server/build";
|
import { build } from "@server/build";
|
||||||
|
import { s3Client } from "@server/lib/s3";
|
||||||
|
import cache from "@server/lib/cache";
|
||||||
|
|
||||||
interface StripeEvent {
|
interface StripeEvent {
|
||||||
identifier?: string;
|
identifier?: string;
|
||||||
@@ -45,8 +31,18 @@ interface StripeEvent {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function noop() {
|
||||||
|
if (
|
||||||
|
build !== "saas" ||
|
||||||
|
!process.env.S3_BUCKET ||
|
||||||
|
!process.env.LOCAL_FILE_PATH
|
||||||
|
) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
export class UsageService {
|
export class UsageService {
|
||||||
private cache: NodeCache;
|
|
||||||
private bucketName: string | undefined;
|
private bucketName: string | undefined;
|
||||||
private currentEventFile: string | null = null;
|
private currentEventFile: string | null = null;
|
||||||
private currentFileStartTime: number = 0;
|
private currentFileStartTime: number = 0;
|
||||||
@@ -54,12 +50,13 @@ export class UsageService {
|
|||||||
private uploadingFiles: Set<string> = new Set();
|
private uploadingFiles: Set<string> = new Set();
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
this.cache = new NodeCache({ stdTTL: 300 }); // 5 minute TTL
|
if (noop()) {
|
||||||
if (build !== "saas") {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.bucketName = config.getRawPrivateConfig().stripe?.s3Bucket;
|
// this.bucketName = privateConfig.getRawPrivateConfig().stripe?.s3Bucket;
|
||||||
this.eventsDir = config.getRawPrivateConfig().stripe?.localFilePath;
|
// this.eventsDir = privateConfig.getRawPrivateConfig().stripe?.localFilePath;
|
||||||
|
this.bucketName = process.env.S3_BUCKET || undefined;
|
||||||
|
this.eventsDir = process.env.LOCAL_FILE_PATH || undefined;
|
||||||
|
|
||||||
// Ensure events directory exists
|
// Ensure events directory exists
|
||||||
this.initializeEventsDirectory().then(() => {
|
this.initializeEventsDirectory().then(() => {
|
||||||
@@ -83,7 +80,9 @@ export class UsageService {
|
|||||||
|
|
||||||
private async initializeEventsDirectory(): Promise<void> {
|
private async initializeEventsDirectory(): Promise<void> {
|
||||||
if (!this.eventsDir) {
|
if (!this.eventsDir) {
|
||||||
logger.warn("Stripe local file path is not configured, skipping events directory initialization.");
|
logger.warn(
|
||||||
|
"Stripe local file path is not configured, skipping events directory initialization."
|
||||||
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
@@ -95,7 +94,9 @@ export class UsageService {
|
|||||||
|
|
||||||
private async uploadPendingEventFilesOnStartup(): Promise<void> {
|
private async uploadPendingEventFilesOnStartup(): Promise<void> {
|
||||||
if (!this.eventsDir || !this.bucketName) {
|
if (!this.eventsDir || !this.bucketName) {
|
||||||
logger.warn("Stripe local file path or bucket name is not configured, skipping leftover event file upload.");
|
logger.warn(
|
||||||
|
"Stripe local file path or bucket name is not configured, skipping leftover event file upload."
|
||||||
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
@@ -118,15 +119,17 @@ export class UsageService {
|
|||||||
ContentType: "application/json"
|
ContentType: "application/json"
|
||||||
});
|
});
|
||||||
await s3Client.send(uploadCommand);
|
await s3Client.send(uploadCommand);
|
||||||
|
|
||||||
// Check if file still exists before unlinking
|
// Check if file still exists before unlinking
|
||||||
try {
|
try {
|
||||||
await fs.access(filePath);
|
await fs.access(filePath);
|
||||||
await fs.unlink(filePath);
|
await fs.unlink(filePath);
|
||||||
} catch (unlinkError) {
|
} catch (unlinkError) {
|
||||||
logger.debug(`Startup file ${file} was already deleted`);
|
logger.debug(
|
||||||
|
`Startup file ${file} was already deleted`
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.info(
|
logger.info(
|
||||||
`Uploaded leftover event file ${file} to S3 with ${events.length} events`
|
`Uploaded leftover event file ${file} to S3 with ${events.length} events`
|
||||||
);
|
);
|
||||||
@@ -136,7 +139,9 @@ export class UsageService {
|
|||||||
await fs.access(filePath);
|
await fs.access(filePath);
|
||||||
await fs.unlink(filePath);
|
await fs.unlink(filePath);
|
||||||
} catch (unlinkError) {
|
} catch (unlinkError) {
|
||||||
logger.debug(`Empty startup file ${file} was already deleted`);
|
logger.debug(
|
||||||
|
`Empty startup file ${file} was already deleted`
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@@ -147,8 +152,8 @@ export class UsageService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (error) {
|
||||||
logger.error("Failed to scan for leftover event files:", err);
|
logger.error("Failed to scan for leftover event files");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -158,17 +163,17 @@ export class UsageService {
|
|||||||
value: number,
|
value: number,
|
||||||
transaction: any = null
|
transaction: any = null
|
||||||
): Promise<Usage | null> {
|
): Promise<Usage | null> {
|
||||||
if (build !== "saas") {
|
if (noop()) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Truncate value to 11 decimal places
|
// Truncate value to 11 decimal places
|
||||||
value = this.truncateValue(value);
|
value = this.truncateValue(value);
|
||||||
|
|
||||||
// Implement retry logic for deadlock handling
|
// Implement retry logic for deadlock handling
|
||||||
const maxRetries = 3;
|
const maxRetries = 3;
|
||||||
let attempt = 0;
|
let attempt = 0;
|
||||||
|
|
||||||
while (attempt <= maxRetries) {
|
while (attempt <= maxRetries) {
|
||||||
try {
|
try {
|
||||||
// Get subscription data for this org (with caching)
|
// Get subscription data for this org (with caching)
|
||||||
@@ -191,7 +196,12 @@ export class UsageService {
|
|||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
await db.transaction(async (trx) => {
|
await db.transaction(async (trx) => {
|
||||||
usage = await this.internalAddUsage(orgId, featureId, value, trx);
|
usage = await this.internalAddUsage(
|
||||||
|
orgId,
|
||||||
|
featureId,
|
||||||
|
value,
|
||||||
|
trx
|
||||||
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -201,25 +211,26 @@ export class UsageService {
|
|||||||
return usage || null;
|
return usage || null;
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
// Check if this is a deadlock error
|
// Check if this is a deadlock error
|
||||||
const isDeadlock = error?.code === '40P01' ||
|
const isDeadlock =
|
||||||
error?.cause?.code === '40P01' ||
|
error?.code === "40P01" ||
|
||||||
(error?.message && error.message.includes('deadlock'));
|
error?.cause?.code === "40P01" ||
|
||||||
|
(error?.message && error.message.includes("deadlock"));
|
||||||
|
|
||||||
if (isDeadlock && attempt < maxRetries) {
|
if (isDeadlock && attempt < maxRetries) {
|
||||||
attempt++;
|
attempt++;
|
||||||
// Exponential backoff with jitter: 50-150ms, 100-300ms, 200-600ms
|
// Exponential backoff with jitter: 50-150ms, 100-300ms, 200-600ms
|
||||||
const baseDelay = Math.pow(2, attempt - 1) * 50;
|
const baseDelay = Math.pow(2, attempt - 1) * 50;
|
||||||
const jitter = Math.random() * baseDelay;
|
const jitter = Math.random() * baseDelay;
|
||||||
const delay = baseDelay + jitter;
|
const delay = baseDelay + jitter;
|
||||||
|
|
||||||
logger.warn(
|
logger.warn(
|
||||||
`Deadlock detected for ${orgId}/${featureId}, retrying attempt ${attempt}/${maxRetries} after ${delay.toFixed(0)}ms`
|
`Deadlock detected for ${orgId}/${featureId}, retrying attempt ${attempt}/${maxRetries} after ${delay.toFixed(0)}ms`
|
||||||
);
|
);
|
||||||
|
|
||||||
await new Promise(resolve => setTimeout(resolve, delay));
|
await new Promise((resolve) => setTimeout(resolve, delay));
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.error(
|
logger.error(
|
||||||
`Failed to add usage for ${orgId}/${featureId} after ${attempt} attempts:`,
|
`Failed to add usage for ${orgId}/${featureId} after ${attempt} attempts:`,
|
||||||
error
|
error
|
||||||
@@ -239,10 +250,10 @@ export class UsageService {
|
|||||||
): Promise<Usage> {
|
): Promise<Usage> {
|
||||||
// Truncate value to 11 decimal places
|
// Truncate value to 11 decimal places
|
||||||
value = this.truncateValue(value);
|
value = this.truncateValue(value);
|
||||||
|
|
||||||
const usageId = `${orgId}-${featureId}`;
|
const usageId = `${orgId}-${featureId}`;
|
||||||
const meterId = getFeatureMeterId(featureId);
|
const meterId = getFeatureMeterId(featureId);
|
||||||
|
|
||||||
// Use upsert: insert if not exists, otherwise increment
|
// Use upsert: insert if not exists, otherwise increment
|
||||||
const [returnUsage] = await trx
|
const [returnUsage] = await trx
|
||||||
.insert(usage)
|
.insert(usage)
|
||||||
@@ -259,7 +270,8 @@ export class UsageService {
|
|||||||
set: {
|
set: {
|
||||||
latestValue: sql`${usage.latestValue} + ${value}`
|
latestValue: sql`${usage.latestValue} + ${value}`
|
||||||
}
|
}
|
||||||
}).returning();
|
})
|
||||||
|
.returning();
|
||||||
|
|
||||||
return returnUsage;
|
return returnUsage;
|
||||||
}
|
}
|
||||||
@@ -280,7 +292,7 @@ export class UsageService {
|
|||||||
value?: number,
|
value?: number,
|
||||||
customerId?: string
|
customerId?: string
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
if (build !== "saas") {
|
if (noop()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
@@ -351,7 +363,7 @@ export class UsageService {
|
|||||||
.set({
|
.set({
|
||||||
latestValue: newRunningTotal,
|
latestValue: newRunningTotal,
|
||||||
instantaneousValue: value,
|
instantaneousValue: value,
|
||||||
updatedAt: Math.floor(Date.now() / 1000)
|
updatedAt: Math.floor(Date.now() / 1000)
|
||||||
})
|
})
|
||||||
.where(eq(usage.usageId, usageId));
|
.where(eq(usage.usageId, usageId));
|
||||||
}
|
}
|
||||||
@@ -366,7 +378,7 @@ export class UsageService {
|
|||||||
meterId,
|
meterId,
|
||||||
instantaneousValue: truncatedValue,
|
instantaneousValue: truncatedValue,
|
||||||
latestValue: truncatedValue,
|
latestValue: truncatedValue,
|
||||||
updatedAt: Math.floor(Date.now() / 1000)
|
updatedAt: Math.floor(Date.now() / 1000)
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -385,7 +397,7 @@ export class UsageService {
|
|||||||
featureId: FeatureId
|
featureId: FeatureId
|
||||||
): Promise<string | null> {
|
): Promise<string | null> {
|
||||||
const cacheKey = `customer_${orgId}_${featureId}`;
|
const cacheKey = `customer_${orgId}_${featureId}`;
|
||||||
const cached = this.cache.get<string>(cacheKey);
|
const cached = cache.get<string>(cacheKey);
|
||||||
|
|
||||||
if (cached) {
|
if (cached) {
|
||||||
return cached;
|
return cached;
|
||||||
@@ -408,7 +420,7 @@ export class UsageService {
|
|||||||
const customerId = customer.customerId;
|
const customerId = customer.customerId;
|
||||||
|
|
||||||
// Cache the result
|
// Cache the result
|
||||||
this.cache.set(cacheKey, customerId);
|
cache.set(cacheKey, customerId, 300); // 5 minute TTL
|
||||||
|
|
||||||
return customerId;
|
return customerId;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -427,7 +439,7 @@ export class UsageService {
|
|||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
// Truncate value to 11 decimal places before sending to Stripe
|
// Truncate value to 11 decimal places before sending to Stripe
|
||||||
const truncatedValue = this.truncateValue(value);
|
const truncatedValue = this.truncateValue(value);
|
||||||
|
|
||||||
const event: StripeEvent = {
|
const event: StripeEvent = {
|
||||||
identifier: uuidv4(),
|
identifier: uuidv4(),
|
||||||
timestamp: Math.floor(new Date().getTime() / 1000),
|
timestamp: Math.floor(new Date().getTime() / 1000),
|
||||||
@@ -444,7 +456,9 @@ export class UsageService {
|
|||||||
|
|
||||||
private async writeEventToFile(event: StripeEvent): Promise<void> {
|
private async writeEventToFile(event: StripeEvent): Promise<void> {
|
||||||
if (!this.eventsDir || !this.bucketName) {
|
if (!this.eventsDir || !this.bucketName) {
|
||||||
logger.warn("Stripe local file path or bucket name is not configured, skipping event file write.");
|
logger.warn(
|
||||||
|
"Stripe local file path or bucket name is not configured, skipping event file write."
|
||||||
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!this.currentEventFile) {
|
if (!this.currentEventFile) {
|
||||||
@@ -493,7 +507,9 @@ export class UsageService {
|
|||||||
|
|
||||||
private async uploadFileToS3(): Promise<void> {
|
private async uploadFileToS3(): Promise<void> {
|
||||||
if (!this.bucketName || !this.eventsDir) {
|
if (!this.bucketName || !this.eventsDir) {
|
||||||
logger.warn("Stripe local file path or bucket name is not configured, skipping S3 upload.");
|
logger.warn(
|
||||||
|
"Stripe local file path or bucket name is not configured, skipping S3 upload."
|
||||||
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!this.currentEventFile) {
|
if (!this.currentEventFile) {
|
||||||
@@ -505,7 +521,9 @@ export class UsageService {
|
|||||||
|
|
||||||
// Check if this file is already being uploaded
|
// Check if this file is already being uploaded
|
||||||
if (this.uploadingFiles.has(fileName)) {
|
if (this.uploadingFiles.has(fileName)) {
|
||||||
logger.debug(`File ${fileName} is already being uploaded, skipping`);
|
logger.debug(
|
||||||
|
`File ${fileName} is already being uploaded, skipping`
|
||||||
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -517,7 +535,9 @@ export class UsageService {
|
|||||||
try {
|
try {
|
||||||
await fs.access(filePath);
|
await fs.access(filePath);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.debug(`File ${fileName} does not exist, may have been already processed`);
|
logger.debug(
|
||||||
|
`File ${fileName} does not exist, may have been already processed`
|
||||||
|
);
|
||||||
this.uploadingFiles.delete(fileName);
|
this.uploadingFiles.delete(fileName);
|
||||||
// Reset current file if it was this file
|
// Reset current file if it was this file
|
||||||
if (this.currentEventFile === fileName) {
|
if (this.currentEventFile === fileName) {
|
||||||
@@ -537,7 +557,9 @@ export class UsageService {
|
|||||||
await fs.unlink(filePath);
|
await fs.unlink(filePath);
|
||||||
} catch (unlinkError) {
|
} catch (unlinkError) {
|
||||||
// File may have been already deleted
|
// File may have been already deleted
|
||||||
logger.debug(`File ${fileName} was already deleted during cleanup`);
|
logger.debug(
|
||||||
|
`File ${fileName} was already deleted during cleanup`
|
||||||
|
);
|
||||||
}
|
}
|
||||||
this.currentEventFile = null;
|
this.currentEventFile = null;
|
||||||
this.uploadingFiles.delete(fileName);
|
this.uploadingFiles.delete(fileName);
|
||||||
@@ -560,7 +582,9 @@ export class UsageService {
|
|||||||
await fs.unlink(filePath);
|
await fs.unlink(filePath);
|
||||||
} catch (unlinkError) {
|
} catch (unlinkError) {
|
||||||
// File may have been already deleted by another process
|
// File may have been already deleted by another process
|
||||||
logger.debug(`File ${fileName} was already deleted during upload`);
|
logger.debug(
|
||||||
|
`File ${fileName} was already deleted during upload`
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.info(
|
logger.info(
|
||||||
@@ -571,10 +595,7 @@ export class UsageService {
|
|||||||
this.currentEventFile = null;
|
this.currentEventFile = null;
|
||||||
this.currentFileStartTime = 0;
|
this.currentFileStartTime = 0;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error(
|
logger.error(`Failed to upload ${fileName} to S3:`, error);
|
||||||
`Failed to upload ${fileName} to S3:`,
|
|
||||||
error
|
|
||||||
);
|
|
||||||
} finally {
|
} finally {
|
||||||
// Always remove from uploading set
|
// Always remove from uploading set
|
||||||
this.uploadingFiles.delete(fileName);
|
this.uploadingFiles.delete(fileName);
|
||||||
@@ -589,16 +610,17 @@ export class UsageService {
|
|||||||
|
|
||||||
public async getUsage(
|
public async getUsage(
|
||||||
orgId: string,
|
orgId: string,
|
||||||
featureId: FeatureId
|
featureId: FeatureId,
|
||||||
|
trx: Transaction | typeof db = db
|
||||||
): Promise<Usage | null> {
|
): Promise<Usage | null> {
|
||||||
if (build !== "saas") {
|
if (noop()) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const usageId = `${orgId}-${featureId}`;
|
const usageId = `${orgId}-${featureId}`;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const [result] = await db
|
const [result] = await trx
|
||||||
.select()
|
.select()
|
||||||
.from(usage)
|
.from(usage)
|
||||||
.where(eq(usage.usageId, usageId))
|
.where(eq(usage.usageId, usageId))
|
||||||
@@ -610,9 +632,9 @@ export class UsageService {
|
|||||||
`Creating new usage record for ${orgId}/${featureId}`
|
`Creating new usage record for ${orgId}/${featureId}`
|
||||||
);
|
);
|
||||||
const meterId = getFeatureMeterId(featureId);
|
const meterId = getFeatureMeterId(featureId);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const [newUsage] = await db
|
const [newUsage] = await trx
|
||||||
.insert(usage)
|
.insert(usage)
|
||||||
.values({
|
.values({
|
||||||
usageId,
|
usageId,
|
||||||
@@ -629,7 +651,7 @@ export class UsageService {
|
|||||||
return newUsage;
|
return newUsage;
|
||||||
} else {
|
} else {
|
||||||
// Record was created by another process, fetch it
|
// Record was created by another process, fetch it
|
||||||
const [existingUsage] = await db
|
const [existingUsage] = await trx
|
||||||
.select()
|
.select()
|
||||||
.from(usage)
|
.from(usage)
|
||||||
.where(eq(usage.usageId, usageId))
|
.where(eq(usage.usageId, usageId))
|
||||||
@@ -642,7 +664,7 @@ export class UsageService {
|
|||||||
`Insert failed for ${orgId}/${featureId}, attempting to fetch existing record:`,
|
`Insert failed for ${orgId}/${featureId}, attempting to fetch existing record:`,
|
||||||
insertError
|
insertError
|
||||||
);
|
);
|
||||||
const [existingUsage] = await db
|
const [existingUsage] = await trx
|
||||||
.select()
|
.select()
|
||||||
.from(usage)
|
.from(usage)
|
||||||
.where(eq(usage.usageId, usageId))
|
.where(eq(usage.usageId, usageId))
|
||||||
@@ -665,7 +687,7 @@ export class UsageService {
|
|||||||
orgId: string,
|
orgId: string,
|
||||||
featureId: FeatureId
|
featureId: FeatureId
|
||||||
): Promise<Usage | null> {
|
): Promise<Usage | null> {
|
||||||
if (build !== "saas") {
|
if (noop()) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
await this.updateDaily(orgId, featureId); // Ensure daily usage is updated
|
await this.updateDaily(orgId, featureId); // Ensure daily usage is updated
|
||||||
@@ -676,16 +698,14 @@ export class UsageService {
|
|||||||
await this.uploadFileToS3();
|
await this.uploadFileToS3();
|
||||||
}
|
}
|
||||||
|
|
||||||
public clearCache(): void {
|
|
||||||
this.cache.flushAll();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Scan the events directory for files older than 1 minute and upload them if not empty.
|
* Scan the events directory for files older than 1 minute and upload them if not empty.
|
||||||
*/
|
*/
|
||||||
private async uploadOldEventFiles(): Promise<void> {
|
private async uploadOldEventFiles(): Promise<void> {
|
||||||
if (!this.eventsDir || !this.bucketName) {
|
if (!this.eventsDir || !this.bucketName) {
|
||||||
logger.warn("Stripe local file path or bucket name is not configured, skipping old event file upload.");
|
logger.warn(
|
||||||
|
"Stripe local file path or bucket name is not configured, skipping old event file upload."
|
||||||
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
@@ -693,15 +713,17 @@ export class UsageService {
|
|||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
for (const file of files) {
|
for (const file of files) {
|
||||||
if (!file.endsWith(".json")) continue;
|
if (!file.endsWith(".json")) continue;
|
||||||
|
|
||||||
// Skip files that are already being uploaded
|
// Skip files that are already being uploaded
|
||||||
if (this.uploadingFiles.has(file)) {
|
if (this.uploadingFiles.has(file)) {
|
||||||
logger.debug(`Skipping file ${file} as it's already being uploaded`);
|
logger.debug(
|
||||||
|
`Skipping file ${file} as it's already being uploaded`
|
||||||
|
);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
const filePath = path.join(this.eventsDir, file);
|
const filePath = path.join(this.eventsDir, file);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Check if file still exists before processing
|
// Check if file still exists before processing
|
||||||
try {
|
try {
|
||||||
@@ -716,7 +738,7 @@ export class UsageService {
|
|||||||
if (age >= 90000) {
|
if (age >= 90000) {
|
||||||
// 1.5 minutes - Mark as being uploaded
|
// 1.5 minutes - Mark as being uploaded
|
||||||
this.uploadingFiles.add(file);
|
this.uploadingFiles.add(file);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const fileContent = await fs.readFile(
|
const fileContent = await fs.readFile(
|
||||||
filePath,
|
filePath,
|
||||||
@@ -732,15 +754,17 @@ export class UsageService {
|
|||||||
ContentType: "application/json"
|
ContentType: "application/json"
|
||||||
});
|
});
|
||||||
await s3Client.send(uploadCommand);
|
await s3Client.send(uploadCommand);
|
||||||
|
|
||||||
// Check if file still exists before unlinking
|
// Check if file still exists before unlinking
|
||||||
try {
|
try {
|
||||||
await fs.access(filePath);
|
await fs.access(filePath);
|
||||||
await fs.unlink(filePath);
|
await fs.unlink(filePath);
|
||||||
} catch (unlinkError) {
|
} catch (unlinkError) {
|
||||||
logger.debug(`File ${file} was already deleted during interval upload`);
|
logger.debug(
|
||||||
|
`File ${file} was already deleted during interval upload`
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.info(
|
logger.info(
|
||||||
`Interval: Uploaded event file ${file} to S3 with ${events.length} events`
|
`Interval: Uploaded event file ${file} to S3 with ${events.length} events`
|
||||||
);
|
);
|
||||||
@@ -755,7 +779,9 @@ export class UsageService {
|
|||||||
await fs.access(filePath);
|
await fs.access(filePath);
|
||||||
await fs.unlink(filePath);
|
await fs.unlink(filePath);
|
||||||
} catch (unlinkError) {
|
} catch (unlinkError) {
|
||||||
logger.debug(`Empty file ${file} was already deleted`);
|
logger.debug(
|
||||||
|
`Empty file ${file} was already deleted`
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
@@ -777,19 +803,25 @@ export class UsageService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async checkLimitSet(orgId: string, kickSites = false, featureId?: FeatureId, usage?: Usage): Promise<boolean> {
|
public async checkLimitSet(
|
||||||
if (build !== "saas") {
|
orgId: string,
|
||||||
|
kickSites = false,
|
||||||
|
featureId?: FeatureId,
|
||||||
|
usage?: Usage,
|
||||||
|
trx: Transaction | typeof db = db
|
||||||
|
): Promise<boolean> {
|
||||||
|
if (noop()) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
// This method should check the current usage against the limits set for the organization
|
// This method should check the current usage against the limits set for the organization
|
||||||
// and kick out all of the sites on the org
|
// and kick out all of the sites on the org
|
||||||
let hasExceededLimits = false;
|
let hasExceededLimits = false;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
let orgLimits: Limit[] = [];
|
let orgLimits: Limit[] = [];
|
||||||
if (featureId) {
|
if (featureId) {
|
||||||
// Get all limits set for this organization
|
// Get all limits set for this organization
|
||||||
orgLimits = await db
|
orgLimits = await trx
|
||||||
.select()
|
.select()
|
||||||
.from(limits)
|
.from(limits)
|
||||||
.where(
|
.where(
|
||||||
@@ -800,7 +832,7 @@ export class UsageService {
|
|||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
// Get all limits set for this organization
|
// Get all limits set for this organization
|
||||||
orgLimits = await db
|
orgLimits = await trx
|
||||||
.select()
|
.select()
|
||||||
.from(limits)
|
.from(limits)
|
||||||
.where(eq(limits.orgId, orgId));
|
.where(eq(limits.orgId, orgId));
|
||||||
@@ -817,16 +849,31 @@ export class UsageService {
|
|||||||
if (usage) {
|
if (usage) {
|
||||||
currentUsage = usage;
|
currentUsage = usage;
|
||||||
} else {
|
} else {
|
||||||
currentUsage = await this.getUsage(orgId, limit.featureId as FeatureId);
|
currentUsage = await this.getUsage(
|
||||||
|
orgId,
|
||||||
|
limit.featureId as FeatureId,
|
||||||
|
trx
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const usageValue = currentUsage?.instantaneousValue || currentUsage?.latestValue || 0;
|
const usageValue =
|
||||||
logger.debug(`Current usage for org ${orgId} on feature ${limit.featureId}: ${usageValue}`);
|
currentUsage?.instantaneousValue ||
|
||||||
logger.debug(`Limit for org ${orgId} on feature ${limit.featureId}: ${limit.value}`);
|
currentUsage?.latestValue ||
|
||||||
if (currentUsage && limit.value !== null && usageValue > limit.value) {
|
0;
|
||||||
|
logger.debug(
|
||||||
|
`Current usage for org ${orgId} on feature ${limit.featureId}: ${usageValue}`
|
||||||
|
);
|
||||||
|
logger.debug(
|
||||||
|
`Limit for org ${orgId} on feature ${limit.featureId}: ${limit.value}`
|
||||||
|
);
|
||||||
|
if (
|
||||||
|
currentUsage &&
|
||||||
|
limit.value !== null &&
|
||||||
|
usageValue > limit.value
|
||||||
|
) {
|
||||||
logger.debug(
|
logger.debug(
|
||||||
`Org ${orgId} has exceeded limit for ${limit.featureId}: ` +
|
`Org ${orgId} has exceeded limit for ${limit.featureId}: ` +
|
||||||
`${usageValue} > ${limit.value}`
|
`${usageValue} > ${limit.value}`
|
||||||
);
|
);
|
||||||
hasExceededLimits = true;
|
hasExceededLimits = true;
|
||||||
break; // Exit early if any limit is exceeded
|
break; // Exit early if any limit is exceeded
|
||||||
@@ -835,22 +882,24 @@ export class UsageService {
|
|||||||
|
|
||||||
// If any limits are exceeded, disconnect all sites for this organization
|
// If any limits are exceeded, disconnect all sites for this organization
|
||||||
if (hasExceededLimits && kickSites) {
|
if (hasExceededLimits && kickSites) {
|
||||||
logger.warn(`Disconnecting all sites for org ${orgId} due to exceeded limits`);
|
logger.warn(
|
||||||
|
`Disconnecting all sites for org ${orgId} due to exceeded limits`
|
||||||
|
);
|
||||||
|
|
||||||
// Get all sites for this organization
|
// Get all sites for this organization
|
||||||
const orgSites = await db
|
const orgSites = await trx
|
||||||
.select()
|
.select()
|
||||||
.from(sites)
|
.from(sites)
|
||||||
.where(eq(sites.orgId, orgId));
|
.where(eq(sites.orgId, orgId));
|
||||||
|
|
||||||
// Mark all sites as offline and send termination messages
|
// Mark all sites as offline and send termination messages
|
||||||
const siteUpdates = orgSites.map(site => site.siteId);
|
const siteUpdates = orgSites.map((site) => site.siteId);
|
||||||
|
|
||||||
if (siteUpdates.length > 0) {
|
if (siteUpdates.length > 0) {
|
||||||
// Send termination messages to newt sites
|
// Send termination messages to newt sites
|
||||||
for (const site of orgSites) {
|
for (const site of orgSites) {
|
||||||
if (site.type === "newt") {
|
if (site.type === "newt") {
|
||||||
const [newt] = await db
|
const [newt] = await trx
|
||||||
.select()
|
.select()
|
||||||
.from(newts)
|
.from(newts)
|
||||||
.where(eq(newts.siteId, site.siteId))
|
.where(eq(newts.siteId, site.siteId))
|
||||||
@@ -865,17 +914,21 @@ export class UsageService {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Don't await to prevent blocking
|
// Don't await to prevent blocking
|
||||||
sendToClient(newt.newtId, payload).catch((error: any) => {
|
await sendToClient(newt.newtId, payload).catch(
|
||||||
logger.error(
|
(error: any) => {
|
||||||
`Failed to send termination message to newt ${newt.newtId}:`,
|
logger.error(
|
||||||
error
|
`Failed to send termination message to newt ${newt.newtId}:`,
|
||||||
);
|
error
|
||||||
});
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.info(`Disconnected ${orgSites.length} sites for org ${orgId} due to exceeded limits`);
|
logger.info(
|
||||||
|
`Disconnected ${orgSites.length} sites for org ${orgId} due to exceeded limits`
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -1,22 +1,36 @@
|
|||||||
import { db, newts, Target } from "@server/db";
|
import { db, newts, blueprints, Blueprint } from "@server/db";
|
||||||
import { Config, ConfigSchema } from "./types";
|
import { Config, ConfigSchema } from "./types";
|
||||||
import { ProxyResourcesResults, updateProxyResources } from "./proxyResources";
|
import { ProxyResourcesResults, updateProxyResources } from "./proxyResources";
|
||||||
import { fromError } from "zod-validation-error";
|
import { fromError } from "zod-validation-error";
|
||||||
import logger from "@server/logger";
|
import logger from "@server/logger";
|
||||||
import { resources, targets, sites } from "@server/db";
|
import { sites } from "@server/db";
|
||||||
import { eq, and, asc, or, ne, count, isNotNull } from "drizzle-orm";
|
import { eq, and, isNotNull } from "drizzle-orm";
|
||||||
import { addTargets as addProxyTargets } from "@server/routers/newt/targets";
|
import { addTargets as addProxyTargets } from "@server/routers/newt/targets";
|
||||||
import { addTargets as addClientTargets } from "@server/routers/client/targets";
|
import { addTargets as addClientTargets } from "@server/routers/client/targets";
|
||||||
import {
|
import {
|
||||||
ClientResourcesResults,
|
ClientResourcesResults,
|
||||||
updateClientResources
|
updateClientResources
|
||||||
} from "./clientResources";
|
} from "./clientResources";
|
||||||
|
import { BlueprintSource } from "@server/routers/blueprints/types";
|
||||||
|
import { stringify as stringifyYaml } from "yaml";
|
||||||
|
import { faker } from "@faker-js/faker";
|
||||||
|
import { handleMessagingForUpdatedSiteResource } from "@server/routers/siteResource";
|
||||||
|
|
||||||
export async function applyBlueprint(
|
type ApplyBlueprintArgs = {
|
||||||
orgId: string,
|
orgId: string;
|
||||||
configData: unknown,
|
configData: unknown;
|
||||||
siteId?: number
|
name?: string;
|
||||||
): Promise<void> {
|
siteId?: number;
|
||||||
|
source?: BlueprintSource;
|
||||||
|
};
|
||||||
|
|
||||||
|
export async function applyBlueprint({
|
||||||
|
orgId,
|
||||||
|
configData,
|
||||||
|
siteId,
|
||||||
|
name,
|
||||||
|
source = "API"
|
||||||
|
}: ApplyBlueprintArgs): Promise<Blueprint> {
|
||||||
// Validate the input data
|
// Validate the input data
|
||||||
const validationResult = ConfigSchema.safeParse(configData);
|
const validationResult = ConfigSchema.safeParse(configData);
|
||||||
if (!validationResult.success) {
|
if (!validationResult.success) {
|
||||||
@@ -24,6 +38,9 @@ export async function applyBlueprint(
|
|||||||
}
|
}
|
||||||
|
|
||||||
const config: Config = validationResult.data;
|
const config: Config = validationResult.data;
|
||||||
|
let blueprintSucceeded: boolean = false;
|
||||||
|
let blueprintMessage: string;
|
||||||
|
let error: any | null = null;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
let proxyResourcesResults: ProxyResourcesResults = [];
|
let proxyResourcesResults: ProxyResourcesResults = [];
|
||||||
@@ -41,22 +58,63 @@ export async function applyBlueprint(
|
|||||||
trx,
|
trx,
|
||||||
siteId
|
siteId
|
||||||
);
|
);
|
||||||
});
|
|
||||||
|
|
||||||
logger.debug(
|
logger.debug(
|
||||||
`Successfully updated proxy resources for org ${orgId}: ${JSON.stringify(proxyResourcesResults)}`
|
`Successfully updated proxy resources for org ${orgId}: ${JSON.stringify(proxyResourcesResults)}`
|
||||||
);
|
);
|
||||||
|
|
||||||
// We need to update the targets on the newts from the successfully updated information
|
// We need to update the targets on the newts from the successfully updated information
|
||||||
for (const result of proxyResourcesResults) {
|
for (const result of proxyResourcesResults) {
|
||||||
for (const target of result.targetsToUpdate) {
|
for (const target of result.targetsToUpdate) {
|
||||||
const [site] = await db
|
const [site] = await trx
|
||||||
|
.select()
|
||||||
|
.from(sites)
|
||||||
|
.innerJoin(newts, eq(sites.siteId, newts.siteId))
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
eq(sites.siteId, target.siteId),
|
||||||
|
eq(sites.orgId, orgId),
|
||||||
|
eq(sites.type, "newt"),
|
||||||
|
isNotNull(sites.pubKey)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.limit(1);
|
||||||
|
|
||||||
|
if (site) {
|
||||||
|
logger.debug(
|
||||||
|
`Updating target ${target.targetId} on site ${site.sites.siteId}`
|
||||||
|
);
|
||||||
|
|
||||||
|
// see if you can find a matching target health check from the healthchecksToUpdate array
|
||||||
|
const matchingHealthcheck =
|
||||||
|
result.healthchecksToUpdate.find(
|
||||||
|
(hc) => hc.targetId === target.targetId
|
||||||
|
);
|
||||||
|
|
||||||
|
await addProxyTargets(
|
||||||
|
site.newt.newtId,
|
||||||
|
[target],
|
||||||
|
matchingHealthcheck ? [matchingHealthcheck] : [],
|
||||||
|
result.proxyResource.protocol,
|
||||||
|
result.proxyResource.proxyPort
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.debug(
|
||||||
|
`Successfully updated client resources for org ${orgId}: ${JSON.stringify(clientResourcesResults)}`
|
||||||
|
);
|
||||||
|
|
||||||
|
// We need to update the targets on the newts from the successfully updated information
|
||||||
|
for (const result of clientResourcesResults) {
|
||||||
|
const [site] = await trx
|
||||||
.select()
|
.select()
|
||||||
.from(sites)
|
.from(sites)
|
||||||
.innerJoin(newts, eq(sites.siteId, newts.siteId))
|
.innerJoin(newts, eq(sites.siteId, newts.siteId))
|
||||||
.where(
|
.where(
|
||||||
and(
|
and(
|
||||||
eq(sites.siteId, target.siteId),
|
eq(sites.siteId, result.newSiteResource.siteId),
|
||||||
eq(sites.orgId, orgId),
|
eq(sites.orgId, orgId),
|
||||||
eq(sites.type, "newt"),
|
eq(sites.type, "newt"),
|
||||||
isNotNull(sites.pubKey)
|
isNotNull(sites.pubKey)
|
||||||
@@ -64,114 +122,67 @@ export async function applyBlueprint(
|
|||||||
)
|
)
|
||||||
.limit(1);
|
.limit(1);
|
||||||
|
|
||||||
if (site) {
|
if (!site) {
|
||||||
logger.debug(
|
logger.debug(
|
||||||
`Updating target ${target.targetId} on site ${site.sites.siteId}`
|
`No newt site found for client resource ${result.newSiteResource.siteResourceId}, skipping target update`
|
||||||
);
|
|
||||||
|
|
||||||
// see if you can find a matching target health check from the healthchecksToUpdate array
|
|
||||||
const matchingHealthcheck =
|
|
||||||
result.healthchecksToUpdate.find(
|
|
||||||
(hc) => hc.targetId === target.targetId
|
|
||||||
);
|
|
||||||
|
|
||||||
await addProxyTargets(
|
|
||||||
site.newt.newtId,
|
|
||||||
[target],
|
|
||||||
matchingHealthcheck ? [matchingHealthcheck] : [],
|
|
||||||
result.proxyResource.protocol,
|
|
||||||
result.proxyResource.proxyPort
|
|
||||||
);
|
);
|
||||||
|
continue;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.debug(
|
|
||||||
`Successfully updated client resources for org ${orgId}: ${JSON.stringify(clientResourcesResults)}`
|
|
||||||
);
|
|
||||||
|
|
||||||
// We need to update the targets on the newts from the successfully updated information
|
|
||||||
for (const result of clientResourcesResults) {
|
|
||||||
const [site] = await db
|
|
||||||
.select()
|
|
||||||
.from(sites)
|
|
||||||
.innerJoin(newts, eq(sites.siteId, newts.siteId))
|
|
||||||
.where(
|
|
||||||
and(
|
|
||||||
eq(sites.siteId, result.resource.siteId),
|
|
||||||
eq(sites.orgId, orgId),
|
|
||||||
eq(sites.type, "newt"),
|
|
||||||
isNotNull(sites.pubKey)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
.limit(1);
|
|
||||||
|
|
||||||
if (site) {
|
|
||||||
logger.debug(
|
logger.debug(
|
||||||
`Updating client resource ${result.resource.siteResourceId} on site ${site.sites.siteId}`
|
`Updating client resource ${result.newSiteResource.siteResourceId} on site ${site.sites.siteId}`
|
||||||
);
|
);
|
||||||
|
|
||||||
await addClientTargets(
|
await handleMessagingForUpdatedSiteResource(
|
||||||
site.newt.newtId,
|
result.oldSiteResource,
|
||||||
result.resource.destinationIp,
|
result.newSiteResource,
|
||||||
result.resource.destinationPort,
|
{ siteId: site.sites.siteId, orgId: site.sites.orgId },
|
||||||
result.resource.protocol,
|
trx
|
||||||
result.resource.proxyPort
|
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// await addClientTargets(
|
||||||
|
// site.newt.newtId,
|
||||||
|
// result.resource.destination,
|
||||||
|
// result.resource.destinationPort,
|
||||||
|
// result.resource.protocol,
|
||||||
|
// result.resource.proxyPort
|
||||||
|
// );
|
||||||
}
|
}
|
||||||
}
|
});
|
||||||
} catch (error) {
|
|
||||||
logger.error(`Failed to update database from config: ${error}`);
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// await updateDatabaseFromConfig("org_i21aifypnlyxur2", {
|
blueprintSucceeded = true;
|
||||||
// resources: {
|
blueprintMessage = "Blueprint applied successfully";
|
||||||
// "resource-nice-id": {
|
} catch (err) {
|
||||||
// name: "this is my resource",
|
blueprintSucceeded = false;
|
||||||
// protocol: "http",
|
blueprintMessage = `Blueprint applied with errors: ${err}`;
|
||||||
// "full-domain": "level1.test.example.com",
|
logger.error(blueprintMessage);
|
||||||
// "host-header": "example.com",
|
error = err;
|
||||||
// "tls-server-name": "example.com",
|
}
|
||||||
// auth: {
|
|
||||||
// pincode: 123456,
|
let blueprint: Blueprint | null = null;
|
||||||
// password: "sadfasdfadsf",
|
await db.transaction(async (trx) => {
|
||||||
// "sso-enabled": true,
|
const newBlueprint = await trx
|
||||||
// "sso-roles": ["Member"],
|
.insert(blueprints)
|
||||||
// "sso-users": ["owen@fossorial.io"],
|
.values({
|
||||||
// "whitelist-users": ["owen@fossorial.io"]
|
orgId,
|
||||||
// },
|
name:
|
||||||
// targets: [
|
name ??
|
||||||
// {
|
`${faker.word.adjective()} ${faker.word.adjective()} ${faker.word.noun()}`,
|
||||||
// site: "glossy-plains-viscacha-rat",
|
contents: stringifyYaml(configData),
|
||||||
// hostname: "localhost",
|
createdAt: Math.floor(Date.now() / 1000),
|
||||||
// method: "http",
|
succeeded: blueprintSucceeded,
|
||||||
// port: 8000,
|
message: blueprintMessage,
|
||||||
// healthcheck: {
|
source
|
||||||
// port: 8000,
|
})
|
||||||
// hostname: "localhost"
|
.returning();
|
||||||
// }
|
|
||||||
// },
|
blueprint = newBlueprint[0];
|
||||||
// {
|
});
|
||||||
// site: "glossy-plains-viscacha-rat",
|
|
||||||
// hostname: "localhost",
|
if (!blueprint || (source !== "UI" && !blueprintSucceeded)) {
|
||||||
// method: "http",
|
// ^^^^^^^^^^^^^^^ The UI considers a failed blueprint as a valid response
|
||||||
// port: 8001
|
throw error ?? "Unknown Server Error";
|
||||||
// }
|
}
|
||||||
// ]
|
|
||||||
// },
|
return blueprint;
|
||||||
// "resource-nice-id2": {
|
}
|
||||||
// name: "http server",
|
|
||||||
// protocol: "tcp",
|
|
||||||
// "proxy-port": 3000,
|
|
||||||
// targets: [
|
|
||||||
// {
|
|
||||||
// site: "glossy-plains-viscacha-rat",
|
|
||||||
// hostname: "localhost",
|
|
||||||
// port: 3000,
|
|
||||||
// }
|
|
||||||
// ]
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// });
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { sendToClient } from "@server/routers/ws";
|
import { sendToClient } from "#dynamic/routers/ws";
|
||||||
import { processContainerLabels } from "./parseDockerContainers";
|
import { processContainerLabels } from "./parseDockerContainers";
|
||||||
import { applyBlueprint } from "./applyBlueprint";
|
import { applyBlueprint } from "./applyBlueprint";
|
||||||
import { db, sites } from "@server/db";
|
import { db, sites } from "@server/db";
|
||||||
@@ -29,15 +29,29 @@ export async function applyNewtDockerBlueprint(
|
|||||||
|
|
||||||
logger.debug(`Received Docker blueprint: ${JSON.stringify(blueprint)}`);
|
logger.debug(`Received Docker blueprint: ${JSON.stringify(blueprint)}`);
|
||||||
|
|
||||||
|
// make sure this is not an empty object
|
||||||
|
if (isEmptyObject(blueprint)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isEmptyObject(blueprint["proxy-resources"]) && isEmptyObject(blueprint["client-resources"])) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Update the blueprint in the database
|
// Update the blueprint in the database
|
||||||
await applyBlueprint(site.orgId, blueprint, site.siteId);
|
await applyBlueprint({
|
||||||
|
orgId: site.orgId,
|
||||||
|
configData: blueprint,
|
||||||
|
siteId: site.siteId,
|
||||||
|
source: "NEWT"
|
||||||
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error(`Failed to update database from config: ${error}`);
|
logger.error(`Failed to update database from config: ${error}`);
|
||||||
await sendToClient(newtId, {
|
await sendToClient(newtId, {
|
||||||
type: "newt/blueprint/results",
|
type: "newt/blueprint/results",
|
||||||
data: {
|
data: {
|
||||||
success: false,
|
success: false,
|
||||||
message: `Failed to update database from config: ${error}`
|
message: `Failed to apply blueprint from config: ${error}`
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
@@ -51,3 +65,10 @@ export async function applyNewtDockerBlueprint(
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isEmptyObject(obj: any) {
|
||||||
|
if (obj === null || obj === undefined) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return Object.keys(obj).length === 0 && obj.constructor === Object;
|
||||||
|
}
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user