mirror of
https://github.com/fosrl/pangolin.git
synced 2026-04-04 08:56:37 +00:00
Compare commits
1624 Commits
cicd
...
1.17.0-s.2
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
035644eaf7 | ||
|
|
8ce45a1acd | ||
|
|
16e7233a3e | ||
|
|
a331dd3fb4 | ||
|
|
e3e2938b28 | ||
|
|
73e96b1b28 | ||
|
|
b8194295ec | ||
|
|
382a46dfff | ||
|
|
1f74e1b320 | ||
|
|
fee780cb81 | ||
|
|
5056cba85d | ||
|
|
dab38ff82c | ||
|
|
d83fa63af5 | ||
|
|
d5837ab718 | ||
|
|
f85cfc4c68 | ||
|
|
0b2aceafe0 | ||
|
|
059db34a53 | ||
|
|
bc1ea86b4e | ||
|
|
9f2ced1933 | ||
|
|
013cff9b6e | ||
|
|
aa19437031 | ||
|
|
e848ef848b | ||
|
|
bb6605337f | ||
|
|
8df8383468 | ||
|
|
a7e9de3ac4 | ||
|
|
8df41f514e | ||
|
|
c2bf50b121 | ||
|
|
4e7dcbd7b5 | ||
|
|
b7ccb92236 | ||
|
|
23a151dd45 | ||
|
|
122079ddb2 | ||
|
|
1d0b0ae6ec | ||
|
|
a55dd769cf | ||
|
|
f1a0bc97e3 | ||
|
|
a57dfd1d12 | ||
|
|
c0a8304b91 | ||
|
|
ab7b968e28 | ||
|
|
f10b40c3b0 | ||
|
|
7878ac9c76 | ||
|
|
0752951842 | ||
|
|
06bb6636a1 | ||
|
|
2fdd332a31 | ||
|
|
98b1e9546a | ||
|
|
184aa65c6d | ||
|
|
70b3a432a4 | ||
|
|
fb4fc75bd8 | ||
|
|
0479ed9e7f | ||
|
|
1dc3409135 | ||
|
|
1bb89fce26 | ||
|
|
8f3fbb94d2 | ||
|
|
e8c35bec1c | ||
|
|
728e7252eb | ||
|
|
1218507f7d | ||
|
|
a2dff0a35d | ||
|
|
f411180908 | ||
|
|
231a19b679 | ||
|
|
58a87a986a | ||
|
|
61a78ef352 | ||
|
|
e28e5ebb4e | ||
|
|
19cef8c453 | ||
|
|
1290d6cd5c | ||
|
|
ad301074db | ||
|
|
30a756d254 | ||
|
|
363c13c387 | ||
|
|
08e4afaef0 | ||
|
|
69aa6e2d1d | ||
|
|
547865e0da | ||
|
|
3a9e79e6d5 | ||
|
|
0fc1aa9191 | ||
|
|
f34a3559be | ||
|
|
ddf417f4ca | ||
|
|
f2abbf01e5 | ||
|
|
d08be59055 | ||
|
|
44bb87e4ac | ||
|
|
1d2f1405aa | ||
|
|
ff64a79014 | ||
|
|
f6cdadbc2d | ||
|
|
546769ca66 | ||
|
|
d07996d435 | ||
|
|
467808f174 | ||
|
|
64d3c6b2d9 | ||
|
|
75193bb0a2 | ||
|
|
82ba2bd809 | ||
|
|
8559942c5c | ||
|
|
a7fefc84a8 | ||
|
|
c8e83fedeb | ||
|
|
4bf148a4bf | ||
|
|
44664faf3c | ||
|
|
322c136d1f | ||
|
|
3b8dd45a73 | ||
|
|
c1bd36231d | ||
|
|
2cee723f0e | ||
|
|
edfeec900d | ||
|
|
958bde2090 | ||
|
|
29b272f5d5 | ||
|
|
9162ac6d91 | ||
|
|
fe30bb280e | ||
|
|
a1e9396999 | ||
|
|
2a1c290dff | ||
|
|
d155d7e31b | ||
|
|
3dc258da16 | ||
|
|
0db1397f2f | ||
|
|
0254fb1695 | ||
|
|
954b492aa9 | ||
|
|
8aadc10530 | ||
|
|
6ea719c50f | ||
|
|
b50886179a | ||
|
|
e06f2f47b1 | ||
|
|
ed8c8bedcd | ||
|
|
1711e39219 | ||
|
|
a73879ec7a | ||
|
|
45c613dec4 | ||
|
|
5150a2c386 | ||
|
|
ca0dd09964 | ||
|
|
5e0e4f1452 | ||
|
|
3ed72dd96b | ||
|
|
b8d7d5c910 | ||
|
|
673b8b7af5 | ||
|
|
a651e50759 | ||
|
|
6484e8e302 | ||
|
|
b01d266629 | ||
|
|
4465b05404 | ||
|
|
d1182c3a59 | ||
|
|
cb6c47678b | ||
|
|
8106620a19 | ||
|
|
be3e066843 | ||
|
|
e345c6ee6e | ||
|
|
073b89b355 | ||
|
|
5cad07f8ad | ||
|
|
f9d872558e | ||
|
|
c5015d02ae | ||
|
|
48013228c1 | ||
|
|
dbafffe73d | ||
|
|
61cbcb2a06 | ||
|
|
89c1ad5d98 | ||
|
|
b343ca6290 | ||
|
|
b913466671 | ||
|
|
9054f4f9c3 | ||
|
|
3915024d9a | ||
|
|
7d1085b43f | ||
|
|
7c2477cccc | ||
|
|
5aecb5fb90 | ||
|
|
f86d040ee4 | ||
|
|
ed32717b3f | ||
|
|
aab8462134 | ||
|
|
04943fb4a6 | ||
|
|
e0c96e7224 | ||
|
|
caacd1e677 | ||
|
|
c995c5a674 | ||
|
|
1e9544af07 | ||
|
|
c20dfdabfb | ||
|
|
11a6f1f47f | ||
|
|
fcf92d4e2c | ||
|
|
77cef554be | ||
|
|
9dc9b6a2c3 | ||
|
|
9808a48da0 | ||
|
|
8a6960d9c3 | ||
|
|
d966ef66e1 | ||
|
|
ed97cf5d97 | ||
|
|
a3b088f8d2 | ||
|
|
2828dee94c | ||
|
|
bdc45887f9 | ||
|
|
ee6fb34906 | ||
|
|
bff2ba7cc2 | ||
|
|
8e821b397f | ||
|
|
6f71af278e | ||
|
|
757bb39622 | ||
|
|
00ef6d617f | ||
|
|
d1b2105c80 | ||
|
|
50ee28b1f7 | ||
|
|
ba529ad14e | ||
|
|
6ab0555148 | ||
|
|
c6f269b3fa | ||
|
|
8e160902af | ||
|
|
7bcb852dba | ||
|
|
ed604c8810 | ||
|
|
bea20674a8 | ||
|
|
177926932b | ||
|
|
04dfbd0a14 | ||
|
|
06f840a680 | ||
|
|
5ddcfeb506 | ||
|
|
a143b7de7c | ||
|
|
63372b174f | ||
|
|
ad7d68d2b4 | ||
|
|
e05af54f76 | ||
|
|
914e95e47f | ||
|
|
13eadeaa8f | ||
|
|
19a686b3e4 | ||
|
|
d046084e84 | ||
|
|
e13a076939 | ||
|
|
b4ca6432db | ||
|
|
5b9efc3c5f | ||
|
|
6d7a19b0a0 | ||
|
|
6b3a6fa380 | ||
|
|
e2a65b4b74 | ||
|
|
1f01108b62 | ||
|
|
c80c7df1d0 | ||
|
|
99a064b77a | ||
|
|
9b84623d0c | ||
|
|
6bb6cf8a48 | ||
|
|
348fcbcabf | ||
|
|
1f4cde5f7f | ||
|
|
3e3b02021c | ||
|
|
17eb93d045 | ||
|
|
660420ddef | ||
|
|
395cab795c | ||
|
|
0fecbe704b | ||
|
|
ce59a8a52b | ||
|
|
2091b5f359 | ||
|
|
62c63ddcaa | ||
|
|
dfd604c781 | ||
|
|
3525b367b3 | ||
|
|
0b5b6ed5a3 | ||
|
|
6fe9494df4 | ||
|
|
b2eab95a3b | ||
|
|
38d30b0214 | ||
|
|
c96c5e8ae8 | ||
|
|
6f71e9f0f2 | ||
|
|
d17ec6dc1f | ||
|
|
212b7a104f | ||
|
|
d21dfb750e | ||
|
|
c36a019f5d | ||
|
|
cf2dfdea5b | ||
|
|
985e1bb9ab | ||
|
|
fff38aac85 | ||
|
|
7db58f920c | ||
|
|
e9b16b8801 | ||
|
|
5a2a97b23a | ||
|
|
5b894e8682 | ||
|
|
84925f724d | ||
|
|
7b78b91449 | ||
|
|
f9bff5954f | ||
|
|
2c6e9507b5 | ||
|
|
6471571bc6 | ||
|
|
fe40ea58c1 | ||
|
|
0d4edcd1c7 | ||
|
|
7d8797840a | ||
|
|
19f8c1772f | ||
|
|
37d331e813 | ||
|
|
c660df55cd | ||
|
|
60982bf19f | ||
|
|
7c8b865379 | ||
|
|
3cca0c09c0 | ||
|
|
85335bfecc | ||
|
|
7c2b4f422a | ||
|
|
ad2a0ae127 | ||
|
|
871f14ef3a | ||
|
|
6c2c620c99 | ||
|
|
f643abf19a | ||
|
|
a1729033cf | ||
|
|
7311766512 | ||
|
|
17105f3a51 | ||
|
|
edcfbd26e4 | ||
|
|
0c4d9ea164 | ||
|
|
a5a5224f5c | ||
|
|
8773f7c0a7 | ||
|
|
f385bc2d22 | ||
|
|
a8c9d2e7e6 | ||
|
|
db3f90318b | ||
|
|
2d4d0df5ca | ||
|
|
569ebc671d | ||
|
|
8c8e4e6233 | ||
|
|
c7901ef74b | ||
|
|
be3bd72c1b | ||
|
|
73d1f9288d | ||
|
|
fb7e9f6898 | ||
|
|
38e4b3077f | ||
|
|
312cdc563b | ||
|
|
48ff6dd705 | ||
|
|
695e831090 | ||
|
|
046b431bb8 | ||
|
|
ce2704fc1a | ||
|
|
7e89b36188 | ||
|
|
222dd6bba3 | ||
|
|
ca9ab65228 | ||
|
|
ee4e8f7029 | ||
|
|
f86a1eb32b | ||
|
|
ffd648ed74 | ||
|
|
b2b72169fd | ||
|
|
76746fb6e1 | ||
|
|
6258787c73 | ||
|
|
720080e487 | ||
|
|
46ad1317e4 | ||
|
|
cd28720e46 | ||
|
|
38af02ad3c | ||
|
|
5eed547f91 | ||
|
|
d363ee02ed | ||
|
|
594ee31f43 | ||
|
|
56e25d01ae | ||
|
|
e0fa5607e5 | ||
|
|
572c9bf319 | ||
|
|
52cac4aa21 | ||
|
|
e358d12765 | ||
|
|
02697e27a4 | ||
|
|
ce58e71c44 | ||
|
|
d9766b0f99 | ||
|
|
eeaa1d56ad | ||
|
|
e7f5bc585c | ||
|
|
4f26fb7750 | ||
|
|
cdbc190bfc | ||
|
|
1b1f9ab4cf | ||
|
|
2efe6cfdb3 | ||
|
|
517c607ecf | ||
|
|
802e8f7a22 | ||
|
|
c7cfe2efcb | ||
|
|
ae1f36f39a | ||
|
|
a479ef28ac | ||
|
|
ce2cf50b5a | ||
|
|
f48d01acde | ||
|
|
991fed93ee | ||
|
|
26ab63d0e4 | ||
|
|
e15703164d | ||
|
|
8f33e25782 | ||
|
|
722595c131 | ||
|
|
4843268537 | ||
|
|
c9be84a8a8 | ||
|
|
03288d2a60 | ||
|
|
f60ae13e4e | ||
|
|
e72697f8b8 | ||
|
|
0c3dc1ad14 | ||
|
|
840fe86f78 | ||
|
|
e079927a5b | ||
|
|
63379964fa | ||
|
|
0cfaf6ed7f | ||
|
|
043ee9e9d2 | ||
|
|
1d5dfd6db2 | ||
|
|
b63e3e5888 | ||
|
|
4f82470506 | ||
|
|
40e21b6f28 | ||
|
|
67fab1928d | ||
|
|
eb98374566 | ||
|
|
1169b68619 | ||
|
|
6c83e78256 | ||
|
|
d3bfd67738 | ||
|
|
0908f0f057 | ||
|
|
2785449c7a | ||
|
|
d2419ba572 | ||
|
|
d44292cf33 | ||
|
|
435cae06a2 | ||
|
|
18ed38889f | ||
|
|
aed86ce4ba | ||
|
|
2c2be50b19 | ||
|
|
e2db4c6246 | ||
|
|
c4839fee08 | ||
|
|
965b7026f0 | ||
|
|
e14e15fcbb | ||
|
|
4ca5acf158 | ||
|
|
ea41fcc566 | ||
|
|
5736c1d8ce | ||
|
|
d142366dd9 | ||
|
|
bab09dff95 | ||
|
|
23d3345ab9 | ||
|
|
09a64815d4 | ||
|
|
6d5f969798 | ||
|
|
10349932f4 | ||
|
|
9c430b37aa | ||
|
|
ad3fe2fa76 | ||
|
|
863eb8efe9 | ||
|
|
86bba494fe | ||
|
|
1a43f1ef4b | ||
|
|
75ab074805 | ||
|
|
dc4e0253de | ||
|
|
47a99e35ee | ||
|
|
cccf236042 | ||
|
|
63fd63c65c | ||
|
|
beee1d692d | ||
|
|
fde786ca84 | ||
|
|
3086fdd064 | ||
|
|
6c30f6db31 | ||
|
|
84b082e194 | ||
|
|
f021b73458 | ||
|
|
74f4751bcc | ||
|
|
e5bce4e180 | ||
|
|
9b0e7b381c | ||
|
|
90afe5a7ac | ||
|
|
b24de85157 | ||
|
|
eda43dffe1 | ||
|
|
82c9a1eb70 | ||
|
|
a3d4553d14 | ||
|
|
1cc5f59f66 | ||
|
|
4e2d88efdd | ||
|
|
4975cabb2c | ||
|
|
225591094f | ||
|
|
82f88f2cd3 | ||
|
|
99e6bd31b6 | ||
|
|
5c50590d7b | ||
|
|
072c89e704 | ||
|
|
dbdff6812d | ||
|
|
42b9d5158d | ||
|
|
2ba225299e | ||
|
|
cc841d5640 | ||
|
|
fa0818d3fa | ||
|
|
dec358c4cd | ||
|
|
5455d1c118 | ||
|
|
ae39084a75 | ||
|
|
e98f873f81 | ||
|
|
e9a2a7e752 | ||
|
|
06015d5191 | ||
|
|
af688d2a23 | ||
|
|
7d0b3ec6b5 | ||
|
|
cf5fb8dc33 | ||
|
|
27d20eb1bc | ||
|
|
2e2684c695 | ||
|
|
7e2fd8f49d | ||
|
|
9a0a255445 | ||
|
|
91b7ceb2cf | ||
|
|
d5a37436c0 | ||
|
|
be609b5000 | ||
|
|
0503c6e66e | ||
|
|
d4b830b9bb | ||
|
|
14d6ff25a7 | ||
|
|
1f62f305ce | ||
|
|
9405b0b70a | ||
|
|
a26ee4ac1a | ||
|
|
cebcf3e337 | ||
|
|
4cfcc64481 | ||
|
|
1a2069a6d9 | ||
|
|
2a5c9465e9 | ||
|
|
f36b66e397 | ||
|
|
8c6d44677d | ||
|
|
1bfff630bf | ||
|
|
ebcef28b05 | ||
|
|
e87e12898c | ||
|
|
d60ab281cf | ||
|
|
483d54a9f0 | ||
|
|
0ab6ff9148 | ||
|
|
c73a39f797 | ||
|
|
c87b6872e5 | ||
|
|
f315c8bc43 | ||
|
|
20fa1519fd | ||
|
|
54430afc40 | ||
|
|
7990d08fee | ||
|
|
e9042d9e2e | ||
|
|
24a15841e4 | ||
|
|
bb8f6e09fd | ||
|
|
04bc8ab694 | ||
|
|
6ac8335cf2 | ||
|
|
4c6144f8fb | ||
|
|
255003794e | ||
|
|
119d5c79a0 | ||
|
|
8e2d7c25df | ||
|
|
753dee3023 | ||
|
|
cac0272952 | ||
|
|
ee5b74f9fc | ||
|
|
1362b72cd3 | ||
|
|
35b1566962 | ||
|
|
a4bcce5a0c | ||
|
|
c03f1946e8 | ||
|
|
c11e107758 | ||
|
|
3b4e49f63a | ||
|
|
ea7253f7e8 | ||
|
|
8a529f7946 | ||
|
|
e76612e018 | ||
|
|
e1f99985d8 | ||
|
|
e0c2735635 | ||
|
|
8e6b4e243d | ||
|
|
2623fa8f02 | ||
|
|
7ff92d32cd | ||
|
|
c7f691b20a | ||
|
|
db042e520e | ||
|
|
4cab693cfc | ||
|
|
c9515ae77c | ||
|
|
d14de86f65 | ||
|
|
f6ee9db730 | ||
|
|
94353aea44 | ||
|
|
b01fcc70fe | ||
|
|
ed95f10fcc | ||
|
|
35fed74e49 | ||
|
|
64bae5b142 | ||
|
|
6cf1b9b010 | ||
|
|
dae169540b | ||
|
|
19f9dda490 | ||
|
|
a060c8029f | ||
|
|
aca9d1e070 | ||
|
|
cdf79edb00 | ||
|
|
df53dfc936 | ||
|
|
8e2e09ab81 | ||
|
|
1eac7cbccd | ||
|
|
ddaaed65e4 | ||
|
|
8e633c21c7 | ||
|
|
e7c4ef44d8 | ||
|
|
3d71470bd2 | ||
|
|
dd627a222e | ||
|
|
62cc20fa1c | ||
|
|
0450fc9f57 | ||
|
|
c58aaf5ba6 | ||
|
|
655522d4e2 | ||
|
|
225475dcae | ||
|
|
ccb977fdfb | ||
|
|
280cbb6e22 | ||
|
|
c20babcb53 | ||
|
|
768eebe2cd | ||
|
|
44e3eedffa | ||
|
|
bb189874cb | ||
|
|
34dadd0e16 | ||
|
|
87b5cd9988 | ||
|
|
6a537a23e8 | ||
|
|
e63a6e9b77 | ||
|
|
7ce589c4f2 | ||
|
|
75a909784a | ||
|
|
244f497a9c | ||
|
|
e58f0c9f07 | ||
|
|
5f18c06e03 | ||
|
|
f36cf06e26 | ||
|
|
27d52646a0 | ||
|
|
4dd8080c55 | ||
|
|
0b35d4f2e3 | ||
|
|
54a9fb9e54 | ||
|
|
60a9e68f02 | ||
|
|
ad374298e3 | ||
|
|
c5dc4e6127 | ||
|
|
291ad831c5 | ||
|
|
0a018f0ca8 | ||
|
|
6673eeb1bb | ||
|
|
4641f0b9ef | ||
|
|
a4487964e5 | ||
|
|
fe42fdd1ec | ||
|
|
375211f184 | ||
|
|
66c377a5c9 | ||
|
|
50c2aa0111 | ||
|
|
fdeb891137 | ||
|
|
6a6e3a43b1 | ||
|
|
b0a34fa21b | ||
|
|
5c4de03588 | ||
|
|
72bf6f3c41 | ||
|
|
ad9289e0c1 | ||
|
|
b0cb0e5a99 | ||
|
|
8347203bbe | ||
|
|
4aa1186aed | ||
|
|
eed87af61d | ||
|
|
daeea8e7ea | ||
|
|
0d63a15715 | ||
|
|
fa2e229ada | ||
|
|
81c1a1da9c | ||
|
|
5d9700d84c | ||
|
|
f8a8cdaa5f | ||
|
|
e23e446476 | ||
|
|
fa097df50b | ||
|
|
75f34ff127 | ||
|
|
c9586b4d93 | ||
|
|
52937a6d90 | ||
|
|
186c131cce | ||
|
|
8de3f9a440 | ||
|
|
ea49e179f9 | ||
|
|
485f4f1c8e | ||
|
|
5fb35d12d7 | ||
|
|
ec8a9fe3d2 | ||
|
|
411a34e15e | ||
|
|
3df71fd2bc | ||
|
|
5e1f6085e3 | ||
|
|
53fc7ab6e3 | ||
|
|
7779ed24fe | ||
|
|
6e4193dae3 | ||
|
|
f138609f48 | ||
|
|
98154b5de3 | ||
|
|
6322fd9eef | ||
|
|
1c0949e957 | ||
|
|
52f26396ac | ||
|
|
c3847e6001 | ||
|
|
5cf13a963d | ||
|
|
b017877826 | ||
|
|
959f68b520 | ||
|
|
14cab3fdb8 | ||
|
|
b8d468f6de | ||
|
|
fc66394243 | ||
|
|
8fca243c9a | ||
|
|
388f710379 | ||
|
|
ba3ab4362b | ||
|
|
e18c9afc2d | ||
|
|
a9b4a86c4a | ||
|
|
200ea502dd | ||
|
|
de36db97eb | ||
|
|
30283b044f | ||
|
|
055bed8a07 | ||
|
|
12b5c2ab34 | ||
|
|
dd78674888 | ||
|
|
0d0df63847 | ||
|
|
3ab00d9da8 | ||
|
|
3e6e72c5c7 | ||
|
|
5d8a55f08c | ||
|
|
81c569aae4 | ||
|
|
88fd3fc4da | ||
|
|
2282d3ae39 | ||
|
|
c4dcec463a | ||
|
|
5b7f893ad7 | ||
|
|
2ede0d498a | ||
|
|
f518e8a0ff | ||
|
|
767284408a | ||
|
|
eef51f3b84 | ||
|
|
69b7114a49 | ||
|
|
0ea38ea568 | ||
|
|
c600da71e3 | ||
|
|
c64dd14b1a | ||
|
|
8ea6d9fa67 | ||
|
|
978ac8f53c | ||
|
|
49a326cde7 | ||
|
|
63e208f4ec | ||
|
|
f50d1549b0 | ||
|
|
55e24df671 | ||
|
|
b37e1d0cc0 | ||
|
|
afa26c0dd4 | ||
|
|
c71f46ede5 | ||
|
|
20e547a0f6 | ||
|
|
2edebaddc2 | ||
|
|
119e1d4867 | ||
|
|
63e30d3378 | ||
|
|
d6fe04ec4e | ||
|
|
b8a364af6a | ||
|
|
5ef808d4a2 | ||
|
|
848d4d91e6 | ||
|
|
a502780c9b | ||
|
|
418e099804 | ||
|
|
06258aa386 | ||
|
|
d7608b1cc8 | ||
|
|
cb86ad4104 | ||
|
|
8cd51df1e1 | ||
|
|
8ef7220766 | ||
|
|
b5333a3686 | ||
|
|
e6e92dbc0f | ||
|
|
01fdd41a10 | ||
|
|
6af06a38ae | ||
|
|
5d9c66d22d | ||
|
|
81f5a4b127 | ||
|
|
da3e68a20b | ||
|
|
8712c1719e | ||
|
|
593c5db0e8 | ||
|
|
b28391feae | ||
|
|
5f8df6d4cd | ||
|
|
c36efe7f14 | ||
|
|
cf97b6df9c | ||
|
|
720d3a8135 | ||
|
|
9c42458fa5 | ||
|
|
6d9b129ac9 | ||
|
|
e17ec798d4 | ||
|
|
58ac499f30 | ||
|
|
f07f0092ad | ||
|
|
bcd3475d17 | ||
|
|
7c04526088 | ||
|
|
2d7ab68576 | ||
|
|
218a4893b6 | ||
|
|
266bf261aa | ||
|
|
63694032e8 | ||
|
|
b77aaedb58 | ||
|
|
a316d0301f | ||
|
|
dcd499720e | ||
|
|
e18fe21eca | ||
|
|
2970b51fb8 | ||
|
|
b9236ff52e | ||
|
|
38eb0ec7ed | ||
|
|
ecba4a0b80 | ||
|
|
e6da18c952 | ||
|
|
12941ac5ae | ||
|
|
11085bda63 | ||
|
|
c03211cc53 | ||
|
|
2867459600 | ||
|
|
32b24db9bf | ||
|
|
660bf9ff87 | ||
|
|
78c4ddebba | ||
|
|
f2dfadb37b | ||
|
|
3f2bdf081f | ||
|
|
d6ba34aeea | ||
|
|
b622aca221 | ||
|
|
6442eb12fb | ||
|
|
01c15afa74 | ||
|
|
4e88f1f38a | ||
|
|
13ab505f4d | ||
|
|
7d112aab27 | ||
|
|
b786497299 | ||
|
|
eedf57af89 | ||
|
|
7a01a4e090 | ||
|
|
874794c996 | ||
|
|
5e37c4e85f | ||
|
|
4e7eac368f | ||
|
|
e8398cb221 | ||
|
|
9460e28c7b | ||
|
|
756f3f32ca | ||
|
|
362981ad19 | ||
|
|
fa4f7e4ac2 | ||
|
|
c6bca4e2ab | ||
|
|
e28b361e05 | ||
|
|
a18691011b | ||
|
|
c4a6403cba | ||
|
|
1851bf941a | ||
|
|
b7ab3c2e92 | ||
|
|
ce1ad032ba | ||
|
|
8446c68e1b | ||
|
|
40ed388b0f | ||
|
|
ce1693aa2f | ||
|
|
11d16a1552 | ||
|
|
0ac54a2c88 | ||
|
|
b7d8b32123 | ||
|
|
5987f6b2cd | ||
|
|
7ad76f5683 | ||
|
|
09a9457021 | ||
|
|
d8b45396e3 | ||
|
|
ca4643ec36 | ||
|
|
e2f78ba476 | ||
|
|
5d92190d50 | ||
|
|
2b0d6de986 | ||
|
|
057f82a561 | ||
|
|
719d2a5ffe | ||
|
|
d4bff9d5cb | ||
|
|
19fcc1f93b | ||
|
|
d45ea127c2 | ||
|
|
f591cf8601 | ||
|
|
6661a76aa8 | ||
|
|
a2ed22bfcc | ||
|
|
e370f8891a | ||
|
|
8a83e32c42 | ||
|
|
831eb6325c | ||
|
|
4d6240c987 | ||
|
|
79cf7c84dc | ||
|
|
b71f582329 | ||
|
|
8315d4b6ae | ||
|
|
b8c3cc751a | ||
|
|
d00262dc31 | ||
|
|
952d0c74d0 | ||
|
|
ffbea7af59 | ||
|
|
3debc6c8d3 | ||
|
|
5092eb58fb | ||
|
|
f0b9240575 | ||
|
|
9cf59c409e | ||
|
|
971c375398 | ||
|
|
ac4439c5ae | ||
|
|
bfd5aa30a7 | ||
|
|
9737170665 | ||
|
|
922a040466 | ||
|
|
9eacefb155 | ||
|
|
33f0782f3a | ||
|
|
e6a5cef945 | ||
|
|
4c8edb80b3 | ||
|
|
d4668fae99 | ||
|
|
ddfe55e3ae | ||
|
|
761a5f1d4c | ||
|
|
1fbcad8787 | ||
|
|
aba586e605 | ||
|
|
27b21b5ad4 | ||
|
|
b6e54dab17 | ||
|
|
1f8e89772d | ||
|
|
843b13ed57 | ||
|
|
be89e5ca55 | ||
|
|
5f3657fd56 | ||
|
|
494162400e | ||
|
|
ab65bb6a8a | ||
|
|
333625f199 | ||
|
|
dbfd715381 | ||
|
|
f1d989964e | ||
|
|
b701629498 | ||
|
|
8250946325 | ||
|
|
71f63d8e6f | ||
|
|
dd5e834db0 | ||
|
|
970ecb52f0 | ||
|
|
62ea1b40e1 | ||
|
|
3b0fd5c592 | ||
|
|
b7616026dd | ||
|
|
16ad60b89a | ||
|
|
db7971d2f7 | ||
|
|
f3f8bd3125 | ||
|
|
516fd0ee8f | ||
|
|
8d6700d493 | ||
|
|
9d4ace9b3e | ||
|
|
2800655e33 | ||
|
|
91eecee11d | ||
|
|
899e5aa395 | ||
|
|
d5820c4902 | ||
|
|
a91c002274 | ||
|
|
4d142b93dd | ||
|
|
04dcf57ff3 | ||
|
|
975550c755 | ||
|
|
a964a80d85 | ||
|
|
22c3b8f116 | ||
|
|
c4b1831cfe | ||
|
|
cdb6813384 | ||
|
|
b14b68d83c | ||
|
|
3c2f930e6b | ||
|
|
ca9c7ce555 | ||
|
|
c2e95a0607 | ||
|
|
2767ee9e80 | ||
|
|
d998a8087f | ||
|
|
fdce016921 | ||
|
|
c73d70933b | ||
|
|
e9d0ad6e37 | ||
|
|
a35586f762 | ||
|
|
f527c30923 | ||
|
|
94e70219cf | ||
|
|
6496763aae | ||
|
|
a409ec269b | ||
|
|
bc7bc8da66 | ||
|
|
52484c774e | ||
|
|
4e1e0cade1 | ||
|
|
fda5904dac | ||
|
|
69ecc22318 | ||
|
|
bff9d33ee6 | ||
|
|
edf506953b | ||
|
|
5e11746549 | ||
|
|
1ae315e303 | ||
|
|
758b03ab25 | ||
|
|
e756fad573 | ||
|
|
3547450b03 | ||
|
|
733f6692c6 | ||
|
|
2d83160b16 | ||
|
|
256fa880dd | ||
|
|
b08c5f5c67 | ||
|
|
d0862a2d26 | ||
|
|
e97340ed52 | ||
|
|
e27c81eea6 | ||
|
|
7f7f3d43b2 | ||
|
|
4b1b772098 | ||
|
|
f66b88490f | ||
|
|
18f9157169 | ||
|
|
6eb82a807b | ||
|
|
bf57a97833 | ||
|
|
e9e2093220 | ||
|
|
c3540da2e3 | ||
|
|
d228cf56dd | ||
|
|
8f4cecd963 | ||
|
|
66adff44bb | ||
|
|
be41c094dc | ||
|
|
273848ca18 | ||
|
|
1e9dbead3b | ||
|
|
aeaa8ba133 | ||
|
|
24654af635 | ||
|
|
e88a21d6db | ||
|
|
bcd01badaf | ||
|
|
8e063506e0 | ||
|
|
84f5d6137a | ||
|
|
0a8565f5e8 | ||
|
|
bd8da25a46 | ||
|
|
a841f588dd | ||
|
|
75a4362ce3 | ||
|
|
e763e001e5 | ||
|
|
69475a0ae7 | ||
|
|
53e14c2ad7 | ||
|
|
1edc33148a | ||
|
|
a4cbfc74e4 | ||
|
|
c0d25aeb02 | ||
|
|
40f49bf6da | ||
|
|
0bfce87dc6 | ||
|
|
2a0655e9de | ||
|
|
a86cfa5934 | ||
|
|
54b77523c5 | ||
|
|
ba06c8928d | ||
|
|
c8a4ac1ed4 | ||
|
|
143acbae48 | ||
|
|
937f6fdae8 | ||
|
|
ba7239ac08 | ||
|
|
2e748274c0 | ||
|
|
eab2750953 | ||
|
|
17b6cb0c73 | ||
|
|
98a4c453c1 | ||
|
|
6475dceab9 | ||
|
|
040a945774 | ||
|
|
47743a5fa8 | ||
|
|
d47d6de985 | ||
|
|
37818b8594 | ||
|
|
3b184acddd | ||
|
|
9c80404d17 | ||
|
|
aaa7082f9d | ||
|
|
a45b45b2ce | ||
|
|
e4bfbd267e | ||
|
|
65b4dcc672 | ||
|
|
36fc30b524 | ||
|
|
e724ed9137 | ||
|
|
7ca992af05 | ||
|
|
37f1c714ac | ||
|
|
397a43fb60 | ||
|
|
45e0a648c6 | ||
|
|
7336aa81d9 | ||
|
|
d727c10d98 | ||
|
|
321d77a317 | ||
|
|
19b8a6b737 | ||
|
|
f2e69dfb96 | ||
|
|
8207e49317 | ||
|
|
b75600b9ea | ||
|
|
7b01f1bef6 | ||
|
|
e7bd2c0001 | ||
|
|
a26076e9db | ||
|
|
9711a0fb8e | ||
|
|
accc670411 | ||
|
|
071c41a54f | ||
|
|
35ba6c19c3 | ||
|
|
14c8348166 | ||
|
|
7d6ee72025 | ||
|
|
ea0e770b57 | ||
|
|
193b7ff21e | ||
|
|
d814ad9f3e | ||
|
|
da8b620c75 | ||
|
|
911b5e6814 | ||
|
|
f991fd9c71 | ||
|
|
652e4c922d | ||
|
|
4364e3fbc1 | ||
|
|
a783fdecbc | ||
|
|
16f67455a2 | ||
|
|
0850a28d20 | ||
|
|
5ca598139e | ||
|
|
df1bf09163 | ||
|
|
50bc8d3e9c | ||
|
|
86d089024e | ||
|
|
d5c1cf594d | ||
|
|
a0b5731e69 | ||
|
|
ceb359d614 | ||
|
|
a49a9f8e3b | ||
|
|
766606b08d | ||
|
|
fed56c1959 | ||
|
|
ae6ed8ad97 | ||
|
|
c1ca0b8e2c | ||
|
|
569dc735ce | ||
|
|
dd11c2c871 | ||
|
|
8def4a2b68 | ||
|
|
13a5f24b07 | ||
|
|
0989d6353e | ||
|
|
4139a7b73f | ||
|
|
be60d66ce3 | ||
|
|
0a33043874 | ||
|
|
96d1d983e5 | ||
|
|
7ffb260d7c | ||
|
|
ce74489df5 | ||
|
|
342b188fae | ||
|
|
fa6fee7b55 | ||
|
|
c53d5a4d7d | ||
|
|
521e905724 | ||
|
|
4623090050 | ||
|
|
dd9e5cc541 | ||
|
|
626be6a347 | ||
|
|
56327ed503 | ||
|
|
6d1665004b | ||
|
|
59b8119fbd | ||
|
|
9ff863db5e | ||
|
|
e2ac6e6d4d | ||
|
|
df4101875a | ||
|
|
3f5c788d48 | ||
|
|
45cd4df6e5 | ||
|
|
94ac3ec76e | ||
|
|
af7263a0b1 | ||
|
|
035396f95c | ||
|
|
f318f6304b | ||
|
|
9d0ff472e5 | ||
|
|
d27482e812 | ||
|
|
d5b6de70da | ||
|
|
69c2212ea0 | ||
|
|
10be9bcd56 | ||
|
|
f531def0d2 | ||
|
|
ed40eae655 | ||
|
|
ba5ae6ed04 | ||
|
|
d6ade102dc | ||
|
|
0a6301697e | ||
|
|
13b4fc6725 | ||
|
|
c94d246c24 | ||
|
|
5b779ba9fe | ||
|
|
3ba2cb19a9 | ||
|
|
a095dddd01 | ||
|
|
1b5cfaa49b | ||
|
|
66f3fabbae | ||
|
|
0be8fb7931 | ||
|
|
431e6ffaae | ||
|
|
7d8185e0ee | ||
|
|
dff45748bd | ||
|
|
da514ef314 | ||
|
|
7f73cde794 | ||
|
|
b0af0d9cd5 | ||
|
|
e6464929ff | ||
|
|
122053939d | ||
|
|
8429197b07 | ||
|
|
44f2081882 | ||
|
|
300b4a3706 | ||
|
|
81ef2db7f8 | ||
|
|
c41e8be3e8 | ||
|
|
41bab0ce0b | ||
|
|
5f26b9eeea | ||
|
|
1cca69ad23 | ||
|
|
410ed3949b | ||
|
|
efc6ef3075 | ||
|
|
63f7dd1d20 | ||
|
|
57b8c69983 | ||
|
|
aad060810a | ||
|
|
9222b00a6f | ||
|
|
ff61b22e7e | ||
|
|
577cb91343 | ||
|
|
1889386f64 | ||
|
|
5d7f082ebf | ||
|
|
db6327c4ff | ||
|
|
fd7f6b2b99 | ||
|
|
49435398a8 | ||
|
|
e101ac341b | ||
|
|
6cfc7b7c69 | ||
|
|
313acabc86 | ||
|
|
34cced872f | ||
|
|
ac09e3aaf9 | ||
|
|
9f2fd34e99 | ||
|
|
67b63d3084 | ||
|
|
4a31a7b84b | ||
|
|
538b601b1e | ||
|
|
588f064c25 | ||
|
|
d521e79662 | ||
|
|
ccddb9244d | ||
|
|
0547396213 | ||
|
|
6c85171091 | ||
|
|
a8f6b6c1da | ||
|
|
f899326189 | ||
|
|
0f4d1d2a74 | ||
|
|
941d5c08e3 | ||
|
|
db9f74158b | ||
|
|
b4c01349d1 | ||
|
|
165bbd3584 | ||
|
|
ffb253e0e9 | ||
|
|
e5e9fe456f | ||
|
|
c63589b204 | ||
|
|
11408c2656 | ||
|
|
7d4aed8819 | ||
|
|
609ffccd67 | ||
|
|
508369a59d | ||
|
|
748af1d8cb | ||
|
|
26a91cd5e1 | ||
|
|
48dd4d5913 | ||
|
|
d309ec249e | ||
|
|
72d46b7352 | ||
|
|
4613aae47d | ||
|
|
1bc4480d84 | ||
|
|
b5d76f73e8 | ||
|
|
a5c7913e77 | ||
|
|
34b914f509 | ||
|
|
5a3d75ca12 | ||
|
|
158d7b23d8 | ||
|
|
67949b4968 | ||
|
|
1fc40b3017 | ||
|
|
bb1a375484 | ||
|
|
bf5dd3b0a1 | ||
|
|
e4d4c62833 | ||
|
|
20ae903d7f | ||
|
|
f5f757e4bd | ||
|
|
13c011895d | ||
|
|
bd8d0e3392 | ||
|
|
5ad564d21b | ||
|
|
8f8775cb93 | ||
|
|
37695827aa | ||
|
|
7a72d209ea | ||
|
|
cda6b67bef | ||
|
|
066305b095 | ||
|
|
f2ba4b270f | ||
|
|
89695df012 | ||
|
|
b0566d3c6f | ||
|
|
5dda8c384f | ||
|
|
b04385a340 | ||
|
|
d374ea6ea6 | ||
|
|
01a2820390 | ||
|
|
c89c1a03da | ||
|
|
873408270e | ||
|
|
8fec8f35bc | ||
|
|
141c846fe2 | ||
|
|
cb569ff14d | ||
|
|
1497469016 | ||
|
|
e356a6d33b | ||
|
|
38ac4c5980 | ||
|
|
ed3ee64e4b | ||
|
|
12aea2901d | ||
|
|
5ff56467ea | ||
|
|
3a8718a4b0 | ||
|
|
8c15855fc3 | ||
|
|
37c4a7b690 | ||
|
|
b735e7c34d | ||
|
|
5f85c3b3b8 | ||
|
|
5d9cb9fa21 | ||
|
|
643d56958d | ||
|
|
f378d6f040 | ||
|
|
bb57794388 | ||
|
|
a9ca49b8a2 | ||
|
|
c1b473294e | ||
|
|
e3e4bdfe09 | ||
|
|
bfbeace2e2 | ||
|
|
efcf46ce8a | ||
|
|
2085715965 | ||
|
|
d227db7b7b | ||
|
|
2af67ad355 | ||
|
|
f100854423 | ||
|
|
92331d7a33 | ||
|
|
9a5bcb9099 | ||
|
|
8eb6bb2a95 | ||
|
|
2aa65ccab3 | ||
|
|
be1577a3e7 | ||
|
|
c8e1b3bf29 | ||
|
|
e17b986628 | ||
|
|
5f19918ca0 | ||
|
|
2959ad0e70 | ||
|
|
a76eec7bb7 | ||
|
|
068b2a0dcd | ||
|
|
316b7e5653 | ||
|
|
00fc1da33c | ||
|
|
9ef93df54f | ||
|
|
fd9fdf6399 | ||
|
|
8fa1701e06 | ||
|
|
4abe83f8a9 | ||
|
|
0a7564acb6 | ||
|
|
db0f7cfbae | ||
|
|
1724885371 | ||
|
|
a97e9ea8b1 | ||
|
|
9d30e97526 | ||
|
|
b91330a27a | ||
|
|
744bc9ebe9 | ||
|
|
89ed9e6d7f | ||
|
|
b007e7f54a | ||
|
|
6651a6df42 | ||
|
|
3f29b165aa | ||
|
|
b13b91face | ||
|
|
63c14fe2d5 | ||
|
|
14e74ed02d | ||
|
|
7e30750618 | ||
|
|
4d1dd16be5 | ||
|
|
fa49cf5eba | ||
|
|
26b39fc1c6 | ||
|
|
0d36e368ea | ||
|
|
859f265c68 | ||
|
|
3219f520ba | ||
|
|
97e27b6caf | ||
|
|
09da83a72b | ||
|
|
d13b210e2f | ||
|
|
09fb672718 | ||
|
|
9797ad0e17 | ||
|
|
8b3d61ac36 | ||
|
|
7161c9547a | ||
|
|
60d4362a87 | ||
|
|
1836e0c8fc | ||
|
|
d3344aeb34 | ||
|
|
cfeb093fa6 | ||
|
|
a469b3ffcc | ||
|
|
14b3a3fdd8 | ||
|
|
94367ce387 | ||
|
|
5be518aa50 | ||
|
|
d059a8da9e | ||
|
|
1dcacbef7a | ||
|
|
a25edeccf7 | ||
|
|
315f73c77d | ||
|
|
666288fccc | ||
|
|
0ccf61c2a9 | ||
|
|
c16b1b27a3 | ||
|
|
ed9ba60be6 | ||
|
|
24d047e3d8 | ||
|
|
9671079ffb | ||
|
|
688892523c | ||
|
|
b02c341f62 | ||
|
|
3e9bcada1e | ||
|
|
93d4bd6438 | ||
|
|
5146498b33 | ||
|
|
72da4f39a8 | ||
|
|
a2b2fb804b | ||
|
|
3eac80e666 | ||
|
|
718d2122a4 | ||
|
|
310c6c90a3 | ||
|
|
9d80f62d58 | ||
|
|
77032fc989 | ||
|
|
64e6086f0c | ||
|
|
3aa58fdc8f | ||
|
|
93bc6ba615 | ||
|
|
36690d63cb | ||
|
|
9896e9799a | ||
|
|
27afc82b79 | ||
|
|
1c8f01ce7b | ||
|
|
4038ccff0d | ||
|
|
5b41bc2f59 | ||
|
|
014ba760b5 | ||
|
|
96a91ccf09 | ||
|
|
347fbd2a48 | ||
|
|
29723052ab | ||
|
|
86415d675b | ||
|
|
8fc4a0dc48 | ||
|
|
e14670cdda | ||
|
|
4d73488f0c | ||
|
|
46e62b24cf | ||
|
|
17c3041fe9 | ||
|
|
d5ae381528 | ||
|
|
e2e09527ec | ||
|
|
3ce1afbcc9 | ||
|
|
1f077d7ec2 | ||
|
|
adf3d0347b | ||
|
|
7ed8b16a53 | ||
|
|
9f7c162107 | ||
|
|
fb15f8cde6 | ||
|
|
45ecfcc6bb | ||
|
|
c6f947e470 | ||
|
|
adf5caf18a | ||
|
|
0b8068e13d | ||
|
|
f143d2e214 | ||
|
|
2e802301ae | ||
|
|
7305c721a6 | ||
|
|
b299f3d6aa | ||
|
|
e09cd6c16c | ||
|
|
b7df8b7319 | ||
|
|
c92b5942fc | ||
|
|
fe729ec762 | ||
|
|
915673798e | ||
|
|
9527fe4f26 | ||
|
|
e8a8b3f664 | ||
|
|
d6a829abc2 | ||
|
|
1a36cd0317 | ||
|
|
75005ccf81 | ||
|
|
fd6c600531 | ||
|
|
6996c2501e | ||
|
|
efbd9bdb56 | ||
|
|
0d34213647 | ||
|
|
870b85d71b | ||
|
|
86ba6b6f86 | ||
|
|
02be3cd0c4 | ||
|
|
1b756ef9a0 | ||
|
|
ceda06f9ae | ||
|
|
068eba015b | ||
|
|
7ae6b2df05 | ||
|
|
6765d5ad26 | ||
|
|
35cfd6bec9 | ||
|
|
90f66baf85 | ||
|
|
5edfed78f2 | ||
|
|
fd6a3e5a17 | ||
|
|
14a4b1b4b4 | ||
|
|
5743c0bb72 | ||
|
|
acca1b6a91 | ||
|
|
355265cd1e | ||
|
|
6ec8d143fa | ||
|
|
8ae327e8f5 | ||
|
|
c03a61f613 | ||
|
|
89928c753c | ||
|
|
a56fcc0fba | ||
|
|
43c60bcdbc | ||
|
|
a3fa12f0e4 | ||
|
|
d696556097 | ||
|
|
6a45151741 | ||
|
|
34e2fbefb9 | ||
|
|
f7cede4713 | ||
|
|
610b20c1ff | ||
|
|
fb19e10cdc | ||
|
|
2f1756ccf2 | ||
|
|
ce632a25cf | ||
|
|
ec10c37468 | ||
|
|
5ee3e140ed | ||
|
|
888f5f8bb6 | ||
|
|
9114dd5992 | ||
|
|
a126494c12 | ||
|
|
79ba804c88 | ||
|
|
e2cbe11a5f | ||
|
|
05748bf8ff | ||
|
|
f8c98bf6bf | ||
|
|
f4496bb23a | ||
|
|
c93766bb48 | ||
|
|
a1ea3f74b3 | ||
|
|
06aaa7c680 | ||
|
|
65e8bfc93e | ||
|
|
ff5e12655f | ||
|
|
1065004fa3 | ||
|
|
6d90d734f4 | ||
|
|
6c8757f230 | ||
|
|
40e37b1798 | ||
|
|
8e1fd4474f | ||
|
|
bd87585396 | ||
|
|
e9e935d6c4 | ||
|
|
2f2c2b4222 | ||
|
|
9749a272ec | ||
|
|
b76a50238e | ||
|
|
a4f3963a5a | ||
|
|
d52bd65d21 | ||
|
|
fb51f42f35 | ||
|
|
c910a715bd | ||
|
|
9040f9b82a | ||
|
|
fc0ec0d754 | ||
|
|
b3569174b6 | ||
|
|
0cae624995 | ||
|
|
cbf184342b | ||
|
|
ce123a7f1a | ||
|
|
0c5daa7173 | ||
|
|
bc20a34a49 | ||
|
|
d5b6a426a9 | ||
|
|
4c78e93143 | ||
|
|
5f184e9e5e | ||
|
|
2201b0395d | ||
|
|
51818044b1 | ||
|
|
30943010e6 | ||
|
|
dd5ca10226 | ||
|
|
a56b058858 | ||
|
|
eade72e2c6 | ||
|
|
e9bc9747b8 | ||
|
|
eb0cdda0f9 | ||
|
|
552adf3200 | ||
|
|
eba25fcc4d | ||
|
|
673cd0fcd1 | ||
|
|
b941b5571f | ||
|
|
ca026b41c0 | ||
|
|
29a683a815 | ||
|
|
69dbd20ea5 | ||
|
|
427ee026ac | ||
|
|
0a537c6830 | ||
|
|
89682a2ee4 | ||
|
|
78b00a18cc | ||
|
|
192702daf9 | ||
|
|
fcee735578 | ||
|
|
2ba49e84bb | ||
|
|
262376aa75 | ||
|
|
4c8d2266ec | ||
|
|
bb98bf03aa | ||
|
|
19c3efc9e9 | ||
|
|
7164721ee0 | ||
|
|
74b16809ec | ||
|
|
220723d25f | ||
|
|
fdb03c9626 | ||
|
|
a81bbb9192 | ||
|
|
7a4aff8e4b | ||
|
|
2810632f4a | ||
|
|
2d0dd067b8 | ||
|
|
3ab25f5ff1 | ||
|
|
39bebea5f7 | ||
|
|
57681dcd3d | ||
|
|
168ce549f7 | ||
|
|
9ec94441f3 | ||
|
|
53e7b99605 | ||
|
|
abfe476cb9 | ||
|
|
bbca200ceb | ||
|
|
cb21cab117 | ||
|
|
1f80845a7a | ||
|
|
20088ef82b | ||
|
|
1e0b1a3607 | ||
|
|
24e8455c73 | ||
|
|
e42a732e93 | ||
|
|
0f2b94307f | ||
|
|
d333cb5199 | ||
|
|
a6db4f20ad | ||
|
|
9ed9472c01 | ||
|
|
f7fcde8312 | ||
|
|
6660c850f3 | ||
|
|
8a08bdf9f0 | ||
|
|
87807e22e0 | ||
|
|
0eb39abdb4 | ||
|
|
a499ebc158 | ||
|
|
9467e6c032 | ||
|
|
9d849a0ced | ||
|
|
2ca400ab16 | ||
|
|
4183067c77 | ||
|
|
5eb4691973 | ||
|
|
d14dfbf360 | ||
|
|
493a5ad02a | ||
|
|
481beff028 | ||
|
|
f1f7e438b4 | ||
|
|
00f84c9d8e | ||
|
|
f75b9c6c86 | ||
|
|
31bc6d5773 | ||
|
|
51dc1450d3 | ||
|
|
fcbea08c87 | ||
|
|
8d60a87aa1 | ||
|
|
956aa64519 | ||
|
|
fd1cb6ca23 | ||
|
|
37082ae436 | ||
|
|
bb47ca3d2e | ||
|
|
0dd3c84b24 | ||
|
|
848fca7e1b | ||
|
|
2500f99722 | ||
|
|
c7737c444f | ||
|
|
4d1a7ed69b | ||
|
|
626d5df67e | ||
|
|
e4c369deec | ||
|
|
307209e73f | ||
|
|
dc84935ee6 | ||
|
|
998c1f52ca | ||
|
|
2766758c66 | ||
|
|
258d1d82f3 | ||
|
|
46aaadb76a | ||
|
|
ea7a618810 | ||
|
|
c0e503b31f | ||
|
|
55f5a41752 | ||
|
|
b0be82be86 | ||
|
|
96a9bdb700 | ||
|
|
74e6d39c24 | ||
|
|
61dfa00222 | ||
|
|
476281db2b | ||
|
|
f32e31c73d | ||
|
|
ea72279080 | ||
|
|
16ba56af84 | ||
|
|
f13ddde988 | ||
|
|
67dc10dfe9 | ||
|
|
5fd216adc2 | ||
|
|
6f0268f6c0 | ||
|
|
2996dfb33a | ||
|
|
c92f2cd4ba | ||
|
|
8164d5c1ad | ||
|
|
d9d8d85f6e | ||
|
|
d49720703f | ||
|
|
2362a9b4dd | ||
|
|
a8265a5286 | ||
|
|
9ea7431b73 | ||
|
|
37e6f320fe | ||
|
|
c0c0d48edf | ||
|
|
284cccbe17 | ||
|
|
81a9a94264 | ||
|
|
dccf101554 | ||
|
|
a01c06bbc7 | ||
|
|
db43cf1b30 | ||
|
|
2f561b5604 | ||
|
|
5a30f036ff | ||
|
|
768b9ffd09 | ||
|
|
8732e50047 | ||
|
|
d6e0024c96 | ||
|
|
9759e86921 | ||
|
|
982c692c40 | ||
|
|
0c3ce7836c | ||
|
|
7ef86c5707 | ||
|
|
f62b88b930 | ||
|
|
03a326c841 | ||
|
|
4df4cafd70 | ||
|
|
4b9539cc6d | ||
|
|
87135c90bd | ||
|
|
853d416b2f | ||
|
|
bfd14b87bd | ||
|
|
88aba4e169 | ||
|
|
99e2fcb2e8 | ||
|
|
1f138ab68c | ||
|
|
99ded7454e | ||
|
|
f82cacac6d | ||
|
|
a548f61ea6 | ||
|
|
bfae715076 | ||
|
|
358e25b7c2 | ||
|
|
2c3fa54933 | ||
|
|
00cdd5833e | ||
|
|
52b1164e58 | ||
|
|
657bc9cdf0 | ||
|
|
ec6bcd41b0 | ||
|
|
1721cce040 | ||
|
|
e41a5ad6b0 | ||
|
|
ee1eca9e66 | ||
|
|
d049369172 | ||
|
|
6280a68d51 | ||
|
|
32054dc4f6 | ||
|
|
831c631048 | ||
|
|
e23711bcce | ||
|
|
440bff57d0 | ||
|
|
7345cc81c1 | ||
|
|
164ab26069 | ||
|
|
4b6ace80d3 | ||
|
|
653127a0f7 | ||
|
|
bf3a1e20fc | ||
|
|
d7a44e7589 | ||
|
|
6c0d583557 | ||
|
|
13f0fb25da | ||
|
|
818aca9ec8 | ||
|
|
1c7fb476b0 | ||
|
|
93843ed733 | ||
|
|
0973313703 | ||
|
|
bfbfbe8b11 | ||
|
|
8c62d9fe78 | ||
|
|
d5558f55ed | ||
|
|
a96ad6bd07 | ||
|
|
00d9482a99 | ||
|
|
0f90e2a30f | ||
|
|
3eed636404 | ||
|
|
a67f88381f | ||
|
|
808fd856d1 | ||
|
|
5b9b532458 | ||
|
|
9fba9bd6b7 | ||
|
|
c5ece144d0 | ||
|
|
b64e2e11db | ||
|
|
0ccd5714f9 | ||
|
|
e2dfc3eb20 | ||
|
|
40eeb9b7cb | ||
|
|
8fa62a0908 | ||
|
|
446eba8bc9 | ||
|
|
18579c0647 | ||
|
|
2bb94e24eb | ||
|
|
0d37e08638 | ||
|
|
ca89c5feca | ||
|
|
729c2adb3f | ||
|
|
a21f49cb02 | ||
|
|
ef697c4864 | ||
|
|
2652dea09a | ||
|
|
efa9312fca | ||
|
|
074ee70025 | ||
|
|
77117e48e3 | ||
|
|
da112d3417 | ||
|
|
ddaaf34dbd | ||
|
|
373e35324e | ||
|
|
09b2f27749 | ||
|
|
7e9f18bf24 | ||
|
|
ab3be26790 | ||
|
|
5c67a1cb12 | ||
|
|
e28ab19ed4 | ||
|
|
59f8334cfd | ||
|
|
718bec4bbc | ||
|
|
2d731cb24b | ||
|
|
1905936950 | ||
|
|
c362bc673c | ||
|
|
4da0a752ef | ||
|
|
221ee6a1c2 | ||
|
|
2e60ecec87 | ||
|
|
3d4df906cf | ||
|
|
71386d3b05 | ||
|
|
e051142334 | ||
|
|
89a7e2e4dc | ||
|
|
27440700a5 | ||
|
|
b5019cef12 | ||
|
|
7e48cbe1aa | ||
|
|
4b2c570e73 | ||
|
|
972febf0ea | ||
|
|
6060b1d60d | ||
|
|
c91b4beac5 | ||
|
|
3577b5efb9 | ||
|
|
6069b84e58 | ||
|
|
cbccea0bbc | ||
|
|
e17212c584 | ||
|
|
e38102b022 | ||
|
|
5749704cf1 | ||
|
|
f584cba6be | ||
|
|
0661a950c7 | ||
|
|
8f5ac1282a | ||
|
|
232a178e0f | ||
|
|
4f64db1d82 | ||
|
|
d91356574b | ||
|
|
bd666d46b2 | ||
|
|
881346c31a | ||
|
|
ec1b41ebbb | ||
|
|
abe7bbf068 | ||
|
|
a1d33f8103 | ||
|
|
b55386e301 | ||
|
|
59fc5713ca | ||
|
|
b9bd6433a7 | ||
|
|
6b2e77262e | ||
|
|
8904db8dd1 | ||
|
|
0da15ae1e6 | ||
|
|
0086818928 | ||
|
|
d7abf9369e | ||
|
|
5fcc7bbff4 | ||
|
|
e90d87f26d | ||
|
|
46a6d2be9e | ||
|
|
f6709c1bdf | ||
|
|
c2a721791f | ||
|
|
ec1f5eff19 | ||
|
|
10463e5b55 | ||
|
|
30fa048637 | ||
|
|
6bae89023b | ||
|
|
874aac010e | ||
|
|
465a007f2e | ||
|
|
a678a18bcf | ||
|
|
dbb0979e86 | ||
|
|
2571ade633 | ||
|
|
38bae0dc6a | ||
|
|
7409d44923 | ||
|
|
84b4f15ed4 | ||
|
|
412c4717ad | ||
|
|
88d3e76c44 | ||
|
|
8626454811 | ||
|
|
f7c0a6875c | ||
|
|
f89f3398fa | ||
|
|
32898eb5d3 | ||
|
|
9fe05b7af4 | ||
|
|
f8be34370d | ||
|
|
91bd38227e | ||
|
|
101ee581e8 | ||
|
|
8d099c51e1 | ||
|
|
939264014b | ||
|
|
bbeb4c029c | ||
|
|
59a37cc606 | ||
|
|
ce3962f97a | ||
|
|
3c20fd0a55 | ||
|
|
e320f9f16e | ||
|
|
11200b99d3 | ||
|
|
7507806aaa | ||
|
|
90c48f20e0 | ||
|
|
9e68c6c004 | ||
|
|
130c890678 | ||
|
|
5e183911e1 | ||
|
|
74479c984c | ||
|
|
1d5d856799 | ||
|
|
8ea6b0cd9e | ||
|
|
90d07f9794 | ||
|
|
e9a29e7db2 | ||
|
|
b2df8eb72e | ||
|
|
3f81b88073 | ||
|
|
dedc13ab98 | ||
|
|
2f8ecf17ed | ||
|
|
81149085fa | ||
|
|
0aa56d441e | ||
|
|
757b735d98 | ||
|
|
4af7900dae | ||
|
|
a3610b7dde | ||
|
|
af4f85a081 | ||
|
|
6a5939599c | ||
|
|
51ef859349 | ||
|
|
e477a5a1b5 | ||
|
|
4a98061a62 | ||
|
|
be20289140 | ||
|
|
3ce0cc1992 | ||
|
|
a9a0fbe244 | ||
|
|
03d1f4bbb9 | ||
|
|
9b3d066a91 | ||
|
|
ccd4f9b65c | ||
|
|
d8344988c0 | ||
|
|
bb5594ab2f | ||
|
|
19f8cda3d9 | ||
|
|
d3c4688c0f | ||
|
|
2d92111f1d | ||
|
|
b8ffc601d4 | ||
|
|
8af95ea1ca | ||
|
|
beddb0d187 | ||
|
|
bd20bb0dd1 | ||
|
|
662e63317b | ||
|
|
d82535d3e1 | ||
|
|
1d862131dd | ||
|
|
150c51c9eb | ||
|
|
8618a5c2fd | ||
|
|
795302a351 | ||
|
|
096a2bfa10 | ||
|
|
188994ce84 | ||
|
|
800bdcb277 | ||
|
|
c033fd4e8b | ||
|
|
d2fa55dd11 | ||
|
|
7e047d9e34 | ||
|
|
9d9401d2ee | ||
|
|
9a621044d8 | ||
|
|
3a6fbb67a5 | ||
|
|
c7c70fa736 | ||
|
|
eafcefbe45 | ||
|
|
ce3cb98422 | ||
|
|
ae5bdcd88b | ||
|
|
428a76d742 | ||
|
|
8d2955475b | ||
|
|
1f4ebf1907 | ||
|
|
4cb5c22268 | ||
|
|
b7b65bb295 | ||
|
|
75b9703793 | ||
|
|
e983e1166a | ||
|
|
322f3bfb1d | ||
|
|
009b86c33b | ||
|
|
a5775a0f4f | ||
|
|
46ed27a218 |
@@ -28,7 +28,9 @@ LICENSE
|
|||||||
CONTRIBUTING.md
|
CONTRIBUTING.md
|
||||||
dist
|
dist
|
||||||
.git
|
.git
|
||||||
migrations/
|
server/migrations/
|
||||||
config/
|
config/
|
||||||
build.ts
|
build.ts
|
||||||
tsconfig.json
|
tsconfig.json
|
||||||
|
Dockerfile*
|
||||||
|
drizzle.config.ts
|
||||||
|
|||||||
14
.github/dependabot.yml
vendored
14
.github/dependabot.yml
vendored
@@ -44,19 +44,9 @@ updates:
|
|||||||
schedule:
|
schedule:
|
||||||
interval: "daily"
|
interval: "daily"
|
||||||
groups:
|
groups:
|
||||||
dev-patch-updates:
|
patch-updates:
|
||||||
dependency-type: "development"
|
|
||||||
update-types:
|
update-types:
|
||||||
- "patch"
|
- "patch"
|
||||||
dev-minor-updates:
|
minor-updates:
|
||||||
dependency-type: "development"
|
|
||||||
update-types:
|
|
||||||
- "minor"
|
|
||||||
prod-patch-updates:
|
|
||||||
dependency-type: "production"
|
|
||||||
update-types:
|
|
||||||
- "patch"
|
|
||||||
prod-minor-updates:
|
|
||||||
dependency-type: "production"
|
|
||||||
update-types:
|
update-types:
|
||||||
- "minor"
|
- "minor"
|
||||||
329
.github/workflows/cicd.yml
vendored
329
.github/workflows/cicd.yml
vendored
@@ -1,4 +1,4 @@
|
|||||||
name: CI/CD Pipeline
|
name: Public CICD Pipeline
|
||||||
|
|
||||||
# CI/CD workflow for building, publishing, mirroring, signing container images and building release binaries.
|
# 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.
|
# Actions are pinned to specific SHAs to reduce supply-chain risk. This workflow triggers on tag push events.
|
||||||
@@ -17,7 +17,7 @@ on:
|
|||||||
push:
|
push:
|
||||||
tags:
|
tags:
|
||||||
- "[0-9]+.[0-9]+.[0-9]+"
|
- "[0-9]+.[0-9]+.[0-9]+"
|
||||||
- "[0-9]+.[0-9]+.[0-9]+.rc.[0-9]+"
|
- "[0-9]+.[0-9]+.[0-9]+-rc.[0-9]+"
|
||||||
|
|
||||||
concurrency:
|
concurrency:
|
||||||
group: ${{ github.ref }}
|
group: ${{ github.ref }}
|
||||||
@@ -29,7 +29,7 @@ jobs:
|
|||||||
permissions: write-all
|
permissions: write-all
|
||||||
steps:
|
steps:
|
||||||
- name: Configure AWS credentials
|
- name: Configure AWS credentials
|
||||||
uses: aws-actions/configure-aws-credentials@v2
|
uses: aws-actions/configure-aws-credentials@v6
|
||||||
with:
|
with:
|
||||||
role-to-assume: arn:aws:iam::${{ secrets.AWS_ACCOUNT_ID }}:role/${{ secrets.AWS_ROLE_NAME }}
|
role-to-assume: arn:aws:iam::${{ secrets.AWS_ACCOUNT_ID }}:role/${{ secrets.AWS_ROLE_NAME }}
|
||||||
role-duration-seconds: 3600
|
role-duration-seconds: 3600
|
||||||
@@ -62,7 +62,7 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
|
|
||||||
- name: Monitor storage space
|
- name: Monitor storage space
|
||||||
run: |
|
run: |
|
||||||
@@ -77,7 +77,7 @@ jobs:
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
- name: Log in to Docker Hub
|
- name: Log in to Docker Hub
|
||||||
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
|
uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4.0.0
|
||||||
with:
|
with:
|
||||||
registry: docker.io
|
registry: docker.io
|
||||||
username: ${{ secrets.DOCKER_HUB_USERNAME }}
|
username: ${{ secrets.DOCKER_HUB_USERNAME }}
|
||||||
@@ -95,10 +95,25 @@ jobs:
|
|||||||
cat server/lib/consts.ts
|
cat server/lib/consts.ts
|
||||||
shell: bash
|
shell: bash
|
||||||
|
|
||||||
|
- name: Check if release candidate
|
||||||
|
id: check-rc
|
||||||
|
run: |
|
||||||
|
TAG=${{ env.TAG }}
|
||||||
|
if [[ "$TAG" == *"-rc."* ]]; then
|
||||||
|
echo "IS_RC=true" >> $GITHUB_ENV
|
||||||
|
else
|
||||||
|
echo "IS_RC=false" >> $GITHUB_ENV
|
||||||
|
fi
|
||||||
|
shell: bash
|
||||||
|
|
||||||
- name: Build and push Docker images (Docker Hub - ARM64)
|
- name: Build and push Docker images (Docker Hub - ARM64)
|
||||||
run: |
|
run: |
|
||||||
TAG=${{ env.TAG }}
|
TAG=${{ env.TAG }}
|
||||||
|
if [ "$IS_RC" = "true" ]; then
|
||||||
|
make build-rc-arm tag=$TAG
|
||||||
|
else
|
||||||
make build-release-arm tag=$TAG
|
make build-release-arm tag=$TAG
|
||||||
|
fi
|
||||||
echo "Built & pushed ARM64 images to: ${{ env.DOCKERHUB_IMAGE }}:${TAG}"
|
echo "Built & pushed ARM64 images to: ${{ env.DOCKERHUB_IMAGE }}:${TAG}"
|
||||||
shell: bash
|
shell: bash
|
||||||
|
|
||||||
@@ -119,7 +134,7 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
|
|
||||||
- name: Monitor storage space
|
- name: Monitor storage space
|
||||||
run: |
|
run: |
|
||||||
@@ -134,7 +149,7 @@ jobs:
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
- name: Log in to Docker Hub
|
- name: Log in to Docker Hub
|
||||||
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
|
uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4.0.0
|
||||||
with:
|
with:
|
||||||
registry: docker.io
|
registry: docker.io
|
||||||
username: ${{ secrets.DOCKER_HUB_USERNAME }}
|
username: ${{ secrets.DOCKER_HUB_USERNAME }}
|
||||||
@@ -152,15 +167,30 @@ jobs:
|
|||||||
cat server/lib/consts.ts
|
cat server/lib/consts.ts
|
||||||
shell: bash
|
shell: bash
|
||||||
|
|
||||||
|
- name: Check if release candidate
|
||||||
|
id: check-rc
|
||||||
|
run: |
|
||||||
|
TAG=${{ env.TAG }}
|
||||||
|
if [[ "$TAG" == *"-rc."* ]]; then
|
||||||
|
echo "IS_RC=true" >> $GITHUB_ENV
|
||||||
|
else
|
||||||
|
echo "IS_RC=false" >> $GITHUB_ENV
|
||||||
|
fi
|
||||||
|
shell: bash
|
||||||
|
|
||||||
- name: Build and push Docker images (Docker Hub - AMD64)
|
- name: Build and push Docker images (Docker Hub - AMD64)
|
||||||
run: |
|
run: |
|
||||||
TAG=${{ env.TAG }}
|
TAG=${{ env.TAG }}
|
||||||
|
if [ "$IS_RC" = "true" ]; then
|
||||||
|
make build-rc-amd tag=$TAG
|
||||||
|
else
|
||||||
make build-release-amd tag=$TAG
|
make build-release-amd tag=$TAG
|
||||||
|
fi
|
||||||
echo "Built & pushed AMD64 images to: ${{ env.DOCKERHUB_IMAGE }}:${TAG}"
|
echo "Built & pushed AMD64 images to: ${{ env.DOCKERHUB_IMAGE }}:${TAG}"
|
||||||
shell: bash
|
shell: bash
|
||||||
|
|
||||||
sign-and-package:
|
create-manifest:
|
||||||
name: Sign and Package
|
name: Create Multi-Arch Manifests
|
||||||
runs-on: [self-hosted, linux, x64, us-east-1]
|
runs-on: [self-hosted, linux, x64, us-east-1]
|
||||||
needs: [release-arm, release-amd]
|
needs: [release-arm, release-amd]
|
||||||
if: >-
|
if: >-
|
||||||
@@ -168,6 +198,55 @@ jobs:
|
|||||||
needs.release-arm.result == 'success' &&
|
needs.release-arm.result == 'success' &&
|
||||||
needs.release-amd.result == 'success'
|
needs.release-amd.result == 'success'
|
||||||
}}
|
}}
|
||||||
|
timeout-minutes: 30
|
||||||
|
steps:
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
|
|
||||||
|
- name: Log in to Docker Hub
|
||||||
|
uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4.0.0
|
||||||
|
with:
|
||||||
|
registry: docker.io
|
||||||
|
username: ${{ secrets.DOCKER_HUB_USERNAME }}
|
||||||
|
password: ${{ secrets.DOCKER_HUB_ACCESS_TOKEN }}
|
||||||
|
|
||||||
|
- name: Extract tag name
|
||||||
|
id: get-tag
|
||||||
|
run: echo "TAG=${GITHUB_REF#refs/tags/}" >> $GITHUB_ENV
|
||||||
|
shell: bash
|
||||||
|
|
||||||
|
- name: Check if release candidate
|
||||||
|
id: check-rc
|
||||||
|
run: |
|
||||||
|
TAG=${{ env.TAG }}
|
||||||
|
if [[ "$TAG" == *"-rc."* ]]; then
|
||||||
|
echo "IS_RC=true" >> $GITHUB_ENV
|
||||||
|
else
|
||||||
|
echo "IS_RC=false" >> $GITHUB_ENV
|
||||||
|
fi
|
||||||
|
shell: bash
|
||||||
|
|
||||||
|
- name: Create multi-arch manifests
|
||||||
|
run: |
|
||||||
|
TAG=${{ env.TAG }}
|
||||||
|
if [ "$IS_RC" = "true" ]; then
|
||||||
|
make create-manifests-rc tag=$TAG
|
||||||
|
else
|
||||||
|
make create-manifests tag=$TAG
|
||||||
|
fi
|
||||||
|
echo "Created multi-arch manifests for tag: ${TAG}"
|
||||||
|
shell: bash
|
||||||
|
|
||||||
|
sign-and-package:
|
||||||
|
name: Sign and Package
|
||||||
|
runs-on: [self-hosted, linux, x64, us-east-1]
|
||||||
|
needs: [release-arm, release-amd, create-manifest]
|
||||||
|
if: >-
|
||||||
|
${{
|
||||||
|
needs.release-arm.result == 'success' &&
|
||||||
|
needs.release-amd.result == 'success' &&
|
||||||
|
needs.create-manifest.result == 'success'
|
||||||
|
}}
|
||||||
# Job-level timeout to avoid runaway or stuck runs
|
# Job-level timeout to avoid runaway or stuck runs
|
||||||
timeout-minutes: 120
|
timeout-minutes: 120
|
||||||
env:
|
env:
|
||||||
@@ -177,7 +256,7 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
|
|
||||||
- name: Extract tag name
|
- name: Extract tag name
|
||||||
id: get-tag
|
id: get-tag
|
||||||
@@ -185,9 +264,9 @@ jobs:
|
|||||||
shell: bash
|
shell: bash
|
||||||
|
|
||||||
- name: Install Go
|
- name: Install Go
|
||||||
uses: actions/setup-go@4dc6199c7b1a012772edbd06daecab0f50c9053c # v6.1.0
|
uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # v6.3.0
|
||||||
with:
|
with:
|
||||||
go-version: 1.24
|
go-version: 1.25
|
||||||
|
|
||||||
- name: Update version in package.json
|
- name: Update version in package.json
|
||||||
run: |
|
run: |
|
||||||
@@ -210,25 +289,17 @@ jobs:
|
|||||||
echo "LATEST_BADGER_TAG=$LATEST_TAG" >> $GITHUB_ENV
|
echo "LATEST_BADGER_TAG=$LATEST_TAG" >> $GITHUB_ENV
|
||||||
shell: bash
|
shell: bash
|
||||||
|
|
||||||
- name: Update install/main.go
|
|
||||||
run: |
|
|
||||||
PANGOLIN_VERSION=${{ env.TAG }}
|
|
||||||
GERBIL_VERSION=${{ env.LATEST_GERBIL_TAG }}
|
|
||||||
BADGER_VERSION=${{ env.LATEST_BADGER_TAG }}
|
|
||||||
sed -i "s/config.PangolinVersion = \".*\"/config.PangolinVersion = \"$PANGOLIN_VERSION\"/" install/main.go
|
|
||||||
sed -i "s/config.GerbilVersion = \".*\"/config.GerbilVersion = \"$GERBIL_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"
|
|
||||||
cat install/main.go
|
|
||||||
shell: bash
|
|
||||||
|
|
||||||
- name: Build installer
|
- name: Build installer
|
||||||
working-directory: install
|
working-directory: install
|
||||||
run: |
|
run: |
|
||||||
make go-build-release
|
make go-build-release \
|
||||||
|
PANGOLIN_VERSION=${{ env.TAG }} \
|
||||||
|
GERBIL_VERSION=${{ env.LATEST_GERBIL_TAG }} \
|
||||||
|
BADGER_VERSION=${{ env.LATEST_BADGER_TAG }}
|
||||||
|
shell: bash
|
||||||
|
|
||||||
- name: Upload artifacts from /install/bin
|
- name: Upload artifacts from /install/bin
|
||||||
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
|
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
||||||
with:
|
with:
|
||||||
name: install-bin
|
name: install-bin
|
||||||
path: install/bin/
|
path: install/bin/
|
||||||
@@ -243,26 +314,100 @@ jobs:
|
|||||||
shell: bash
|
shell: bash
|
||||||
|
|
||||||
- name: Login to GHCR
|
- name: Login to GHCR
|
||||||
|
env:
|
||||||
|
REGISTRY_AUTH_FILE: ${{ runner.temp }}/containers/auth.json
|
||||||
run: |
|
run: |
|
||||||
|
mkdir -p "$(dirname "$REGISTRY_AUTH_FILE")"
|
||||||
skopeo login ghcr.io -u "${{ github.actor }}" -p "${{ secrets.GITHUB_TOKEN }}"
|
skopeo login ghcr.io -u "${{ github.actor }}" -p "${{ secrets.GITHUB_TOKEN }}"
|
||||||
shell: bash
|
shell: bash
|
||||||
|
|
||||||
- name: Copy tag from Docker Hub to GHCR
|
- name: Copy tags from Docker Hub to GHCR
|
||||||
# Mirror the already-built image (all architectures) to GHCR so we can sign it
|
# Mirror the already-built images (all architectures) to GHCR so we can sign them
|
||||||
# Wait a bit for both architectures to be available in Docker Hub manifest
|
# Wait a bit for both architectures to be available in Docker Hub manifest
|
||||||
|
env:
|
||||||
|
REGISTRY_AUTH_FILE: ${{ runner.temp }}/containers/auth.json
|
||||||
run: |
|
run: |
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
TAG=${{ env.TAG }}
|
TAG=${{ env.TAG }}
|
||||||
echo "Waiting for multi-arch manifest to be ready..."
|
MAJOR_TAG=$(echo $TAG | cut -d. -f1)
|
||||||
|
MINOR_TAG=$(echo $TAG | cut -d. -f1,2)
|
||||||
|
|
||||||
|
echo "Waiting for multi-arch manifests to be ready..."
|
||||||
sleep 30
|
sleep 30
|
||||||
|
|
||||||
|
# Determine if this is an RC release
|
||||||
|
IS_RC="false"
|
||||||
|
if [[ "$TAG" == *"-rc."* ]]; then
|
||||||
|
IS_RC="true"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "$IS_RC" = "true" ]; then
|
||||||
|
echo "RC release detected - copying version-specific tags only"
|
||||||
|
|
||||||
|
# SQLite OSS
|
||||||
echo "Copying ${{ env.DOCKERHUB_IMAGE }}:${TAG} -> ${{ env.GHCR_IMAGE }}:${TAG}"
|
echo "Copying ${{ env.DOCKERHUB_IMAGE }}:${TAG} -> ${{ env.GHCR_IMAGE }}:${TAG}"
|
||||||
skopeo copy --all --retry-times 3 \
|
skopeo copy --all --retry-times 3 \
|
||||||
docker://$DOCKERHUB_IMAGE:$TAG \
|
docker://$DOCKERHUB_IMAGE:$TAG \
|
||||||
docker://$GHCR_IMAGE:$TAG
|
docker://$GHCR_IMAGE:$TAG
|
||||||
|
|
||||||
|
# PostgreSQL OSS
|
||||||
|
echo "Copying ${{ env.DOCKERHUB_IMAGE }}:postgresql-${TAG} -> ${{ env.GHCR_IMAGE }}:postgresql-${TAG}"
|
||||||
|
skopeo copy --all --retry-times 3 \
|
||||||
|
docker://$DOCKERHUB_IMAGE:postgresql-$TAG \
|
||||||
|
docker://$GHCR_IMAGE:postgresql-$TAG
|
||||||
|
|
||||||
|
# SQLite Enterprise
|
||||||
|
echo "Copying ${{ env.DOCKERHUB_IMAGE }}:ee-${TAG} -> ${{ env.GHCR_IMAGE }}:ee-${TAG}"
|
||||||
|
skopeo copy --all --retry-times 3 \
|
||||||
|
docker://$DOCKERHUB_IMAGE:ee-$TAG \
|
||||||
|
docker://$GHCR_IMAGE:ee-$TAG
|
||||||
|
|
||||||
|
# PostgreSQL Enterprise
|
||||||
|
echo "Copying ${{ env.DOCKERHUB_IMAGE }}:ee-postgresql-${TAG} -> ${{ env.GHCR_IMAGE }}:ee-postgresql-${TAG}"
|
||||||
|
skopeo copy --all --retry-times 3 \
|
||||||
|
docker://$DOCKERHUB_IMAGE:ee-postgresql-$TAG \
|
||||||
|
docker://$GHCR_IMAGE:ee-postgresql-$TAG
|
||||||
|
else
|
||||||
|
echo "Regular release detected - copying all tags (latest, major, minor, full version)"
|
||||||
|
|
||||||
|
# SQLite OSS - all tags
|
||||||
|
for TAG_SUFFIX in "latest" "$MAJOR_TAG" "$MINOR_TAG" "$TAG"; do
|
||||||
|
echo "Copying ${{ env.DOCKERHUB_IMAGE }}:${TAG_SUFFIX} -> ${{ env.GHCR_IMAGE }}:${TAG_SUFFIX}"
|
||||||
|
skopeo copy --all --retry-times 3 \
|
||||||
|
docker://$DOCKERHUB_IMAGE:$TAG_SUFFIX \
|
||||||
|
docker://$GHCR_IMAGE:$TAG_SUFFIX
|
||||||
|
done
|
||||||
|
|
||||||
|
# PostgreSQL OSS - all tags
|
||||||
|
for TAG_SUFFIX in "latest" "$MAJOR_TAG" "$MINOR_TAG" "$TAG"; do
|
||||||
|
echo "Copying ${{ env.DOCKERHUB_IMAGE }}:postgresql-${TAG_SUFFIX} -> ${{ env.GHCR_IMAGE }}:postgresql-${TAG_SUFFIX}"
|
||||||
|
skopeo copy --all --retry-times 3 \
|
||||||
|
docker://$DOCKERHUB_IMAGE:postgresql-$TAG_SUFFIX \
|
||||||
|
docker://$GHCR_IMAGE:postgresql-$TAG_SUFFIX
|
||||||
|
done
|
||||||
|
|
||||||
|
# SQLite Enterprise - all tags
|
||||||
|
for TAG_SUFFIX in "latest" "$MAJOR_TAG" "$MINOR_TAG" "$TAG"; do
|
||||||
|
echo "Copying ${{ env.DOCKERHUB_IMAGE }}:ee-${TAG_SUFFIX} -> ${{ env.GHCR_IMAGE }}:ee-${TAG_SUFFIX}"
|
||||||
|
skopeo copy --all --retry-times 3 \
|
||||||
|
docker://$DOCKERHUB_IMAGE:ee-$TAG_SUFFIX \
|
||||||
|
docker://$GHCR_IMAGE:ee-$TAG_SUFFIX
|
||||||
|
done
|
||||||
|
|
||||||
|
# PostgreSQL Enterprise - all tags
|
||||||
|
for TAG_SUFFIX in "latest" "$MAJOR_TAG" "$MINOR_TAG" "$TAG"; do
|
||||||
|
echo "Copying ${{ env.DOCKERHUB_IMAGE }}:ee-postgresql-${TAG_SUFFIX} -> ${{ env.GHCR_IMAGE }}:ee-postgresql-${TAG_SUFFIX}"
|
||||||
|
skopeo copy --all --retry-times 3 \
|
||||||
|
docker://$DOCKERHUB_IMAGE:ee-postgresql-$TAG_SUFFIX \
|
||||||
|
docker://$GHCR_IMAGE:ee-postgresql-$TAG_SUFFIX
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "All images copied successfully to GHCR!"
|
||||||
shell: bash
|
shell: bash
|
||||||
|
|
||||||
- name: Login to GitHub Container Registry (for cosign)
|
- name: Login to GitHub Container Registry (for cosign)
|
||||||
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
|
uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4.0.0
|
||||||
with:
|
with:
|
||||||
registry: ghcr.io
|
registry: ghcr.io
|
||||||
username: ${{ github.actor }}
|
username: ${{ github.actor }}
|
||||||
@@ -270,7 +415,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Install cosign
|
- name: Install cosign
|
||||||
# cosign is used to sign and verify container images (key and keyless)
|
# cosign is used to sign and verify container images (key and keyless)
|
||||||
uses: sigstore/cosign-installer@faadad0cce49287aee09b3a48701e75088a2c6ad # v4.0.0
|
uses: sigstore/cosign-installer@cad07c2e89fa2edd6e2d7bab4c1aa38e53f76003 # v4.1.1
|
||||||
|
|
||||||
- name: Dual-sign and verify (GHCR & Docker Hub)
|
- name: Dual-sign and verify (GHCR & Docker Hub)
|
||||||
# Sign each image by digest using keyless (OIDC) and key-based signing,
|
# Sign each image by digest using keyless (OIDC) and key-based signing,
|
||||||
@@ -287,11 +432,48 @@ jobs:
|
|||||||
issuer="https://token.actions.githubusercontent.com"
|
issuer="https://token.actions.githubusercontent.com"
|
||||||
id_regex="^https://github.com/${{ github.repository }}/.+" # accept this repo (all workflows/refs)
|
id_regex="^https://github.com/${{ github.repository }}/.+" # accept this repo (all workflows/refs)
|
||||||
|
|
||||||
for IMAGE in "${GHCR_IMAGE}" "${DOCKERHUB_IMAGE}"; do
|
# Track failures
|
||||||
echo "Processing ${IMAGE}:${TAG}"
|
FAILED_TAGS=()
|
||||||
|
SUCCESSFUL_TAGS=()
|
||||||
|
|
||||||
DIGEST="$(skopeo inspect --retry-times 3 docker://${IMAGE}:${TAG} | jq -r '.Digest')"
|
# Determine if this is an RC release
|
||||||
REF="${IMAGE}@${DIGEST}"
|
IS_RC="false"
|
||||||
|
if [[ "$TAG" == *"-rc."* ]]; then
|
||||||
|
IS_RC="true"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Define image variants to sign
|
||||||
|
if [ "$IS_RC" = "true" ]; then
|
||||||
|
echo "RC release - signing version-specific tags only"
|
||||||
|
IMAGE_TAGS=(
|
||||||
|
"${TAG}"
|
||||||
|
"postgresql-${TAG}"
|
||||||
|
"ee-${TAG}"
|
||||||
|
"ee-postgresql-${TAG}"
|
||||||
|
)
|
||||||
|
else
|
||||||
|
echo "Regular release - signing all tags"
|
||||||
|
MAJOR_TAG=$(echo $TAG | cut -d. -f1)
|
||||||
|
MINOR_TAG=$(echo $TAG | cut -d. -f1,2)
|
||||||
|
IMAGE_TAGS=(
|
||||||
|
"latest" "$MAJOR_TAG" "$MINOR_TAG" "$TAG"
|
||||||
|
"postgresql-latest" "postgresql-$MAJOR_TAG" "postgresql-$MINOR_TAG" "postgresql-$TAG"
|
||||||
|
"ee-latest" "ee-$MAJOR_TAG" "ee-$MINOR_TAG" "ee-$TAG"
|
||||||
|
"ee-postgresql-latest" "ee-postgresql-$MAJOR_TAG" "ee-postgresql-$MINOR_TAG" "ee-postgresql-$TAG"
|
||||||
|
)
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Sign each image variant for both registries
|
||||||
|
for BASE_IMAGE in "${GHCR_IMAGE}" "${DOCKERHUB_IMAGE}"; do
|
||||||
|
for IMAGE_TAG in "${IMAGE_TAGS[@]}"; do
|
||||||
|
echo "Processing ${BASE_IMAGE}:${IMAGE_TAG}"
|
||||||
|
TAG_FAILED=false
|
||||||
|
|
||||||
|
# Wrap the entire tag processing in error handling
|
||||||
|
(
|
||||||
|
set -e
|
||||||
|
DIGEST="$(skopeo inspect --retry-times 3 docker://${BASE_IMAGE}:${IMAGE_TAG} | jq -r '.Digest')"
|
||||||
|
REF="${BASE_IMAGE}@${DIGEST}"
|
||||||
echo "Resolved digest: ${REF}"
|
echo "Resolved digest: ${REF}"
|
||||||
|
|
||||||
echo "==> cosign sign (keyless) --recursive ${REF}"
|
echo "==> cosign sign (keyless) --recursive ${REF}"
|
||||||
@@ -300,32 +482,95 @@ jobs:
|
|||||||
echo "==> cosign sign (key) --recursive ${REF}"
|
echo "==> cosign sign (key) --recursive ${REF}"
|
||||||
cosign sign --key env://COSIGN_PRIVATE_KEY --recursive "${REF}"
|
cosign sign --key env://COSIGN_PRIVATE_KEY --recursive "${REF}"
|
||||||
|
|
||||||
|
# Retry wrapper for verification to handle registry propagation delays
|
||||||
|
retry_verify() {
|
||||||
|
local cmd="$1"
|
||||||
|
local attempts=6
|
||||||
|
local delay=5
|
||||||
|
local i=1
|
||||||
|
until eval "$cmd"; do
|
||||||
|
if [ $i -ge $attempts ]; then
|
||||||
|
echo "Verification failed after $attempts attempts"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
echo "Verification not yet available. Retry $i/$attempts after ${delay}s..."
|
||||||
|
sleep $delay
|
||||||
|
i=$((i+1))
|
||||||
|
delay=$((delay*2))
|
||||||
|
# Cap the delay to avoid very long waits
|
||||||
|
if [ $delay -gt 60 ]; then delay=60; fi
|
||||||
|
done
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
echo "==> cosign verify (public key) ${REF}"
|
echo "==> cosign verify (public key) ${REF}"
|
||||||
cosign verify --key env://COSIGN_PUBLIC_KEY "${REF}" -o text
|
if retry_verify "cosign verify --key env://COSIGN_PUBLIC_KEY '${REF}' -o text"; then
|
||||||
|
VERIFIED_INDEX=true
|
||||||
|
else
|
||||||
|
VERIFIED_INDEX=false
|
||||||
|
fi
|
||||||
|
|
||||||
echo "==> cosign verify (keyless policy) ${REF}"
|
echo "==> cosign verify (keyless policy) ${REF}"
|
||||||
cosign verify \
|
if retry_verify "cosign verify --certificate-oidc-issuer '${issuer}' --certificate-identity-regexp '${id_regex}' '${REF}' -o text"; then
|
||||||
--certificate-oidc-issuer "${issuer}" \
|
VERIFIED_INDEX_KEYLESS=true
|
||||||
--certificate-identity-regexp "${id_regex}" \
|
else
|
||||||
"${REF}" -o text
|
VERIFIED_INDEX_KEYLESS=false
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check if verification succeeded
|
||||||
|
if [ "${VERIFIED_INDEX}" != "true" ] && [ "${VERIFIED_INDEX_KEYLESS}" != "true" ]; then
|
||||||
|
echo "⚠️ WARNING: Verification not available for ${BASE_IMAGE}:${IMAGE_TAG}"
|
||||||
|
echo "This may be due to registry propagation delays. Continuing anyway."
|
||||||
|
fi
|
||||||
|
) || TAG_FAILED=true
|
||||||
|
|
||||||
|
if [ "$TAG_FAILED" = "true" ]; then
|
||||||
|
echo "⚠️ WARNING: Failed to sign/verify ${BASE_IMAGE}:${IMAGE_TAG}"
|
||||||
|
FAILED_TAGS+=("${BASE_IMAGE}:${IMAGE_TAG}")
|
||||||
|
else
|
||||||
|
echo "✓ Successfully signed and verified ${BASE_IMAGE}:${IMAGE_TAG}"
|
||||||
|
SUCCESSFUL_TAGS+=("${BASE_IMAGE}:${IMAGE_TAG}")
|
||||||
|
fi
|
||||||
done
|
done
|
||||||
|
done
|
||||||
|
|
||||||
|
# Report summary
|
||||||
|
echo ""
|
||||||
|
echo "=========================================="
|
||||||
|
echo "Sign and Verify Summary"
|
||||||
|
echo "=========================================="
|
||||||
|
echo "Successful: ${#SUCCESSFUL_TAGS[@]}"
|
||||||
|
echo "Failed: ${#FAILED_TAGS[@]}"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
if [ ${#FAILED_TAGS[@]} -gt 0 ]; then
|
||||||
|
echo "Failed tags:"
|
||||||
|
for tag in "${FAILED_TAGS[@]}"; do
|
||||||
|
echo " - $tag"
|
||||||
|
done
|
||||||
|
echo ""
|
||||||
|
echo "⚠️ WARNING: Some tags failed to sign/verify, but continuing anyway"
|
||||||
|
else
|
||||||
|
echo "✓ All images signed and verified successfully!"
|
||||||
|
fi
|
||||||
shell: bash
|
shell: bash
|
||||||
|
|
||||||
post-run:
|
post-run:
|
||||||
needs: [pre-run, release-arm, release-amd, sign-and-package]
|
needs: [pre-run, release-arm, release-amd, create-manifest, sign-and-package]
|
||||||
if: >-
|
if: >-
|
||||||
${{
|
${{
|
||||||
always() &&
|
always() &&
|
||||||
needs.pre-run.result == 'success' &&
|
needs.pre-run.result == 'success' &&
|
||||||
(needs.release-arm.result == 'success' || needs.release-arm.result == 'skipped' || needs.release-arm.result == 'failure') &&
|
(needs.release-arm.result == 'success' || needs.release-arm.result == 'skipped' || needs.release-arm.result == 'failure') &&
|
||||||
(needs.release-amd.result == 'success' || needs.release-amd.result == 'skipped' || needs.release-amd.result == 'failure') &&
|
(needs.release-amd.result == 'success' || needs.release-amd.result == 'skipped' || needs.release-amd.result == 'failure') &&
|
||||||
|
(needs.create-manifest.result == 'success' || needs.create-manifest.result == 'skipped' || needs.create-manifest.result == 'failure') &&
|
||||||
(needs.sign-and-package.result == 'success' || needs.sign-and-package.result == 'skipped' || needs.sign-and-package.result == 'failure')
|
(needs.sign-and-package.result == 'success' || needs.sign-and-package.result == 'skipped' || needs.sign-and-package.result == 'failure')
|
||||||
}}
|
}}
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
permissions: write-all
|
permissions: write-all
|
||||||
steps:
|
steps:
|
||||||
- name: Configure AWS credentials
|
- name: Configure AWS credentials
|
||||||
uses: aws-actions/configure-aws-credentials@v2
|
uses: aws-actions/configure-aws-credentials@v6
|
||||||
with:
|
with:
|
||||||
role-to-assume: arn:aws:iam::${{ secrets.AWS_ACCOUNT_ID }}:role/${{ secrets.AWS_ROLE_NAME }}
|
role-to-assume: arn:aws:iam::${{ secrets.AWS_ACCOUNT_ID }}:role/${{ secrets.AWS_ROLE_NAME }}
|
||||||
role-duration-seconds: 3600
|
role-duration-seconds: 3600
|
||||||
|
|||||||
6
.github/workflows/linting.yml
vendored
6
.github/workflows/linting.yml
vendored
@@ -21,12 +21,12 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
|
|
||||||
- name: Set up Node.js
|
- name: Set up Node.js
|
||||||
uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0
|
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
|
||||||
with:
|
with:
|
||||||
node-version: '22'
|
node-version: '24'
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: npm ci
|
run: npm ci
|
||||||
|
|||||||
4
.github/workflows/mirror.yaml
vendored
4
.github/workflows/mirror.yaml
vendored
@@ -23,7 +23,7 @@ jobs:
|
|||||||
skopeo --version
|
skopeo --version
|
||||||
|
|
||||||
- name: Install cosign
|
- name: Install cosign
|
||||||
uses: sigstore/cosign-installer@faadad0cce49287aee09b3a48701e75088a2c6ad # v4.0.0
|
uses: sigstore/cosign-installer@cad07c2e89fa2edd6e2d7bab4c1aa38e53f76003 # v4.1.1
|
||||||
|
|
||||||
- name: Input check
|
- name: Input check
|
||||||
run: |
|
run: |
|
||||||
@@ -45,7 +45,7 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
skopeo list-tags --retry-times 3 docker://"${SOURCE_IMAGE}" \
|
skopeo list-tags --retry-times 3 docker://"${SOURCE_IMAGE}" \
|
||||||
| jq -r '.Tags[]' | sort -u > src-tags.txt
|
| jq -r '.Tags[]' | grep -v -e '-arm64' -e '-amd64' | sort -u > src-tags.txt
|
||||||
echo "Found source tags: $(wc -l < src-tags.txt)"
|
echo "Found source tags: $(wc -l < src-tags.txt)"
|
||||||
head -n 20 src-tags.txt || true
|
head -n 20 src-tags.txt || true
|
||||||
|
|
||||||
|
|||||||
2
.github/workflows/restart-runners.yml
vendored
2
.github/workflows/restart-runners.yml
vendored
@@ -14,7 +14,7 @@ jobs:
|
|||||||
permissions: write-all
|
permissions: write-all
|
||||||
steps:
|
steps:
|
||||||
- name: Configure AWS credentials
|
- name: Configure AWS credentials
|
||||||
uses: aws-actions/configure-aws-credentials@v2
|
uses: aws-actions/configure-aws-credentials@v6
|
||||||
with:
|
with:
|
||||||
role-to-assume: arn:aws:iam::${{ secrets.AWS_ACCOUNT_ID }}:role/${{ secrets.AWS_ROLE_NAME }}
|
role-to-assume: arn:aws:iam::${{ secrets.AWS_ACCOUNT_ID }}:role/${{ secrets.AWS_ROLE_NAME }}
|
||||||
role-duration-seconds: 3600
|
role-duration-seconds: 3600
|
||||||
|
|||||||
160
.github/workflows/saas.yml
vendored
Normal file
160
.github/workflows/saas.yml
vendored
Normal file
@@ -0,0 +1,160 @@
|
|||||||
|
name: SAAS 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
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
tags:
|
||||||
|
- "[0-9]+.[0-9]+.[0-9]+-s.[0-9]+"
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: ${{ github.ref }}
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
pre-run:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
permissions: write-all
|
||||||
|
steps:
|
||||||
|
- name: Configure AWS credentials
|
||||||
|
uses: aws-actions/configure-aws-credentials@v6
|
||||||
|
with:
|
||||||
|
role-to-assume: arn:aws:iam::${{ secrets.AWS_ACCOUNT_ID }}:role/${{ secrets.AWS_ROLE_NAME }}
|
||||||
|
role-duration-seconds: 3600
|
||||||
|
aws-region: ${{ secrets.AWS_REGION }}
|
||||||
|
|
||||||
|
- name: Verify AWS identity
|
||||||
|
run: aws sts get-caller-identity
|
||||||
|
|
||||||
|
- name: Start EC2 instances
|
||||||
|
run: |
|
||||||
|
aws ec2 start-instances --instance-ids ${{ secrets.EC2_INSTANCE_ID_ARM_RUNNER }}
|
||||||
|
echo "EC2 instances started"
|
||||||
|
|
||||||
|
|
||||||
|
release-arm:
|
||||||
|
name: Build and Release (ARM64)
|
||||||
|
runs-on: [self-hosted, linux, arm64, us-east-1]
|
||||||
|
needs: [pre-run]
|
||||||
|
if: >-
|
||||||
|
${{
|
||||||
|
needs.pre-run.result == 'success'
|
||||||
|
}}
|
||||||
|
# Job-level timeout to avoid runaway or stuck runs
|
||||||
|
timeout-minutes: 120
|
||||||
|
env:
|
||||||
|
# Target images
|
||||||
|
AWS_IMAGE: ${{ secrets.aws_account_id }}.dkr.ecr.us-east-1.amazonaws.com/${{ github.event.repository.name }}
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
|
|
||||||
|
- name: Download MaxMind GeoLite2 databases
|
||||||
|
env:
|
||||||
|
MAXMIND_LICENSE_KEY: ${{ secrets.MAXMIND_LICENSE_KEY }}
|
||||||
|
run: |
|
||||||
|
echo "Downloading MaxMind GeoLite2 databases..."
|
||||||
|
|
||||||
|
# Download GeoLite2-Country
|
||||||
|
curl -L "https://download.maxmind.com/app/geoip_download?edition_id=GeoLite2-Country&license_key=${MAXMIND_LICENSE_KEY}&suffix=tar.gz" \
|
||||||
|
-o GeoLite2-Country.tar.gz
|
||||||
|
|
||||||
|
# Download GeoLite2-ASN
|
||||||
|
curl -L "https://download.maxmind.com/app/geoip_download?edition_id=GeoLite2-ASN&license_key=${MAXMIND_LICENSE_KEY}&suffix=tar.gz" \
|
||||||
|
-o GeoLite2-ASN.tar.gz
|
||||||
|
|
||||||
|
# Extract the .mmdb files
|
||||||
|
tar -xzf GeoLite2-Country.tar.gz --strip-components=1 --wildcards '*.mmdb'
|
||||||
|
tar -xzf GeoLite2-ASN.tar.gz --strip-components=1 --wildcards '*.mmdb'
|
||||||
|
|
||||||
|
# Verify files exist
|
||||||
|
if [ ! -f "GeoLite2-Country.mmdb" ]; then
|
||||||
|
echo "ERROR: Failed to download GeoLite2-Country.mmdb"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ ! -f "GeoLite2-ASN.mmdb" ]; then
|
||||||
|
echo "ERROR: Failed to download GeoLite2-ASN.mmdb"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Clean up tar files
|
||||||
|
rm -f GeoLite2-Country.tar.gz GeoLite2-ASN.tar.gz
|
||||||
|
|
||||||
|
echo "MaxMind databases downloaded successfully"
|
||||||
|
ls -lh GeoLite2-*.mmdb
|
||||||
|
|
||||||
|
- name: Monitor storage space
|
||||||
|
run: |
|
||||||
|
THRESHOLD=75
|
||||||
|
USED_SPACE=$(df / | grep / | awk '{ print $5 }' | sed 's/%//g')
|
||||||
|
echo "Used space: $USED_SPACE%"
|
||||||
|
if [ "$USED_SPACE" -ge "$THRESHOLD" ]; then
|
||||||
|
echo "Used space is below the threshold of 75% free. Running Docker system prune."
|
||||||
|
echo y | docker system prune -a
|
||||||
|
else
|
||||||
|
echo "Storage space is above the threshold. No action needed."
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Configure AWS credentials
|
||||||
|
uses: aws-actions/configure-aws-credentials@v6
|
||||||
|
with:
|
||||||
|
role-to-assume: arn:aws:iam::${{ secrets.aws_account_id }}:role/${{ secrets.AWS_ROLE_NAME }}
|
||||||
|
role-duration-seconds: 3600
|
||||||
|
aws-region: ${{ secrets.AWS_REGION }}
|
||||||
|
|
||||||
|
- name: Login to Amazon ECR
|
||||||
|
id: login-ecr
|
||||||
|
uses: aws-actions/amazon-ecr-login@v2
|
||||||
|
|
||||||
|
- name: Extract tag name
|
||||||
|
id: get-tag
|
||||||
|
run: echo "TAG=${GITHUB_REF#refs/tags/}" >> $GITHUB_ENV
|
||||||
|
shell: bash
|
||||||
|
|
||||||
|
- name: Update version in package.json
|
||||||
|
run: |
|
||||||
|
TAG=${{ env.TAG }}
|
||||||
|
sed -i "s/export const APP_VERSION = \".*\";/export const APP_VERSION = \"$TAG\";/" server/lib/consts.ts
|
||||||
|
cat server/lib/consts.ts
|
||||||
|
shell: bash
|
||||||
|
|
||||||
|
- name: Build and push Docker images (Docker Hub - ARM64)
|
||||||
|
run: |
|
||||||
|
TAG=${{ env.TAG }}
|
||||||
|
make build-saas tag=$TAG
|
||||||
|
echo "Built & pushed ARM64 images to: ${{ env.AWS_IMAGE }}:${TAG}"
|
||||||
|
shell: bash
|
||||||
|
|
||||||
|
post-run:
|
||||||
|
needs: [pre-run, release-arm]
|
||||||
|
if: >-
|
||||||
|
${{
|
||||||
|
always() &&
|
||||||
|
needs.pre-run.result == 'success' &&
|
||||||
|
(needs.release-arm.result == 'success' || needs.release-arm.result == 'skipped' || needs.release-arm.result == 'failure')
|
||||||
|
}}
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
permissions: write-all
|
||||||
|
steps:
|
||||||
|
- name: Configure AWS credentials
|
||||||
|
uses: aws-actions/configure-aws-credentials@v6
|
||||||
|
with:
|
||||||
|
role-to-assume: arn:aws:iam::${{ secrets.AWS_ACCOUNT_ID }}:role/${{ secrets.AWS_ROLE_NAME }}
|
||||||
|
role-duration-seconds: 3600
|
||||||
|
aws-region: ${{ secrets.AWS_REGION }}
|
||||||
|
|
||||||
|
- name: Verify AWS identity
|
||||||
|
run: aws sts get-caller-identity
|
||||||
|
|
||||||
|
- name: Stop EC2 instances
|
||||||
|
run: |
|
||||||
|
aws ec2 stop-instances --instance-ids ${{ secrets.EC2_INSTANCE_ID_ARM_RUNNER }}
|
||||||
|
echo "EC2 instances stopped"
|
||||||
2
.github/workflows/stale-bot.yml
vendored
2
.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@997185467fa4f803885201cee163a9f38240193d # v10.1.1
|
- uses: actions/stale@b5d41d4e1d5dceea10e7104786b73624c18a190f # v10.2.0
|
||||||
with:
|
with:
|
||||||
days-before-stale: 14
|
days-before-stale: 14
|
||||||
days-before-close: 14
|
days-before-close: 14
|
||||||
|
|||||||
20
.github/workflows/test.yml
vendored
20
.github/workflows/test.yml
vendored
@@ -14,12 +14,12 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
|
|
||||||
- name: Install Node
|
- name: Install Node
|
||||||
uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0
|
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
|
||||||
with:
|
with:
|
||||||
node-version: '22'
|
node-version: '24'
|
||||||
|
|
||||||
- name: Copy config file
|
- name: Copy config file
|
||||||
run: cp config/config.example.yml config/config.yml
|
run: cp config/config.example.yml config/config.yml
|
||||||
@@ -34,10 +34,10 @@ jobs:
|
|||||||
run: npm run set:oss
|
run: npm run set:oss
|
||||||
|
|
||||||
- name: Generate database migrations
|
- name: Generate database migrations
|
||||||
run: npm run db:sqlite:generate
|
run: npm run db:generate
|
||||||
|
|
||||||
- name: Apply database migrations
|
- name: Apply database migrations
|
||||||
run: npm run db:sqlite:push
|
run: npm run db:push
|
||||||
|
|
||||||
- name: Test with tsc
|
- name: Test with tsc
|
||||||
run: npx tsc --noEmit
|
run: npx tsc --noEmit
|
||||||
@@ -62,10 +62,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
|
|
||||||
- name: Copy config file
|
|
||||||
run: cp config/config.example.yml config/config.yml
|
|
||||||
|
|
||||||
- name: Build Docker image sqlite
|
- name: Build Docker image sqlite
|
||||||
run: make dev-build-sqlite
|
run: make dev-build-sqlite
|
||||||
@@ -74,10 +71,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
|
|
||||||
- name: Copy config file
|
|
||||||
run: cp config/config.example.yml config/config.yml
|
|
||||||
|
|
||||||
- name: Build Docker image pg
|
- name: Build Docker image pg
|
||||||
run: make dev-build-pg
|
run: make dev-build-pg
|
||||||
|
|||||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -51,3 +51,6 @@ dynamic/
|
|||||||
scratch/
|
scratch/
|
||||||
tsconfig.json
|
tsconfig.json
|
||||||
hydrateSaas.ts
|
hydrateSaas.ts
|
||||||
|
CLAUDE.md
|
||||||
|
drizzle.config.ts
|
||||||
|
server/setup/migrations.ts
|
||||||
|
|||||||
2
.vscode/settings.json
vendored
2
.vscode/settings.json
vendored
@@ -4,7 +4,7 @@
|
|||||||
},
|
},
|
||||||
"editor.defaultFormatter": "esbenp.prettier-vscode",
|
"editor.defaultFormatter": "esbenp.prettier-vscode",
|
||||||
"[jsonc]": {
|
"[jsonc]": {
|
||||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
"editor.defaultFormatter": "vscode.json-language-features"
|
||||||
},
|
},
|
||||||
"[javascript]": {
|
"[javascript]": {
|
||||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||||
|
|||||||
109
Dockerfile
109
Dockerfile
@@ -1,72 +1,93 @@
|
|||||||
FROM node:24-alpine AS builder
|
# FROM node:24-slim AS base
|
||||||
|
FROM public.ecr.aws/docker/library/node:24-slim AS base
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
ARG BUILD=oss
|
RUN apt-get update && apt-get install -y python3 make g++ && rm -rf /var/lib/apt/lists/*
|
||||||
ARG DATABASE=sqlite
|
|
||||||
|
|
||||||
RUN apk add --no-cache curl tzdata python3 make g++
|
|
||||||
|
|
||||||
# COPY package.json package-lock.json ./
|
|
||||||
COPY package*.json ./
|
COPY package*.json ./
|
||||||
|
|
||||||
|
FROM base AS builder-dev
|
||||||
|
|
||||||
RUN npm ci
|
RUN npm ci
|
||||||
|
|
||||||
COPY . .
|
COPY . .
|
||||||
|
|
||||||
RUN echo "export * from \"./$DATABASE\";" > server/db/index.ts
|
ARG BUILD=oss
|
||||||
RUN echo "export const driver: \"pg\" | \"sqlite\" = \"$DATABASE\";" >> server/db/index.ts
|
ARG DATABASE=sqlite
|
||||||
|
|
||||||
RUN echo "export const build = \"$BUILD\" as \"saas\" | \"enterprise\" | \"oss\";" > server/build.ts
|
RUN if [ "$BUILD" = "oss" ]; then rm -rf server/private; fi && \
|
||||||
|
npm run set:$DATABASE && \
|
||||||
|
npm run set:$BUILD && \
|
||||||
|
npm run db:generate && \
|
||||||
|
npm run build && \
|
||||||
|
npm run build:cli && \
|
||||||
|
test -f dist/server.mjs
|
||||||
|
|
||||||
# Copy the appropriate TypeScript configuration based on build type
|
# Create placeholder files for MaxMind databases to avoid COPY errors
|
||||||
RUN if [ "$BUILD" = "oss" ]; then cp tsconfig.oss.json tsconfig.json; \
|
# Real files should be present for saas builds, placeholders for oss builds
|
||||||
elif [ "$BUILD" = "saas" ]; then cp tsconfig.saas.json tsconfig.json; \
|
RUN touch /app/GeoLite2-Country.mmdb /app/GeoLite2-ASN.mmdb
|
||||||
elif [ "$BUILD" = "enterprise" ]; then cp tsconfig.enterprise.json tsconfig.json; \
|
|
||||||
fi
|
|
||||||
|
|
||||||
# if the build is oss then remove the server/private directory
|
FROM base AS builder
|
||||||
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 npm ci --omit=dev
|
||||||
|
|
||||||
RUN mkdir -p dist
|
# FROM node:24-slim AS runner
|
||||||
RUN npm run next:build
|
FROM public.ecr.aws/docker/library/node:24-slim AS runner
|
||||||
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:cli
|
|
||||||
|
|
||||||
# Prune dev dependencies and clean up to prepare for copy to runner
|
|
||||||
RUN npm prune --omit=dev && npm cache clean --force
|
|
||||||
|
|
||||||
FROM node:24-alpine AS runner
|
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
# Only curl and tzdata needed at runtime - no build tools!
|
RUN apt-get update && apt-get install -y curl tzdata && rm -rf /var/lib/apt/lists/*
|
||||||
RUN apk add --no-cache curl tzdata
|
|
||||||
|
|
||||||
# Copy pre-built node_modules from builder (already pruned to production only)
|
|
||||||
# This includes the compiled native modules like better-sqlite3
|
|
||||||
COPY --from=builder /app/node_modules ./node_modules
|
COPY --from=builder /app/node_modules ./node_modules
|
||||||
|
|
||||||
COPY --from=builder /app/.next/standalone ./
|
|
||||||
COPY --from=builder /app/.next/static ./.next/static
|
|
||||||
COPY --from=builder /app/dist ./dist
|
|
||||||
COPY --from=builder /app/init ./dist/init
|
|
||||||
COPY --from=builder /app/package.json ./package.json
|
COPY --from=builder /app/package.json ./package.json
|
||||||
|
|
||||||
|
COPY --from=builder-dev /app/.next/standalone ./
|
||||||
|
COPY --from=builder-dev /app/.next/static ./.next/static
|
||||||
|
COPY --from=builder-dev /app/dist ./dist
|
||||||
|
COPY --from=builder-dev /app/server/migrations ./dist/init
|
||||||
|
|
||||||
COPY ./cli/wrapper.sh /usr/local/bin/pangctl
|
COPY ./cli/wrapper.sh /usr/local/bin/pangctl
|
||||||
RUN chmod +x /usr/local/bin/pangctl ./dist/cli.mjs
|
RUN chmod +x /usr/local/bin/pangctl ./dist/cli.mjs
|
||||||
|
|
||||||
COPY server/db/names.json ./dist/names.json
|
COPY server/db/names.json ./dist/names.json
|
||||||
|
COPY server/db/ios_models.json ./dist/ios_models.json
|
||||||
|
COPY server/db/mac_models.json ./dist/mac_models.json
|
||||||
COPY public ./public
|
COPY public ./public
|
||||||
|
|
||||||
|
# Copy MaxMind databases for SaaS builds
|
||||||
|
ARG BUILD=oss
|
||||||
|
|
||||||
|
RUN mkdir -p ./maxmind
|
||||||
|
|
||||||
|
# Copy MaxMind databases (placeholders exist for oss builds, real files for saas)
|
||||||
|
COPY --from=builder-dev /app/GeoLite2-Country.mmdb ./maxmind/GeoLite2-Country.mmdb
|
||||||
|
COPY --from=builder-dev /app/GeoLite2-ASN.mmdb ./maxmind/GeoLite2-ASN.mmdb
|
||||||
|
|
||||||
|
# Remove MaxMind databases for non-saas builds (keep only for saas)
|
||||||
|
RUN if [ "$BUILD" != "saas" ]; then rm -rf ./maxmind; fi
|
||||||
|
|
||||||
|
# OCI Image Labels - Build Args for dynamic values
|
||||||
|
ARG VERSION="dev"
|
||||||
|
ARG REVISION=""
|
||||||
|
ARG CREATED=""
|
||||||
|
ARG LICENSE="AGPL-3.0"
|
||||||
|
|
||||||
|
# Derive title and description based on BUILD type
|
||||||
|
ARG IMAGE_TITLE="Pangolin"
|
||||||
|
ARG IMAGE_DESCRIPTION="Identity-aware VPN and proxy for remote access to anything, anywhere"
|
||||||
|
|
||||||
|
# OCI Image Labels
|
||||||
|
# https://github.com/opencontainers/image-spec/blob/main/annotations.md
|
||||||
|
LABEL org.opencontainers.image.source="https://github.com/fosrl/pangolin" \
|
||||||
|
org.opencontainers.image.url="https://github.com/fosrl/pangolin" \
|
||||||
|
org.opencontainers.image.documentation="https://docs.pangolin.net" \
|
||||||
|
org.opencontainers.image.vendor="Fossorial" \
|
||||||
|
org.opencontainers.image.licenses="${LICENSE}" \
|
||||||
|
org.opencontainers.image.title="${IMAGE_TITLE}" \
|
||||||
|
org.opencontainers.image.description="${IMAGE_DESCRIPTION}" \
|
||||||
|
org.opencontainers.image.version="${VERSION}" \
|
||||||
|
org.opencontainers.image.revision="${REVISION}" \
|
||||||
|
org.opencontainers.image.created="${CREATED}"
|
||||||
|
|
||||||
CMD ["npm", "run", "start"]
|
CMD ["npm", "run", "start"]
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
FROM node:22-alpine
|
FROM node:24-alpine
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
|
RUN apk add --no-cache python3 make g++
|
||||||
|
|
||||||
COPY package*.json ./
|
COPY package*.json ./
|
||||||
|
|
||||||
# Install dependencies
|
# Install dependencies
|
||||||
|
|||||||
399
Makefile
399
Makefile
@@ -1,8 +1,27 @@
|
|||||||
.PHONY: build build-pg build-release build-release-arm build-release-amd build-arm build-x86 test clean
|
.PHONY: build build-pg build-release build-release-arm build-release-amd create-manifests build-arm build-x86 test clean
|
||||||
|
|
||||||
major_tag := $(shell echo $(tag) | cut -d. -f1)
|
major_tag := $(shell echo $(tag) | cut -d. -f1)
|
||||||
minor_tag := $(shell echo $(tag) | cut -d. -f1,2)
|
minor_tag := $(shell echo $(tag) | cut -d. -f1,2)
|
||||||
|
|
||||||
|
# OCI label variables
|
||||||
|
CREATED := $(shell date -u +"%Y-%m-%dT%H:%M:%SZ")
|
||||||
|
REVISION := $(shell git rev-parse HEAD 2>/dev/null || echo "unknown")
|
||||||
|
|
||||||
|
# Common OCI build args for OSS builds
|
||||||
|
OCI_ARGS_OSS = --build-arg VERSION=$(tag) \
|
||||||
|
--build-arg REVISION=$(REVISION) \
|
||||||
|
--build-arg CREATED=$(CREATED) \
|
||||||
|
--build-arg IMAGE_TITLE="Pangolin" \
|
||||||
|
--build-arg IMAGE_DESCRIPTION="Identity-aware VPN and proxy for remote access to anything, anywhere"
|
||||||
|
|
||||||
|
# Common OCI build args for Enterprise builds
|
||||||
|
OCI_ARGS_EE = --build-arg VERSION=$(tag) \
|
||||||
|
--build-arg REVISION=$(REVISION) \
|
||||||
|
--build-arg CREATED=$(CREATED) \
|
||||||
|
--build-arg LICENSE="Fossorial Commercial" \
|
||||||
|
--build-arg IMAGE_TITLE="Pangolin EE" \
|
||||||
|
--build-arg IMAGE_DESCRIPTION="Pangolin Enterprise Edition - Identity-aware VPN and proxy for remote access to anything, anywhere"
|
||||||
|
|
||||||
.PHONY: build-release build-sqlite build-postgresql build-ee-sqlite build-ee-postgresql
|
.PHONY: build-release build-sqlite build-postgresql build-ee-sqlite build-ee-postgresql
|
||||||
|
|
||||||
build-release: build-sqlite build-postgresql build-ee-sqlite build-ee-postgresql
|
build-release: build-sqlite build-postgresql build-ee-sqlite build-ee-postgresql
|
||||||
@@ -15,6 +34,7 @@ build-sqlite:
|
|||||||
docker buildx build \
|
docker buildx build \
|
||||||
--build-arg BUILD=oss \
|
--build-arg BUILD=oss \
|
||||||
--build-arg DATABASE=sqlite \
|
--build-arg DATABASE=sqlite \
|
||||||
|
$(OCI_ARGS_OSS) \
|
||||||
--platform linux/arm64,linux/amd64 \
|
--platform linux/arm64,linux/amd64 \
|
||||||
--tag fosrl/pangolin:latest \
|
--tag fosrl/pangolin:latest \
|
||||||
--tag fosrl/pangolin:$(major_tag) \
|
--tag fosrl/pangolin:$(major_tag) \
|
||||||
@@ -30,6 +50,7 @@ build-postgresql:
|
|||||||
docker buildx build \
|
docker buildx build \
|
||||||
--build-arg BUILD=oss \
|
--build-arg BUILD=oss \
|
||||||
--build-arg DATABASE=pg \
|
--build-arg DATABASE=pg \
|
||||||
|
$(OCI_ARGS_OSS) \
|
||||||
--platform linux/arm64,linux/amd64 \
|
--platform linux/arm64,linux/amd64 \
|
||||||
--tag fosrl/pangolin:postgresql-latest \
|
--tag fosrl/pangolin:postgresql-latest \
|
||||||
--tag fosrl/pangolin:postgresql-$(major_tag) \
|
--tag fosrl/pangolin:postgresql-$(major_tag) \
|
||||||
@@ -45,6 +66,7 @@ build-ee-sqlite:
|
|||||||
docker buildx build \
|
docker buildx build \
|
||||||
--build-arg BUILD=enterprise \
|
--build-arg BUILD=enterprise \
|
||||||
--build-arg DATABASE=sqlite \
|
--build-arg DATABASE=sqlite \
|
||||||
|
$(OCI_ARGS_EE) \
|
||||||
--platform linux/arm64,linux/amd64 \
|
--platform linux/arm64,linux/amd64 \
|
||||||
--tag fosrl/pangolin:ee-latest \
|
--tag fosrl/pangolin:ee-latest \
|
||||||
--tag fosrl/pangolin:ee-$(major_tag) \
|
--tag fosrl/pangolin:ee-$(major_tag) \
|
||||||
@@ -60,6 +82,7 @@ build-ee-postgresql:
|
|||||||
docker buildx build \
|
docker buildx build \
|
||||||
--build-arg BUILD=enterprise \
|
--build-arg BUILD=enterprise \
|
||||||
--build-arg DATABASE=pg \
|
--build-arg DATABASE=pg \
|
||||||
|
$(OCI_ARGS_EE) \
|
||||||
--platform linux/arm64,linux/amd64 \
|
--platform linux/arm64,linux/amd64 \
|
||||||
--tag fosrl/pangolin:ee-postgresql-latest \
|
--tag fosrl/pangolin:ee-postgresql-latest \
|
||||||
--tag fosrl/pangolin:ee-postgresql-$(major_tag) \
|
--tag fosrl/pangolin:ee-postgresql-$(major_tag) \
|
||||||
@@ -67,6 +90,18 @@ build-ee-postgresql:
|
|||||||
--tag fosrl/pangolin:ee-postgresql-$(tag) \
|
--tag fosrl/pangolin:ee-postgresql-$(tag) \
|
||||||
--push .
|
--push .
|
||||||
|
|
||||||
|
build-saas:
|
||||||
|
@if [ -z "$(tag)" ]; then \
|
||||||
|
echo "Error: tag is required. Usage: make build-release tag=<tag>"; \
|
||||||
|
exit 1; \
|
||||||
|
fi
|
||||||
|
docker buildx build \
|
||||||
|
--build-arg BUILD=saas \
|
||||||
|
--build-arg DATABASE=pg \
|
||||||
|
--platform linux/arm64 \
|
||||||
|
--tag $(AWS_IMAGE):$(tag) \
|
||||||
|
--push .
|
||||||
|
|
||||||
build-release-arm:
|
build-release-arm:
|
||||||
@if [ -z "$(tag)" ]; then \
|
@if [ -z "$(tag)" ]; then \
|
||||||
echo "Error: tag is required. Usage: make build-release-arm tag=<tag>"; \
|
echo "Error: tag is required. Usage: make build-release-arm tag=<tag>"; \
|
||||||
@@ -74,41 +109,65 @@ build-release-arm:
|
|||||||
fi
|
fi
|
||||||
@MAJOR_TAG=$$(echo $(tag) | cut -d. -f1); \
|
@MAJOR_TAG=$$(echo $(tag) | cut -d. -f1); \
|
||||||
MINOR_TAG=$$(echo $(tag) | cut -d. -f1,2); \
|
MINOR_TAG=$$(echo $(tag) | cut -d. -f1,2); \
|
||||||
|
CREATED=$$(date -u +"%Y-%m-%dT%H:%M:%SZ"); \
|
||||||
|
REVISION=$$(git rev-parse HEAD 2>/dev/null || echo "unknown"); \
|
||||||
docker buildx build \
|
docker buildx build \
|
||||||
--build-arg BUILD=oss \
|
--build-arg BUILD=oss \
|
||||||
--build-arg DATABASE=sqlite \
|
--build-arg DATABASE=sqlite \
|
||||||
|
--build-arg VERSION=$(tag) \
|
||||||
|
--build-arg REVISION=$$REVISION \
|
||||||
|
--build-arg CREATED=$$CREATED \
|
||||||
|
--build-arg IMAGE_TITLE="Pangolin" \
|
||||||
|
--build-arg IMAGE_DESCRIPTION="Identity-aware VPN and proxy for remote access to anything, anywhere" \
|
||||||
--platform linux/arm64 \
|
--platform linux/arm64 \
|
||||||
--tag fosrl/pangolin:latest \
|
--tag fosrl/pangolin:latest-arm64 \
|
||||||
--tag fosrl/pangolin:$$MAJOR_TAG \
|
--tag fosrl/pangolin:$$MAJOR_TAG-arm64 \
|
||||||
--tag fosrl/pangolin:$$MINOR_TAG \
|
--tag fosrl/pangolin:$$MINOR_TAG-arm64 \
|
||||||
--tag fosrl/pangolin:$(tag) \
|
--tag fosrl/pangolin:$(tag)-arm64 \
|
||||||
--push . && \
|
--push . && \
|
||||||
docker buildx build \
|
docker buildx build \
|
||||||
--build-arg BUILD=oss \
|
--build-arg BUILD=oss \
|
||||||
--build-arg DATABASE=pg \
|
--build-arg DATABASE=pg \
|
||||||
|
--build-arg VERSION=$(tag) \
|
||||||
|
--build-arg REVISION=$$REVISION \
|
||||||
|
--build-arg CREATED=$$CREATED \
|
||||||
|
--build-arg IMAGE_TITLE="Pangolin" \
|
||||||
|
--build-arg IMAGE_DESCRIPTION="Identity-aware VPN and proxy for remote access to anything, anywhere" \
|
||||||
--platform linux/arm64 \
|
--platform linux/arm64 \
|
||||||
--tag fosrl/pangolin:postgresql-latest \
|
--tag fosrl/pangolin:postgresql-latest-arm64 \
|
||||||
--tag fosrl/pangolin:postgresql-$$MAJOR_TAG \
|
--tag fosrl/pangolin:postgresql-$$MAJOR_TAG-arm64 \
|
||||||
--tag fosrl/pangolin:postgresql-$$MINOR_TAG \
|
--tag fosrl/pangolin:postgresql-$$MINOR_TAG-arm64 \
|
||||||
--tag fosrl/pangolin:postgresql-$(tag) \
|
--tag fosrl/pangolin:postgresql-$(tag)-arm64 \
|
||||||
--push . && \
|
--push . && \
|
||||||
docker buildx build \
|
docker buildx build \
|
||||||
--build-arg BUILD=enterprise \
|
--build-arg BUILD=enterprise \
|
||||||
--build-arg DATABASE=sqlite \
|
--build-arg DATABASE=sqlite \
|
||||||
|
--build-arg VERSION=$(tag) \
|
||||||
|
--build-arg REVISION=$$REVISION \
|
||||||
|
--build-arg CREATED=$$CREATED \
|
||||||
|
--build-arg LICENSE="Fossorial Commercial" \
|
||||||
|
--build-arg IMAGE_TITLE="Pangolin EE" \
|
||||||
|
--build-arg IMAGE_DESCRIPTION="Pangolin Enterprise Edition - Identity-aware VPN and proxy for remote access to anything, anywhere" \
|
||||||
--platform linux/arm64 \
|
--platform linux/arm64 \
|
||||||
--tag fosrl/pangolin:ee-latest \
|
--tag fosrl/pangolin:ee-latest-arm64 \
|
||||||
--tag fosrl/pangolin:ee-$$MAJOR_TAG \
|
--tag fosrl/pangolin:ee-$$MAJOR_TAG-arm64 \
|
||||||
--tag fosrl/pangolin:ee-$$MINOR_TAG \
|
--tag fosrl/pangolin:ee-$$MINOR_TAG-arm64 \
|
||||||
--tag fosrl/pangolin:ee-$(tag) \
|
--tag fosrl/pangolin:ee-$(tag)-arm64 \
|
||||||
--push . && \
|
--push . && \
|
||||||
docker buildx build \
|
docker buildx build \
|
||||||
--build-arg BUILD=enterprise \
|
--build-arg BUILD=enterprise \
|
||||||
--build-arg DATABASE=pg \
|
--build-arg DATABASE=pg \
|
||||||
|
--build-arg VERSION=$(tag) \
|
||||||
|
--build-arg REVISION=$$REVISION \
|
||||||
|
--build-arg CREATED=$$CREATED \
|
||||||
|
--build-arg LICENSE="Fossorial Commercial" \
|
||||||
|
--build-arg IMAGE_TITLE="Pangolin EE" \
|
||||||
|
--build-arg IMAGE_DESCRIPTION="Pangolin Enterprise Edition - Identity-aware VPN and proxy for remote access to anything, anywhere" \
|
||||||
--platform linux/arm64 \
|
--platform linux/arm64 \
|
||||||
--tag fosrl/pangolin:ee-postgresql-latest \
|
--tag fosrl/pangolin:ee-postgresql-latest-arm64 \
|
||||||
--tag fosrl/pangolin:ee-postgresql-$$MAJOR_TAG \
|
--tag fosrl/pangolin:ee-postgresql-$$MAJOR_TAG-arm64 \
|
||||||
--tag fosrl/pangolin:ee-postgresql-$$MINOR_TAG \
|
--tag fosrl/pangolin:ee-postgresql-$$MINOR_TAG-arm64 \
|
||||||
--tag fosrl/pangolin:ee-postgresql-$(tag) \
|
--tag fosrl/pangolin:ee-postgresql-$(tag)-arm64 \
|
||||||
--push .
|
--push .
|
||||||
|
|
||||||
build-release-amd:
|
build-release-amd:
|
||||||
@@ -118,84 +177,344 @@ build-release-amd:
|
|||||||
fi
|
fi
|
||||||
@MAJOR_TAG=$$(echo $(tag) | cut -d. -f1); \
|
@MAJOR_TAG=$$(echo $(tag) | cut -d. -f1); \
|
||||||
MINOR_TAG=$$(echo $(tag) | cut -d. -f1,2); \
|
MINOR_TAG=$$(echo $(tag) | cut -d. -f1,2); \
|
||||||
|
CREATED=$$(date -u +"%Y-%m-%dT%H:%M:%SZ"); \
|
||||||
|
REVISION=$$(git rev-parse HEAD 2>/dev/null || echo "unknown"); \
|
||||||
docker buildx build \
|
docker buildx build \
|
||||||
--build-arg BUILD=oss \
|
--build-arg BUILD=oss \
|
||||||
--build-arg DATABASE=sqlite \
|
--build-arg DATABASE=sqlite \
|
||||||
|
--build-arg VERSION=$(tag) \
|
||||||
|
--build-arg REVISION=$$REVISION \
|
||||||
|
--build-arg CREATED=$$CREATED \
|
||||||
|
--build-arg IMAGE_TITLE="Pangolin" \
|
||||||
|
--build-arg IMAGE_DESCRIPTION="Identity-aware VPN and proxy for remote access to anything, anywhere" \
|
||||||
--platform linux/amd64 \
|
--platform linux/amd64 \
|
||||||
|
--tag fosrl/pangolin:latest-amd64 \
|
||||||
|
--tag fosrl/pangolin:$$MAJOR_TAG-amd64 \
|
||||||
|
--tag fosrl/pangolin:$$MINOR_TAG-amd64 \
|
||||||
|
--tag fosrl/pangolin:$(tag)-amd64 \
|
||||||
|
--push . && \
|
||||||
|
docker buildx build \
|
||||||
|
--build-arg BUILD=oss \
|
||||||
|
--build-arg DATABASE=pg \
|
||||||
|
--build-arg VERSION=$(tag) \
|
||||||
|
--build-arg REVISION=$$REVISION \
|
||||||
|
--build-arg CREATED=$$CREATED \
|
||||||
|
--build-arg IMAGE_TITLE="Pangolin" \
|
||||||
|
--build-arg IMAGE_DESCRIPTION="Identity-aware VPN and proxy for remote access to anything, anywhere" \
|
||||||
|
--platform linux/amd64 \
|
||||||
|
--tag fosrl/pangolin:postgresql-latest-amd64 \
|
||||||
|
--tag fosrl/pangolin:postgresql-$$MAJOR_TAG-amd64 \
|
||||||
|
--tag fosrl/pangolin:postgresql-$$MINOR_TAG-amd64 \
|
||||||
|
--tag fosrl/pangolin:postgresql-$(tag)-amd64 \
|
||||||
|
--push . && \
|
||||||
|
docker buildx build \
|
||||||
|
--build-arg BUILD=enterprise \
|
||||||
|
--build-arg DATABASE=sqlite \
|
||||||
|
--build-arg VERSION=$(tag) \
|
||||||
|
--build-arg REVISION=$$REVISION \
|
||||||
|
--build-arg CREATED=$$CREATED \
|
||||||
|
--build-arg LICENSE="Fossorial Commercial" \
|
||||||
|
--build-arg IMAGE_TITLE="Pangolin EE" \
|
||||||
|
--build-arg IMAGE_DESCRIPTION="Pangolin Enterprise Edition - Identity-aware VPN and proxy for remote access to anything, anywhere" \
|
||||||
|
--platform linux/amd64 \
|
||||||
|
--tag fosrl/pangolin:ee-latest-amd64 \
|
||||||
|
--tag fosrl/pangolin:ee-$$MAJOR_TAG-amd64 \
|
||||||
|
--tag fosrl/pangolin:ee-$$MINOR_TAG-amd64 \
|
||||||
|
--tag fosrl/pangolin:ee-$(tag)-amd64 \
|
||||||
|
--push . && \
|
||||||
|
docker buildx build \
|
||||||
|
--build-arg BUILD=enterprise \
|
||||||
|
--build-arg DATABASE=pg \
|
||||||
|
--build-arg VERSION=$(tag) \
|
||||||
|
--build-arg REVISION=$$REVISION \
|
||||||
|
--build-arg CREATED=$$CREATED \
|
||||||
|
--build-arg LICENSE="Fossorial Commercial" \
|
||||||
|
--build-arg IMAGE_TITLE="Pangolin EE" \
|
||||||
|
--build-arg IMAGE_DESCRIPTION="Pangolin Enterprise Edition - Identity-aware VPN and proxy for remote access to anything, anywhere" \
|
||||||
|
--platform linux/amd64 \
|
||||||
|
--tag fosrl/pangolin:ee-postgresql-latest-amd64 \
|
||||||
|
--tag fosrl/pangolin:ee-postgresql-$$MAJOR_TAG-amd64 \
|
||||||
|
--tag fosrl/pangolin:ee-postgresql-$$MINOR_TAG-amd64 \
|
||||||
|
--tag fosrl/pangolin:ee-postgresql-$(tag)-amd64 \
|
||||||
|
--push .
|
||||||
|
|
||||||
|
create-manifests:
|
||||||
|
@if [ -z "$(tag)" ]; then \
|
||||||
|
echo "Error: tag is required. Usage: make create-manifests tag=<tag>"; \
|
||||||
|
exit 1; \
|
||||||
|
fi
|
||||||
|
@MAJOR_TAG=$$(echo $(tag) | cut -d. -f1); \
|
||||||
|
MINOR_TAG=$$(echo $(tag) | cut -d. -f1,2); \
|
||||||
|
echo "Creating multi-arch manifests for sqlite (oss)..." && \
|
||||||
|
docker buildx imagetools create \
|
||||||
--tag fosrl/pangolin:latest \
|
--tag fosrl/pangolin:latest \
|
||||||
--tag fosrl/pangolin:$$MAJOR_TAG \
|
--tag fosrl/pangolin:$$MAJOR_TAG \
|
||||||
--tag fosrl/pangolin:$$MINOR_TAG \
|
--tag fosrl/pangolin:$$MINOR_TAG \
|
||||||
--tag fosrl/pangolin:$(tag) \
|
--tag fosrl/pangolin:$(tag) \
|
||||||
--push . && \
|
fosrl/pangolin:latest-arm64 \
|
||||||
docker buildx build \
|
fosrl/pangolin:latest-amd64 && \
|
||||||
--build-arg BUILD=oss \
|
echo "Creating multi-arch manifests for postgresql (oss)..." && \
|
||||||
--build-arg DATABASE=pg \
|
docker buildx imagetools create \
|
||||||
--platform linux/amd64 \
|
|
||||||
--tag fosrl/pangolin:postgresql-latest \
|
--tag fosrl/pangolin:postgresql-latest \
|
||||||
--tag fosrl/pangolin:postgresql-$$MAJOR_TAG \
|
--tag fosrl/pangolin:postgresql-$$MAJOR_TAG \
|
||||||
--tag fosrl/pangolin:postgresql-$$MINOR_TAG \
|
--tag fosrl/pangolin:postgresql-$$MINOR_TAG \
|
||||||
--tag fosrl/pangolin:postgresql-$(tag) \
|
--tag fosrl/pangolin:postgresql-$(tag) \
|
||||||
--push . && \
|
fosrl/pangolin:postgresql-latest-arm64 \
|
||||||
docker buildx build \
|
fosrl/pangolin:postgresql-latest-amd64 && \
|
||||||
--build-arg BUILD=enterprise \
|
echo "Creating multi-arch manifests for sqlite (enterprise)..." && \
|
||||||
--build-arg DATABASE=sqlite \
|
docker buildx imagetools create \
|
||||||
--platform linux/amd64 \
|
|
||||||
--tag fosrl/pangolin:ee-latest \
|
--tag fosrl/pangolin:ee-latest \
|
||||||
--tag fosrl/pangolin:ee-$$MAJOR_TAG \
|
--tag fosrl/pangolin:ee-$$MAJOR_TAG \
|
||||||
--tag fosrl/pangolin:ee-$$MINOR_TAG \
|
--tag fosrl/pangolin:ee-$$MINOR_TAG \
|
||||||
--tag fosrl/pangolin:ee-$(tag) \
|
--tag fosrl/pangolin:ee-$(tag) \
|
||||||
--push . && \
|
fosrl/pangolin:ee-latest-arm64 \
|
||||||
docker buildx build \
|
fosrl/pangolin:ee-latest-amd64 && \
|
||||||
--build-arg BUILD=enterprise \
|
echo "Creating multi-arch manifests for postgresql (enterprise)..." && \
|
||||||
--build-arg DATABASE=pg \
|
docker buildx imagetools create \
|
||||||
--platform linux/amd64 \
|
|
||||||
--tag fosrl/pangolin:ee-postgresql-latest \
|
--tag fosrl/pangolin:ee-postgresql-latest \
|
||||||
--tag fosrl/pangolin:ee-postgresql-$$MAJOR_TAG \
|
--tag fosrl/pangolin:ee-postgresql-$$MAJOR_TAG \
|
||||||
--tag fosrl/pangolin:ee-postgresql-$$MINOR_TAG \
|
--tag fosrl/pangolin:ee-postgresql-$$MINOR_TAG \
|
||||||
--tag fosrl/pangolin:ee-postgresql-$(tag) \
|
--tag fosrl/pangolin:ee-postgresql-$(tag) \
|
||||||
--push .
|
fosrl/pangolin:ee-postgresql-latest-arm64 \
|
||||||
|
fosrl/pangolin:ee-postgresql-latest-amd64 && \
|
||||||
|
echo "All multi-arch manifests created successfully!"
|
||||||
|
|
||||||
build-rc:
|
build-rc:
|
||||||
@if [ -z "$(tag)" ]; then \
|
@if [ -z "$(tag)" ]; then \
|
||||||
echo "Error: tag is required. Usage: make build-release tag=<tag>"; \
|
echo "Error: tag is required. Usage: make build-release tag=<tag>"; \
|
||||||
exit 1; \
|
exit 1; \
|
||||||
fi
|
fi
|
||||||
|
@CREATED=$$(date -u +"%Y-%m-%dT%H:%M:%SZ"); \
|
||||||
|
REVISION=$$(git rev-parse HEAD 2>/dev/null || echo "unknown"); \
|
||||||
docker buildx build \
|
docker buildx build \
|
||||||
--build-arg BUILD=oss \
|
--build-arg BUILD=oss \
|
||||||
--build-arg DATABASE=sqlite \
|
--build-arg DATABASE=sqlite \
|
||||||
|
--build-arg VERSION=$(tag) \
|
||||||
|
--build-arg REVISION=$$REVISION \
|
||||||
|
--build-arg CREATED=$$CREATED \
|
||||||
|
--build-arg IMAGE_TITLE="Pangolin" \
|
||||||
|
--build-arg IMAGE_DESCRIPTION="Identity-aware VPN and proxy for remote access to anything, anywhere" \
|
||||||
--platform linux/arm64,linux/amd64 \
|
--platform linux/arm64,linux/amd64 \
|
||||||
--tag fosrl/pangolin:$(tag) \
|
--tag fosrl/pangolin:$(tag) \
|
||||||
--push .
|
--push . && \
|
||||||
docker buildx build \
|
docker buildx build \
|
||||||
--build-arg BUILD=oss \
|
--build-arg BUILD=oss \
|
||||||
--build-arg DATABASE=pg \
|
--build-arg DATABASE=pg \
|
||||||
|
--build-arg VERSION=$(tag) \
|
||||||
|
--build-arg REVISION=$$REVISION \
|
||||||
|
--build-arg CREATED=$$CREATED \
|
||||||
|
--build-arg IMAGE_TITLE="Pangolin" \
|
||||||
|
--build-arg IMAGE_DESCRIPTION="Identity-aware VPN and proxy for remote access to anything, anywhere" \
|
||||||
--platform linux/arm64,linux/amd64 \
|
--platform linux/arm64,linux/amd64 \
|
||||||
--tag fosrl/pangolin:postgresql-$(tag) \
|
--tag fosrl/pangolin:postgresql-$(tag) \
|
||||||
--push .
|
--push . && \
|
||||||
docker buildx build \
|
docker buildx build \
|
||||||
--build-arg BUILD=enterprise \
|
--build-arg BUILD=enterprise \
|
||||||
--build-arg DATABASE=sqlite \
|
--build-arg DATABASE=sqlite \
|
||||||
|
--build-arg VERSION=$(tag) \
|
||||||
|
--build-arg REVISION=$$REVISION \
|
||||||
|
--build-arg CREATED=$$CREATED \
|
||||||
|
--build-arg LICENSE="Fossorial Commercial" \
|
||||||
|
--build-arg IMAGE_TITLE="Pangolin EE" \
|
||||||
|
--build-arg IMAGE_DESCRIPTION="Pangolin Enterprise Edition - Identity-aware VPN and proxy for remote access to anything, anywhere" \
|
||||||
--platform linux/arm64,linux/amd64 \
|
--platform linux/arm64,linux/amd64 \
|
||||||
--tag fosrl/pangolin:ee-$(tag) \
|
--tag fosrl/pangolin:ee-$(tag) \
|
||||||
--push .
|
--push . && \
|
||||||
docker buildx build \
|
docker buildx build \
|
||||||
--build-arg BUILD=enterprise \
|
--build-arg BUILD=enterprise \
|
||||||
--build-arg DATABASE=pg \
|
--build-arg DATABASE=pg \
|
||||||
|
--build-arg VERSION=$(tag) \
|
||||||
|
--build-arg REVISION=$$REVISION \
|
||||||
|
--build-arg CREATED=$$CREATED \
|
||||||
|
--build-arg LICENSE="Fossorial Commercial" \
|
||||||
|
--build-arg IMAGE_TITLE="Pangolin EE" \
|
||||||
|
--build-arg IMAGE_DESCRIPTION="Pangolin Enterprise Edition - Identity-aware VPN and proxy for remote access to anything, anywhere" \
|
||||||
--platform linux/arm64,linux/amd64 \
|
--platform linux/arm64,linux/amd64 \
|
||||||
--tag fosrl/pangolin:ee-postgresql-$(tag) \
|
--tag fosrl/pangolin:ee-postgresql-$(tag) \
|
||||||
--push .
|
--push .
|
||||||
|
|
||||||
|
build-rc-arm:
|
||||||
|
@if [ -z "$(tag)" ]; then \
|
||||||
|
echo "Error: tag is required. Usage: make build-rc-arm tag=<tag>"; \
|
||||||
|
exit 1; \
|
||||||
|
fi
|
||||||
|
@CREATED=$$(date -u +"%Y-%m-%dT%H:%M:%SZ"); \
|
||||||
|
REVISION=$$(git rev-parse HEAD 2>/dev/null || echo "unknown"); \
|
||||||
|
docker buildx build \
|
||||||
|
--build-arg BUILD=oss \
|
||||||
|
--build-arg DATABASE=sqlite \
|
||||||
|
--build-arg VERSION=$(tag) \
|
||||||
|
--build-arg REVISION=$$REVISION \
|
||||||
|
--build-arg CREATED=$$CREATED \
|
||||||
|
--build-arg IMAGE_TITLE="Pangolin" \
|
||||||
|
--build-arg IMAGE_DESCRIPTION="Identity-aware VPN and proxy for remote access to anything, anywhere" \
|
||||||
|
--platform linux/arm64 \
|
||||||
|
--tag fosrl/pangolin:$(tag)-arm64 \
|
||||||
|
--push . && \
|
||||||
|
docker buildx build \
|
||||||
|
--build-arg BUILD=oss \
|
||||||
|
--build-arg DATABASE=pg \
|
||||||
|
--build-arg VERSION=$(tag) \
|
||||||
|
--build-arg REVISION=$$REVISION \
|
||||||
|
--build-arg CREATED=$$CREATED \
|
||||||
|
--build-arg IMAGE_TITLE="Pangolin" \
|
||||||
|
--build-arg IMAGE_DESCRIPTION="Identity-aware VPN and proxy for remote access to anything, anywhere" \
|
||||||
|
--platform linux/arm64 \
|
||||||
|
--tag fosrl/pangolin:postgresql-$(tag)-arm64 \
|
||||||
|
--push . && \
|
||||||
|
docker buildx build \
|
||||||
|
--build-arg BUILD=enterprise \
|
||||||
|
--build-arg DATABASE=sqlite \
|
||||||
|
--build-arg VERSION=$(tag) \
|
||||||
|
--build-arg REVISION=$$REVISION \
|
||||||
|
--build-arg CREATED=$$CREATED \
|
||||||
|
--build-arg LICENSE="Fossorial Commercial" \
|
||||||
|
--build-arg IMAGE_TITLE="Pangolin EE" \
|
||||||
|
--build-arg IMAGE_DESCRIPTION="Pangolin Enterprise Edition - Identity-aware VPN and proxy for remote access to anything, anywhere" \
|
||||||
|
--platform linux/arm64 \
|
||||||
|
--tag fosrl/pangolin:ee-$(tag)-arm64 \
|
||||||
|
--push . && \
|
||||||
|
docker buildx build \
|
||||||
|
--build-arg BUILD=enterprise \
|
||||||
|
--build-arg DATABASE=pg \
|
||||||
|
--build-arg VERSION=$(tag) \
|
||||||
|
--build-arg REVISION=$$REVISION \
|
||||||
|
--build-arg CREATED=$$CREATED \
|
||||||
|
--build-arg LICENSE="Fossorial Commercial" \
|
||||||
|
--build-arg IMAGE_TITLE="Pangolin EE" \
|
||||||
|
--build-arg IMAGE_DESCRIPTION="Pangolin Enterprise Edition - Identity-aware VPN and proxy for remote access to anything, anywhere" \
|
||||||
|
--platform linux/arm64 \
|
||||||
|
--tag fosrl/pangolin:ee-postgresql-$(tag)-arm64 \
|
||||||
|
--push .
|
||||||
|
|
||||||
|
build-rc-amd:
|
||||||
|
@if [ -z "$(tag)" ]; then \
|
||||||
|
echo "Error: tag is required. Usage: make build-rc-amd tag=<tag>"; \
|
||||||
|
exit 1; \
|
||||||
|
fi
|
||||||
|
@CREATED=$$(date -u +"%Y-%m-%dT%H:%M:%SZ"); \
|
||||||
|
REVISION=$$(git rev-parse HEAD 2>/dev/null || echo "unknown"); \
|
||||||
|
docker buildx build \
|
||||||
|
--build-arg BUILD=oss \
|
||||||
|
--build-arg DATABASE=sqlite \
|
||||||
|
--build-arg VERSION=$(tag) \
|
||||||
|
--build-arg REVISION=$$REVISION \
|
||||||
|
--build-arg CREATED=$$CREATED \
|
||||||
|
--build-arg IMAGE_TITLE="Pangolin" \
|
||||||
|
--build-arg IMAGE_DESCRIPTION="Identity-aware VPN and proxy for remote access to anything, anywhere" \
|
||||||
|
--platform linux/amd64 \
|
||||||
|
--tag fosrl/pangolin:$(tag)-amd64 \
|
||||||
|
--push . && \
|
||||||
|
docker buildx build \
|
||||||
|
--build-arg BUILD=oss \
|
||||||
|
--build-arg DATABASE=pg \
|
||||||
|
--build-arg VERSION=$(tag) \
|
||||||
|
--build-arg REVISION=$$REVISION \
|
||||||
|
--build-arg CREATED=$$CREATED \
|
||||||
|
--build-arg IMAGE_TITLE="Pangolin" \
|
||||||
|
--build-arg IMAGE_DESCRIPTION="Identity-aware VPN and proxy for remote access to anything, anywhere" \
|
||||||
|
--platform linux/amd64 \
|
||||||
|
--tag fosrl/pangolin:postgresql-$(tag)-amd64 \
|
||||||
|
--push . && \
|
||||||
|
docker buildx build \
|
||||||
|
--build-arg BUILD=enterprise \
|
||||||
|
--build-arg DATABASE=sqlite \
|
||||||
|
--build-arg VERSION=$(tag) \
|
||||||
|
--build-arg REVISION=$$REVISION \
|
||||||
|
--build-arg CREATED=$$CREATED \
|
||||||
|
--build-arg LICENSE="Fossorial Commercial" \
|
||||||
|
--build-arg IMAGE_TITLE="Pangolin EE" \
|
||||||
|
--build-arg IMAGE_DESCRIPTION="Pangolin Enterprise Edition - Identity-aware VPN and proxy for remote access to anything, anywhere" \
|
||||||
|
--platform linux/amd64 \
|
||||||
|
--tag fosrl/pangolin:ee-$(tag)-amd64 \
|
||||||
|
--push . && \
|
||||||
|
docker buildx build \
|
||||||
|
--build-arg BUILD=enterprise \
|
||||||
|
--build-arg DATABASE=pg \
|
||||||
|
--build-arg VERSION=$(tag) \
|
||||||
|
--build-arg REVISION=$$REVISION \
|
||||||
|
--build-arg CREATED=$$CREATED \
|
||||||
|
--build-arg LICENSE="Fossorial Commercial" \
|
||||||
|
--build-arg IMAGE_TITLE="Pangolin EE" \
|
||||||
|
--build-arg IMAGE_DESCRIPTION="Pangolin Enterprise Edition - Identity-aware VPN and proxy for remote access to anything, anywhere" \
|
||||||
|
--platform linux/amd64 \
|
||||||
|
--tag fosrl/pangolin:ee-postgresql-$(tag)-amd64 \
|
||||||
|
--push .
|
||||||
|
|
||||||
|
create-manifests-rc:
|
||||||
|
@if [ -z "$(tag)" ]; then \
|
||||||
|
echo "Error: tag is required. Usage: make create-manifests-rc tag=<tag>"; \
|
||||||
|
exit 1; \
|
||||||
|
fi
|
||||||
|
@echo "Creating multi-arch manifests for RC sqlite (oss)..." && \
|
||||||
|
docker buildx imagetools create \
|
||||||
|
--tag fosrl/pangolin:$(tag) \
|
||||||
|
fosrl/pangolin:$(tag)-arm64 \
|
||||||
|
fosrl/pangolin:$(tag)-amd64 && \
|
||||||
|
echo "Creating multi-arch manifests for RC postgresql (oss)..." && \
|
||||||
|
docker buildx imagetools create \
|
||||||
|
--tag fosrl/pangolin:postgresql-$(tag) \
|
||||||
|
fosrl/pangolin:postgresql-$(tag)-arm64 \
|
||||||
|
fosrl/pangolin:postgresql-$(tag)-amd64 && \
|
||||||
|
echo "Creating multi-arch manifests for RC sqlite (enterprise)..." && \
|
||||||
|
docker buildx imagetools create \
|
||||||
|
--tag fosrl/pangolin:ee-$(tag) \
|
||||||
|
fosrl/pangolin:ee-$(tag)-arm64 \
|
||||||
|
fosrl/pangolin:ee-$(tag)-amd64 && \
|
||||||
|
echo "Creating multi-arch manifests for RC postgresql (enterprise)..." && \
|
||||||
|
docker buildx imagetools create \
|
||||||
|
--tag fosrl/pangolin:ee-postgresql-$(tag) \
|
||||||
|
fosrl/pangolin:ee-postgresql-$(tag)-arm64 \
|
||||||
|
fosrl/pangolin:ee-postgresql-$(tag)-amd64 && \
|
||||||
|
echo "All RC multi-arch manifests created successfully!"
|
||||||
|
|
||||||
build-arm:
|
build-arm:
|
||||||
docker buildx build --platform linux/arm64 -t fosrl/pangolin:latest .
|
@CREATED=$$(date -u +"%Y-%m-%dT%H:%M:%SZ"); \
|
||||||
|
REVISION=$$(git rev-parse HEAD 2>/dev/null || echo "unknown"); \
|
||||||
|
docker buildx build \
|
||||||
|
--build-arg VERSION=dev \
|
||||||
|
--build-arg REVISION=$$REVISION \
|
||||||
|
--build-arg CREATED=$$CREATED \
|
||||||
|
--build-arg IMAGE_TITLE="Pangolin" \
|
||||||
|
--build-arg IMAGE_DESCRIPTION="Identity-aware VPN and proxy for remote access to anything, anywhere" \
|
||||||
|
--platform linux/arm64 \
|
||||||
|
-t fosrl/pangolin:latest .
|
||||||
|
|
||||||
build-x86:
|
build-x86:
|
||||||
docker buildx build --platform linux/amd64 -t fosrl/pangolin:latest .
|
@CREATED=$$(date -u +"%Y-%m-%dT%H:%M:%SZ"); \
|
||||||
|
REVISION=$$(git rev-parse HEAD 2>/dev/null || echo "unknown"); \
|
||||||
|
docker buildx build \
|
||||||
|
--build-arg VERSION=dev \
|
||||||
|
--build-arg REVISION=$$REVISION \
|
||||||
|
--build-arg CREATED=$$CREATED \
|
||||||
|
--build-arg IMAGE_TITLE="Pangolin" \
|
||||||
|
--build-arg IMAGE_DESCRIPTION="Identity-aware VPN and proxy for remote access to anything, anywhere" \
|
||||||
|
--platform linux/amd64 \
|
||||||
|
-t fosrl/pangolin:latest .
|
||||||
|
|
||||||
dev-build-sqlite:
|
dev-build-sqlite:
|
||||||
docker build --build-arg DATABASE=sqlite -t fosrl/pangolin:latest .
|
@CREATED=$$(date -u +"%Y-%m-%dT%H:%M:%SZ"); \
|
||||||
|
REVISION=$$(git rev-parse HEAD 2>/dev/null || echo "unknown"); \
|
||||||
|
docker build \
|
||||||
|
--build-arg DATABASE=sqlite \
|
||||||
|
--build-arg VERSION=dev \
|
||||||
|
--build-arg REVISION=$$REVISION \
|
||||||
|
--build-arg CREATED=$$CREATED \
|
||||||
|
--build-arg IMAGE_TITLE="Pangolin" \
|
||||||
|
--build-arg IMAGE_DESCRIPTION="Identity-aware VPN and proxy for remote access to anything, anywhere" \
|
||||||
|
-t fosrl/pangolin:latest .
|
||||||
|
|
||||||
dev-build-pg:
|
dev-build-pg:
|
||||||
docker build --build-arg DATABASE=pg -t fosrl/pangolin:postgresql-latest .
|
@CREATED=$$(date -u +"%Y-%m-%dT%H:%M:%SZ"); \
|
||||||
|
REVISION=$$(git rev-parse HEAD 2>/dev/null || echo "unknown"); \
|
||||||
|
docker build \
|
||||||
|
--build-arg DATABASE=pg \
|
||||||
|
--build-arg VERSION=dev \
|
||||||
|
--build-arg REVISION=$$REVISION \
|
||||||
|
--build-arg CREATED=$$CREATED \
|
||||||
|
--build-arg IMAGE_TITLE="Pangolin" \
|
||||||
|
--build-arg IMAGE_DESCRIPTION="Identity-aware VPN and proxy for remote access to anything, anywhere" \
|
||||||
|
-t fosrl/pangolin:postgresql-latest .
|
||||||
|
|
||||||
test:
|
test:
|
||||||
docker run -it -p 3000:3000 -p 3001:3001 -p 3002:3002 -v ./config:/app/config fosrl/pangolin:latest
|
docker run -it -p 3000:3000 -p 3001:3001 -p 3002:3002 -v ./config:/app/config fosrl/pangolin:latest
|
||||||
|
|||||||
21
README.md
21
README.md
@@ -35,9 +35,15 @@
|
|||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<p align="center">
|
||||||
|
<a href="https://docs.pangolin.net/careers/join-us">
|
||||||
|
<img src="https://img.shields.io/badge/🚀_We're_Hiring!-Join_Our_Team-brightgreen?style=for-the-badge" alt="We're Hiring!" />
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
|
||||||
<p align="center">
|
<p align="center">
|
||||||
<strong>
|
<strong>
|
||||||
Start testing Pangolin at <a href="https://app.pangolin.net/auth/signup">app.pangolin.net</a>
|
Get started with Pangolin at <a href="https://app.pangolin.net/auth/signup">app.pangolin.net</a>
|
||||||
</strong>
|
</strong>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
@@ -54,9 +60,9 @@ Pangolin is an open-source, identity-based remote access platform built on WireG
|
|||||||
|
|
||||||
| <img width=500 /> | Description |
|
| <img width=500 /> | Description |
|
||||||
|-----------------|--------------|
|
|-----------------|--------------|
|
||||||
|
| **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/understanding-nodes) and connect to our control plane. |
|
||||||
| **Self-Host: Community Edition** | Free, open source, and licensed under AGPL-3. |
|
| **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. |
|
| **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. |
|
|
||||||
|
|
||||||
## Key Features
|
## Key Features
|
||||||
|
|
||||||
@@ -74,20 +80,21 @@ Download the Pangolin client for your platform:
|
|||||||
- [Mac](https://pangolin.net/downloads/mac)
|
- [Mac](https://pangolin.net/downloads/mac)
|
||||||
- [Windows](https://pangolin.net/downloads/windows)
|
- [Windows](https://pangolin.net/downloads/windows)
|
||||||
- [Linux](https://pangolin.net/downloads/linux)
|
- [Linux](https://pangolin.net/downloads/linux)
|
||||||
|
- [iOS](https://pangolin.net/downloads/ios)
|
||||||
|
- [Android](https://pangolin.net/downloads/android)
|
||||||
|
|
||||||
## Get Started
|
## Get Started
|
||||||
|
|
||||||
|
### Sign up now
|
||||||
|
|
||||||
|
Create an account at [app.pangolin.net](https://app.pangolin.net) to get started with Pangolin Cloud. A generous free tier is available.
|
||||||
|
|
||||||
### Check out the docs
|
### Check out the docs
|
||||||
|
|
||||||
We encourage everyone to read the full documentation first, which is
|
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
|
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.
|
the docs to illustrate some basic ideas.
|
||||||
|
|
||||||
### Sign up and try now
|
|
||||||
|
|
||||||
For Pangolin's managed service, you will first need to create an account at
|
|
||||||
[app.pangolin.net](https://app.pangolin.net). We have a generous free tier to get started.
|
|
||||||
|
|
||||||
## Licensing
|
## Licensing
|
||||||
|
|
||||||
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).
|
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).
|
||||||
|
|||||||
@@ -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@pangolin.net](mailto:security@pangolin.net) 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) with the following information:
|
||||||
|
|
||||||
- Description and location of the vulnerability.
|
- Description and location of the vulnerability.
|
||||||
- Potential impact of the vulnerability.
|
- Potential impact of the vulnerability.
|
||||||
|
|||||||
72
blueprint.py
72
blueprint.py
@@ -1,72 +0,0 @@
|
|||||||
import requests
|
|
||||||
import yaml
|
|
||||||
import json
|
|
||||||
import base64
|
|
||||||
|
|
||||||
# The file path for the YAML file to be read
|
|
||||||
# You can change this to the path of your YAML file
|
|
||||||
YAML_FILE_PATH = 'blueprint.yaml'
|
|
||||||
|
|
||||||
# The API endpoint and headers from the curl request
|
|
||||||
API_URL = 'http://api.pangolin.net/v1/org/test/blueprint'
|
|
||||||
HEADERS = {
|
|
||||||
'accept': '*/*',
|
|
||||||
'Authorization': 'Bearer <your_token_here>',
|
|
||||||
'Content-Type': 'application/json'
|
|
||||||
}
|
|
||||||
|
|
||||||
def convert_and_send(file_path, url, headers):
|
|
||||||
"""
|
|
||||||
Reads a YAML file, converts its content to a JSON payload,
|
|
||||||
and sends it via a PUT request to a specified URL.
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
# Read the YAML file content
|
|
||||||
with open(file_path, 'r') as file:
|
|
||||||
yaml_content = file.read()
|
|
||||||
|
|
||||||
# Parse the YAML string to a Python dictionary
|
|
||||||
# This will be used to ensure the YAML is valid before sending
|
|
||||||
parsed_yaml = yaml.safe_load(yaml_content)
|
|
||||||
|
|
||||||
# convert the parsed YAML to a JSON string
|
|
||||||
json_payload = json.dumps(parsed_yaml)
|
|
||||||
print("Converted JSON payload:")
|
|
||||||
print(json_payload)
|
|
||||||
|
|
||||||
# Encode the JSON string to Base64
|
|
||||||
encoded_json = base64.b64encode(json_payload.encode('utf-8')).decode('utf-8')
|
|
||||||
|
|
||||||
# Create the final payload with the base64 encoded data
|
|
||||||
final_payload = {
|
|
||||||
"blueprint": encoded_json
|
|
||||||
}
|
|
||||||
|
|
||||||
print("Sending the following Base64 encoded JSON payload:")
|
|
||||||
print(final_payload)
|
|
||||||
print("-" * 20)
|
|
||||||
|
|
||||||
# Make the PUT request with the base64 encoded payload
|
|
||||||
response = requests.put(url, headers=headers, json=final_payload)
|
|
||||||
|
|
||||||
# Print the API response for debugging
|
|
||||||
print(f"API Response Status Code: {response.status_code}")
|
|
||||||
print("API Response Content:")
|
|
||||||
print(response.text)
|
|
||||||
|
|
||||||
# Raise an exception for bad status codes (4xx or 5xx)
|
|
||||||
response.raise_for_status()
|
|
||||||
|
|
||||||
except FileNotFoundError:
|
|
||||||
print(f"Error: The file '{file_path}' was not found.")
|
|
||||||
except yaml.YAMLError as e:
|
|
||||||
print(f"Error parsing YAML file: {e}")
|
|
||||||
except requests.exceptions.RequestException as e:
|
|
||||||
print(f"An error occurred during the API request: {e}")
|
|
||||||
except Exception as e:
|
|
||||||
print(f"An unexpected error occurred: {e}")
|
|
||||||
|
|
||||||
# Run the function
|
|
||||||
if __name__ == "__main__":
|
|
||||||
convert_and_send(YAML_FILE_PATH, API_URL, HEADERS)
|
|
||||||
|
|
||||||
@@ -1,70 +0,0 @@
|
|||||||
client-resources:
|
|
||||||
client-resource-nice-id-uno:
|
|
||||||
name: this is my resource
|
|
||||||
protocol: tcp
|
|
||||||
proxy-port: 3001
|
|
||||||
hostname: localhost
|
|
||||||
internal-port: 3000
|
|
||||||
site: lively-yosemite-toad
|
|
||||||
client-resource-nice-id-duce:
|
|
||||||
name: this is my resource
|
|
||||||
protocol: udp
|
|
||||||
proxy-port: 3000
|
|
||||||
hostname: localhost
|
|
||||||
internal-port: 3000
|
|
||||||
site: lively-yosemite-toad
|
|
||||||
|
|
||||||
proxy-resources:
|
|
||||||
resource-nice-id-uno:
|
|
||||||
name: this is my resource
|
|
||||||
protocol: http
|
|
||||||
full-domain: duce.test.example.com
|
|
||||||
host-header: example.com
|
|
||||||
tls-server-name: example.com
|
|
||||||
# auth:
|
|
||||||
# pincode: 123456
|
|
||||||
# password: sadfasdfadsf
|
|
||||||
# sso-enabled: true
|
|
||||||
# sso-roles:
|
|
||||||
# - Member
|
|
||||||
# sso-users:
|
|
||||||
# - owen@pangolin.net
|
|
||||||
# whitelist-users:
|
|
||||||
# - owen@pangolin.net
|
|
||||||
# auto-login-idp: 1
|
|
||||||
headers:
|
|
||||||
- name: X-Example-Header
|
|
||||||
value: example-value
|
|
||||||
- name: X-Another-Header
|
|
||||||
value: another-value
|
|
||||||
rules:
|
|
||||||
- action: allow
|
|
||||||
match: ip
|
|
||||||
value: 1.1.1.1
|
|
||||||
- action: deny
|
|
||||||
match: cidr
|
|
||||||
value: 2.2.2.2/32
|
|
||||||
- action: pass
|
|
||||||
match: path
|
|
||||||
value: /admin
|
|
||||||
targets:
|
|
||||||
- site: lively-yosemite-toad
|
|
||||||
path: /path
|
|
||||||
pathMatchType: prefix
|
|
||||||
hostname: localhost
|
|
||||||
method: http
|
|
||||||
port: 8000
|
|
||||||
- site: slim-alpine-chipmunk
|
|
||||||
hostname: localhost
|
|
||||||
path: /yoman
|
|
||||||
pathMatchType: exact
|
|
||||||
method: http
|
|
||||||
port: 8001
|
|
||||||
resource-nice-id-duce:
|
|
||||||
name: this is other resource
|
|
||||||
protocol: tcp
|
|
||||||
proxy-port: 3000
|
|
||||||
targets:
|
|
||||||
- site: lively-yosemite-toad
|
|
||||||
hostname: localhost
|
|
||||||
port: 3000
|
|
||||||
36
cli/commands/clearLicenseKeys.ts
Normal file
36
cli/commands/clearLicenseKeys.ts
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
import { CommandModule } from "yargs";
|
||||||
|
import { db, licenseKey } from "@server/db";
|
||||||
|
import { eq } from "drizzle-orm";
|
||||||
|
|
||||||
|
type ClearLicenseKeysArgs = { };
|
||||||
|
|
||||||
|
export const clearLicenseKeys: CommandModule<
|
||||||
|
{},
|
||||||
|
ClearLicenseKeysArgs
|
||||||
|
> = {
|
||||||
|
command: "clear-license-keys",
|
||||||
|
describe:
|
||||||
|
"Clear all license keys from the database",
|
||||||
|
// no args
|
||||||
|
builder: (yargs) => {
|
||||||
|
return yargs;
|
||||||
|
},
|
||||||
|
handler: async (argv: {}) => {
|
||||||
|
try {
|
||||||
|
|
||||||
|
console.log(`Clearing all license keys from the database`);
|
||||||
|
|
||||||
|
// Delete all license keys
|
||||||
|
const deletedCount = await db
|
||||||
|
.delete(licenseKey)
|
||||||
|
.where(eq(licenseKey.licenseKeyId, licenseKey.licenseKeyId)) .returning();; // delete all
|
||||||
|
|
||||||
|
console.log(`Deleted ${deletedCount.length} license key(s) from the database`);
|
||||||
|
|
||||||
|
process.exit(0);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error:", error);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
123
cli/commands/deleteClient.ts
Normal file
123
cli/commands/deleteClient.ts
Normal file
@@ -0,0 +1,123 @@
|
|||||||
|
import { CommandModule } from "yargs";
|
||||||
|
import { db, clients, olms, currentFingerprint, userClients, approvals } from "@server/db";
|
||||||
|
import { eq, and, inArray } from "drizzle-orm";
|
||||||
|
|
||||||
|
type DeleteClientArgs = {
|
||||||
|
orgId: string;
|
||||||
|
niceId: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const deleteClient: CommandModule<{}, DeleteClientArgs> = {
|
||||||
|
command: "delete-client",
|
||||||
|
describe:
|
||||||
|
"Delete a client and all associated data (OLMs, current fingerprint, userClients, approvals). Snapshots are preserved.",
|
||||||
|
builder: (yargs) => {
|
||||||
|
return yargs
|
||||||
|
.option("orgId", {
|
||||||
|
type: "string",
|
||||||
|
demandOption: true,
|
||||||
|
describe: "The organization ID"
|
||||||
|
})
|
||||||
|
.option("niceId", {
|
||||||
|
type: "string",
|
||||||
|
demandOption: true,
|
||||||
|
describe: "The client niceId (identifier)"
|
||||||
|
});
|
||||||
|
},
|
||||||
|
handler: async (argv: { orgId: string; niceId: string }) => {
|
||||||
|
try {
|
||||||
|
const { orgId, niceId } = argv;
|
||||||
|
|
||||||
|
console.log(
|
||||||
|
`Deleting client with orgId: ${orgId}, niceId: ${niceId}...`
|
||||||
|
);
|
||||||
|
|
||||||
|
// Find the client
|
||||||
|
const [client] = await db
|
||||||
|
.select()
|
||||||
|
.from(clients)
|
||||||
|
.where(and(eq(clients.orgId, orgId), eq(clients.niceId, niceId)))
|
||||||
|
.limit(1);
|
||||||
|
|
||||||
|
if (!client) {
|
||||||
|
console.error(
|
||||||
|
`Error: Client with orgId "${orgId}" and niceId "${niceId}" not found.`
|
||||||
|
);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
const clientId = client.clientId;
|
||||||
|
console.log(`Found client with clientId: ${clientId}`);
|
||||||
|
|
||||||
|
// Find all OLMs associated with this client
|
||||||
|
const associatedOlms = await db
|
||||||
|
.select()
|
||||||
|
.from(olms)
|
||||||
|
.where(eq(olms.clientId, clientId));
|
||||||
|
|
||||||
|
console.log(`Found ${associatedOlms.length} OLM(s) associated with this client`);
|
||||||
|
|
||||||
|
// Delete in a transaction to ensure atomicity
|
||||||
|
await db.transaction(async (trx) => {
|
||||||
|
// Delete currentFingerprint entries for the associated OLMs
|
||||||
|
// Note: We delete these explicitly before deleting OLMs to ensure
|
||||||
|
// we have control, even though cascade would handle it
|
||||||
|
let fingerprintCount = 0;
|
||||||
|
if (associatedOlms.length > 0) {
|
||||||
|
const olmIds = associatedOlms.map((olm) => olm.olmId);
|
||||||
|
const deletedFingerprints = await trx
|
||||||
|
.delete(currentFingerprint)
|
||||||
|
.where(inArray(currentFingerprint.olmId, olmIds))
|
||||||
|
.returning();
|
||||||
|
fingerprintCount = deletedFingerprints.length;
|
||||||
|
}
|
||||||
|
console.log(`Deleted ${fingerprintCount} current fingerprint(s)`);
|
||||||
|
|
||||||
|
// Delete OLMs
|
||||||
|
// Note: OLMs have onDelete: "set null" for clientId, so we need to delete them explicitly
|
||||||
|
const deletedOlms = await trx
|
||||||
|
.delete(olms)
|
||||||
|
.where(eq(olms.clientId, clientId))
|
||||||
|
.returning();
|
||||||
|
console.log(`Deleted ${deletedOlms.length} OLM(s)`);
|
||||||
|
|
||||||
|
// Delete approvals
|
||||||
|
// Note: Approvals have onDelete: "cascade" but we delete explicitly for clarity
|
||||||
|
const deletedApprovals = await trx
|
||||||
|
.delete(approvals)
|
||||||
|
.where(eq(approvals.clientId, clientId))
|
||||||
|
.returning();
|
||||||
|
console.log(`Deleted ${deletedApprovals.length} approval(s)`);
|
||||||
|
|
||||||
|
// Delete userClients
|
||||||
|
// Note: userClients have onDelete: "cascade" but we delete explicitly for clarity
|
||||||
|
const deletedUserClients = await trx
|
||||||
|
.delete(userClients)
|
||||||
|
.where(eq(userClients.clientId, clientId))
|
||||||
|
.returning();
|
||||||
|
console.log(`Deleted ${deletedUserClients.length} userClient association(s)`);
|
||||||
|
|
||||||
|
// Finally, delete the client itself
|
||||||
|
const deletedClients = await trx
|
||||||
|
.delete(clients)
|
||||||
|
.where(eq(clients.clientId, clientId))
|
||||||
|
.returning();
|
||||||
|
console.log(`Deleted client: ${deletedClients[0]?.name || niceId}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log("\nClient deletion completed successfully!");
|
||||||
|
console.log("\nSummary:");
|
||||||
|
console.log(` - Client: ${niceId} (clientId: ${clientId})`);
|
||||||
|
console.log(` - Olm(s): ${associatedOlms.length}`);
|
||||||
|
console.log(` - Current fingerprints: deleted`);
|
||||||
|
console.log(` - Approvals: deleted`);
|
||||||
|
console.log(` - UserClients: deleted`);
|
||||||
|
console.log(` - Snapshots: preserved (not deleted)`);
|
||||||
|
|
||||||
|
process.exit(0);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error deleting client:", error);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
121
cli/commands/generateOrgCaKeys.ts
Normal file
121
cli/commands/generateOrgCaKeys.ts
Normal file
@@ -0,0 +1,121 @@
|
|||||||
|
import { CommandModule } from "yargs";
|
||||||
|
import { db, orgs } from "@server/db";
|
||||||
|
import { eq } from "drizzle-orm";
|
||||||
|
import { encrypt } from "@server/lib/crypto";
|
||||||
|
import { configFilePath1, configFilePath2 } from "@server/lib/consts";
|
||||||
|
import { generateCA } from "@server/lib/sshCA";
|
||||||
|
import fs from "fs";
|
||||||
|
import yaml from "js-yaml";
|
||||||
|
|
||||||
|
type GenerateOrgCaKeysArgs = {
|
||||||
|
orgId: string;
|
||||||
|
secret?: string;
|
||||||
|
force?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const generateOrgCaKeys: CommandModule<{}, GenerateOrgCaKeysArgs> = {
|
||||||
|
command: "generate-org-ca-keys",
|
||||||
|
describe:
|
||||||
|
"Generate SSH CA public/private key pair for an organization and store them in the database (private key encrypted with server secret)",
|
||||||
|
builder: (yargs) => {
|
||||||
|
return yargs
|
||||||
|
.option("orgId", {
|
||||||
|
type: "string",
|
||||||
|
demandOption: true,
|
||||||
|
describe: "The organization ID"
|
||||||
|
})
|
||||||
|
.option("secret", {
|
||||||
|
type: "string",
|
||||||
|
describe:
|
||||||
|
"Server secret used to encrypt the CA private key. If omitted, read from config file (config.yml or config.yaml)."
|
||||||
|
})
|
||||||
|
.option("force", {
|
||||||
|
type: "boolean",
|
||||||
|
default: false,
|
||||||
|
describe:
|
||||||
|
"Overwrite existing CA keys for the org if they already exist"
|
||||||
|
});
|
||||||
|
},
|
||||||
|
handler: async (argv: {
|
||||||
|
orgId: string;
|
||||||
|
secret?: string;
|
||||||
|
force?: boolean;
|
||||||
|
}) => {
|
||||||
|
try {
|
||||||
|
const { orgId, force } = argv;
|
||||||
|
let secret = argv.secret;
|
||||||
|
|
||||||
|
if (!secret) {
|
||||||
|
const configPath = fs.existsSync(configFilePath1)
|
||||||
|
? configFilePath1
|
||||||
|
: fs.existsSync(configFilePath2)
|
||||||
|
? configFilePath2
|
||||||
|
: null;
|
||||||
|
|
||||||
|
if (!configPath) {
|
||||||
|
console.error(
|
||||||
|
"Error: No server secret provided and config file not found. " +
|
||||||
|
"Expected config.yml or config.yaml in the config directory, or pass --secret."
|
||||||
|
);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
const configContent = fs.readFileSync(configPath, "utf8");
|
||||||
|
const config = yaml.load(configContent) as {
|
||||||
|
server?: { secret?: string };
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!config?.server?.secret) {
|
||||||
|
console.error(
|
||||||
|
"Error: No server.secret in config file. Pass --secret or set server.secret in config."
|
||||||
|
);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
secret = config.server.secret;
|
||||||
|
}
|
||||||
|
|
||||||
|
const [org] = await db
|
||||||
|
.select({
|
||||||
|
orgId: orgs.orgId,
|
||||||
|
sshCaPrivateKey: orgs.sshCaPrivateKey,
|
||||||
|
sshCaPublicKey: orgs.sshCaPublicKey
|
||||||
|
})
|
||||||
|
.from(orgs)
|
||||||
|
.where(eq(orgs.orgId, orgId))
|
||||||
|
.limit(1);
|
||||||
|
|
||||||
|
if (!org) {
|
||||||
|
console.error(`Error: Organization with orgId "${orgId}" not found.`);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (org.sshCaPrivateKey != null || org.sshCaPublicKey != null) {
|
||||||
|
if (!force) {
|
||||||
|
console.error(
|
||||||
|
"Error: This organization already has CA keys. Use --force to overwrite."
|
||||||
|
);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const ca = generateCA(`pangolin-ssh-ca-${orgId}`);
|
||||||
|
const encryptedPrivateKey = encrypt(ca.privateKeyPem, secret);
|
||||||
|
|
||||||
|
await db
|
||||||
|
.update(orgs)
|
||||||
|
.set({
|
||||||
|
sshCaPrivateKey: encryptedPrivateKey,
|
||||||
|
sshCaPublicKey: ca.publicKeyOpenSSH
|
||||||
|
})
|
||||||
|
.where(eq(orgs.orgId, orgId));
|
||||||
|
|
||||||
|
console.log("SSH CA keys generated and stored for org:", orgId);
|
||||||
|
console.log("\nPublic key (OpenSSH format):");
|
||||||
|
console.log(ca.publicKeyOpenSSH);
|
||||||
|
process.exit(0);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error generating org CA keys:", error);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -7,8 +7,8 @@ import fs from "fs";
|
|||||||
import yaml from "js-yaml";
|
import yaml from "js-yaml";
|
||||||
|
|
||||||
type RotateServerSecretArgs = {
|
type RotateServerSecretArgs = {
|
||||||
oldSecret: string;
|
"old-secret": string;
|
||||||
newSecret: string;
|
"new-secret": string;
|
||||||
force?: boolean;
|
force?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -21,12 +21,12 @@ export const rotateServerSecret: CommandModule<
|
|||||||
"Rotate the server secret by decrypting all encrypted values with the old secret and re-encrypting with a new secret",
|
"Rotate the server secret by decrypting all encrypted values with the old secret and re-encrypting with a new secret",
|
||||||
builder: (yargs) => {
|
builder: (yargs) => {
|
||||||
return yargs
|
return yargs
|
||||||
.option("oldSecret", {
|
.option("old-secret", {
|
||||||
type: "string",
|
type: "string",
|
||||||
demandOption: true,
|
demandOption: true,
|
||||||
describe: "The current server secret (for verification)"
|
describe: "The current server secret (for verification)"
|
||||||
})
|
})
|
||||||
.option("newSecret", {
|
.option("new-secret", {
|
||||||
type: "string",
|
type: "string",
|
||||||
demandOption: true,
|
demandOption: true,
|
||||||
describe: "The new server secret to use"
|
describe: "The new server secret to use"
|
||||||
@@ -42,8 +42,8 @@ export const rotateServerSecret: CommandModule<
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
handler: async (argv: {
|
handler: async (argv: {
|
||||||
oldSecret: string;
|
"old-secret": string;
|
||||||
newSecret: string;
|
"new-secret": string;
|
||||||
force?: boolean;
|
force?: boolean;
|
||||||
}) => {
|
}) => {
|
||||||
try {
|
try {
|
||||||
@@ -73,8 +73,8 @@ export const rotateServerSecret: CommandModule<
|
|||||||
}
|
}
|
||||||
|
|
||||||
const configSecret = config.server.secret;
|
const configSecret = config.server.secret;
|
||||||
const oldSecret = argv.oldSecret;
|
const oldSecret = argv["old-secret"];
|
||||||
const newSecret = argv.newSecret;
|
const newSecret = argv["new-secret"];
|
||||||
const force = argv.force || false;
|
const force = argv.force || false;
|
||||||
|
|
||||||
// Verify that the provided old secret matches the one in config
|
// Verify that the provided old secret matches the one in config
|
||||||
|
|||||||
@@ -6,6 +6,9 @@ import { setAdminCredentials } from "@cli/commands/setAdminCredentials";
|
|||||||
import { resetUserSecurityKeys } from "@cli/commands/resetUserSecurityKeys";
|
import { resetUserSecurityKeys } from "@cli/commands/resetUserSecurityKeys";
|
||||||
import { clearExitNodes } from "./commands/clearExitNodes";
|
import { clearExitNodes } from "./commands/clearExitNodes";
|
||||||
import { rotateServerSecret } from "./commands/rotateServerSecret";
|
import { rotateServerSecret } from "./commands/rotateServerSecret";
|
||||||
|
import { clearLicenseKeys } from "./commands/clearLicenseKeys";
|
||||||
|
import { deleteClient } from "./commands/deleteClient";
|
||||||
|
import { generateOrgCaKeys } from "./commands/generateOrgCaKeys";
|
||||||
|
|
||||||
yargs(hideBin(process.argv))
|
yargs(hideBin(process.argv))
|
||||||
.scriptName("pangctl")
|
.scriptName("pangctl")
|
||||||
@@ -13,5 +16,8 @@ yargs(hideBin(process.argv))
|
|||||||
.command(resetUserSecurityKeys)
|
.command(resetUserSecurityKeys)
|
||||||
.command(clearExitNodes)
|
.command(clearExitNodes)
|
||||||
.command(rotateServerSecret)
|
.command(rotateServerSecret)
|
||||||
|
.command(clearLicenseKeys)
|
||||||
|
.command(deleteClient)
|
||||||
|
.command(generateOrgCaKeys)
|
||||||
.demandCommand()
|
.demandCommand()
|
||||||
.help().argv;
|
.help().argv;
|
||||||
|
|||||||
@@ -1,27 +1,30 @@
|
|||||||
# To see all available options, please visit the docs:
|
# To see all available options, please visit the docs:
|
||||||
# https://docs.pangolin.net/self-host/advanced/config-file
|
# https://docs.pangolin.net/
|
||||||
|
|
||||||
|
gerbil:
|
||||||
|
start_port: 51820
|
||||||
|
base_endpoint: "{{.DashboardDomain}}"
|
||||||
|
|
||||||
app:
|
app:
|
||||||
dashboard_url: http://localhost:3002
|
dashboard_url: "https://{{.DashboardDomain}}"
|
||||||
log_level: debug
|
log_level: "info"
|
||||||
|
telemetry:
|
||||||
|
anonymous_usage: true
|
||||||
|
|
||||||
domains:
|
domains:
|
||||||
domain1:
|
domain1:
|
||||||
base_domain: example.com
|
base_domain: "{{.BaseDomain}}"
|
||||||
|
|
||||||
server:
|
server:
|
||||||
secret: my_secret_key
|
secret: "{{.Secret}}"
|
||||||
|
cors:
|
||||||
gerbil:
|
origins: ["https://{{.DashboardDomain}}"]
|
||||||
base_endpoint: example.com
|
methods: ["GET", "POST", "PUT", "DELETE", "PATCH"]
|
||||||
|
allowed_headers: ["X-CSRF-Token", "Content-Type"]
|
||||||
orgs:
|
credentials: false
|
||||||
block_size: 24
|
|
||||||
subnet_group: 100.90.137.0/20
|
|
||||||
|
|
||||||
flags:
|
flags:
|
||||||
require_email_verification: false
|
require_email_verification: false
|
||||||
disable_signup_without_invite: true
|
disable_signup_without_invite: true
|
||||||
disable_user_create_org: true
|
disable_user_create_org: false
|
||||||
allow_raw_resources: true
|
allow_raw_resources: true
|
||||||
enable_integration_api: true
|
|
||||||
|
|||||||
@@ -1,5 +1,9 @@
|
|||||||
http:
|
http:
|
||||||
middlewares:
|
middlewares:
|
||||||
|
badger:
|
||||||
|
plugin:
|
||||||
|
badger:
|
||||||
|
disableForwardAuth: true
|
||||||
redirect-to-https:
|
redirect-to-https:
|
||||||
redirectScheme:
|
redirectScheme:
|
||||||
scheme: https
|
scheme: https
|
||||||
@@ -13,14 +17,16 @@ http:
|
|||||||
- web
|
- web
|
||||||
middlewares:
|
middlewares:
|
||||||
- redirect-to-https
|
- redirect-to-https
|
||||||
|
- badger
|
||||||
|
|
||||||
# Next.js router (handles everything except API and WebSocket paths)
|
# Next.js router (handles everything except API and WebSocket paths)
|
||||||
next-router:
|
next-router:
|
||||||
rule: "Host(`{{.DashboardDomain}}`)"
|
rule: "Host(`{{.DashboardDomain}}`) && !PathPrefix(`/api/v1`)"
|
||||||
service: next-service
|
service: next-service
|
||||||
priority: 10
|
|
||||||
entryPoints:
|
entryPoints:
|
||||||
- websecure
|
- websecure
|
||||||
|
middlewares:
|
||||||
|
- badger
|
||||||
tls:
|
tls:
|
||||||
certResolver: letsencrypt
|
certResolver: letsencrypt
|
||||||
|
|
||||||
@@ -28,9 +34,10 @@ http:
|
|||||||
api-router:
|
api-router:
|
||||||
rule: "Host(`{{.DashboardDomain}}`) && PathPrefix(`/api/v1`)"
|
rule: "Host(`{{.DashboardDomain}}`) && PathPrefix(`/api/v1`)"
|
||||||
service: api-service
|
service: api-service
|
||||||
priority: 100
|
|
||||||
entryPoints:
|
entryPoints:
|
||||||
- websecure
|
- websecure
|
||||||
|
middlewares:
|
||||||
|
- badger
|
||||||
tls:
|
tls:
|
||||||
certResolver: letsencrypt
|
certResolver: letsencrypt
|
||||||
|
|
||||||
@@ -44,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,32 +3,52 @@ api:
|
|||||||
dashboard: true
|
dashboard: true
|
||||||
|
|
||||||
providers:
|
providers:
|
||||||
|
http:
|
||||||
|
endpoint: "http://pangolin:3001/api/v1/traefik-config"
|
||||||
|
pollInterval: "5s"
|
||||||
file:
|
file:
|
||||||
directory: "/var/dynamic"
|
filename: "/etc/traefik/dynamic_config.yml"
|
||||||
watch: true
|
|
||||||
|
|
||||||
experimental:
|
experimental:
|
||||||
plugins:
|
plugins:
|
||||||
badger:
|
badger:
|
||||||
moduleName: "github.com/fosrl/badger"
|
moduleName: "github.com/fosrl/badger"
|
||||||
version: "v1.2.0"
|
version: "{{.BadgerVersion}}"
|
||||||
|
|
||||||
log:
|
log:
|
||||||
level: "DEBUG"
|
level: "INFO"
|
||||||
format: "common"
|
format: "common"
|
||||||
maxSize: 100
|
maxSize: 100
|
||||||
maxBackups: 3
|
maxBackups: 3
|
||||||
maxAge: 3
|
maxAge: 3
|
||||||
compress: true
|
compress: true
|
||||||
|
|
||||||
|
certificatesResolvers:
|
||||||
|
letsencrypt:
|
||||||
|
acme:
|
||||||
|
httpChallenge:
|
||||||
|
entryPoint: web
|
||||||
|
email: "{{.LetsEncryptEmail}}"
|
||||||
|
storage: "/letsencrypt/acme.json"
|
||||||
|
caServer: "https://acme-v02.api.letsencrypt.org/directory"
|
||||||
|
|
||||||
entryPoints:
|
entryPoints:
|
||||||
web:
|
web:
|
||||||
address: ":80"
|
address: ":80"
|
||||||
websecure:
|
websecure:
|
||||||
address: ":9443"
|
address: ":443"
|
||||||
transport:
|
transport:
|
||||||
respondingTimeouts:
|
respondingTimeouts:
|
||||||
readTimeout: "30m"
|
readTimeout: "30m"
|
||||||
|
http:
|
||||||
|
tls:
|
||||||
|
certResolver: "letsencrypt"
|
||||||
|
encodedCharacters:
|
||||||
|
allowEncodedSlash: true
|
||||||
|
allowEncodedQuestionMark: true
|
||||||
|
|
||||||
serversTransport:
|
serversTransport:
|
||||||
insecureSkipVerify: true
|
insecureSkipVerify: true
|
||||||
|
|
||||||
|
ping:
|
||||||
|
entryPoint: "web"
|
||||||
|
|||||||
@@ -4,6 +4,12 @@ services:
|
|||||||
image: fosrl/pangolin:latest
|
image: fosrl/pangolin:latest
|
||||||
container_name: pangolin
|
container_name: pangolin
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
|
deploy:
|
||||||
|
resources:
|
||||||
|
limits:
|
||||||
|
memory: 1g
|
||||||
|
reservations:
|
||||||
|
memory: 256m
|
||||||
volumes:
|
volumes:
|
||||||
- ./config:/app/config
|
- ./config:/app/config
|
||||||
healthcheck:
|
healthcheck:
|
||||||
|
|||||||
@@ -7,8 +7,8 @@ services:
|
|||||||
POSTGRES_DB: postgres # Default database name
|
POSTGRES_DB: postgres # Default database name
|
||||||
POSTGRES_USER: postgres # Default user
|
POSTGRES_USER: postgres # Default user
|
||||||
POSTGRES_PASSWORD: password # Default password (change for production!)
|
POSTGRES_PASSWORD: password # Default password (change for production!)
|
||||||
volumes:
|
# volumes:
|
||||||
- ./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
|
||||||
|
|||||||
10
esbuild.mjs
10
esbuild.mjs
@@ -6,6 +6,12 @@ import path from "path";
|
|||||||
import fs from "fs";
|
import fs from "fs";
|
||||||
// import { glob } from "glob";
|
// import { glob } from "glob";
|
||||||
|
|
||||||
|
// Read default build type from server/build.ts
|
||||||
|
let build = "oss";
|
||||||
|
const buildFile = fs.readFileSync(path.resolve("server/build.ts"), "utf8");
|
||||||
|
const m = buildFile.match(/export\s+const\s+build\s*=\s*["'](oss|saas|enterprise)["']/);
|
||||||
|
if (m) build = m[1];
|
||||||
|
|
||||||
const banner = `
|
const banner = `
|
||||||
// patch __dirname
|
// patch __dirname
|
||||||
// import { fileURLToPath } from "url";
|
// import { fileURLToPath } from "url";
|
||||||
@@ -37,7 +43,7 @@ const argv = yargs(hideBin(process.argv))
|
|||||||
describe: "Build type (oss, saas, enterprise)",
|
describe: "Build type (oss, saas, enterprise)",
|
||||||
type: "string",
|
type: "string",
|
||||||
choices: ["oss", "saas", "enterprise"],
|
choices: ["oss", "saas", "enterprise"],
|
||||||
default: "oss"
|
default: build
|
||||||
})
|
})
|
||||||
.help()
|
.help()
|
||||||
.alias("help", "h").argv;
|
.alias("help", "h").argv;
|
||||||
@@ -275,7 +281,7 @@ esbuild
|
|||||||
})
|
})
|
||||||
],
|
],
|
||||||
sourcemap: "inline",
|
sourcemap: "inline",
|
||||||
target: "node22"
|
target: "node24"
|
||||||
})
|
})
|
||||||
.then((result) => {
|
.then((result) => {
|
||||||
// Check if there were any errors in the build result
|
// Check if there were any errors in the build result
|
||||||
|
|||||||
@@ -1,41 +1,24 @@
|
|||||||
all: update-versions go-build-release put-back
|
all: go-build-release
|
||||||
dev-all: dev-update-versions dev-build dev-clean
|
|
||||||
|
# Build with version injection via ldflags
|
||||||
|
# Versions can be passed via: make go-build-release PANGOLIN_VERSION=x.x.x GERBIL_VERSION=x.x.x BADGER_VERSION=x.x.x
|
||||||
|
# Or fetched automatically if not provided (requires curl and jq)
|
||||||
|
|
||||||
|
PANGOLIN_VERSION ?= $(shell curl -s https://api.github.com/repos/fosrl/pangolin/tags | jq -r '.[0].name')
|
||||||
|
GERBIL_VERSION ?= $(shell curl -s https://api.github.com/repos/fosrl/gerbil/tags | jq -r '.[0].name')
|
||||||
|
BADGER_VERSION ?= $(shell curl -s https://api.github.com/repos/fosrl/badger/tags | jq -r '.[0].name')
|
||||||
|
|
||||||
|
LDFLAGS = -X main.pangolinVersion=$(PANGOLIN_VERSION) \
|
||||||
|
-X main.gerbilVersion=$(GERBIL_VERSION) \
|
||||||
|
-X main.badgerVersion=$(BADGER_VERSION)
|
||||||
|
|
||||||
go-build-release:
|
go-build-release:
|
||||||
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o bin/installer_linux_amd64
|
@echo "Building with versions - Pangolin: $(PANGOLIN_VERSION), Gerbil: $(GERBIL_VERSION), Badger: $(BADGER_VERSION)"
|
||||||
CGO_ENABLED=0 GOOS=linux GOARCH=arm64 go build -o bin/installer_linux_arm64
|
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -ldflags "$(LDFLAGS)" -o bin/installer_linux_amd64
|
||||||
|
CGO_ENABLED=0 GOOS=linux GOARCH=arm64 go build -ldflags "$(LDFLAGS)" -o bin/installer_linux_arm64
|
||||||
|
|
||||||
clean:
|
clean:
|
||||||
rm -f bin/installer_linux_amd64
|
rm -f bin/installer_linux_amd64
|
||||||
rm -f bin/installer_linux_arm64
|
rm -f bin/installer_linux_arm64
|
||||||
|
|
||||||
update-versions:
|
.PHONY: all go-build-release clean
|
||||||
@echo "Fetching latest versions..."
|
|
||||||
cp main.go main.go.bak && \
|
|
||||||
$(MAKE) dev-update-versions
|
|
||||||
|
|
||||||
put-back:
|
|
||||||
mv main.go.bak main.go
|
|
||||||
|
|
||||||
dev-update-versions:
|
|
||||||
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') && \
|
|
||||||
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" && \
|
|
||||||
sed -i "s/config.PangolinVersion = \".*\"/config.PangolinVersion = \"$$PANGOLIN_VERSION\"/" main.go && \
|
|
||||||
sed -i "s/config.GerbilVersion = \".*\"/config.GerbilVersion = \"$$GERBIL_VERSION\"/" main.go && \
|
|
||||||
sed -i "s/config.BadgerVersion = \".*\"/config.BadgerVersion = \"$$BADGER_VERSION\"/" main.go && \
|
|
||||||
echo "Updated main.go with latest versions"
|
|
||||||
|
|
||||||
dev-build: go-build-release
|
|
||||||
|
|
||||||
dev-clean:
|
|
||||||
@echo "Restoring version values ..."
|
|
||||||
sed -i "s/config.PangolinVersion = \".*\"/config.PangolinVersion = \"replaceme\"/" main.go && \
|
|
||||||
sed -i "s/config.GerbilVersion = \".*\"/config.GerbilVersion = \"replaceme\"/" main.go && \
|
|
||||||
sed -i "s/config.BadgerVersion = \".*\"/config.BadgerVersion = \"replaceme\"/" main.go
|
|
||||||
@echo "Restored version strings in main.go"
|
|
||||||
|
|||||||
@@ -99,11 +99,6 @@ func ReadAppConfig(configPath string) (*AppConfigValues, error) {
|
|||||||
return values, nil
|
return values, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// findPattern finds the start of a pattern in a string
|
|
||||||
func findPattern(s, pattern string) int {
|
|
||||||
return bytes.Index([]byte(s), []byte(pattern))
|
|
||||||
}
|
|
||||||
|
|
||||||
func copyDockerService(sourceFile, destFile, serviceName string) error {
|
func copyDockerService(sourceFile, destFile, serviceName string) error {
|
||||||
// Read source file
|
// Read source file
|
||||||
sourceData, err := os.ReadFile(sourceFile)
|
sourceData, err := os.ReadFile(sourceFile)
|
||||||
@@ -118,19 +113,19 @@ func copyDockerService(sourceFile, destFile, serviceName string) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Parse source Docker Compose YAML
|
// Parse source Docker Compose YAML
|
||||||
var sourceCompose map[string]interface{}
|
var sourceCompose map[string]any
|
||||||
if err := yaml.Unmarshal(sourceData, &sourceCompose); err != nil {
|
if err := yaml.Unmarshal(sourceData, &sourceCompose); err != nil {
|
||||||
return fmt.Errorf("error parsing source Docker Compose file: %w", err)
|
return fmt.Errorf("error parsing source Docker Compose file: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Parse destination Docker Compose YAML
|
// Parse destination Docker Compose YAML
|
||||||
var destCompose map[string]interface{}
|
var destCompose map[string]any
|
||||||
if err := yaml.Unmarshal(destData, &destCompose); err != nil {
|
if err := yaml.Unmarshal(destData, &destCompose); err != nil {
|
||||||
return fmt.Errorf("error parsing destination Docker Compose file: %w", err)
|
return fmt.Errorf("error parsing destination Docker Compose file: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get services section from source
|
// Get services section from source
|
||||||
sourceServices, ok := sourceCompose["services"].(map[string]interface{})
|
sourceServices, ok := sourceCompose["services"].(map[string]any)
|
||||||
if !ok {
|
if !ok {
|
||||||
return fmt.Errorf("services section not found in source file or has invalid format")
|
return fmt.Errorf("services section not found in source file or has invalid format")
|
||||||
}
|
}
|
||||||
@@ -142,10 +137,10 @@ func copyDockerService(sourceFile, destFile, serviceName string) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Get or create services section in destination
|
// Get or create services section in destination
|
||||||
destServices, ok := destCompose["services"].(map[string]interface{})
|
destServices, ok := destCompose["services"].(map[string]any)
|
||||||
if !ok {
|
if !ok {
|
||||||
// If services section doesn't exist, create it
|
// If services section doesn't exist, create it
|
||||||
destServices = make(map[string]interface{})
|
destServices = make(map[string]any)
|
||||||
destCompose["services"] = destServices
|
destCompose["services"] = destServices
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -187,17 +182,21 @@ func backupConfig() error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func MarshalYAMLWithIndent(data interface{}, indent int) ([]byte, error) {
|
func MarshalYAMLWithIndent(data any, indent int) (resp []byte, err error) {
|
||||||
buffer := new(bytes.Buffer)
|
buffer := new(bytes.Buffer)
|
||||||
encoder := yaml.NewEncoder(buffer)
|
encoder := yaml.NewEncoder(buffer)
|
||||||
encoder.SetIndent(indent)
|
encoder.SetIndent(indent)
|
||||||
|
|
||||||
err := encoder.Encode(data)
|
if err := encoder.Encode(data); err != nil {
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
defer encoder.Close()
|
defer func() {
|
||||||
|
if cerr := encoder.Close(); cerr != nil && err == nil {
|
||||||
|
err = cerr
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
return buffer.Bytes(), nil
|
return buffer.Bytes(), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -209,7 +208,7 @@ func replaceInFile(filepath, oldStr, newStr string) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Replace the string
|
// Replace the string
|
||||||
newContent := strings.Replace(string(content), oldStr, newStr, -1)
|
newContent := strings.ReplaceAll(string(content), oldStr, newStr)
|
||||||
|
|
||||||
// Write the modified content back to the file
|
// Write the modified content back to the file
|
||||||
err = os.WriteFile(filepath, []byte(newContent), 0644)
|
err = os.WriteFile(filepath, []byte(newContent), 0644)
|
||||||
@@ -228,28 +227,28 @@ func CheckAndAddTraefikLogVolume(composePath string) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Parse YAML into a generic map
|
// Parse YAML into a generic map
|
||||||
var compose map[string]interface{}
|
var compose map[string]any
|
||||||
if err := yaml.Unmarshal(data, &compose); err != nil {
|
if err := yaml.Unmarshal(data, &compose); err != nil {
|
||||||
return fmt.Errorf("error parsing compose file: %w", err)
|
return fmt.Errorf("error parsing compose file: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get services section
|
// Get services section
|
||||||
services, ok := compose["services"].(map[string]interface{})
|
services, ok := compose["services"].(map[string]any)
|
||||||
if !ok {
|
if !ok {
|
||||||
return fmt.Errorf("services section not found or invalid")
|
return fmt.Errorf("services section not found or invalid")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get traefik service
|
// Get traefik service
|
||||||
traefik, ok := services["traefik"].(map[string]interface{})
|
traefik, ok := services["traefik"].(map[string]any)
|
||||||
if !ok {
|
if !ok {
|
||||||
return fmt.Errorf("traefik service not found or invalid")
|
return fmt.Errorf("traefik service not found or invalid")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check volumes
|
// Check volumes
|
||||||
logVolume := "./config/traefik/logs:/var/log/traefik"
|
logVolume := "./config/traefik/logs:/var/log/traefik"
|
||||||
var volumes []interface{}
|
var volumes []any
|
||||||
|
|
||||||
if existingVolumes, ok := traefik["volumes"].([]interface{}); ok {
|
if existingVolumes, ok := traefik["volumes"].([]any); ok {
|
||||||
// Check if volume already exists
|
// Check if volume already exists
|
||||||
for _, v := range existingVolumes {
|
for _, v := range existingVolumes {
|
||||||
if v.(string) == logVolume {
|
if v.(string) == logVolume {
|
||||||
@@ -295,13 +294,13 @@ func MergeYAML(baseFile, overlayFile string) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Parse base YAML into a map
|
// Parse base YAML into a map
|
||||||
var baseMap map[string]interface{}
|
var baseMap map[string]any
|
||||||
if err := yaml.Unmarshal(baseContent, &baseMap); err != nil {
|
if err := yaml.Unmarshal(baseContent, &baseMap); err != nil {
|
||||||
return fmt.Errorf("error parsing base YAML: %v", err)
|
return fmt.Errorf("error parsing base YAML: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Parse overlay YAML into a map
|
// Parse overlay YAML into a map
|
||||||
var overlayMap map[string]interface{}
|
var overlayMap map[string]any
|
||||||
if err := yaml.Unmarshal(overlayContent, &overlayMap); err != nil {
|
if err := yaml.Unmarshal(overlayContent, &overlayMap); err != nil {
|
||||||
return fmt.Errorf("error parsing overlay YAML: %v", err)
|
return fmt.Errorf("error parsing overlay YAML: %v", err)
|
||||||
}
|
}
|
||||||
@@ -324,8 +323,8 @@ func MergeYAML(baseFile, overlayFile string) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// mergeMap recursively merges two maps
|
// mergeMap recursively merges two maps
|
||||||
func mergeMap(base, overlay map[string]interface{}) map[string]interface{} {
|
func mergeMap(base, overlay map[string]any) map[string]any {
|
||||||
result := make(map[string]interface{})
|
result := make(map[string]any)
|
||||||
|
|
||||||
// Copy all key-values from base map
|
// Copy all key-values from base map
|
||||||
for k, v := range base {
|
for k, v := range base {
|
||||||
@@ -336,8 +335,8 @@ func mergeMap(base, overlay map[string]interface{}) map[string]interface{} {
|
|||||||
for k, v := range overlay {
|
for k, v := range overlay {
|
||||||
// If both maps have the same key and both values are maps, merge recursively
|
// If both maps have the same key and both values are maps, merge recursively
|
||||||
if baseVal, ok := base[k]; ok {
|
if baseVal, ok := base[k]; ok {
|
||||||
if baseMap, isBaseMap := baseVal.(map[string]interface{}); isBaseMap {
|
if baseMap, isBaseMap := baseVal.(map[string]any); isBaseMap {
|
||||||
if overlayMap, isOverlayMap := v.(map[string]interface{}); isOverlayMap {
|
if overlayMap, isOverlayMap := v.(map[string]any); isOverlayMap {
|
||||||
result[k] = mergeMap(baseMap, overlayMap)
|
result[k] = mergeMap(baseMap, overlayMap)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,9 @@
|
|||||||
http:
|
http:
|
||||||
middlewares:
|
middlewares:
|
||||||
|
badger:
|
||||||
|
plugin:
|
||||||
|
badger:
|
||||||
|
disableForwardAuth: true
|
||||||
redirect-to-https:
|
redirect-to-https:
|
||||||
redirectScheme:
|
redirectScheme:
|
||||||
scheme: https
|
scheme: https
|
||||||
@@ -63,6 +67,7 @@ http:
|
|||||||
- web
|
- web
|
||||||
middlewares:
|
middlewares:
|
||||||
- redirect-to-https
|
- redirect-to-https
|
||||||
|
- badger
|
||||||
|
|
||||||
# Next.js router (handles everything except API and WebSocket paths)
|
# Next.js router (handles everything except API and WebSocket paths)
|
||||||
next-router:
|
next-router:
|
||||||
@@ -72,6 +77,7 @@ http:
|
|||||||
- websecure
|
- websecure
|
||||||
middlewares:
|
middlewares:
|
||||||
- security-headers # Add security headers middleware
|
- security-headers # Add security headers middleware
|
||||||
|
- badger
|
||||||
tls:
|
tls:
|
||||||
certResolver: letsencrypt
|
certResolver: letsencrypt
|
||||||
|
|
||||||
@@ -83,6 +89,7 @@ http:
|
|||||||
- websecure
|
- websecure
|
||||||
middlewares:
|
middlewares:
|
||||||
- security-headers # Add security headers middleware
|
- security-headers # Add security headers middleware
|
||||||
|
- badger
|
||||||
tls:
|
tls:
|
||||||
certResolver: letsencrypt
|
certResolver: letsencrypt
|
||||||
|
|
||||||
@@ -94,6 +101,7 @@ http:
|
|||||||
- websecure
|
- websecure
|
||||||
middlewares:
|
middlewares:
|
||||||
- security-headers # Add security headers middleware
|
- security-headers # Add security headers middleware
|
||||||
|
- badger
|
||||||
tls:
|
tls:
|
||||||
certResolver: letsencrypt
|
certResolver: letsencrypt
|
||||||
|
|
||||||
|
|||||||
@@ -81,11 +81,17 @@ entryPoints:
|
|||||||
transport:
|
transport:
|
||||||
respondingTimeouts:
|
respondingTimeouts:
|
||||||
readTimeout: "30m"
|
readTimeout: "30m"
|
||||||
|
http3:
|
||||||
|
advertisedPort: 443
|
||||||
http:
|
http:
|
||||||
tls:
|
tls:
|
||||||
certResolver: "letsencrypt"
|
certResolver: "letsencrypt"
|
||||||
middlewares:
|
encodedCharacters:
|
||||||
- crowdsec@file
|
allowEncodedSlash: true
|
||||||
|
allowEncodedQuestionMark: true
|
||||||
|
|
||||||
serversTransport:
|
serversTransport:
|
||||||
insecureSkipVerify: true
|
insecureSkipVerify: true
|
||||||
|
|
||||||
|
ping:
|
||||||
|
entryPoint: "web"
|
||||||
|
|||||||
@@ -1,9 +1,15 @@
|
|||||||
name: pangolin
|
name: pangolin
|
||||||
services:
|
services:
|
||||||
pangolin:
|
pangolin:
|
||||||
image: docker.io/fosrl/pangolin:{{.PangolinVersion}}
|
image: docker.io/fosrl/pangolin:{{if .IsEnterprise}}ee-{{end}}{{.PangolinVersion}}
|
||||||
container_name: pangolin
|
container_name: pangolin
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
|
deploy:
|
||||||
|
resources:
|
||||||
|
limits:
|
||||||
|
memory: 1g
|
||||||
|
reservations:
|
||||||
|
memory: 256m
|
||||||
volumes:
|
volumes:
|
||||||
- ./config:/app/config
|
- ./config:/app/config
|
||||||
healthcheck:
|
healthcheck:
|
||||||
@@ -32,15 +38,14 @@ services:
|
|||||||
- 51820:51820/udp
|
- 51820:51820/udp
|
||||||
- 21820:21820/udp
|
- 21820:21820/udp
|
||||||
- 443:443
|
- 443:443
|
||||||
|
- 443:443/udp # For http3 QUIC if desired
|
||||||
- 80:80
|
- 80:80
|
||||||
{{end}}
|
{{end}}
|
||||||
traefik:
|
traefik:
|
||||||
image: docker.io/traefik:v3.6
|
image: docker.io/traefik:v3.6
|
||||||
container_name: traefik
|
container_name: traefik
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
{{if .InstallGerbil}}
|
{{if .InstallGerbil}} network_mode: service:gerbil # Ports appear on the gerbil service{{end}}{{if not .InstallGerbil}}
|
||||||
network_mode: service:gerbil # Ports appear on the gerbil service
|
|
||||||
{{end}}{{if not .InstallGerbil}}
|
|
||||||
ports:
|
ports:
|
||||||
- 443:443
|
- 443:443
|
||||||
- 80:80
|
- 80:80
|
||||||
|
|||||||
@@ -1,5 +1,9 @@
|
|||||||
http:
|
http:
|
||||||
middlewares:
|
middlewares:
|
||||||
|
badger:
|
||||||
|
plugin:
|
||||||
|
badger:
|
||||||
|
disableForwardAuth: true
|
||||||
redirect-to-https:
|
redirect-to-https:
|
||||||
redirectScheme:
|
redirectScheme:
|
||||||
scheme: https
|
scheme: https
|
||||||
@@ -13,6 +17,7 @@ http:
|
|||||||
- web
|
- web
|
||||||
middlewares:
|
middlewares:
|
||||||
- redirect-to-https
|
- redirect-to-https
|
||||||
|
- badger
|
||||||
|
|
||||||
# Next.js router (handles everything except API and WebSocket paths)
|
# Next.js router (handles everything except API and WebSocket paths)
|
||||||
next-router:
|
next-router:
|
||||||
@@ -20,6 +25,8 @@ http:
|
|||||||
service: next-service
|
service: next-service
|
||||||
entryPoints:
|
entryPoints:
|
||||||
- websecure
|
- websecure
|
||||||
|
middlewares:
|
||||||
|
- badger
|
||||||
tls:
|
tls:
|
||||||
certResolver: letsencrypt
|
certResolver: letsencrypt
|
||||||
|
|
||||||
@@ -29,6 +36,8 @@ http:
|
|||||||
service: api-service
|
service: api-service
|
||||||
entryPoints:
|
entryPoints:
|
||||||
- websecure
|
- websecure
|
||||||
|
middlewares:
|
||||||
|
- badger
|
||||||
tls:
|
tls:
|
||||||
certResolver: letsencrypt
|
certResolver: letsencrypt
|
||||||
|
|
||||||
@@ -38,6 +47,8 @@ http:
|
|||||||
service: api-service
|
service: api-service
|
||||||
entryPoints:
|
entryPoints:
|
||||||
- websecure
|
- websecure
|
||||||
|
middlewares:
|
||||||
|
- badger
|
||||||
tls:
|
tls:
|
||||||
certResolver: letsencrypt
|
certResolver: letsencrypt
|
||||||
|
|
||||||
|
|||||||
@@ -40,9 +40,14 @@ entryPoints:
|
|||||||
transport:
|
transport:
|
||||||
respondingTimeouts:
|
respondingTimeouts:
|
||||||
readTimeout: "30m"
|
readTimeout: "30m"
|
||||||
|
http3:
|
||||||
|
advertisedPort: 443
|
||||||
http:
|
http:
|
||||||
tls:
|
tls:
|
||||||
certResolver: "letsencrypt"
|
certResolver: "letsencrypt"
|
||||||
|
encodedCharacters:
|
||||||
|
allowEncodedSlash: true
|
||||||
|
allowEncodedQuestionMark: true
|
||||||
|
|
||||||
serversTransport:
|
serversTransport:
|
||||||
insecureSkipVerify: true
|
insecureSkipVerify: true
|
||||||
|
|||||||
@@ -144,12 +144,13 @@ func installDocker() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func startDockerService() error {
|
func startDockerService() error {
|
||||||
if runtime.GOOS == "linux" {
|
switch runtime.GOOS {
|
||||||
|
case "linux":
|
||||||
cmd := exec.Command("systemctl", "enable", "--now", "docker")
|
cmd := exec.Command("systemctl", "enable", "--now", "docker")
|
||||||
cmd.Stdout = os.Stdout
|
cmd.Stdout = os.Stdout
|
||||||
cmd.Stderr = os.Stderr
|
cmd.Stderr = os.Stderr
|
||||||
return cmd.Run()
|
return cmd.Run()
|
||||||
} else if runtime.GOOS == "darwin" {
|
case "darwin":
|
||||||
// On macOS, Docker is usually started via the Docker Desktop application
|
// On macOS, Docker is usually started via the Docker Desktop application
|
||||||
fmt.Println("Please start Docker Desktop manually on macOS.")
|
fmt.Println("Please start Docker Desktop manually on macOS.")
|
||||||
return nil
|
return nil
|
||||||
@@ -210,6 +211,47 @@ func isDockerRunning() bool {
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func isPodmanRunning() bool {
|
||||||
|
cmd := exec.Command("podman", "info")
|
||||||
|
if err := cmd.Run(); err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// detectContainerType detects whether the system is currently using Docker or Podman
|
||||||
|
// by checking which container runtime is running and has containers
|
||||||
|
func detectContainerType() SupportedContainer {
|
||||||
|
// Check if we have running containers with podman
|
||||||
|
if isPodmanRunning() {
|
||||||
|
cmd := exec.Command("podman", "ps", "-q")
|
||||||
|
output, err := cmd.Output()
|
||||||
|
if err == nil && len(strings.TrimSpace(string(output))) > 0 {
|
||||||
|
return Podman
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if we have running containers with docker
|
||||||
|
if isDockerRunning() {
|
||||||
|
cmd := exec.Command("docker", "ps", "-q")
|
||||||
|
output, err := cmd.Output()
|
||||||
|
if err == nil && len(strings.TrimSpace(string(output))) > 0 {
|
||||||
|
return Docker
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If no containers are running, check which one is installed and running
|
||||||
|
if isPodmanRunning() && isPodmanInstalled() {
|
||||||
|
return Podman
|
||||||
|
}
|
||||||
|
|
||||||
|
if isDockerRunning() && isDockerInstalled() {
|
||||||
|
return Docker
|
||||||
|
}
|
||||||
|
|
||||||
|
return Undefined
|
||||||
|
}
|
||||||
|
|
||||||
// executeDockerComposeCommandWithArgs executes the appropriate docker command with arguments supplied
|
// executeDockerComposeCommandWithArgs executes the appropriate docker command with arguments supplied
|
||||||
func executeDockerComposeCommandWithArgs(args ...string) error {
|
func executeDockerComposeCommandWithArgs(args ...string) error {
|
||||||
var cmd *exec.Cmd
|
var cmd *exec.Cmd
|
||||||
@@ -261,7 +303,7 @@ func pullContainers(containerType SupportedContainer) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
return fmt.Errorf("Unsupported container type: %s", containerType)
|
return fmt.Errorf("unsupported container type: %s", containerType)
|
||||||
}
|
}
|
||||||
|
|
||||||
// startContainers starts the containers using the appropriate command.
|
// startContainers starts the containers using the appropriate command.
|
||||||
@@ -284,7 +326,7 @@ func startContainers(containerType SupportedContainer) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
return fmt.Errorf("Unsupported container type: %s", containerType)
|
return fmt.Errorf("unsupported container type: %s", containerType)
|
||||||
}
|
}
|
||||||
|
|
||||||
// stopContainers stops the containers using the appropriate command.
|
// stopContainers stops the containers using the appropriate command.
|
||||||
@@ -306,7 +348,7 @@ func stopContainers(containerType SupportedContainer) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
return fmt.Errorf("Unsupported container type: %s", containerType)
|
return fmt.Errorf("unsupported container type: %s", containerType)
|
||||||
}
|
}
|
||||||
|
|
||||||
// restartContainer restarts a specific container using the appropriate command.
|
// restartContainer restarts a specific container using the appropriate command.
|
||||||
@@ -328,5 +370,5 @@ func restartContainer(container string, containerType SupportedContainer) error
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
return fmt.Errorf("Unsupported container type: %s", containerType)
|
return fmt.Errorf("unsupported container type: %s", containerType)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,9 +27,18 @@ func installCrowdsec(config Config) error {
|
|||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
os.MkdirAll("config/crowdsec/db", 0755)
|
if err := os.MkdirAll("config/crowdsec/db", 0755); err != nil {
|
||||||
os.MkdirAll("config/crowdsec/acquis.d", 0755)
|
fmt.Printf("Error creating config files: %v\n", err)
|
||||||
os.MkdirAll("config/traefik/logs", 0755)
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
if err := os.MkdirAll("config/crowdsec/acquis.d", 0755); err != nil {
|
||||||
|
fmt.Printf("Error creating config files: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
if err := os.MkdirAll("config/traefik/logs", 0755); err != nil {
|
||||||
|
fmt.Printf("Error creating config files: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
if err := copyDockerService("config/crowdsec/docker-compose.yml", "docker-compose.yml", "crowdsec"); err != nil {
|
if err := copyDockerService("config/crowdsec/docker-compose.yml", "docker-compose.yml", "crowdsec"); err != nil {
|
||||||
fmt.Printf("Error copying docker service: %v\n", err)
|
fmt.Printf("Error copying docker service: %v\n", err)
|
||||||
@@ -93,7 +102,7 @@ func installCrowdsec(config Config) error {
|
|||||||
|
|
||||||
if checkIfTextInFile("config/traefik/dynamic_config.yml", "PUT_YOUR_BOUNCER_KEY_HERE_OR_IT_WILL_NOT_WORK") {
|
if checkIfTextInFile("config/traefik/dynamic_config.yml", "PUT_YOUR_BOUNCER_KEY_HERE_OR_IT_WILL_NOT_WORK") {
|
||||||
fmt.Println("Failed to replace bouncer key! Please retrieve the key and replace it in the config/traefik/dynamic_config.yml file using the following command:")
|
fmt.Println("Failed to replace bouncer key! Please retrieve the key and replace it in the config/traefik/dynamic_config.yml file using the following command:")
|
||||||
fmt.Println(" docker exec crowdsec cscli bouncers add traefik-bouncer")
|
fmt.Printf(" %s exec crowdsec cscli bouncers add traefik-bouncer\n", config.InstallationContainerType)
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
@@ -117,7 +126,7 @@ func GetCrowdSecAPIKey(containerType SupportedContainer) (string, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Execute the command to get the API key
|
// Execute the command to get the API key
|
||||||
cmd := exec.Command("docker", "exec", "crowdsec", "cscli", "bouncers", "add", "traefik-bouncer", "-o", "raw")
|
cmd := exec.Command(string(containerType), "exec", "crowdsec", "cscli", "bouncers", "add", "traefik-bouncer", "-o", "raw")
|
||||||
var out bytes.Buffer
|
var out bytes.Buffer
|
||||||
cmd.Stdout = &out
|
cmd.Stdout = &out
|
||||||
|
|
||||||
@@ -153,34 +162,34 @@ func CheckAndAddCrowdsecDependency(composePath string) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Parse YAML into a generic map
|
// Parse YAML into a generic map
|
||||||
var compose map[string]interface{}
|
var compose map[string]any
|
||||||
if err := yaml.Unmarshal(data, &compose); err != nil {
|
if err := yaml.Unmarshal(data, &compose); err != nil {
|
||||||
return fmt.Errorf("error parsing compose file: %w", err)
|
return fmt.Errorf("error parsing compose file: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get services section
|
// Get services section
|
||||||
services, ok := compose["services"].(map[string]interface{})
|
services, ok := compose["services"].(map[string]any)
|
||||||
if !ok {
|
if !ok {
|
||||||
return fmt.Errorf("services section not found or invalid")
|
return fmt.Errorf("services section not found or invalid")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get traefik service
|
// Get traefik service
|
||||||
traefik, ok := services["traefik"].(map[string]interface{})
|
traefik, ok := services["traefik"].(map[string]any)
|
||||||
if !ok {
|
if !ok {
|
||||||
return fmt.Errorf("traefik service not found or invalid")
|
return fmt.Errorf("traefik service not found or invalid")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get dependencies
|
// Get dependencies
|
||||||
dependsOn, ok := traefik["depends_on"].(map[string]interface{})
|
dependsOn, ok := traefik["depends_on"].(map[string]any)
|
||||||
if ok {
|
if ok {
|
||||||
// Append the new block for crowdsec
|
// Append the new block for crowdsec
|
||||||
dependsOn["crowdsec"] = map[string]interface{}{
|
dependsOn["crowdsec"] = map[string]any{
|
||||||
"condition": "service_healthy",
|
"condition": "service_healthy",
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// No dependencies exist, create it
|
// No dependencies exist, create it
|
||||||
traefik["depends_on"] = map[string]interface{}{
|
traefik["depends_on"] = map[string]any{
|
||||||
"crowdsec": map[string]interface{}{
|
"crowdsec": map[string]any{
|
||||||
"condition": "service_healthy",
|
"condition": "service_healthy",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,38 @@
|
|||||||
module installer
|
module installer
|
||||||
|
|
||||||
go 1.24.0
|
go 1.25.0
|
||||||
|
|
||||||
require (
|
require (
|
||||||
golang.org/x/term v0.38.0
|
github.com/charmbracelet/huh v1.0.0
|
||||||
|
github.com/charmbracelet/lipgloss v1.1.0
|
||||||
|
golang.org/x/term v0.41.0
|
||||||
gopkg.in/yaml.v3 v3.0.1
|
gopkg.in/yaml.v3 v3.0.1
|
||||||
)
|
)
|
||||||
|
|
||||||
require golang.org/x/sys v0.39.0 // indirect
|
require (
|
||||||
|
github.com/atotto/clipboard v0.1.4 // indirect
|
||||||
|
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
|
||||||
|
github.com/catppuccin/go v0.3.0 // indirect
|
||||||
|
github.com/charmbracelet/bubbles v0.21.1-0.20250623103423-23b8fd6302d7 // indirect
|
||||||
|
github.com/charmbracelet/bubbletea v1.3.6 // indirect
|
||||||
|
github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc // indirect
|
||||||
|
github.com/charmbracelet/x/ansi v0.9.3 // indirect
|
||||||
|
github.com/charmbracelet/x/cellbuf v0.0.13 // indirect
|
||||||
|
github.com/charmbracelet/x/exp/strings v0.0.0-20240722160745-212f7b056ed0 // indirect
|
||||||
|
github.com/charmbracelet/x/term v0.2.1 // indirect
|
||||||
|
github.com/dustin/go-humanize v1.0.1 // indirect
|
||||||
|
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect
|
||||||
|
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
|
||||||
|
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||||
|
github.com/mattn/go-localereader v0.0.1 // indirect
|
||||||
|
github.com/mattn/go-runewidth v0.0.16 // indirect
|
||||||
|
github.com/mitchellh/hashstructure/v2 v2.0.2 // indirect
|
||||||
|
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect
|
||||||
|
github.com/muesli/cancelreader v0.2.2 // indirect
|
||||||
|
github.com/muesli/termenv v0.16.0 // indirect
|
||||||
|
github.com/rivo/uniseg v0.4.7 // indirect
|
||||||
|
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
|
||||||
|
golang.org/x/sync v0.15.0 // indirect
|
||||||
|
golang.org/x/sys v0.42.0 // indirect
|
||||||
|
golang.org/x/text v0.23.0 // indirect
|
||||||
|
)
|
||||||
|
|||||||
@@ -1,7 +1,80 @@
|
|||||||
golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk=
|
github.com/MakeNowJust/heredoc v1.0.0 h1:cXCdzVdstXyiTqTvfqk9SDHpKNjxuom+DOlyEeQ4pzQ=
|
||||||
golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
github.com/MakeNowJust/heredoc v1.0.0/go.mod h1:mG5amYoWBHf8vpLOuehzbGGw0EHxpZZ6lCpQ4fNJ8LE=
|
||||||
golang.org/x/term v0.38.0 h1:PQ5pkm/rLO6HnxFR7N2lJHOZX6Kez5Y1gDSJla6jo7Q=
|
github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4=
|
||||||
golang.org/x/term v0.38.0/go.mod h1:bSEAKrOT1W+VSu9TSCMtoGEOUcKxOKgl3LE5QEF/xVg=
|
github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI=
|
||||||
|
github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=
|
||||||
|
github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8=
|
||||||
|
github.com/aymanbagabas/go-udiff v0.3.1 h1:LV+qyBQ2pqe0u42ZsUEtPiCaUoqgA9gYRDs3vj1nolY=
|
||||||
|
github.com/aymanbagabas/go-udiff v0.3.1/go.mod h1:G0fsKmG+P6ylD0r6N/KgQD/nWzgfnl8ZBcNLgcbrw8E=
|
||||||
|
github.com/catppuccin/go v0.3.0 h1:d+0/YicIq+hSTo5oPuRi5kOpqkVA5tAsU6dNhvRu+aY=
|
||||||
|
github.com/catppuccin/go v0.3.0/go.mod h1:8IHJuMGaUUjQM82qBrGNBv7LFq6JI3NnQCF6MOlZjpc=
|
||||||
|
github.com/charmbracelet/bubbles v0.21.1-0.20250623103423-23b8fd6302d7 h1:JFgG/xnwFfbezlUnFMJy0nusZvytYysV4SCS2cYbvws=
|
||||||
|
github.com/charmbracelet/bubbles v0.21.1-0.20250623103423-23b8fd6302d7/go.mod h1:ISC1gtLcVilLOf23wvTfoQuYbW2q0JevFxPfUzZ9Ybw=
|
||||||
|
github.com/charmbracelet/bubbletea v1.3.6 h1:VkHIxPJQeDt0aFJIsVxw8BQdh/F/L2KKZGsK6et5taU=
|
||||||
|
github.com/charmbracelet/bubbletea v1.3.6/go.mod h1:oQD9VCRQFF8KplacJLo28/jofOI2ToOfGYeFgBBxHOc=
|
||||||
|
github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc h1:4pZI35227imm7yK2bGPcfpFEmuY1gc2YSTShr4iJBfs=
|
||||||
|
github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc/go.mod h1:X4/0JoqgTIPSFcRA/P6INZzIuyqdFY5rm8tb41s9okk=
|
||||||
|
github.com/charmbracelet/huh v1.0.0 h1:wOnedH8G4qzJbmhftTqrpppyqHakl/zbbNdXIWJyIxw=
|
||||||
|
github.com/charmbracelet/huh v1.0.0/go.mod h1:5YVc+SlZ1IhQALxRPpkGwwEKftN/+OlJlnJYlDRFqN4=
|
||||||
|
github.com/charmbracelet/lipgloss v1.1.0 h1:vYXsiLHVkK7fp74RkV7b2kq9+zDLoEU4MZoFqR/noCY=
|
||||||
|
github.com/charmbracelet/lipgloss v1.1.0/go.mod h1:/6Q8FR2o+kj8rz4Dq0zQc3vYf7X+B0binUUBwA0aL30=
|
||||||
|
github.com/charmbracelet/x/ansi v0.9.3 h1:BXt5DHS/MKF+LjuK4huWrC6NCvHtexww7dMayh6GXd0=
|
||||||
|
github.com/charmbracelet/x/ansi v0.9.3/go.mod h1:3RQDQ6lDnROptfpWuUVIUG64bD2g2BgntdxH0Ya5TeE=
|
||||||
|
github.com/charmbracelet/x/cellbuf v0.0.13 h1:/KBBKHuVRbq1lYx5BzEHBAFBP8VcQzJejZ/IA3iR28k=
|
||||||
|
github.com/charmbracelet/x/cellbuf v0.0.13/go.mod h1:xe0nKWGd3eJgtqZRaN9RjMtK7xUYchjzPr7q6kcvCCs=
|
||||||
|
github.com/charmbracelet/x/conpty v0.1.0 h1:4zc8KaIcbiL4mghEON8D72agYtSeIgq8FSThSPQIb+U=
|
||||||
|
github.com/charmbracelet/x/conpty v0.1.0/go.mod h1:rMFsDJoDwVmiYM10aD4bH2XiRgwI7NYJtQgl5yskjEQ=
|
||||||
|
github.com/charmbracelet/x/errors v0.0.0-20240508181413-e8d8b6e2de86 h1:JSt3B+U9iqk37QUU2Rvb6DSBYRLtWqFqfxf8l5hOZUA=
|
||||||
|
github.com/charmbracelet/x/errors v0.0.0-20240508181413-e8d8b6e2de86/go.mod h1:2P0UgXMEa6TsToMSuFqKFQR+fZTO9CNGUNokkPatT/0=
|
||||||
|
github.com/charmbracelet/x/exp/golden v0.0.0-20241011142426-46044092ad91 h1:payRxjMjKgx2PaCWLZ4p3ro9y97+TVLZNaRZgJwSVDQ=
|
||||||
|
github.com/charmbracelet/x/exp/golden v0.0.0-20241011142426-46044092ad91/go.mod h1:wDlXFlCrmJ8J+swcL/MnGUuYnqgQdW9rhSD61oNMb6U=
|
||||||
|
github.com/charmbracelet/x/exp/strings v0.0.0-20240722160745-212f7b056ed0 h1:qko3AQ4gK1MTS/de7F5hPGx6/k1u0w4TeYmBFwzYVP4=
|
||||||
|
github.com/charmbracelet/x/exp/strings v0.0.0-20240722160745-212f7b056ed0/go.mod h1:pBhA0ybfXv6hDjQUZ7hk1lVxBiUbupdw5R31yPUViVQ=
|
||||||
|
github.com/charmbracelet/x/term v0.2.1 h1:AQeHeLZ1OqSXhrAWpYUtZyX1T3zVxfpZuEQMIQaGIAQ=
|
||||||
|
github.com/charmbracelet/x/term v0.2.1/go.mod h1:oQ4enTYFV7QN4m0i9mzHrViD7TQKvNEEkHUMCmsxdUg=
|
||||||
|
github.com/charmbracelet/x/termios v0.1.1 h1:o3Q2bT8eqzGnGPOYheoYS8eEleT5ZVNYNy8JawjaNZY=
|
||||||
|
github.com/charmbracelet/x/termios v0.1.1/go.mod h1:rB7fnv1TgOPOyyKRJ9o+AsTU/vK5WHJ2ivHeut/Pcwo=
|
||||||
|
github.com/charmbracelet/x/xpty v0.1.2 h1:Pqmu4TEJ8KeA9uSkISKMU3f+C1F6OGBn8ABuGlqCbtI=
|
||||||
|
github.com/charmbracelet/x/xpty v0.1.2/go.mod h1:XK2Z0id5rtLWcpeNiMYBccNNBrP2IJnzHI0Lq13Xzq4=
|
||||||
|
github.com/creack/pty v1.1.24 h1:bJrF4RRfyJnbTJqzRLHzcGaZK1NeM5kTC9jGgovnR1s=
|
||||||
|
github.com/creack/pty v1.1.24/go.mod h1:08sCNb52WyoAwi2QDyzUCTgcvVFhUzewun7wtTfvcwE=
|
||||||
|
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
|
||||||
|
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
|
||||||
|
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4=
|
||||||
|
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM=
|
||||||
|
github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
|
||||||
|
github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
|
||||||
|
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||||
|
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||||
|
github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4=
|
||||||
|
github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88=
|
||||||
|
github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
|
||||||
|
github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
|
||||||
|
github.com/mitchellh/hashstructure/v2 v2.0.2 h1:vGKWl0YJqUNxE8d+h8f6NJLcCJrgbhC4NcD46KavDd4=
|
||||||
|
github.com/mitchellh/hashstructure/v2 v2.0.2/go.mod h1:MG3aRVU/N29oo/V/IhBX8GR/zz4kQkprJgF2EVszyDE=
|
||||||
|
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI=
|
||||||
|
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6/go.mod h1:CJlz5H+gyd6CUWT45Oy4q24RdLyn7Md9Vj2/ldJBSIo=
|
||||||
|
github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA=
|
||||||
|
github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo=
|
||||||
|
github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc=
|
||||||
|
github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk=
|
||||||
|
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||||
|
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
|
||||||
|
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
|
||||||
|
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no=
|
||||||
|
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=
|
||||||
|
golang.org/x/exp v0.0.0-20231006140011-7918f672742d h1:jtJma62tbqLibJ5sFQz8bKtEM8rJBtfilJ2qTU199MI=
|
||||||
|
golang.org/x/exp v0.0.0-20231006140011-7918f672742d/go.mod h1:ldy0pHrwJyGW56pPQzzkH36rKxoZW1tw7ZJpeKx+hdo=
|
||||||
|
golang.org/x/sync v0.15.0 h1:KWH3jNZsfyT6xfAfKiz6MRNmd46ByHDYaZ7KSkCtdW8=
|
||||||
|
golang.org/x/sync v0.15.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
|
||||||
|
golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo=
|
||||||
|
golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||||
|
golang.org/x/term v0.41.0 h1:QCgPso/Q3RTJx2Th4bDLqML4W6iJiaXFq2/ftQF13YU=
|
||||||
|
golang.org/x/term v0.41.0/go.mod h1:3pfBgksrReYfZ5lvYM0kSO0LIkAl4Yl2bXOkKP7Ec2A=
|
||||||
|
golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY=
|
||||||
|
golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4=
|
||||||
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=
|
||||||
|
|||||||
244
install/input.go
244
install/input.go
@@ -1,74 +1,208 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bufio"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"strings"
|
"os"
|
||||||
"syscall"
|
"strconv"
|
||||||
|
|
||||||
|
"github.com/charmbracelet/huh"
|
||||||
"golang.org/x/term"
|
"golang.org/x/term"
|
||||||
)
|
)
|
||||||
|
|
||||||
func readString(reader *bufio.Reader, prompt string, defaultValue string) string {
|
// pangolinTheme is the custom theme using brand colors
|
||||||
|
var pangolinTheme = ThemePangolin()
|
||||||
|
|
||||||
|
// isAccessibleMode checks if we should use accessible mode (simple prompts)
|
||||||
|
// This is true for: non-TTY, TERM=dumb, or ACCESSIBLE env var set
|
||||||
|
func isAccessibleMode() bool {
|
||||||
|
// Check if stdin is not a terminal (piped input, CI, etc.)
|
||||||
|
if !term.IsTerminal(int(os.Stdin.Fd())) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
// Check for dumb terminal
|
||||||
|
if os.Getenv("TERM") == "dumb" {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
// Check for explicit accessible mode request
|
||||||
|
if os.Getenv("ACCESSIBLE") != "" {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// handleAbort checks if the error is a user abort (Ctrl+C) and exits if so
|
||||||
|
func handleAbort(err error) {
|
||||||
|
if err != nil && errors.Is(err, huh.ErrUserAborted) {
|
||||||
|
fmt.Println("\nInstallation cancelled.")
|
||||||
|
os.Exit(0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// runField runs a single field with the Pangolin theme, handling accessible mode
|
||||||
|
func runField(field huh.Field) error {
|
||||||
|
if isAccessibleMode() {
|
||||||
|
return field.RunAccessible(os.Stdout, os.Stdin)
|
||||||
|
}
|
||||||
|
form := huh.NewForm(huh.NewGroup(field)).WithTheme(pangolinTheme)
|
||||||
|
return form.Run()
|
||||||
|
}
|
||||||
|
|
||||||
|
func readString(prompt string, defaultValue string) string {
|
||||||
|
var value string
|
||||||
|
|
||||||
|
title := prompt
|
||||||
if defaultValue != "" {
|
if defaultValue != "" {
|
||||||
fmt.Printf("%s (default: %s): ", prompt, defaultValue)
|
title = fmt.Sprintf("%s (default: %s)", prompt, defaultValue)
|
||||||
} else {
|
|
||||||
fmt.Print(prompt + ": ")
|
|
||||||
}
|
|
||||||
input, _ := reader.ReadString('\n')
|
|
||||||
input = strings.TrimSpace(input)
|
|
||||||
if input == "" {
|
|
||||||
return defaultValue
|
|
||||||
}
|
|
||||||
return input
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func readStringNoDefault(reader *bufio.Reader, prompt string) string {
|
input := huh.NewInput().
|
||||||
fmt.Print(prompt + ": ")
|
Title(title).
|
||||||
input, _ := reader.ReadString('\n')
|
Value(&value)
|
||||||
return strings.TrimSpace(input)
|
|
||||||
|
// If no default value, this field is required
|
||||||
|
if defaultValue == "" {
|
||||||
|
input = input.Validate(func(s string) error {
|
||||||
|
if s == "" {
|
||||||
|
return fmt.Errorf("this field is required")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func readPassword(prompt string, reader *bufio.Reader) string {
|
err := runField(input)
|
||||||
if term.IsTerminal(int(syscall.Stdin)) {
|
handleAbort(err)
|
||||||
fmt.Print(prompt + ": ")
|
|
||||||
// Read password without echo if we're in a terminal
|
if value == "" {
|
||||||
password, err := term.ReadPassword(int(syscall.Stdin))
|
value = defaultValue
|
||||||
fmt.Println() // Add a newline since ReadPassword doesn't add one
|
|
||||||
if err != nil {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
input := strings.TrimSpace(string(password))
|
|
||||||
if input == "" {
|
|
||||||
return readPassword(prompt, reader)
|
|
||||||
}
|
|
||||||
return input
|
|
||||||
} else {
|
|
||||||
// Fallback to reading from stdin if not in a terminal
|
|
||||||
return readString(reader, prompt, "")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func readBool(reader *bufio.Reader, prompt string, defaultValue bool) bool {
|
// Print the answer so it remains visible in terminal history (skip in accessible mode as it already shows)
|
||||||
defaultStr := "no"
|
if !isAccessibleMode() {
|
||||||
if defaultValue {
|
fmt.Printf("%s: %s\n", prompt, value)
|
||||||
defaultStr = "yes"
|
|
||||||
}
|
|
||||||
input := readString(reader, prompt+" (yes/no)", defaultStr)
|
|
||||||
return strings.ToLower(input) == "yes"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func readBoolNoDefault(reader *bufio.Reader, prompt string) bool {
|
|
||||||
input := readStringNoDefault(reader, prompt+" (yes/no)")
|
|
||||||
return strings.ToLower(input) == "yes"
|
|
||||||
}
|
|
||||||
|
|
||||||
func readInt(reader *bufio.Reader, prompt string, defaultValue int) int {
|
|
||||||
input := readString(reader, prompt, fmt.Sprintf("%d", defaultValue))
|
|
||||||
if input == "" {
|
|
||||||
return defaultValue
|
|
||||||
}
|
|
||||||
value := defaultValue
|
|
||||||
fmt.Sscanf(input, "%d", &value)
|
|
||||||
return value
|
return value
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func readPassword(prompt string) string {
|
||||||
|
var value string
|
||||||
|
|
||||||
|
for {
|
||||||
|
input := huh.NewInput().
|
||||||
|
Title(prompt).
|
||||||
|
Value(&value).
|
||||||
|
EchoMode(huh.EchoModePassword).
|
||||||
|
Validate(func(s string) error {
|
||||||
|
if s == "" {
|
||||||
|
return fmt.Errorf("password is required")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
|
||||||
|
err := runField(input)
|
||||||
|
handleAbort(err)
|
||||||
|
|
||||||
|
if value != "" {
|
||||||
|
// Print confirmation without revealing the password
|
||||||
|
if !isAccessibleMode() {
|
||||||
|
fmt.Printf("%s: %s\n", prompt, "********")
|
||||||
|
}
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func readBool(prompt string, defaultValue bool) bool {
|
||||||
|
var value = defaultValue
|
||||||
|
|
||||||
|
confirm := huh.NewConfirm().
|
||||||
|
Title(prompt).
|
||||||
|
Value(&value).
|
||||||
|
Affirmative("Yes").
|
||||||
|
Negative("No")
|
||||||
|
|
||||||
|
err := runField(confirm)
|
||||||
|
handleAbort(err)
|
||||||
|
|
||||||
|
// Print the answer so it remains visible in terminal history
|
||||||
|
if !isAccessibleMode() {
|
||||||
|
answer := "No"
|
||||||
|
if value {
|
||||||
|
answer = "Yes"
|
||||||
|
}
|
||||||
|
fmt.Printf("%s: %s\n", prompt, answer)
|
||||||
|
}
|
||||||
|
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
|
||||||
|
func readBoolNoDefault(prompt string) bool {
|
||||||
|
var value bool
|
||||||
|
|
||||||
|
confirm := huh.NewConfirm().
|
||||||
|
Title(prompt).
|
||||||
|
Value(&value).
|
||||||
|
Affirmative("Yes").
|
||||||
|
Negative("No")
|
||||||
|
|
||||||
|
err := runField(confirm)
|
||||||
|
handleAbort(err)
|
||||||
|
|
||||||
|
// Print the answer so it remains visible in terminal history
|
||||||
|
if !isAccessibleMode() {
|
||||||
|
answer := "No"
|
||||||
|
if value {
|
||||||
|
answer = "Yes"
|
||||||
|
}
|
||||||
|
fmt.Printf("%s: %s\n", prompt, answer)
|
||||||
|
}
|
||||||
|
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
|
||||||
|
func readInt(prompt string, defaultValue int) int {
|
||||||
|
var value string
|
||||||
|
|
||||||
|
title := fmt.Sprintf("%s (default: %d)", prompt, defaultValue)
|
||||||
|
|
||||||
|
input := huh.NewInput().
|
||||||
|
Title(title).
|
||||||
|
Value(&value).
|
||||||
|
Validate(func(s string) error {
|
||||||
|
if s == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
_, err := strconv.Atoi(s)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("please enter a valid number")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
|
||||||
|
err := runField(input)
|
||||||
|
handleAbort(err)
|
||||||
|
|
||||||
|
if value == "" {
|
||||||
|
// Print the answer so it remains visible in terminal history
|
||||||
|
if !isAccessibleMode() {
|
||||||
|
fmt.Printf("%s: %d\n", prompt, defaultValue)
|
||||||
|
}
|
||||||
|
return defaultValue
|
||||||
|
}
|
||||||
|
|
||||||
|
result, err := strconv.Atoi(value)
|
||||||
|
if err != nil {
|
||||||
|
if !isAccessibleMode() {
|
||||||
|
fmt.Printf("%s: %d\n", prompt, defaultValue)
|
||||||
|
}
|
||||||
|
return defaultValue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Print the answer so it remains visible in terminal history
|
||||||
|
if !isAccessibleMode() {
|
||||||
|
fmt.Printf("%s: %d\n", prompt, result)
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|||||||
334
install/main.go
334
install/main.go
@@ -1,29 +1,35 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bufio"
|
"crypto/rand"
|
||||||
"embed"
|
"embed"
|
||||||
|
"encoding/base64"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"io/fs"
|
"io/fs"
|
||||||
"math/rand"
|
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
|
||||||
"net/url"
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"runtime"
|
"runtime"
|
||||||
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"text/template"
|
"text/template"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
// DO NOT EDIT THIS FUNCTION; IT MATCHED BY REGEX IN CICD
|
// Version variables injected at build time via -ldflags
|
||||||
|
var (
|
||||||
|
pangolinVersion string
|
||||||
|
gerbilVersion string
|
||||||
|
badgerVersion string
|
||||||
|
)
|
||||||
|
|
||||||
func loadVersions(config *Config) {
|
func loadVersions(config *Config) {
|
||||||
config.PangolinVersion = "replaceme"
|
config.PangolinVersion = pangolinVersion
|
||||||
config.GerbilVersion = "replaceme"
|
config.GerbilVersion = gerbilVersion
|
||||||
config.BadgerVersion = "replaceme"
|
config.BadgerVersion = badgerVersion
|
||||||
}
|
}
|
||||||
|
|
||||||
//go:embed config/*
|
//go:embed config/*
|
||||||
@@ -49,6 +55,7 @@ type Config struct {
|
|||||||
DoCrowdsecInstall bool
|
DoCrowdsecInstall bool
|
||||||
EnableGeoblocking bool
|
EnableGeoblocking bool
|
||||||
Secret string
|
Secret string
|
||||||
|
IsEnterprise bool
|
||||||
}
|
}
|
||||||
|
|
||||||
type SupportedContainer string
|
type SupportedContainer string
|
||||||
@@ -80,14 +87,19 @@ func main() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
reader := bufio.NewReader(os.Stdin)
|
|
||||||
|
|
||||||
var config Config
|
var config Config
|
||||||
var alreadyInstalled = false
|
var alreadyInstalled = false
|
||||||
|
|
||||||
|
// Determine installation directory
|
||||||
|
installDir := findOrSelectInstallDirectory()
|
||||||
|
if err := os.Chdir(installDir); err != nil {
|
||||||
|
fmt.Printf("Error changing to installation directory: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
// check if there is already a config file
|
// check if there is already a config file
|
||||||
if _, err := os.Stat("config/config.yml"); err != nil {
|
if _, err := os.Stat("config/config.yml"); err != nil {
|
||||||
config = collectUserInput(reader)
|
config = collectUserInput()
|
||||||
|
|
||||||
loadVersions(&config)
|
loadVersions(&config)
|
||||||
config.DoCrowdsecInstall = false
|
config.DoCrowdsecInstall = false
|
||||||
@@ -100,7 +112,10 @@ func main() {
|
|||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
moveFile("config/docker-compose.yml", "docker-compose.yml")
|
if err := moveFile("config/docker-compose.yml", "docker-compose.yml"); err != nil {
|
||||||
|
fmt.Printf("Error moving docker-compose.yml: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
fmt.Println("\nConfiguration files created successfully!")
|
fmt.Println("\nConfiguration files created successfully!")
|
||||||
|
|
||||||
@@ -115,13 +130,17 @@ func main() {
|
|||||||
|
|
||||||
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("Would you like to install and start the containers?", true) {
|
||||||
|
|
||||||
config.InstallationContainerType = podmanOrDocker(reader)
|
config.InstallationContainerType = podmanOrDocker()
|
||||||
|
|
||||||
if !isDockerInstalled() && runtime.GOOS == "linux" && config.InstallationContainerType == Docker {
|
if !isDockerInstalled() && runtime.GOOS == "linux" && config.InstallationContainerType == Docker {
|
||||||
if readBool(reader, "Docker is not installed. Would you like to install it?", true) {
|
if readBool("Docker is not installed. Would you like to install it?", true) {
|
||||||
installDocker()
|
if err := installDocker(); err != nil {
|
||||||
|
fmt.Printf("Error installing Docker: %v\n", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// try to start docker service but ignore errors
|
// try to start docker service but ignore errors
|
||||||
if err := startDockerService(); err != nil {
|
if err := startDockerService(); err != nil {
|
||||||
fmt.Println("Error starting Docker service:", err)
|
fmt.Println("Error starting Docker service:", err)
|
||||||
@@ -130,7 +149,7 @@ func main() {
|
|||||||
}
|
}
|
||||||
// wait 10 seconds for docker to start checking if docker is running every 2 seconds
|
// wait 10 seconds for docker to start checking if docker is running every 2 seconds
|
||||||
fmt.Println("Waiting for Docker to start...")
|
fmt.Println("Waiting for Docker to start...")
|
||||||
for i := 0; i < 5; i++ {
|
for range 5 {
|
||||||
if isDockerRunning() {
|
if isDockerRunning() {
|
||||||
fmt.Println("Docker is running!")
|
fmt.Println("Docker is running!")
|
||||||
break
|
break
|
||||||
@@ -165,7 +184,7 @@ func main() {
|
|||||||
fmt.Println("\n=== MaxMind Database Update ===")
|
fmt.Println("\n=== MaxMind Database Update ===")
|
||||||
if _, err := os.Stat("config/GeoLite2-Country.mmdb"); err == nil {
|
if _, err := os.Stat("config/GeoLite2-Country.mmdb"); err == nil {
|
||||||
fmt.Println("MaxMind GeoLite2 Country database found.")
|
fmt.Println("MaxMind GeoLite2 Country database found.")
|
||||||
if readBool(reader, "Would you like to update the MaxMind database to the latest version?", false) {
|
if readBool("Would you like to update the MaxMind database to the latest version?", false) {
|
||||||
if err := downloadMaxMindDatabase(); err != nil {
|
if err := downloadMaxMindDatabase(); err != nil {
|
||||||
fmt.Printf("Error updating MaxMind database: %v\n", err)
|
fmt.Printf("Error updating MaxMind database: %v\n", err)
|
||||||
fmt.Println("You can try updating it manually later if needed.")
|
fmt.Println("You can try updating it manually later if needed.")
|
||||||
@@ -173,13 +192,13 @@ func main() {
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
fmt.Println("MaxMind GeoLite2 Country database not found.")
|
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 readBool("Would you like to download the MaxMind GeoLite2 database for geoblocking functionality?", false) {
|
||||||
if err := downloadMaxMindDatabase(); err != nil {
|
if err := downloadMaxMindDatabase(); err != nil {
|
||||||
fmt.Printf("Error downloading MaxMind database: %v\n", err)
|
fmt.Printf("Error downloading MaxMind database: %v\n", err)
|
||||||
fmt.Println("You can try downloading it manually later if needed.")
|
fmt.Println("You can try downloading it manually later if needed.")
|
||||||
}
|
}
|
||||||
// Now you need to update your config file accordingly to enable geoblocking
|
// 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")
|
fmt.Print("Please remember to update your config/config.yml file to enable geoblocking! \n\n")
|
||||||
// add maxmind_db_path: "./config/GeoLite2-Country.mmdb" under server
|
// add maxmind_db_path: "./config/GeoLite2-Country.mmdb" under server
|
||||||
fmt.Println("Add the following line under the 'server' section:")
|
fmt.Println("Add the following line under the 'server' section:")
|
||||||
fmt.Println(" maxmind_db_path: \"./config/GeoLite2-Country.mmdb\"")
|
fmt.Println(" maxmind_db_path: \"./config/GeoLite2-Country.mmdb\"")
|
||||||
@@ -190,11 +209,11 @@ func main() {
|
|||||||
if !checkIsCrowdsecInstalledInCompose() {
|
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("Would you like to install CrowdSec?", false) {
|
||||||
fmt.Println("This installer constitutes a minimal viable CrowdSec deployment. CrowdSec will add extra complexity to your Pangolin installation and may not work to the best of its abilities out of the box. Users are expected to implement configuration adjustments on their own to achieve the best security posture. Consult the CrowdSec documentation for detailed configuration instructions.")
|
fmt.Println("This installer constitutes a minimal viable CrowdSec deployment. CrowdSec will add extra complexity to your Pangolin installation and may not work to the best of its abilities out of the box. Users are expected to implement configuration adjustments on their own to achieve the best security posture. Consult the CrowdSec documentation for detailed configuration instructions.")
|
||||||
|
|
||||||
// BUG: crowdsec installation will be skipped if the user chooses to install on the first installation.
|
// BUG: crowdsec installation will be skipped if the user chooses to install on the first installation.
|
||||||
if readBool(reader, "Are you willing to manage CrowdSec?", false) {
|
if readBool("Are you willing to manage CrowdSec?", false) {
|
||||||
if config.DashboardDomain == "" {
|
if config.DashboardDomain == "" {
|
||||||
traefikConfig, err := ReadTraefikConfig("config/traefik/traefik_config.yml")
|
traefikConfig, err := ReadTraefikConfig("config/traefik/traefik_config.yml")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -223,12 +242,21 @@ func main() {
|
|||||||
fmt.Printf("Let's Encrypt Email: %s\n", config.LetsEncryptEmail)
|
fmt.Printf("Let's Encrypt Email: %s\n", config.LetsEncryptEmail)
|
||||||
fmt.Printf("Badger Version: %s\n", config.BadgerVersion)
|
fmt.Printf("Badger Version: %s\n", config.BadgerVersion)
|
||||||
|
|
||||||
if !readBool(reader, "Are these values correct?", true) {
|
if !readBool("Are these values correct?", true) {
|
||||||
config = collectUserInput(reader)
|
config = collectUserInput()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
config.InstallationContainerType = podmanOrDocker(reader)
|
// Try to detect container type from existing installation
|
||||||
|
detectedType := detectContainerType()
|
||||||
|
if detectedType == Undefined {
|
||||||
|
// If detection fails, prompt the user
|
||||||
|
fmt.Println("Unable to detect container type from existing installation.")
|
||||||
|
config.InstallationContainerType = podmanOrDocker()
|
||||||
|
} else {
|
||||||
|
config.InstallationContainerType = detectedType
|
||||||
|
fmt.Printf("Detected container type: %s\n", config.InstallationContainerType)
|
||||||
|
}
|
||||||
|
|
||||||
config.DoCrowdsecInstall = true
|
config.DoCrowdsecInstall = true
|
||||||
err := installCrowdsec(config)
|
err := installCrowdsec(config)
|
||||||
@@ -266,8 +294,119 @@ func main() {
|
|||||||
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 hasExistingInstall(dir string) bool {
|
||||||
inputContainer := readString(reader, "Would you like to run Pangolin as Docker or Podman containers?", "docker")
|
configPath := filepath.Join(dir, "config", "config.yml")
|
||||||
|
_, err := os.Stat(configPath)
|
||||||
|
return err == nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func findOrSelectInstallDirectory() string {
|
||||||
|
const defaultInstallDir = "/opt/pangolin"
|
||||||
|
|
||||||
|
// Get current working directory
|
||||||
|
cwd, err := os.Getwd()
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("Error getting current directory: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 1. Check current directory for existing install
|
||||||
|
if hasExistingInstall(cwd) {
|
||||||
|
fmt.Printf("Found existing Pangolin installation in current directory: %s\n", cwd)
|
||||||
|
return cwd
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Check default location (/opt/pangolin) for existing install
|
||||||
|
if cwd != defaultInstallDir && hasExistingInstall(defaultInstallDir) {
|
||||||
|
fmt.Printf("\nFound existing Pangolin installation at: %s\n", defaultInstallDir)
|
||||||
|
if readBool(fmt.Sprintf("Would you like to use the existing installation at %s?", defaultInstallDir), true) {
|
||||||
|
return defaultInstallDir
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. No existing install found, prompt for installation directory
|
||||||
|
fmt.Println("\n=== Installation Directory ===")
|
||||||
|
fmt.Println("No existing Pangolin installation detected.")
|
||||||
|
|
||||||
|
installDir := readString("Enter the installation directory", defaultInstallDir)
|
||||||
|
|
||||||
|
// Expand ~ to home directory if present
|
||||||
|
if strings.HasPrefix(installDir, "~") {
|
||||||
|
home, err := os.UserHomeDir()
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("Error getting home directory: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
installDir = filepath.Join(home, installDir[1:])
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert to absolute path
|
||||||
|
absPath, err := filepath.Abs(installDir)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("Error resolving path: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
installDir = absPath
|
||||||
|
|
||||||
|
// Check if directory exists
|
||||||
|
if _, err := os.Stat(installDir); os.IsNotExist(err) {
|
||||||
|
// Directory doesn't exist, create it
|
||||||
|
if readBool(fmt.Sprintf("Directory %s does not exist. Create it?", installDir), true) {
|
||||||
|
if err := os.MkdirAll(installDir, 0755); err != nil {
|
||||||
|
fmt.Printf("Error creating directory: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
fmt.Printf("Created directory: %s\n", installDir)
|
||||||
|
|
||||||
|
// Offer to change ownership if running via sudo
|
||||||
|
changeDirectoryOwnership(installDir)
|
||||||
|
} else {
|
||||||
|
fmt.Println("Installation cancelled.")
|
||||||
|
os.Exit(0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("Installation directory: %s\n", installDir)
|
||||||
|
return installDir
|
||||||
|
}
|
||||||
|
|
||||||
|
func changeDirectoryOwnership(dir string) {
|
||||||
|
// Check if we're running via sudo by looking for SUDO_USER
|
||||||
|
sudoUser := os.Getenv("SUDO_USER")
|
||||||
|
if sudoUser == "" || os.Geteuid() != 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
sudoUID := os.Getenv("SUDO_UID")
|
||||||
|
sudoGID := os.Getenv("SUDO_GID")
|
||||||
|
|
||||||
|
if sudoUID == "" || sudoGID == "" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("\nRunning as root via sudo (original user: %s)\n", sudoUser)
|
||||||
|
if readBool(fmt.Sprintf("Would you like to change ownership of %s to user '%s'? This makes it easier to manage config files without sudo.", dir, sudoUser), true) {
|
||||||
|
uid, err := strconv.Atoi(sudoUID)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("Warning: Could not parse SUDO_UID: %v\n", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
gid, err := strconv.Atoi(sudoGID)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("Warning: Could not parse SUDO_GID: %v\n", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := os.Chown(dir, uid, gid); err != nil {
|
||||||
|
fmt.Printf("Warning: Could not change ownership: %v\n", err)
|
||||||
|
} else {
|
||||||
|
fmt.Printf("Changed ownership of %s to %s\n", dir, sudoUser)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func podmanOrDocker() SupportedContainer {
|
||||||
|
inputContainer := readString("Would you like to run Pangolin as Docker or Podman containers?", "docker")
|
||||||
|
|
||||||
chosenContainer := Docker
|
chosenContainer := Docker
|
||||||
if strings.EqualFold(inputContainer, "docker") {
|
if strings.EqualFold(inputContainer, "docker") {
|
||||||
@@ -279,16 +418,17 @@ func podmanOrDocker(reader *bufio.Reader) SupportedContainer {
|
|||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
if chosenContainer == Podman {
|
switch chosenContainer {
|
||||||
|
case Podman:
|
||||||
if !isPodmanInstalled() {
|
if !isPodmanInstalled() {
|
||||||
fmt.Println("Podman or podman-compose is not installed. Please install both manually. Automated installation will be available in a later release.")
|
fmt.Println("Podman or podman-compose is not installed. Please install both manually. Automated installation will be available in a later release.")
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := exec.Command("bash", "-c", "cat /etc/sysctl.conf | grep 'net.ipv4.ip_unprivileged_port_start='").Run(); err != nil {
|
if err := exec.Command("bash", "-c", "cat /etc/sysctl.d/99-podman.conf 2>/dev/null | grep 'net.ipv4.ip_unprivileged_port_start=' || cat /etc/sysctl.conf 2>/dev/null | grep 'net.ipv4.ip_unprivileged_port_start='").Run(); err != nil {
|
||||||
fmt.Println("Would you like to configure ports >= 80 as unprivileged ports? This enables podman containers to listen on low-range ports.")
|
fmt.Println("Would you like to configure ports >= 80 as unprivileged ports? This enables podman containers to listen on low-range ports.")
|
||||||
fmt.Println("Pangolin will experience startup issues if this is not configured, because it needs to listen on port 80/443 by default.")
|
fmt.Println("Pangolin will experience startup issues if this is not configured, because it needs to listen on port 80/443 by default.")
|
||||||
approved := readBool(reader, "The installer is about to execute \"echo 'net.ipv4.ip_unprivileged_port_start=80' >> /etc/sysctl.conf && sysctl -p\". Approve?", true)
|
approved := readBool("The installer is about to execute \"echo 'net.ipv4.ip_unprivileged_port_start=80' > /etc/sysctl.d/99-podman.conf && sysctl --system\". Approve?", true)
|
||||||
if approved {
|
if approved {
|
||||||
if os.Geteuid() != 0 {
|
if os.Geteuid() != 0 {
|
||||||
fmt.Println("You need to run the installer as root for such a configuration.")
|
fmt.Println("You need to run the installer as root for such a configuration.")
|
||||||
@@ -299,8 +439,8 @@ func podmanOrDocker(reader *bufio.Reader) SupportedContainer {
|
|||||||
// container low-range ports as unprivileged ports.
|
// container low-range ports as unprivileged ports.
|
||||||
// Linux only.
|
// Linux only.
|
||||||
|
|
||||||
if err := run("bash", "-c", "echo 'net.ipv4.ip_unprivileged_port_start=80' >> /etc/sysctl.conf && sysctl -p"); err != nil {
|
if err := run("bash", "-c", "echo 'net.ipv4.ip_unprivileged_port_start=80' > /etc/sysctl.d/99-podman.conf && sysctl --system"); err != nil {
|
||||||
fmt.Sprintf("failed to configure unprivileged ports: %v.\n", err)
|
fmt.Printf("Error configuring unprivileged ports: %v\n", err)
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -310,7 +450,7 @@ func podmanOrDocker(reader *bufio.Reader) SupportedContainer {
|
|||||||
fmt.Println("Unprivileged ports have been configured.")
|
fmt.Println("Unprivileged ports have been configured.")
|
||||||
}
|
}
|
||||||
|
|
||||||
} else if chosenContainer == Docker {
|
case Docker:
|
||||||
// check if docker is not installed and the user is root
|
// check if docker is not installed and the user is root
|
||||||
if !isDockerInstalled() {
|
if !isDockerInstalled() {
|
||||||
if os.Geteuid() != 0 {
|
if os.Geteuid() != 0 {
|
||||||
@@ -325,7 +465,7 @@ func podmanOrDocker(reader *bufio.Reader) SupportedContainer {
|
|||||||
fmt.Println("The installer will not be able to run docker commands without running it as root.")
|
fmt.Println("The installer will not be able to run docker commands without running it as root.")
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
} else {
|
default:
|
||||||
// This shouldn't happen unless there's a third container runtime.
|
// This shouldn't happen unless there's a third container runtime.
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
@@ -333,33 +473,35 @@ func podmanOrDocker(reader *bufio.Reader) SupportedContainer {
|
|||||||
return chosenContainer
|
return chosenContainer
|
||||||
}
|
}
|
||||||
|
|
||||||
func collectUserInput(reader *bufio.Reader) Config {
|
func collectUserInput() Config {
|
||||||
config := Config{}
|
config := Config{}
|
||||||
|
|
||||||
// Basic configuration
|
// Basic configuration
|
||||||
fmt.Println("\n=== Basic Configuration ===")
|
fmt.Println("\n=== Basic Configuration ===")
|
||||||
|
|
||||||
config.BaseDomain = readString(reader, "Enter your base domain (no subdomain e.g. example.com)", "")
|
config.IsEnterprise = readBoolNoDefault("Do you want to install the Enterprise version of Pangolin? The EE is free for personal use or for businesses making less than 100k USD annually.")
|
||||||
|
|
||||||
|
config.BaseDomain = readString("Enter your base domain (no subdomain e.g. example.com)", "")
|
||||||
|
|
||||||
// Set default dashboard domain after base domain is collected
|
// Set default dashboard domain after base domain is collected
|
||||||
defaultDashboardDomain := ""
|
defaultDashboardDomain := ""
|
||||||
if config.BaseDomain != "" {
|
if config.BaseDomain != "" {
|
||||||
defaultDashboardDomain = "pangolin." + config.BaseDomain
|
defaultDashboardDomain = "pangolin." + config.BaseDomain
|
||||||
}
|
}
|
||||||
config.DashboardDomain = readString(reader, "Enter the domain for the Pangolin dashboard", defaultDashboardDomain)
|
config.DashboardDomain = readString("Enter the domain for the Pangolin dashboard", defaultDashboardDomain)
|
||||||
config.LetsEncryptEmail = readString(reader, "Enter email for Let's Encrypt certificates", "")
|
config.LetsEncryptEmail = readString("Enter email for Let's Encrypt certificates", "")
|
||||||
config.InstallGerbil = readBool(reader, "Do you want to use Gerbil to allow tunneled connections", true)
|
config.InstallGerbil = readBool("Do you want to use Gerbil to allow tunneled connections", true)
|
||||||
|
|
||||||
// Email configuration
|
// Email configuration
|
||||||
fmt.Println("\n=== Email Configuration ===")
|
fmt.Println("\n=== Email Configuration ===")
|
||||||
config.EnableEmail = readBool(reader, "Enable email functionality (SMTP)", false)
|
config.EnableEmail = readBool("Enable email functionality (SMTP)", false)
|
||||||
|
|
||||||
if config.EnableEmail {
|
if config.EnableEmail {
|
||||||
config.EmailSMTPHost = readString(reader, "Enter SMTP host", "")
|
config.EmailSMTPHost = readString("Enter SMTP host", "")
|
||||||
config.EmailSMTPPort = readInt(reader, "Enter SMTP port (default 587)", 587)
|
config.EmailSMTPPort = readInt("Enter SMTP port (default 587)", 587)
|
||||||
config.EmailSMTPUser = readString(reader, "Enter SMTP username", "")
|
config.EmailSMTPUser = readString("Enter SMTP username", "")
|
||||||
config.EmailSMTPPass = readString(reader, "Enter SMTP password", "") // Should this be readPassword?
|
config.EmailSMTPPass = readPassword("Enter SMTP password")
|
||||||
config.EmailNoReply = readString(reader, "Enter no-reply email address (often the same as SMTP username)", "")
|
config.EmailNoReply = readString("Enter no-reply email address (often the same as SMTP username)", "")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate required fields
|
// Validate required fields
|
||||||
@@ -380,8 +522,8 @@ 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("Is your server IPv6 capable?", true)
|
||||||
config.EnableGeoblocking = readBool(reader, "Do you want to download the MaxMind GeoLite2 database for geoblocking functionality?", true)
|
config.EnableGeoblocking = readBool("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")
|
||||||
@@ -392,15 +534,23 @@ func collectUserInput(reader *bufio.Reader) Config {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func createConfigFiles(config Config) error {
|
func createConfigFiles(config Config) error {
|
||||||
os.MkdirAll("config", 0755)
|
if err := os.MkdirAll("config", 0755); err != nil {
|
||||||
os.MkdirAll("config/letsencrypt", 0755)
|
return fmt.Errorf("failed to create config directory: %v", err)
|
||||||
os.MkdirAll("config/db", 0755)
|
}
|
||||||
os.MkdirAll("config/logs", 0755)
|
if err := os.MkdirAll("config/letsencrypt", 0755); err != nil {
|
||||||
|
return fmt.Errorf("failed to create letsencrypt directory: %v", err)
|
||||||
|
}
|
||||||
|
if err := os.MkdirAll("config/db", 0755); err != nil {
|
||||||
|
return fmt.Errorf("failed to create db directory: %v", err)
|
||||||
|
}
|
||||||
|
if err := os.MkdirAll("config/logs", 0755); err != nil {
|
||||||
|
return fmt.Errorf("failed to create logs directory: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
// Walk through all embedded files
|
// Walk through all embedded files
|
||||||
err := fs.WalkDir(configFiles, "config", func(path string, d fs.DirEntry, err error) error {
|
err := fs.WalkDir(configFiles, "config", func(path string, d fs.DirEntry, walkErr error) (err error) {
|
||||||
if err != nil {
|
if walkErr != nil {
|
||||||
return err
|
return walkErr
|
||||||
}
|
}
|
||||||
|
|
||||||
// Skip the root fs directory itself
|
// Skip the root fs directory itself
|
||||||
@@ -451,7 +601,11 @@ func createConfigFiles(config Config) error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to create %s: %v", path, err)
|
return fmt.Errorf("failed to create %s: %v", path, err)
|
||||||
}
|
}
|
||||||
defer outFile.Close()
|
defer func() {
|
||||||
|
if cerr := outFile.Close(); cerr != nil && err == nil {
|
||||||
|
err = cerr
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
// Execute template
|
// Execute template
|
||||||
if err := tmpl.Execute(outFile, config); err != nil {
|
if err := tmpl.Execute(outFile, config); err != nil {
|
||||||
@@ -467,18 +621,26 @@ func createConfigFiles(config Config) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func copyFile(src, dst string) error {
|
func copyFile(src, dst string) (err error) {
|
||||||
source, err := os.Open(src)
|
source, err := os.Open(src)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
defer source.Close()
|
defer func() {
|
||||||
|
if cerr := source.Close(); cerr != nil && err == nil {
|
||||||
|
err = cerr
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
destination, err := os.Create(dst)
|
destination, err := os.Create(dst)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
defer destination.Close()
|
defer func() {
|
||||||
|
if cerr := destination.Close(); cerr != nil && err == nil {
|
||||||
|
err = cerr
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
_, err = io.Copy(destination, source)
|
_, err = io.Copy(destination, source)
|
||||||
return err
|
return err
|
||||||
@@ -549,22 +711,24 @@ func showSetupTokenInstructions(containerType SupportedContainer, dashboardDomai
|
|||||||
fmt.Println("To get your setup token, you need to:")
|
fmt.Println("To get your setup token, you need to:")
|
||||||
fmt.Println("")
|
fmt.Println("")
|
||||||
fmt.Println("1. Start the containers")
|
fmt.Println("1. Start the containers")
|
||||||
if containerType == Docker {
|
switch containerType {
|
||||||
|
case Docker:
|
||||||
fmt.Println(" docker compose up -d")
|
fmt.Println(" docker compose up -d")
|
||||||
} else if containerType == Podman {
|
case Podman:
|
||||||
fmt.Println(" podman-compose up -d")
|
fmt.Println(" podman-compose up -d")
|
||||||
} else {
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Println("")
|
fmt.Println("")
|
||||||
fmt.Println("2. Wait for the Pangolin container to start and generate the token")
|
fmt.Println("2. Wait for the Pangolin container to start and generate the token")
|
||||||
fmt.Println("")
|
fmt.Println("")
|
||||||
fmt.Println("3. Check the container logs for the setup token")
|
fmt.Println("3. Check the container logs for the setup token")
|
||||||
if containerType == Docker {
|
switch containerType {
|
||||||
|
case Docker:
|
||||||
fmt.Println(" docker logs pangolin | grep -A 2 -B 2 'SETUP TOKEN'")
|
fmt.Println(" docker logs pangolin | grep -A 2 -B 2 'SETUP TOKEN'")
|
||||||
} else if containerType == Podman {
|
case Podman:
|
||||||
fmt.Println(" podman logs pangolin | grep -A 2 -B 2 'SETUP TOKEN'")
|
fmt.Println(" podman logs pangolin | grep -A 2 -B 2 'SETUP TOKEN'")
|
||||||
} else {
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Println("")
|
fmt.Println("")
|
||||||
fmt.Println("4. Look for output like")
|
fmt.Println("4. Look for output like")
|
||||||
fmt.Println(" === SETUP TOKEN GENERATED ===")
|
fmt.Println(" === SETUP TOKEN GENERATED ===")
|
||||||
@@ -580,43 +744,12 @@ func showSetupTokenInstructions(containerType SupportedContainer, dashboardDomai
|
|||||||
}
|
}
|
||||||
|
|
||||||
func generateRandomSecretKey() string {
|
func generateRandomSecretKey() string {
|
||||||
const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
|
secret := make([]byte, 32)
|
||||||
const length = 32
|
_, err := rand.Read(secret)
|
||||||
|
|
||||||
var seededRand *rand.Rand = rand.New(
|
|
||||||
rand.NewSource(time.Now().UnixNano()))
|
|
||||||
|
|
||||||
b := make([]byte, length)
|
|
||||||
for i := range b {
|
|
||||||
b[i] = charset[seededRand.Intn(len(charset))]
|
|
||||||
}
|
|
||||||
return string(b)
|
|
||||||
}
|
|
||||||
|
|
||||||
func getPublicIP() string {
|
|
||||||
client := &http.Client{
|
|
||||||
Timeout: 10 * time.Second,
|
|
||||||
}
|
|
||||||
|
|
||||||
resp, err := client.Get("https://ifconfig.io/ip")
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return ""
|
panic(fmt.Sprintf("Failed to generate random secret key: %v", err))
|
||||||
}
|
}
|
||||||
defer resp.Body.Close()
|
return base64.StdEncoding.EncodeToString(secret)
|
||||||
|
|
||||||
body, err := io.ReadAll(resp.Body)
|
|
||||||
if err != nil {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
|
|
||||||
ip := strings.TrimSpace(string(body))
|
|
||||||
|
|
||||||
// Validate that it's a valid IP address
|
|
||||||
if net.ParseIP(ip) != nil {
|
|
||||||
return ip
|
|
||||||
}
|
|
||||||
|
|
||||||
return ""
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Run external commands with stdio/stderr attached.
|
// Run external commands with stdio/stderr attached.
|
||||||
@@ -631,10 +764,7 @@ func checkPortsAvailable(port int) error {
|
|||||||
addr := fmt.Sprintf(":%d", port)
|
addr := fmt.Sprintf(":%d", port)
|
||||||
ln, err := net.Listen("tcp", addr)
|
ln, err := net.Listen("tcp", addr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf(
|
return fmt.Errorf("ERROR: port %d is occupied or cannot be bound: %w", port, err)
|
||||||
"ERROR: port %d is occupied or cannot be bound: %w\n\n",
|
|
||||||
port, err,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
if closeErr := ln.Close(); closeErr != nil {
|
if closeErr := ln.Close(); closeErr != nil {
|
||||||
fmt.Fprintf(os.Stderr,
|
fmt.Fprintf(os.Stderr,
|
||||||
|
|||||||
51
install/theme.go
Normal file
51
install/theme.go
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/charmbracelet/huh"
|
||||||
|
"github.com/charmbracelet/lipgloss"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Pangolin brand colors (converted from oklch to hex)
|
||||||
|
var (
|
||||||
|
// Primary orange/amber - oklch(0.6717 0.1946 41.93)
|
||||||
|
primaryColor = lipgloss.AdaptiveColor{Light: "#D97706", Dark: "#F59E0B"}
|
||||||
|
// Muted foreground
|
||||||
|
mutedColor = lipgloss.AdaptiveColor{Light: "#737373", Dark: "#A3A3A3"}
|
||||||
|
// Success green
|
||||||
|
successColor = lipgloss.AdaptiveColor{Light: "#16A34A", Dark: "#22C55E"}
|
||||||
|
// Error red - oklch(0.577 0.245 27.325)
|
||||||
|
errorColor = lipgloss.AdaptiveColor{Light: "#DC2626", Dark: "#EF4444"}
|
||||||
|
// Normal text
|
||||||
|
normalFg = lipgloss.AdaptiveColor{Light: "#171717", Dark: "#FAFAFA"}
|
||||||
|
)
|
||||||
|
|
||||||
|
// ThemePangolin returns a huh theme using Pangolin brand colors
|
||||||
|
func ThemePangolin() *huh.Theme {
|
||||||
|
t := huh.ThemeBase()
|
||||||
|
|
||||||
|
// Focused state styles
|
||||||
|
t.Focused.Base = t.Focused.Base.BorderForeground(primaryColor)
|
||||||
|
t.Focused.Title = t.Focused.Title.Foreground(primaryColor).Bold(true)
|
||||||
|
t.Focused.Description = t.Focused.Description.Foreground(mutedColor)
|
||||||
|
t.Focused.ErrorIndicator = t.Focused.ErrorIndicator.Foreground(errorColor)
|
||||||
|
t.Focused.ErrorMessage = t.Focused.ErrorMessage.Foreground(errorColor)
|
||||||
|
t.Focused.SelectSelector = t.Focused.SelectSelector.Foreground(primaryColor)
|
||||||
|
t.Focused.NextIndicator = t.Focused.NextIndicator.Foreground(primaryColor)
|
||||||
|
t.Focused.PrevIndicator = t.Focused.PrevIndicator.Foreground(primaryColor)
|
||||||
|
t.Focused.Option = t.Focused.Option.Foreground(normalFg)
|
||||||
|
t.Focused.SelectedOption = t.Focused.SelectedOption.Foreground(primaryColor)
|
||||||
|
t.Focused.SelectedPrefix = lipgloss.NewStyle().Foreground(successColor).SetString("✓ ")
|
||||||
|
t.Focused.UnselectedPrefix = lipgloss.NewStyle().Foreground(mutedColor).SetString(" ")
|
||||||
|
t.Focused.FocusedButton = t.Focused.FocusedButton.Foreground(lipgloss.Color("#FFFFFF")).Background(primaryColor)
|
||||||
|
t.Focused.BlurredButton = t.Focused.BlurredButton.Foreground(normalFg).Background(lipgloss.AdaptiveColor{Light: "#E5E5E5", Dark: "#404040"})
|
||||||
|
t.Focused.TextInput.Cursor = t.Focused.TextInput.Cursor.Foreground(primaryColor)
|
||||||
|
t.Focused.TextInput.Prompt = t.Focused.TextInput.Prompt.Foreground(primaryColor)
|
||||||
|
|
||||||
|
// Blurred state inherits from focused but with hidden border
|
||||||
|
t.Blurred = t.Focused
|
||||||
|
t.Blurred.Base = t.Focused.Base.BorderStyle(lipgloss.HiddenBorder())
|
||||||
|
t.Blurred.Title = t.Blurred.Title.Foreground(mutedColor).Bold(false)
|
||||||
|
t.Blurred.TextInput.Prompt = t.Blurred.TextInput.Prompt.Foreground(mutedColor)
|
||||||
|
|
||||||
|
return t
|
||||||
|
}
|
||||||
115
license.py
Normal file
115
license.py
Normal file
@@ -0,0 +1,115 @@
|
|||||||
|
import os
|
||||||
|
import sys
|
||||||
|
|
||||||
|
# --- Configuration ---
|
||||||
|
# The header text to be added to the files.
|
||||||
|
HEADER_TEXT = """/*
|
||||||
|
* 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.
|
||||||
|
*/
|
||||||
|
"""
|
||||||
|
|
||||||
|
def should_add_header(file_path):
|
||||||
|
"""
|
||||||
|
Checks if a file should receive the commercial license header.
|
||||||
|
Returns True if 'private' is in the path or file content.
|
||||||
|
"""
|
||||||
|
# Check if 'private' is in the file path (case-insensitive)
|
||||||
|
if 'server/private' in file_path.lower():
|
||||||
|
return True
|
||||||
|
|
||||||
|
# Check if 'private' is in the file content (case-insensitive)
|
||||||
|
# try:
|
||||||
|
# with open(file_path, 'r', encoding='utf-8', errors='ignore') as f:
|
||||||
|
# content = f.read()
|
||||||
|
# if 'private' in content.lower():
|
||||||
|
# return True
|
||||||
|
# except Exception as e:
|
||||||
|
# print(f"Could not read file {file_path}: {e}")
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
|
def process_directory(root_dir):
|
||||||
|
"""
|
||||||
|
Recursively scans a directory and adds headers to qualifying .ts or .tsx files,
|
||||||
|
skipping any 'node_modules' directories.
|
||||||
|
"""
|
||||||
|
print(f"Scanning directory: {root_dir}")
|
||||||
|
files_processed = 0
|
||||||
|
headers_added = 0
|
||||||
|
|
||||||
|
for root, dirs, files in os.walk(root_dir):
|
||||||
|
# --- MODIFICATION ---
|
||||||
|
# Exclude 'node_modules' directories from the scan to improve performance.
|
||||||
|
if 'node_modules' in dirs:
|
||||||
|
dirs.remove('node_modules')
|
||||||
|
|
||||||
|
for file in files:
|
||||||
|
if file.endswith('.ts') or file.endswith('.tsx'):
|
||||||
|
file_path = os.path.join(root, file)
|
||||||
|
files_processed += 1
|
||||||
|
|
||||||
|
try:
|
||||||
|
with open(file_path, 'r+', encoding='utf-8') as f:
|
||||||
|
original_content = f.read()
|
||||||
|
has_header = original_content.startswith(HEADER_TEXT.strip())
|
||||||
|
|
||||||
|
if should_add_header(file_path):
|
||||||
|
# Add header only if it's not already there
|
||||||
|
if not has_header:
|
||||||
|
f.seek(0, 0) # Go to the beginning of the file
|
||||||
|
f.write(HEADER_TEXT.strip() + '\n\n' + original_content)
|
||||||
|
print(f"Added header to: {file_path}")
|
||||||
|
headers_added += 1
|
||||||
|
else:
|
||||||
|
print(f"Header already exists in: {file_path}")
|
||||||
|
else:
|
||||||
|
# Remove header if it exists but shouldn't be there
|
||||||
|
if has_header:
|
||||||
|
# Find the end of the header and remove it (including following newlines)
|
||||||
|
header_with_newlines = HEADER_TEXT.strip() + '\n\n'
|
||||||
|
if original_content.startswith(header_with_newlines):
|
||||||
|
content_without_header = original_content[len(header_with_newlines):]
|
||||||
|
else:
|
||||||
|
# Handle case where there might be different newline patterns
|
||||||
|
header_end = len(HEADER_TEXT.strip())
|
||||||
|
# Skip any newlines after the header
|
||||||
|
while header_end < len(original_content) and original_content[header_end] in '\n\r':
|
||||||
|
header_end += 1
|
||||||
|
content_without_header = original_content[header_end:]
|
||||||
|
|
||||||
|
f.seek(0)
|
||||||
|
f.write(content_without_header)
|
||||||
|
f.truncate()
|
||||||
|
print(f"Removed header from: {file_path}")
|
||||||
|
headers_added += 1 # Reusing counter for modifications
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error processing file {file_path}: {e}")
|
||||||
|
|
||||||
|
print("\n--- Scan Complete ---")
|
||||||
|
print(f"Total .ts or .tsx files found: {files_processed}")
|
||||||
|
print(f"Files modified (headers added/removed): {headers_added}")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
# Get the target directory from the command line arguments.
|
||||||
|
# If no directory is provided, it uses the current directory ('.').
|
||||||
|
if len(sys.argv) > 1:
|
||||||
|
target_directory = sys.argv[1]
|
||||||
|
else:
|
||||||
|
target_directory = '.' # Default to current directory
|
||||||
|
|
||||||
|
if not os.path.isdir(target_directory):
|
||||||
|
print(f"Error: Directory '{target_directory}' not found.")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
process_directory(os.path.abspath(target_directory))
|
||||||
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
@@ -1,5 +1,7 @@
|
|||||||
{
|
{
|
||||||
"setupCreate": "Create the organization, site, and resources",
|
"setupCreate": "Create the organization, site, and resources",
|
||||||
|
"headerAuthCompatibilityInfo": "Enable this to force a 401 Unauthorized response when an authentication token is missing. This is required for browsers or specific HTTP libraries that do not send credentials without a server challenge.",
|
||||||
|
"headerAuthCompatibility": "Extended compatibility",
|
||||||
"setupNewOrg": "New Organization",
|
"setupNewOrg": "New Organization",
|
||||||
"setupCreateOrg": "Create Organization",
|
"setupCreateOrg": "Create Organization",
|
||||||
"setupCreateResources": "Create Resources",
|
"setupCreateResources": "Create Resources",
|
||||||
@@ -16,6 +18,8 @@
|
|||||||
"componentsMember": "You're a member of {count, plural, =0 {no organization} one {one organization} other {# organizations}}.",
|
"componentsMember": "You're a member of {count, plural, =0 {no organization} one {one organization} other {# organizations}}.",
|
||||||
"componentsInvalidKey": "Invalid or expired license keys detected. Follow license terms to continue using all features.",
|
"componentsInvalidKey": "Invalid or expired license keys detected. Follow license terms to continue using all features.",
|
||||||
"dismiss": "Dismiss",
|
"dismiss": "Dismiss",
|
||||||
|
"subscriptionViolationMessage": "You're beyond your limits for your current plan. Correct the problem by removing sites, users, or other resources to stay within your plan.",
|
||||||
|
"subscriptionViolationViewBilling": "View billing",
|
||||||
"componentsLicenseViolation": "License Violation: This server is using {usedSites} sites which exceeds its licensed limit of {maxSites} sites. Follow license terms to continue using all features.",
|
"componentsLicenseViolation": "License Violation: This server is using {usedSites} sites which exceeds its licensed limit of {maxSites} sites. Follow license terms to continue using all features.",
|
||||||
"componentsSupporterMessage": "Thank you for supporting Pangolin as a {tier}!",
|
"componentsSupporterMessage": "Thank you for supporting Pangolin as a {tier}!",
|
||||||
"inviteErrorNotValid": "We're sorry, but it looks like the invite you're trying to access has not been accepted or is no longer valid.",
|
"inviteErrorNotValid": "We're sorry, but it looks like the invite you're trying to access has not been accepted or is no longer valid.",
|
||||||
@@ -53,7 +57,10 @@
|
|||||||
"siteDescription": "Create and manage sites to enable connectivity to private networks",
|
"siteDescription": "Create and manage sites to enable connectivity to private networks",
|
||||||
"sitesBannerTitle": "Connect Any Network",
|
"sitesBannerTitle": "Connect Any Network",
|
||||||
"sitesBannerDescription": "A site is a connection to a remote network that allows Pangolin to provide access to resources, whether public or private, to users anywhere. Install the site network connector (Newt) anywhere you can run a binary or container to establish the connection.",
|
"sitesBannerDescription": "A site is a connection to a remote network that allows Pangolin to provide access to resources, whether public or private, to users anywhere. Install the site network connector (Newt) anywhere you can run a binary or container to establish the connection.",
|
||||||
"sitesBannerButtonText": "Install Site",
|
"sitesBannerButtonText": "Install Site Connector",
|
||||||
|
"approvalsBannerTitle": "Approve or Deny Device Access",
|
||||||
|
"approvalsBannerDescription": "Review and approve or deny device access requests from users. When device approvals are required, users must get admin approval before their devices can connect to your organization's resources.",
|
||||||
|
"approvalsBannerButtonText": "Learn More",
|
||||||
"siteCreate": "Create Site",
|
"siteCreate": "Create Site",
|
||||||
"siteCreateDescription2": "Follow the steps below to create and connect a new site",
|
"siteCreateDescription2": "Follow the steps below to create and connect a new site",
|
||||||
"siteCreateDescription": "Create a new site to start connecting resources",
|
"siteCreateDescription": "Create a new site to start connecting resources",
|
||||||
@@ -74,8 +81,8 @@
|
|||||||
"siteConfirmCopy": "I have copied the config",
|
"siteConfirmCopy": "I have copied the config",
|
||||||
"searchSitesProgress": "Search sites...",
|
"searchSitesProgress": "Search sites...",
|
||||||
"siteAdd": "Add Site",
|
"siteAdd": "Add Site",
|
||||||
"siteInstallNewt": "Install Newt",
|
"siteInstallNewt": "Install Site",
|
||||||
"siteInstallNewtDescription": "Get Newt running on your system",
|
"siteInstallNewtDescription": "Install the site connector for your system",
|
||||||
"WgConfiguration": "WireGuard Configuration",
|
"WgConfiguration": "WireGuard Configuration",
|
||||||
"WgConfigurationDescription": "Use the following configuration to connect to the network",
|
"WgConfigurationDescription": "Use the following configuration to connect to the network",
|
||||||
"operatingSystem": "Operating System",
|
"operatingSystem": "Operating System",
|
||||||
@@ -141,6 +148,11 @@
|
|||||||
"createLink": "Create Link",
|
"createLink": "Create Link",
|
||||||
"resourcesNotFound": "No resources found",
|
"resourcesNotFound": "No resources found",
|
||||||
"resourceSearch": "Search resources",
|
"resourceSearch": "Search resources",
|
||||||
|
"machineSearch": "Search machines",
|
||||||
|
"machinesSearch": "Search machine clients...",
|
||||||
|
"machineNotFound": "No machines found",
|
||||||
|
"userDeviceSearch": "Search user devices",
|
||||||
|
"userDevicesSearch": "Search user devices...",
|
||||||
"openMenu": "Open menu",
|
"openMenu": "Open menu",
|
||||||
"resource": "Resource",
|
"resource": "Resource",
|
||||||
"title": "Title",
|
"title": "Title",
|
||||||
@@ -168,6 +180,7 @@
|
|||||||
"resourceHTTPDescription": "Proxy requests over HTTPS using a fully qualified domain name.",
|
"resourceHTTPDescription": "Proxy requests over HTTPS using a fully qualified domain name.",
|
||||||
"resourceRaw": "Raw TCP/UDP Resource",
|
"resourceRaw": "Raw TCP/UDP Resource",
|
||||||
"resourceRawDescription": "Proxy requests over raw TCP/UDP using a port number.",
|
"resourceRawDescription": "Proxy requests over raw TCP/UDP using a port number.",
|
||||||
|
"resourceRawDescriptionCloud": "Proxy requests over raw TCP/UDP using a port number. Requires sites to connect to a remote node.",
|
||||||
"resourceCreate": "Create Resource",
|
"resourceCreate": "Create Resource",
|
||||||
"resourceCreateDescription": "Follow the steps below to create a new resource",
|
"resourceCreateDescription": "Follow the steps below to create a new resource",
|
||||||
"resourceSeeAll": "See All Resources",
|
"resourceSeeAll": "See All Resources",
|
||||||
@@ -194,6 +207,7 @@
|
|||||||
"protocolSelect": "Select a protocol",
|
"protocolSelect": "Select a protocol",
|
||||||
"resourcePortNumber": "Port Number",
|
"resourcePortNumber": "Port Number",
|
||||||
"resourcePortNumberDescription": "The external port number to proxy requests.",
|
"resourcePortNumberDescription": "The external port number to proxy requests.",
|
||||||
|
"back": "Back",
|
||||||
"cancel": "Cancel",
|
"cancel": "Cancel",
|
||||||
"resourceConfig": "Configuration Snippets",
|
"resourceConfig": "Configuration Snippets",
|
||||||
"resourceConfigDescription": "Copy and paste these configuration snippets to set up the TCP/UDP resource",
|
"resourceConfigDescription": "Copy and paste these configuration snippets to set up the TCP/UDP resource",
|
||||||
@@ -239,6 +253,17 @@
|
|||||||
"orgErrorDeleteMessage": "An error occurred while deleting the organization.",
|
"orgErrorDeleteMessage": "An error occurred while deleting the organization.",
|
||||||
"orgDeleted": "Organization deleted",
|
"orgDeleted": "Organization deleted",
|
||||||
"orgDeletedMessage": "The organization and its data has been deleted.",
|
"orgDeletedMessage": "The organization and its data has been deleted.",
|
||||||
|
"deleteAccount": "Delete Account",
|
||||||
|
"deleteAccountDescription": "Permanently delete your account, all organizations you own, and all data within those organizations. This cannot be undone.",
|
||||||
|
"deleteAccountButton": "Delete Account",
|
||||||
|
"deleteAccountConfirmTitle": "Delete Account",
|
||||||
|
"deleteAccountConfirmMessage": "This will permanently wipe your account, all organizations you own, and all data within those organizations. This cannot be undone.",
|
||||||
|
"deleteAccountConfirmString": "delete account",
|
||||||
|
"deleteAccountSuccess": "Account Deleted",
|
||||||
|
"deleteAccountSuccessMessage": "Your account has been deleted.",
|
||||||
|
"deleteAccountError": "Failed to delete account",
|
||||||
|
"deleteAccountPreviewAccount": "Your Account",
|
||||||
|
"deleteAccountPreviewOrgs": "Organizations you own (and all their data)",
|
||||||
"orgMissing": "Organization ID Missing",
|
"orgMissing": "Organization ID Missing",
|
||||||
"orgMissingMessage": "Unable to regenerate invitation without an organization ID.",
|
"orgMissingMessage": "Unable to regenerate invitation without an organization ID.",
|
||||||
"accessUsersManage": "Manage Users",
|
"accessUsersManage": "Manage Users",
|
||||||
@@ -255,6 +280,8 @@
|
|||||||
"accessRolesSearch": "Search roles...",
|
"accessRolesSearch": "Search roles...",
|
||||||
"accessRolesAdd": "Add Role",
|
"accessRolesAdd": "Add Role",
|
||||||
"accessRoleDelete": "Delete Role",
|
"accessRoleDelete": "Delete Role",
|
||||||
|
"accessApprovalsManage": "Manage Approvals",
|
||||||
|
"accessApprovalsDescription": "View and manage pending approvals for access to this organization",
|
||||||
"description": "Description",
|
"description": "Description",
|
||||||
"inviteTitle": "Open Invitations",
|
"inviteTitle": "Open Invitations",
|
||||||
"inviteDescription": "Manage invitations for other users to join the organization",
|
"inviteDescription": "Manage invitations for other users to join the organization",
|
||||||
@@ -301,6 +328,54 @@
|
|||||||
"apiKeysDelete": "Delete API Key",
|
"apiKeysDelete": "Delete API Key",
|
||||||
"apiKeysManage": "Manage API Keys",
|
"apiKeysManage": "Manage API Keys",
|
||||||
"apiKeysDescription": "API keys are used to authenticate with the integration API",
|
"apiKeysDescription": "API keys are used to authenticate with the integration API",
|
||||||
|
"provisioningKeysTitle": "Provisioning Key",
|
||||||
|
"provisioningKeysManage": "Manage Provisioning Keys",
|
||||||
|
"provisioningKeysDescription": "Provisioning keys are used to authenticate automated site provisioning for your organization.",
|
||||||
|
"provisioningManage": "Provisioning",
|
||||||
|
"provisioningDescription": "Manage provisioning keys and review pending sites awaiting approval.",
|
||||||
|
"pendingSites": "Pending Sites",
|
||||||
|
"siteApproveSuccess": "Site approved successfully",
|
||||||
|
"siteApproveError": "Error approving site",
|
||||||
|
"provisioningKeys": "Provisioning Keys",
|
||||||
|
"searchProvisioningKeys": "Search provisioning keys...",
|
||||||
|
"provisioningKeysAdd": "Generate Provisioning Key",
|
||||||
|
"provisioningKeysErrorDelete": "Error deleting provisioning key",
|
||||||
|
"provisioningKeysErrorDeleteMessage": "Error deleting provisioning key",
|
||||||
|
"provisioningKeysQuestionRemove": "Are you sure you want to remove this provisioning key from the organization?",
|
||||||
|
"provisioningKeysMessageRemove": "Once removed, the key can no longer be used for site provisioning.",
|
||||||
|
"provisioningKeysDeleteConfirm": "Confirm Delete Provisioning Key",
|
||||||
|
"provisioningKeysDelete": "Delete Provisioning key",
|
||||||
|
"provisioningKeysCreate": "Generate Provisioning Key",
|
||||||
|
"provisioningKeysCreateDescription": "Generate a new provisioning key for the organization",
|
||||||
|
"provisioningKeysSeeAll": "See all provisioning keys",
|
||||||
|
"provisioningKeysSave": "Save the provisioning key",
|
||||||
|
"provisioningKeysSaveDescription": "You will only be able to see this once. Copy it to a secure place.",
|
||||||
|
"provisioningKeysErrorCreate": "Error creating provisioning key",
|
||||||
|
"provisioningKeysList": "New provisioning key",
|
||||||
|
"provisioningKeysMaxBatchSize": "Max batch size",
|
||||||
|
"provisioningKeysUnlimitedBatchSize": "Unlimited batch size (no limit)",
|
||||||
|
"provisioningKeysMaxBatchUnlimited": "Unlimited",
|
||||||
|
"provisioningKeysMaxBatchSizeInvalid": "Enter a valid max batch size (1–1,000,000).",
|
||||||
|
"provisioningKeysValidUntil": "Valid until",
|
||||||
|
"provisioningKeysValidUntilHint": "Leave empty for no expiration.",
|
||||||
|
"provisioningKeysValidUntilInvalid": "Enter a valid date and time.",
|
||||||
|
"provisioningKeysNumUsed": "Times used",
|
||||||
|
"provisioningKeysLastUsed": "Last used",
|
||||||
|
"provisioningKeysNoExpiry": "No expiration",
|
||||||
|
"provisioningKeysNeverUsed": "Never",
|
||||||
|
"provisioningKeysEdit": "Edit Provisioning Key",
|
||||||
|
"provisioningKeysEditDescription": "Update the max batch size and expiration time for this key.",
|
||||||
|
"provisioningKeysApproveNewSites": "Approve new sites",
|
||||||
|
"provisioningKeysApproveNewSitesDescription": "Automatically approve sites that register with this key.",
|
||||||
|
"provisioningKeysUpdateError": "Error updating provisioning key",
|
||||||
|
"provisioningKeysUpdated": "Provisioning key updated",
|
||||||
|
"provisioningKeysUpdatedDescription": "Your changes have been saved.",
|
||||||
|
"provisioningKeysBannerTitle": "Site Provisioning Keys",
|
||||||
|
"provisioningKeysBannerDescription": "Generate a provisioning key and use it with the Newt connector to automatically create sites on first startup - no need to set up separate credentials for each site.",
|
||||||
|
"provisioningKeysBannerButtonText": "Learn More",
|
||||||
|
"pendingSitesBannerTitle": "Pending Sites",
|
||||||
|
"pendingSitesBannerDescription": "Sites that connect using a provisioning key appear here for review.",
|
||||||
|
"pendingSitesBannerButtonText": "Learn More",
|
||||||
"apiKeysSettings": "{apiKeyName} Settings",
|
"apiKeysSettings": "{apiKeyName} Settings",
|
||||||
"userTitle": "Manage All Users",
|
"userTitle": "Manage All Users",
|
||||||
"userDescription": "View and manage all users in the system",
|
"userDescription": "View and manage all users in the system",
|
||||||
@@ -448,6 +523,20 @@
|
|||||||
"selectDuration": "Select duration",
|
"selectDuration": "Select duration",
|
||||||
"selectResource": "Select Resource",
|
"selectResource": "Select Resource",
|
||||||
"filterByResource": "Filter By Resource",
|
"filterByResource": "Filter By Resource",
|
||||||
|
"selectApprovalState": "Select Approval State",
|
||||||
|
"filterByApprovalState": "Filter By Approval State",
|
||||||
|
"approvalListEmpty": "No approvals",
|
||||||
|
"approvalState": "Approval State",
|
||||||
|
"approvalLoadMore": "Load more",
|
||||||
|
"loadingApprovals": "Loading Approvals",
|
||||||
|
"approve": "Approve",
|
||||||
|
"approved": "Approved",
|
||||||
|
"denied": "Denied",
|
||||||
|
"deniedApproval": "Denied Approval",
|
||||||
|
"all": "All",
|
||||||
|
"deny": "Deny",
|
||||||
|
"viewDetails": "View Details",
|
||||||
|
"requestingNewDeviceApproval": "requested a new device",
|
||||||
"resetFilters": "Reset Filters",
|
"resetFilters": "Reset Filters",
|
||||||
"totalBlocked": "Requests Blocked By Pangolin",
|
"totalBlocked": "Requests Blocked By Pangolin",
|
||||||
"totalRequests": "Total Requests",
|
"totalRequests": "Total Requests",
|
||||||
@@ -473,9 +562,12 @@
|
|||||||
"userSaved": "User saved",
|
"userSaved": "User saved",
|
||||||
"userSavedDescription": "The user has been updated.",
|
"userSavedDescription": "The user has been updated.",
|
||||||
"autoProvisioned": "Auto Provisioned",
|
"autoProvisioned": "Auto Provisioned",
|
||||||
|
"autoProvisionSettings": "Auto Provision Settings",
|
||||||
"autoProvisionedDescription": "Allow this user to be automatically managed by identity provider",
|
"autoProvisionedDescription": "Allow this user to be automatically managed by identity provider",
|
||||||
"accessControlsDescription": "Manage what this user can access and do in the organization",
|
"accessControlsDescription": "Manage what this user can access and do in the organization",
|
||||||
"accessControlsSubmit": "Save Access Controls",
|
"accessControlsSubmit": "Save Access Controls",
|
||||||
|
"singleRolePerUserPlanNotice": "Your plan only supports one role per user.",
|
||||||
|
"singleRolePerUserEditionNotice": "This edition only supports one role per user.",
|
||||||
"roles": "Roles",
|
"roles": "Roles",
|
||||||
"accessUsersRoles": "Manage Users & Roles",
|
"accessUsersRoles": "Manage Users & Roles",
|
||||||
"accessUsersRolesDescription": "Invite users and add them to roles to manage access to the organization",
|
"accessUsersRolesDescription": "Invite users and add them to roles to manage access to the organization",
|
||||||
@@ -532,6 +624,8 @@
|
|||||||
"targetErrorInvalidPortDescription": "Please enter a valid port number",
|
"targetErrorInvalidPortDescription": "Please enter a valid port number",
|
||||||
"targetErrorNoSite": "No site selected",
|
"targetErrorNoSite": "No site selected",
|
||||||
"targetErrorNoSiteDescription": "Please select a site for the target",
|
"targetErrorNoSiteDescription": "Please select a site for the target",
|
||||||
|
"targetTargetsCleared": "Targets cleared",
|
||||||
|
"targetTargetsClearedDescription": "All targets have been removed from this resource",
|
||||||
"targetCreated": "Target created",
|
"targetCreated": "Target created",
|
||||||
"targetCreatedDescription": "Target has been created successfully",
|
"targetCreatedDescription": "Target has been created successfully",
|
||||||
"targetErrorCreate": "Failed to create target",
|
"targetErrorCreate": "Failed to create target",
|
||||||
@@ -615,6 +709,7 @@
|
|||||||
"resourcesErrorUpdate": "Failed to toggle resource",
|
"resourcesErrorUpdate": "Failed to toggle resource",
|
||||||
"resourcesErrorUpdateDescription": "An error occurred while updating the resource",
|
"resourcesErrorUpdateDescription": "An error occurred while updating the resource",
|
||||||
"access": "Access",
|
"access": "Access",
|
||||||
|
"accessControl": "Access Control",
|
||||||
"shareLink": "{resource} Share Link",
|
"shareLink": "{resource} Share Link",
|
||||||
"resourceSelect": "Select resource",
|
"resourceSelect": "Select resource",
|
||||||
"shareLinks": "Share Links",
|
"shareLinks": "Share Links",
|
||||||
@@ -727,22 +822,35 @@
|
|||||||
"countries": "Countries",
|
"countries": "Countries",
|
||||||
"accessRoleCreate": "Create Role",
|
"accessRoleCreate": "Create Role",
|
||||||
"accessRoleCreateDescription": "Create a new role to group users and manage their permissions.",
|
"accessRoleCreateDescription": "Create a new role to group users and manage their permissions.",
|
||||||
|
"accessRoleEdit": "Edit Role",
|
||||||
|
"accessRoleEditDescription": "Edit role information.",
|
||||||
"accessRoleCreateSubmit": "Create Role",
|
"accessRoleCreateSubmit": "Create Role",
|
||||||
"accessRoleCreated": "Role created",
|
"accessRoleCreated": "Role created",
|
||||||
"accessRoleCreatedDescription": "The role has been successfully created.",
|
"accessRoleCreatedDescription": "The role has been successfully created.",
|
||||||
"accessRoleErrorCreate": "Failed to create role",
|
"accessRoleErrorCreate": "Failed to create role",
|
||||||
"accessRoleErrorCreateDescription": "An error occurred while creating the role.",
|
"accessRoleErrorCreateDescription": "An error occurred while creating the role.",
|
||||||
|
"accessRoleUpdateSubmit": "Update Role",
|
||||||
|
"accessRoleUpdated": "Role updated",
|
||||||
|
"accessRoleUpdatedDescription": "The role has been successfully updated.",
|
||||||
|
"accessApprovalUpdated": "Approval processed",
|
||||||
|
"accessApprovalApprovedDescription": "Set Approval Request decision to approved.",
|
||||||
|
"accessApprovalDeniedDescription": "Set Approval Request decision to denied.",
|
||||||
|
"accessRoleErrorUpdate": "Failed to update role",
|
||||||
|
"accessRoleErrorUpdateDescription": "An error occurred while updating the role.",
|
||||||
|
"accessApprovalErrorUpdate": "Failed to process approval",
|
||||||
|
"accessApprovalErrorUpdateDescription": "An error occurred while processing the approval.",
|
||||||
"accessRoleErrorNewRequired": "New role is required",
|
"accessRoleErrorNewRequired": "New role is required",
|
||||||
"accessRoleErrorRemove": "Failed to remove role",
|
"accessRoleErrorRemove": "Failed to remove role",
|
||||||
"accessRoleErrorRemoveDescription": "An error occurred while removing the role.",
|
"accessRoleErrorRemoveDescription": "An error occurred while removing the role.",
|
||||||
"accessRoleName": "Role Name",
|
"accessRoleName": "Role Name",
|
||||||
"accessRoleQuestionRemove": "You're about to delete the {name} role. You cannot undo this action.",
|
"accessRoleQuestionRemove": "You're about to delete the `{name}` role. You cannot undo this action.",
|
||||||
"accessRoleRemove": "Remove Role",
|
"accessRoleRemove": "Remove Role",
|
||||||
"accessRoleRemoveDescription": "Remove a role from the organization",
|
"accessRoleRemoveDescription": "Remove a role from the organization",
|
||||||
"accessRoleRemoveSubmit": "Remove Role",
|
"accessRoleRemoveSubmit": "Remove Role",
|
||||||
"accessRoleRemoved": "Role removed",
|
"accessRoleRemoved": "Role removed",
|
||||||
"accessRoleRemovedDescription": "The role has been successfully removed.",
|
"accessRoleRemovedDescription": "The role has been successfully removed.",
|
||||||
"accessRoleRequiredRemove": "Before deleting this role, please select a new role to transfer existing members to.",
|
"accessRoleRequiredRemove": "Before deleting this role, please select a new role to transfer existing members to.",
|
||||||
|
"network": "Network",
|
||||||
"manage": "Manage",
|
"manage": "Manage",
|
||||||
"sitesNotFound": "No sites found.",
|
"sitesNotFound": "No sites found.",
|
||||||
"pangolinServerAdmin": "Server Admin - Pangolin",
|
"pangolinServerAdmin": "Server Admin - Pangolin",
|
||||||
@@ -758,6 +866,9 @@
|
|||||||
"sitestCountIncrease": "Increase site count",
|
"sitestCountIncrease": "Increase site count",
|
||||||
"idpManage": "Manage Identity Providers",
|
"idpManage": "Manage Identity Providers",
|
||||||
"idpManageDescription": "View and manage identity providers in the system",
|
"idpManageDescription": "View and manage identity providers in the system",
|
||||||
|
"idpGlobalModeBanner": "Identity providers (IdPs) per organization are disabled on this server. It is using global IdPs (shared across all organizations). Manage global IdPs in the <adminPanelLink>admin panel</adminPanelLink>. To enable IdPs per organization, edit the server config and set IdP mode to org. <configDocsLink>See the docs</configDocsLink>. If you want to continue using global IdPs and make this disappear from the organization settings, explicitly set the mode to global in the config.",
|
||||||
|
"idpGlobalModeBannerUpgradeRequired": "Identity providers (IdPs) per organization are disabled on this server. It is using global IdPs (shared across all organizations). Manage global IdPs in the <adminPanelLink>admin panel</adminPanelLink>. To use identity providers per organization, you must upgrade to the Enterprise edition.",
|
||||||
|
"idpGlobalModeBannerLicenseRequired": "Identity providers (IdPs) per organization are disabled on this server. It is using global IdPs (shared across all organizations). Manage global IdPs in the <adminPanelLink>admin panel</adminPanelLink>. To use identity providers per organization, an Enterprise license is required.",
|
||||||
"idpDeletedDescription": "Identity provider deleted successfully",
|
"idpDeletedDescription": "Identity provider deleted successfully",
|
||||||
"idpOidc": "OAuth2/OIDC",
|
"idpOidc": "OAuth2/OIDC",
|
||||||
"idpQuestionRemove": "Are you sure you want to permanently delete the identity provider?",
|
"idpQuestionRemove": "Are you sure you want to permanently delete the identity provider?",
|
||||||
@@ -834,7 +945,7 @@
|
|||||||
"defaultMappingsRole": "Default Role Mapping",
|
"defaultMappingsRole": "Default Role Mapping",
|
||||||
"defaultMappingsRoleDescription": "The result of this expression must return the role name as defined in the organization as a string.",
|
"defaultMappingsRoleDescription": "The result of this expression must return the role name as defined in the organization as a string.",
|
||||||
"defaultMappingsOrg": "Default Organization Mapping",
|
"defaultMappingsOrg": "Default Organization Mapping",
|
||||||
"defaultMappingsOrgDescription": "This expression must return the org ID or true for the user to be allowed to access the organization.",
|
"defaultMappingsOrgDescription": "When set, this expression must return the organization ID or true for the user to access that organization. When unset, defining an organization policy for that org is enough: the user is allowed in as long as a valid role mapping can be resolved for them within the organization.",
|
||||||
"defaultMappingsSubmit": "Save Default Mappings",
|
"defaultMappingsSubmit": "Save Default Mappings",
|
||||||
"orgPoliciesEdit": "Edit Organization Policy",
|
"orgPoliciesEdit": "Edit Organization Policy",
|
||||||
"org": "Organization",
|
"org": "Organization",
|
||||||
@@ -848,6 +959,7 @@
|
|||||||
"orgPolicyConfig": "Configure access for an organization",
|
"orgPolicyConfig": "Configure access for an organization",
|
||||||
"idpUpdatedDescription": "Identity provider updated successfully",
|
"idpUpdatedDescription": "Identity provider updated successfully",
|
||||||
"redirectUrl": "Redirect URL",
|
"redirectUrl": "Redirect URL",
|
||||||
|
"orgIdpRedirectUrls": "Redirect URLs",
|
||||||
"redirectUrlAbout": "About Redirect URL",
|
"redirectUrlAbout": "About Redirect URL",
|
||||||
"redirectUrlAboutDescription": "This is the URL to which users will be redirected after authentication. You need to configure this URL in the identity provider's settings.",
|
"redirectUrlAboutDescription": "This is the URL to which users will be redirected after authentication. You need to configure this URL in the identity provider's settings.",
|
||||||
"pangolinAuth": "Auth - Pangolin",
|
"pangolinAuth": "Auth - Pangolin",
|
||||||
@@ -871,7 +983,7 @@
|
|||||||
"inviteAlready": "Looks like you've been invited!",
|
"inviteAlready": "Looks like you've been invited!",
|
||||||
"inviteAlreadyDescription": "To accept the invite, you must log in or create an account.",
|
"inviteAlreadyDescription": "To accept the invite, you must log in or create an account.",
|
||||||
"signupQuestion": "Already have an account?",
|
"signupQuestion": "Already have an account?",
|
||||||
"login": "Log in",
|
"login": "Log In",
|
||||||
"resourceNotFound": "Resource Not Found",
|
"resourceNotFound": "Resource Not Found",
|
||||||
"resourceNotFoundDescription": "The resource you're trying to access does not exist.",
|
"resourceNotFoundDescription": "The resource you're trying to access does not exist.",
|
||||||
"pincodeRequirementsLength": "PIN must be exactly 6 digits",
|
"pincodeRequirementsLength": "PIN must be exactly 6 digits",
|
||||||
@@ -951,13 +1063,13 @@
|
|||||||
"passwordExpiryDescription": "This organization requires you to change your password every {maxDays} days.",
|
"passwordExpiryDescription": "This organization requires you to change your password every {maxDays} days.",
|
||||||
"changePasswordNow": "Change Password Now",
|
"changePasswordNow": "Change Password Now",
|
||||||
"pincodeAuth": "Authenticator Code",
|
"pincodeAuth": "Authenticator Code",
|
||||||
"pincodeSubmit2": "Submit Code",
|
"pincodeSubmit2": "Submit code",
|
||||||
"passwordResetSubmit": "Request Reset",
|
"passwordResetSubmit": "Request Reset",
|
||||||
"passwordResetAlreadyHaveCode": "Enter Code",
|
"passwordResetAlreadyHaveCode": "Enter Code",
|
||||||
"passwordResetSmtpRequired": "Please contact your administrator",
|
"passwordResetSmtpRequired": "Please contact your administrator",
|
||||||
"passwordResetSmtpRequiredDescription": "A password reset code is required to reset your password. Please contact your administrator for assistance.",
|
"passwordResetSmtpRequiredDescription": "A password reset code is required to reset your password. Please contact your administrator for assistance.",
|
||||||
"passwordBack": "Back to Password",
|
"passwordBack": "Back to Password",
|
||||||
"loginBack": "Go back to log in",
|
"loginBack": "Go back to main login page",
|
||||||
"signup": "Sign up",
|
"signup": "Sign up",
|
||||||
"loginStart": "Log in to get started",
|
"loginStart": "Log in to get started",
|
||||||
"idpOidcTokenValidating": "Validating OIDC token",
|
"idpOidcTokenValidating": "Validating OIDC token",
|
||||||
@@ -980,12 +1092,12 @@
|
|||||||
"pangolinSetup": "Setup - Pangolin",
|
"pangolinSetup": "Setup - Pangolin",
|
||||||
"orgNameRequired": "Organization name is required",
|
"orgNameRequired": "Organization name is required",
|
||||||
"orgIdRequired": "Organization ID is required",
|
"orgIdRequired": "Organization ID is required",
|
||||||
|
"orgIdMaxLength": "Organization ID must be at most 32 characters",
|
||||||
"orgErrorCreate": "An error occurred while creating org",
|
"orgErrorCreate": "An error occurred while creating org",
|
||||||
"pageNotFound": "Page Not Found",
|
"pageNotFound": "Page Not Found",
|
||||||
"pageNotFoundDescription": "Oops! The page you're looking for doesn't exist.",
|
"pageNotFoundDescription": "Oops! The page you're looking for doesn't exist.",
|
||||||
"overview": "Overview",
|
"overview": "Overview",
|
||||||
"home": "Home",
|
"home": "Home",
|
||||||
"accessControl": "Access Control",
|
|
||||||
"settings": "Settings",
|
"settings": "Settings",
|
||||||
"usersAll": "All Users",
|
"usersAll": "All Users",
|
||||||
"license": "License",
|
"license": "License",
|
||||||
@@ -1048,6 +1160,12 @@
|
|||||||
"actionGetUser": "Get User",
|
"actionGetUser": "Get User",
|
||||||
"actionGetOrgUser": "Get Organization User",
|
"actionGetOrgUser": "Get Organization User",
|
||||||
"actionListOrgDomains": "List Organization Domains",
|
"actionListOrgDomains": "List Organization Domains",
|
||||||
|
"actionGetDomain": "Get Domain",
|
||||||
|
"actionCreateOrgDomain": "Create Domain",
|
||||||
|
"actionUpdateOrgDomain": "Update Domain",
|
||||||
|
"actionDeleteOrgDomain": "Delete Domain",
|
||||||
|
"actionGetDNSRecords": "Get DNS Records",
|
||||||
|
"actionRestartOrgDomain": "Restart Domain",
|
||||||
"actionCreateSite": "Create Site",
|
"actionCreateSite": "Create Site",
|
||||||
"actionDeleteSite": "Delete Site",
|
"actionDeleteSite": "Delete Site",
|
||||||
"actionGetSite": "Get Site",
|
"actionGetSite": "Get Site",
|
||||||
@@ -1059,6 +1177,7 @@
|
|||||||
"setupTokenDescription": "Enter the setup token from the server console.",
|
"setupTokenDescription": "Enter the setup token from the server console.",
|
||||||
"setupTokenRequired": "Setup token is required",
|
"setupTokenRequired": "Setup token is required",
|
||||||
"actionUpdateSite": "Update Site",
|
"actionUpdateSite": "Update Site",
|
||||||
|
"actionResetSiteBandwidth": "Reset Organization Bandwidth",
|
||||||
"actionListSiteRoles": "List Allowed Site Roles",
|
"actionListSiteRoles": "List Allowed Site Roles",
|
||||||
"actionCreateResource": "Create Resource",
|
"actionCreateResource": "Create Resource",
|
||||||
"actionDeleteResource": "Delete Resource",
|
"actionDeleteResource": "Delete Resource",
|
||||||
@@ -1088,6 +1207,7 @@
|
|||||||
"actionRemoveUser": "Remove User",
|
"actionRemoveUser": "Remove User",
|
||||||
"actionListUsers": "List Users",
|
"actionListUsers": "List Users",
|
||||||
"actionAddUserRole": "Add User Role",
|
"actionAddUserRole": "Add User Role",
|
||||||
|
"actionSetUserOrgRoles": "Set User Roles",
|
||||||
"actionGenerateAccessToken": "Generate Access Token",
|
"actionGenerateAccessToken": "Generate Access Token",
|
||||||
"actionDeleteAccessToken": "Delete Access Token",
|
"actionDeleteAccessToken": "Delete Access Token",
|
||||||
"actionListAccessTokens": "List Access Tokens",
|
"actionListAccessTokens": "List Access Tokens",
|
||||||
@@ -1115,6 +1235,10 @@
|
|||||||
"actionUpdateIdpOrg": "Update IDP Org",
|
"actionUpdateIdpOrg": "Update IDP Org",
|
||||||
"actionCreateClient": "Create Client",
|
"actionCreateClient": "Create Client",
|
||||||
"actionDeleteClient": "Delete Client",
|
"actionDeleteClient": "Delete Client",
|
||||||
|
"actionArchiveClient": "Archive Client",
|
||||||
|
"actionUnarchiveClient": "Unarchive Client",
|
||||||
|
"actionBlockClient": "Block Client",
|
||||||
|
"actionUnblockClient": "Unblock Client",
|
||||||
"actionUpdateClient": "Update Client",
|
"actionUpdateClient": "Update Client",
|
||||||
"actionListClients": "List Clients",
|
"actionListClients": "List Clients",
|
||||||
"actionGetClient": "Get Client",
|
"actionGetClient": "Get Client",
|
||||||
@@ -1128,17 +1252,18 @@
|
|||||||
"actionViewLogs": "View Logs",
|
"actionViewLogs": "View Logs",
|
||||||
"noneSelected": "None selected",
|
"noneSelected": "None selected",
|
||||||
"orgNotFound2": "No organizations found.",
|
"orgNotFound2": "No organizations found.",
|
||||||
"searchProgress": "Search...",
|
"searchPlaceholder": "Search...",
|
||||||
|
"emptySearchOptions": "No options found",
|
||||||
"create": "Create",
|
"create": "Create",
|
||||||
"orgs": "Organizations",
|
"orgs": "Organizations",
|
||||||
"loginError": "An error occurred while logging in",
|
"loginError": "An unexpected error occurred. Please try again.",
|
||||||
"loginRequiredForDevice": "Login is required to authenticate your device.",
|
"loginRequiredForDevice": "Login is required for your device.",
|
||||||
"passwordForgot": "Forgot your password?",
|
"passwordForgot": "Forgot your password?",
|
||||||
"otpAuth": "Two-Factor Authentication",
|
"otpAuth": "Two-Factor Authentication",
|
||||||
"otpAuthDescription": "Enter the code from your authenticator app or one of your single-use backup codes.",
|
"otpAuthDescription": "Enter the code from your authenticator app or one of your single-use backup codes.",
|
||||||
"otpAuthSubmit": "Submit Code",
|
"otpAuthSubmit": "Submit Code",
|
||||||
"idpContinue": "Or continue with",
|
"idpContinue": "Or continue with",
|
||||||
"otpAuthBack": "Back to Log In",
|
"otpAuthBack": "Back to Password",
|
||||||
"navbar": "Navigation Menu",
|
"navbar": "Navigation Menu",
|
||||||
"navbarDescription": "Main navigation menu for the application",
|
"navbarDescription": "Main navigation menu for the application",
|
||||||
"navbarDocsLink": "Documentation",
|
"navbarDocsLink": "Documentation",
|
||||||
@@ -1186,29 +1311,34 @@
|
|||||||
"sidebarOverview": "Overview",
|
"sidebarOverview": "Overview",
|
||||||
"sidebarHome": "Home",
|
"sidebarHome": "Home",
|
||||||
"sidebarSites": "Sites",
|
"sidebarSites": "Sites",
|
||||||
|
"sidebarApprovals": "Approval Requests",
|
||||||
"sidebarResources": "Resources",
|
"sidebarResources": "Resources",
|
||||||
"sidebarProxyResources": "Public",
|
"sidebarProxyResources": "Public",
|
||||||
"sidebarClientResources": "Private",
|
"sidebarClientResources": "Private",
|
||||||
"sidebarAccessControl": "Access Control",
|
"sidebarAccessControl": "Access Control",
|
||||||
"sidebarLogsAndAnalytics": "Logs & Analytics",
|
"sidebarLogsAndAnalytics": "Logs & Analytics",
|
||||||
|
"sidebarTeam": "Team",
|
||||||
"sidebarUsers": "Users",
|
"sidebarUsers": "Users",
|
||||||
"sidebarAdmin": "Admin",
|
"sidebarAdmin": "Admin",
|
||||||
"sidebarInvitations": "Invitations",
|
"sidebarInvitations": "Invitations",
|
||||||
"sidebarRoles": "Roles",
|
"sidebarRoles": "Roles",
|
||||||
"sidebarShareableLinks": "Links",
|
"sidebarShareableLinks": "Links",
|
||||||
"sidebarApiKeys": "API Keys",
|
"sidebarApiKeys": "API Keys",
|
||||||
|
"sidebarProvisioning": "Provisioning",
|
||||||
"sidebarSettings": "Settings",
|
"sidebarSettings": "Settings",
|
||||||
"sidebarAllUsers": "All Users",
|
"sidebarAllUsers": "All Users",
|
||||||
"sidebarIdentityProviders": "Identity Providers",
|
"sidebarIdentityProviders": "Identity Providers",
|
||||||
"sidebarLicense": "License",
|
"sidebarLicense": "License",
|
||||||
"sidebarClients": "Clients",
|
"sidebarClients": "Clients",
|
||||||
"sidebarUserDevices": "Users",
|
"sidebarUserDevices": "User Devices",
|
||||||
"sidebarMachineClients": "Machines",
|
"sidebarMachineClients": "Machines",
|
||||||
"sidebarDomains": "Domains",
|
"sidebarDomains": "Domains",
|
||||||
"sidebarGeneral": "Manage",
|
"sidebarGeneral": "Manage",
|
||||||
"sidebarLogAndAnalytics": "Log & Analytics",
|
"sidebarLogAndAnalytics": "Log & Analytics",
|
||||||
"sidebarBluePrints": "Blueprints",
|
"sidebarBluePrints": "Blueprints",
|
||||||
"sidebarOrganization": "Organization",
|
"sidebarOrganization": "Organization",
|
||||||
|
"sidebarManagement": "Management",
|
||||||
|
"sidebarBillingAndLicenses": "Billing & Licenses",
|
||||||
"sidebarLogsAnalytics": "Analytics",
|
"sidebarLogsAnalytics": "Analytics",
|
||||||
"blueprints": "Blueprints",
|
"blueprints": "Blueprints",
|
||||||
"blueprintsDescription": "Apply declarative configurations and view previous runs",
|
"blueprintsDescription": "Apply declarative configurations and view previous runs",
|
||||||
@@ -1229,8 +1359,7 @@
|
|||||||
"contents": "Contents",
|
"contents": "Contents",
|
||||||
"parsedContents": "Parsed Contents (Read Only)",
|
"parsedContents": "Parsed Contents (Read Only)",
|
||||||
"enableDockerSocket": "Enable Docker Blueprint",
|
"enableDockerSocket": "Enable Docker Blueprint",
|
||||||
"enableDockerSocketDescription": "Enable Docker Socket label scraping for blueprint labels. Socket path must be provided to Newt.",
|
"enableDockerSocketDescription": "Enable Docker Socket label scraping for blueprint labels. Socket path must be provided to Newt. Read about how this works in <docsLink>the documentation</docsLink>.",
|
||||||
"enableDockerSocketLink": "Learn More",
|
|
||||||
"viewDockerContainers": "View Docker Containers",
|
"viewDockerContainers": "View Docker Containers",
|
||||||
"containersIn": "Containers in {siteName}",
|
"containersIn": "Containers in {siteName}",
|
||||||
"selectContainerDescription": "Select any container to use as a hostname for this target. Click a port to use a port.",
|
"selectContainerDescription": "Select any container to use as a hostname for this target. Click a port to use a port.",
|
||||||
@@ -1274,6 +1403,7 @@
|
|||||||
"setupErrorCreateAdmin": "An error occurred while creating the server admin account.",
|
"setupErrorCreateAdmin": "An error occurred while creating the server admin account.",
|
||||||
"certificateStatus": "Certificate Status",
|
"certificateStatus": "Certificate Status",
|
||||||
"loading": "Loading",
|
"loading": "Loading",
|
||||||
|
"loadingAnalytics": "Loading Analytics",
|
||||||
"restart": "Restart",
|
"restart": "Restart",
|
||||||
"domains": "Domains",
|
"domains": "Domains",
|
||||||
"domainsDescription": "Create and manage domains available in the organization",
|
"domainsDescription": "Create and manage domains available in the organization",
|
||||||
@@ -1301,6 +1431,7 @@
|
|||||||
"refreshError": "Failed to refresh data",
|
"refreshError": "Failed to refresh data",
|
||||||
"verified": "Verified",
|
"verified": "Verified",
|
||||||
"pending": "Pending",
|
"pending": "Pending",
|
||||||
|
"pendingApproval": "Pending Approval",
|
||||||
"sidebarBilling": "Billing",
|
"sidebarBilling": "Billing",
|
||||||
"billing": "Billing",
|
"billing": "Billing",
|
||||||
"orgBillingDescription": "Manage billing information and subscriptions",
|
"orgBillingDescription": "Manage billing information and subscriptions",
|
||||||
@@ -1356,6 +1487,7 @@
|
|||||||
"domainPickerNamespace": "Namespace: {namespace}",
|
"domainPickerNamespace": "Namespace: {namespace}",
|
||||||
"domainPickerShowMore": "Show More",
|
"domainPickerShowMore": "Show More",
|
||||||
"regionSelectorTitle": "Select Region",
|
"regionSelectorTitle": "Select Region",
|
||||||
|
"domainPickerRemoteExitNodeWarning": "Provided domains are not supported when sites connect to remote exit nodes. For resources to be available on remote nodes, use a custom domain instead.",
|
||||||
"regionSelectorInfo": "Selecting a region helps us provide better performance for your location. You do not have to be in the same region as your server.",
|
"regionSelectorInfo": "Selecting a region helps us provide better performance for your location. You do not have to be in the same region as your server.",
|
||||||
"regionSelectorPlaceholder": "Choose a region",
|
"regionSelectorPlaceholder": "Choose a region",
|
||||||
"regionSelectorComingSoon": "Coming Soon",
|
"regionSelectorComingSoon": "Coming Soon",
|
||||||
@@ -1365,10 +1497,11 @@
|
|||||||
"billingUsageLimitsOverview": "Usage Limits Overview",
|
"billingUsageLimitsOverview": "Usage Limits Overview",
|
||||||
"billingMonitorUsage": "Monitor your usage against configured limits. If you need limits increased please contact us support@pangolin.net.",
|
"billingMonitorUsage": "Monitor your usage against configured limits. If you need limits increased please contact us support@pangolin.net.",
|
||||||
"billingDataUsage": "Data Usage",
|
"billingDataUsage": "Data Usage",
|
||||||
"billingOnlineTime": "Site Online Time",
|
"billingSites": "Sites",
|
||||||
"billingUsers": "Active Users",
|
"billingUsers": "Users",
|
||||||
"billingDomains": "Active Domains",
|
"billingDomains": "Domains",
|
||||||
"billingRemoteExitNodes": "Active Self-hosted Nodes",
|
"billingOrganizations": "Orgs",
|
||||||
|
"billingRemoteExitNodes": "Remote Nodes",
|
||||||
"billingNoLimitConfigured": "No limit configured",
|
"billingNoLimitConfigured": "No limit configured",
|
||||||
"billingEstimatedPeriod": "Estimated Billing Period",
|
"billingEstimatedPeriod": "Estimated Billing Period",
|
||||||
"billingIncludedUsage": "Included Usage",
|
"billingIncludedUsage": "Included Usage",
|
||||||
@@ -1393,15 +1526,24 @@
|
|||||||
"billingFailedToGetPortalUrl": "Failed to get portal URL",
|
"billingFailedToGetPortalUrl": "Failed to get portal URL",
|
||||||
"billingPortalError": "Portal Error",
|
"billingPortalError": "Portal Error",
|
||||||
"billingDataUsageInfo": "You're charged for all data transferred through your secure tunnels when connected to the cloud. This includes both incoming and outgoing traffic across all your sites. When you reach your limit, your sites will disconnect until you upgrade your plan or reduce usage. Data is not charged when using nodes.",
|
"billingDataUsageInfo": "You're charged for all data transferred through your secure tunnels when connected to the cloud. This includes both incoming and outgoing traffic across all your sites. When you reach your limit, your sites will disconnect until you upgrade your plan or reduce usage. Data is not charged when using nodes.",
|
||||||
"billingOnlineTimeInfo": "You're charged based on how long your sites stay connected to the cloud. For example, 44,640 minutes equals one site running 24/7 for a full month. When you reach your limit, your sites will disconnect until you upgrade your plan or reduce usage. Time is not charged when using nodes.",
|
"billingSInfo": "How many sites you can use",
|
||||||
"billingUsersInfo": "You're charged for each user in the organization. Billing is calculated daily based on the number of active user accounts in your org.",
|
"billingUsersInfo": "How many users you can use",
|
||||||
"billingDomainInfo": "You're charged for each domain in the organization. Billing is calculated daily based on the number of active domain accounts in your org.",
|
"billingDomainInfo": "How many domains you can use",
|
||||||
"billingRemoteExitNodesInfo": "You're charged for each managed Node in the organization. Billing is calculated daily based on the number of active managed Nodes in your org.",
|
"billingRemoteExitNodesInfo": "How many remote nodes you can use",
|
||||||
|
"billingLicenseKeys": "License Keys",
|
||||||
|
"billingLicenseKeysDescription": "Manage your license key subscriptions",
|
||||||
|
"billingLicenseSubscription": "License Subscription",
|
||||||
|
"billingInactive": "Inactive",
|
||||||
|
"billingLicenseItem": "License Item",
|
||||||
|
"billingQuantity": "Quantity",
|
||||||
|
"billingTotal": "total",
|
||||||
|
"billingModifyLicenses": "Modify License Subscription",
|
||||||
"domainNotFound": "Domain Not Found",
|
"domainNotFound": "Domain Not Found",
|
||||||
"domainNotFoundDescription": "This resource is disabled because the domain no longer exists our system. Please set a new domain for this resource.",
|
"domainNotFoundDescription": "This resource is disabled because the domain no longer exists our system. Please set a new domain for this resource.",
|
||||||
"failed": "Failed",
|
"failed": "Failed",
|
||||||
"createNewOrgDescription": "Create a new organization",
|
"createNewOrgDescription": "Create a new organization",
|
||||||
"organization": "Organization",
|
"organization": "Organization",
|
||||||
|
"primary": "Primary",
|
||||||
"port": "Port",
|
"port": "Port",
|
||||||
"securityKeyManage": "Manage Security Keys",
|
"securityKeyManage": "Manage Security Keys",
|
||||||
"securityKeyDescription": "Add or remove security keys for passwordless authentication",
|
"securityKeyDescription": "Add or remove security keys for passwordless authentication",
|
||||||
@@ -1417,7 +1559,7 @@
|
|||||||
"securityKeyRemoveSuccess": "Security key removed successfully",
|
"securityKeyRemoveSuccess": "Security key removed successfully",
|
||||||
"securityKeyRemoveError": "Failed to remove security key",
|
"securityKeyRemoveError": "Failed to remove security key",
|
||||||
"securityKeyLoadError": "Failed to load security keys",
|
"securityKeyLoadError": "Failed to load security keys",
|
||||||
"securityKeyLogin": "Continue with security key",
|
"securityKeyLogin": "Use Security Key",
|
||||||
"securityKeyAuthError": "Failed to authenticate with security key",
|
"securityKeyAuthError": "Failed to authenticate with security key",
|
||||||
"securityKeyRecommendation": "Register a backup security key on another device to ensure you always have access to your account.",
|
"securityKeyRecommendation": "Register a backup security key on another device to ensure you always have access to your account.",
|
||||||
"registering": "Registering...",
|
"registering": "Registering...",
|
||||||
@@ -1473,11 +1615,47 @@
|
|||||||
"resourcePortRequired": "Port number is required for non-HTTP resources",
|
"resourcePortRequired": "Port number is required for non-HTTP resources",
|
||||||
"resourcePortNotAllowed": "Port number should not be set for HTTP resources",
|
"resourcePortNotAllowed": "Port number should not be set for HTTP resources",
|
||||||
"billingPricingCalculatorLink": "Pricing Calculator",
|
"billingPricingCalculatorLink": "Pricing Calculator",
|
||||||
|
"billingYourPlan": "Your Plan",
|
||||||
|
"billingViewOrModifyPlan": "View or modify your current plan",
|
||||||
|
"billingViewPlanDetails": "View Plan Details",
|
||||||
|
"billingUsageAndLimits": "Usage and Limits",
|
||||||
|
"billingViewUsageAndLimits": "View your plan's limits and current usage",
|
||||||
|
"billingCurrentUsage": "Current Usage",
|
||||||
|
"billingMaximumLimits": "Maximum Limits",
|
||||||
|
"billingRemoteNodes": "Remote Nodes",
|
||||||
|
"billingUnlimited": "Unlimited",
|
||||||
|
"billingPaidLicenseKeys": "Paid License Keys",
|
||||||
|
"billingManageLicenseSubscription": "Manage your subscription for paid self-hosted license keys",
|
||||||
|
"billingCurrentKeys": "Current Keys",
|
||||||
|
"billingModifyCurrentPlan": "Modify Current Plan",
|
||||||
|
"billingConfirmUpgrade": "Confirm Upgrade",
|
||||||
|
"billingConfirmDowngrade": "Confirm Downgrade",
|
||||||
|
"billingConfirmUpgradeDescription": "You are about to upgrade your plan. Review the new limits and pricing below.",
|
||||||
|
"billingConfirmDowngradeDescription": "You are about to downgrade your plan. Review the new limits and pricing below.",
|
||||||
|
"billingPlanIncludes": "Plan Includes",
|
||||||
|
"billingProcessing": "Processing...",
|
||||||
|
"billingConfirmUpgradeButton": "Confirm Upgrade",
|
||||||
|
"billingConfirmDowngradeButton": "Confirm Downgrade",
|
||||||
|
"billingLimitViolationWarning": "Usage Exceeds New Plan Limits",
|
||||||
|
"billingLimitViolationDescription": "Your current usage exceeds the limits of this plan. After downgrading, all actions will be disabled until you reduce usage within the new limits. Please review the features below that are currently over the limits. Limits in violation:",
|
||||||
|
"billingFeatureLossWarning": "Feature Availability Notice",
|
||||||
|
"billingFeatureLossDescription": "By downgrading, features not available in the new plan will be automatically disabled. Some settings and configurations may be lost. Please review the pricing matrix to understand which features will no longer be available.",
|
||||||
|
"billingUsageExceedsLimit": "Current usage ({current}) exceeds limit ({limit})",
|
||||||
|
"billingPastDueTitle": "Payment Past Due",
|
||||||
|
"billingPastDueDescription": "Your payment is past due. Please update your payment method to continue using your current plan features. If not resolved, your subscription will be canceled and you'll be reverted to the free tier.",
|
||||||
|
"billingUnpaidTitle": "Subscription Unpaid",
|
||||||
|
"billingUnpaidDescription": "Your subscription is unpaid and you have been reverted to the free tier. Please update your payment method to restore your subscription.",
|
||||||
|
"billingIncompleteTitle": "Payment Incomplete",
|
||||||
|
"billingIncompleteDescription": "Your payment is incomplete. Please complete the payment process to activate your subscription.",
|
||||||
|
"billingIncompleteExpiredTitle": "Payment Expired",
|
||||||
|
"billingIncompleteExpiredDescription": "Your payment was never completed and has expired. You have been reverted to the free tier. Please subscribe again to restore access to paid features.",
|
||||||
|
"billingManageSubscription": "Manage your subscription",
|
||||||
|
"billingResolvePaymentIssue": "Please resolve your payment issue before upgrading or downgrading",
|
||||||
"signUpTerms": {
|
"signUpTerms": {
|
||||||
"IAgreeToThe": "I agree to the",
|
"IAgreeToThe": "I agree to the",
|
||||||
"termsOfService": "terms of service",
|
"termsOfService": "terms of service",
|
||||||
"and": "and",
|
"and": "and",
|
||||||
"privacyPolicy": "privacy policy"
|
"privacyPolicy": "privacy policy."
|
||||||
},
|
},
|
||||||
"signUpMarketing": {
|
"signUpMarketing": {
|
||||||
"keepMeInTheLoop": "Keep me in the loop with news, updates, and new features by email."
|
"keepMeInTheLoop": "Keep me in the loop with news, updates, and new features by email."
|
||||||
@@ -1497,8 +1675,8 @@
|
|||||||
"addressDescription": "The internal address of the client. Must fall within the organization's subnet.",
|
"addressDescription": "The internal address of the client. Must fall within the organization's subnet.",
|
||||||
"selectSites": "Select sites",
|
"selectSites": "Select sites",
|
||||||
"sitesDescription": "The client will have connectivity to the selected sites",
|
"sitesDescription": "The client will have connectivity to the selected sites",
|
||||||
"clientInstallOlm": "Install Olm",
|
"clientInstallOlm": "Install Machine Client",
|
||||||
"clientInstallOlmDescription": "Get Olm running on your system",
|
"clientInstallOlmDescription": "Install the machine client for your system",
|
||||||
"clientOlmCredentials": "Credentials",
|
"clientOlmCredentials": "Credentials",
|
||||||
"clientOlmCredentialsDescription": "This is how the client will authenticate with the server",
|
"clientOlmCredentialsDescription": "This is how the client will authenticate with the server",
|
||||||
"olmEndpoint": "Endpoint",
|
"olmEndpoint": "Endpoint",
|
||||||
@@ -1522,6 +1700,7 @@
|
|||||||
"addNewTarget": "Add New Target",
|
"addNewTarget": "Add New Target",
|
||||||
"targetsList": "Targets List",
|
"targetsList": "Targets List",
|
||||||
"advancedMode": "Advanced Mode",
|
"advancedMode": "Advanced Mode",
|
||||||
|
"advancedSettings": "Advanced Settings",
|
||||||
"targetErrorDuplicateTargetFound": "Duplicate target found",
|
"targetErrorDuplicateTargetFound": "Duplicate target found",
|
||||||
"healthCheckHealthy": "Healthy",
|
"healthCheckHealthy": "Healthy",
|
||||||
"healthCheckUnhealthy": "Unhealthy",
|
"healthCheckUnhealthy": "Unhealthy",
|
||||||
@@ -1543,6 +1722,26 @@
|
|||||||
"IntervalSeconds": "Healthy Interval",
|
"IntervalSeconds": "Healthy Interval",
|
||||||
"timeoutSeconds": "Timeout (sec)",
|
"timeoutSeconds": "Timeout (sec)",
|
||||||
"timeIsInSeconds": "Time is in seconds",
|
"timeIsInSeconds": "Time is in seconds",
|
||||||
|
"requireDeviceApproval": "Require Device Approvals",
|
||||||
|
"requireDeviceApprovalDescription": "Users with this role need new devices approved by an admin before they can connect and access resources.",
|
||||||
|
"sshAccess": "SSH Access",
|
||||||
|
"roleAllowSsh": "Allow SSH",
|
||||||
|
"roleAllowSshAllow": "Allow",
|
||||||
|
"roleAllowSshDisallow": "Disallow",
|
||||||
|
"roleAllowSshDescription": "Allow users with this role to connect to resources via SSH. When disabled, the role cannot use SSH access.",
|
||||||
|
"sshSudoMode": "Sudo Access",
|
||||||
|
"sshSudoModeNone": "None",
|
||||||
|
"sshSudoModeNoneDescription": "User cannot run commands with sudo.",
|
||||||
|
"sshSudoModeFull": "Full Sudo",
|
||||||
|
"sshSudoModeFullDescription": "User can run any command with sudo.",
|
||||||
|
"sshSudoModeCommands": "Commands",
|
||||||
|
"sshSudoModeCommandsDescription": "User can run only the specified commands with sudo.",
|
||||||
|
"sshSudo": "Allow sudo",
|
||||||
|
"sshSudoCommands": "Sudo Commands",
|
||||||
|
"sshSudoCommandsDescription": "Comma separated list of commands the user is allowed to run with sudo.",
|
||||||
|
"sshCreateHomeDir": "Create Home Directory",
|
||||||
|
"sshUnixGroups": "Unix Groups",
|
||||||
|
"sshUnixGroupsDescription": "Comma separated Unix groups to add the user to on the target host.",
|
||||||
"retryAttempts": "Retry Attempts",
|
"retryAttempts": "Retry Attempts",
|
||||||
"expectedResponseCodes": "Expected Response Codes",
|
"expectedResponseCodes": "Expected Response Codes",
|
||||||
"expectedResponseCodesDescription": "HTTP status code that indicates healthy status. If left blank, 200-300 is considered healthy.",
|
"expectedResponseCodesDescription": "HTTP status code that indicates healthy status. If left blank, 200-300 is considered healthy.",
|
||||||
@@ -1583,6 +1782,8 @@
|
|||||||
"resourcesTableNoInternalResourcesFound": "No internal resources found.",
|
"resourcesTableNoInternalResourcesFound": "No internal resources found.",
|
||||||
"resourcesTableDestination": "Destination",
|
"resourcesTableDestination": "Destination",
|
||||||
"resourcesTableAlias": "Alias",
|
"resourcesTableAlias": "Alias",
|
||||||
|
"resourcesTableAliasAddress": "Alias Address",
|
||||||
|
"resourcesTableAliasAddressInfo": "This address is part of the organization's utility subnet. It's used to resolve alias records using internal DNS resolution.",
|
||||||
"resourcesTableClients": "Clients",
|
"resourcesTableClients": "Clients",
|
||||||
"resourcesTableAndOnlyAccessibleInternally": "and are only accessible internally when connected with a client.",
|
"resourcesTableAndOnlyAccessibleInternally": "and are only accessible internally when connected with a client.",
|
||||||
"resourcesTableNoTargets": "No targets",
|
"resourcesTableNoTargets": "No targets",
|
||||||
@@ -1749,6 +1950,40 @@
|
|||||||
"exitNode": "Exit Node",
|
"exitNode": "Exit Node",
|
||||||
"country": "Country",
|
"country": "Country",
|
||||||
"rulesMatchCountry": "Currently based on source IP",
|
"rulesMatchCountry": "Currently based on source IP",
|
||||||
|
"region": "Region",
|
||||||
|
"selectRegion": "Select region",
|
||||||
|
"searchRegions": "Search regions...",
|
||||||
|
"noRegionFound": "No region found.",
|
||||||
|
"rulesMatchRegion": "Select a regional grouping of countries",
|
||||||
|
"rulesErrorInvalidRegion": "Invalid region",
|
||||||
|
"rulesErrorInvalidRegionDescription": "Please select a valid region.",
|
||||||
|
"regionAfrica": "Africa",
|
||||||
|
"regionNorthernAfrica": "Northern Africa",
|
||||||
|
"regionEasternAfrica": "Eastern Africa",
|
||||||
|
"regionMiddleAfrica": "Middle Africa",
|
||||||
|
"regionSouthernAfrica": "Southern Africa",
|
||||||
|
"regionWesternAfrica": "Western Africa",
|
||||||
|
"regionAmericas": "Americas",
|
||||||
|
"regionCaribbean": "Caribbean",
|
||||||
|
"regionCentralAmerica": "Central America",
|
||||||
|
"regionSouthAmerica": "South America",
|
||||||
|
"regionNorthernAmerica": "Northern America",
|
||||||
|
"regionAsia": "Asia",
|
||||||
|
"regionCentralAsia": "Central Asia",
|
||||||
|
"regionEasternAsia": "Eastern Asia",
|
||||||
|
"regionSouthEasternAsia": "South-Eastern Asia",
|
||||||
|
"regionSouthernAsia": "Southern Asia",
|
||||||
|
"regionWesternAsia": "Western Asia",
|
||||||
|
"regionEurope": "Europe",
|
||||||
|
"regionEasternEurope": "Eastern Europe",
|
||||||
|
"regionNorthernEurope": "Northern Europe",
|
||||||
|
"regionSouthernEurope": "Southern Europe",
|
||||||
|
"regionWesternEurope": "Western Europe",
|
||||||
|
"regionOceania": "Oceania",
|
||||||
|
"regionAustraliaAndNewZealand": "Australia and New Zealand",
|
||||||
|
"regionMelanesia": "Melanesia",
|
||||||
|
"regionMicronesia": "Micronesia",
|
||||||
|
"regionPolynesia": "Polynesia",
|
||||||
"managedSelfHosted": {
|
"managedSelfHosted": {
|
||||||
"title": "Managed Self-Hosted",
|
"title": "Managed Self-Hosted",
|
||||||
"description": "More reliable and low-maintenance self-hosted Pangolin server with extra bells and whistles",
|
"description": "More reliable and low-maintenance self-hosted Pangolin server with extra bells and whistles",
|
||||||
@@ -1797,6 +2032,25 @@
|
|||||||
"invalidValue": "Invalid value",
|
"invalidValue": "Invalid value",
|
||||||
"idpTypeLabel": "Identity Provider Type",
|
"idpTypeLabel": "Identity Provider Type",
|
||||||
"roleMappingExpressionPlaceholder": "e.g., contains(groups, 'admin') && 'Admin' || 'Member'",
|
"roleMappingExpressionPlaceholder": "e.g., contains(groups, 'admin') && 'Admin' || 'Member'",
|
||||||
|
"roleMappingModeFixedRoles": "Fixed Roles",
|
||||||
|
"roleMappingModeMappingBuilder": "Mapping Builder",
|
||||||
|
"roleMappingModeRawExpression": "Raw Expression",
|
||||||
|
"roleMappingFixedRolesPlaceholderSelect": "Select one or more roles",
|
||||||
|
"roleMappingFixedRolesPlaceholderFreeform": "Type role names (exact match per organization)",
|
||||||
|
"roleMappingFixedRolesDescriptionSameForAll": "Assign the same role set to every auto-provisioned user.",
|
||||||
|
"roleMappingFixedRolesDescriptionDefaultPolicy": "For default policies, type role names that exist in each organization where users are provisioned. Names must match exactly.",
|
||||||
|
"roleMappingClaimPath": "Claim Path",
|
||||||
|
"roleMappingClaimPathPlaceholder": "groups",
|
||||||
|
"roleMappingClaimPathDescription": "Path in the token payload that contains source values (for example, groups).",
|
||||||
|
"roleMappingMatchValue": "Match Value",
|
||||||
|
"roleMappingAssignRoles": "Assign Roles",
|
||||||
|
"roleMappingAddMappingRule": "Add Mapping Rule",
|
||||||
|
"roleMappingRawExpressionResultDescription": "Expression must evaluate to a string or string array.",
|
||||||
|
"roleMappingRawExpressionResultDescriptionSingleRole": "Expression must evaluate to a string (a single role name).",
|
||||||
|
"roleMappingMatchValuePlaceholder": "Match value (for example: admin)",
|
||||||
|
"roleMappingAssignRolesPlaceholderFreeform": "Type role names (exact per org)",
|
||||||
|
"roleMappingBuilderFreeformRowHint": "Role names must match a role in each target organization.",
|
||||||
|
"roleMappingRemoveRule": "Remove",
|
||||||
"idpGoogleConfiguration": "Google Configuration",
|
"idpGoogleConfiguration": "Google Configuration",
|
||||||
"idpGoogleConfigurationDescription": "Configure the Google OAuth2 credentials",
|
"idpGoogleConfigurationDescription": "Configure the Google OAuth2 credentials",
|
||||||
"idpGoogleClientIdDescription": "Google OAuth2 Client ID",
|
"idpGoogleClientIdDescription": "Google OAuth2 Client ID",
|
||||||
@@ -1833,6 +2087,9 @@
|
|||||||
"authPageBrandingQuestionRemove": "Are you sure you want to remove the branding for Auth Pages ?",
|
"authPageBrandingQuestionRemove": "Are you sure you want to remove the branding for Auth Pages ?",
|
||||||
"authPageBrandingDeleteConfirm": "Confirm Delete Branding",
|
"authPageBrandingDeleteConfirm": "Confirm Delete Branding",
|
||||||
"brandingLogoURL": "Logo URL",
|
"brandingLogoURL": "Logo URL",
|
||||||
|
"brandingLogoURLOrPath": "Logo URL or Path",
|
||||||
|
"brandingLogoPathDescription": "Enter a URL or a local path.",
|
||||||
|
"brandingLogoURLDescription": "Enter a publicly accessible URL to your logo image.",
|
||||||
"brandingPrimaryColor": "Primary Color",
|
"brandingPrimaryColor": "Primary Color",
|
||||||
"brandingLogoWidth": "Width (px)",
|
"brandingLogoWidth": "Width (px)",
|
||||||
"brandingLogoHeight": "Height (px)",
|
"brandingLogoHeight": "Height (px)",
|
||||||
@@ -1882,6 +2139,13 @@
|
|||||||
"orgAuthBackToSignIn": "Back to standard sign in",
|
"orgAuthBackToSignIn": "Back to standard sign in",
|
||||||
"orgAuthNoAccount": "Don't have an account?",
|
"orgAuthNoAccount": "Don't have an account?",
|
||||||
"subscriptionRequiredToUse": "A subscription is required to use this feature.",
|
"subscriptionRequiredToUse": "A subscription is required to use this feature.",
|
||||||
|
"mustUpgradeToUse": "You must upgrade your subscription to use this feature.",
|
||||||
|
"subscriptionRequiredTierToUse": "This feature requires <tierLink>{tier}</tierLink>.",
|
||||||
|
"upgradeToTierToUse": "Upgrade to <tierLink>{tier}</tierLink> to use this feature.",
|
||||||
|
"subscriptionTierTier1": "Home",
|
||||||
|
"subscriptionTierTier2": "Team",
|
||||||
|
"subscriptionTierTier3": "Business",
|
||||||
|
"subscriptionTierEnterprise": "Enterprise",
|
||||||
"idpDisabled": "Identity providers are disabled.",
|
"idpDisabled": "Identity providers are disabled.",
|
||||||
"orgAuthPageDisabled": "Organization auth page is disabled.",
|
"orgAuthPageDisabled": "Organization auth page is disabled.",
|
||||||
"domainRestartedDescription": "Domain verification restarted successfully",
|
"domainRestartedDescription": "Domain verification restarted successfully",
|
||||||
@@ -1967,10 +2231,10 @@
|
|||||||
"manageMachineClients": "Manage Machine Clients",
|
"manageMachineClients": "Manage Machine Clients",
|
||||||
"manageMachineClientsDescription": "Create and manage clients that servers and systems use to privately connect to resources",
|
"manageMachineClientsDescription": "Create and manage clients that servers and systems use to privately connect to resources",
|
||||||
"machineClientsBannerTitle": "Servers & Automated Systems",
|
"machineClientsBannerTitle": "Servers & Automated Systems",
|
||||||
"machineClientsBannerDescription": "Machine clients are for servers and automated systems that are not associated with a specific user. They authenticate with an ID and secret, and can run with Pangolin CLI, Olm CLI, or Olm as a container.",
|
"machineClientsBannerDescription": "Machine clients are for servers and automated systems that are not associated with a specific user. They authenticate with an ID and secret, and can be deployed as a CLI or a container.",
|
||||||
"machineClientsBannerPangolinCLI": "Pangolin CLI",
|
"machineClientsBannerPangolinCLI": "Pangolin CLI",
|
||||||
"machineClientsBannerOlmCLI": "Olm CLI",
|
"machineClientsBannerOlmCLI": "Olm CLI",
|
||||||
"machineClientsBannerOlmContainer": "Olm Container",
|
"machineClientsBannerOlmContainer": "Container",
|
||||||
"clientsTableUserClients": "User",
|
"clientsTableUserClients": "User",
|
||||||
"clientsTableMachineClients": "Machine",
|
"clientsTableMachineClients": "Machine",
|
||||||
"licenseTableValidUntil": "Valid Until",
|
"licenseTableValidUntil": "Valid Until",
|
||||||
@@ -2069,6 +2333,32 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"newPricingLicenseForm": {
|
||||||
|
"title": "Get a license",
|
||||||
|
"description": "Choose a plan and tell us how you plan to use Pangolin.",
|
||||||
|
"chooseTier": "Choose your plan",
|
||||||
|
"viewPricingLink": "See pricing, features, and limits",
|
||||||
|
"tiers": {
|
||||||
|
"starter": {
|
||||||
|
"title": "Starter",
|
||||||
|
"description": "Enterprise features, 25 users, 25 sites, and community support."
|
||||||
|
},
|
||||||
|
"scale": {
|
||||||
|
"title": "Scale",
|
||||||
|
"description": "Enterprise features, 50 users, 50 sites, and priority support."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"personalUseOnly": "Personal use only (free license - no checkout)",
|
||||||
|
"buttons": {
|
||||||
|
"continueToCheckout": "Continue to Checkout"
|
||||||
|
},
|
||||||
|
"toasts": {
|
||||||
|
"checkoutError": {
|
||||||
|
"title": "Checkout error",
|
||||||
|
"description": "Could not start checkout. Please try again."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"priority": "Priority",
|
"priority": "Priority",
|
||||||
"priorityDescription": "Higher priority routes are evaluated first. Priority = 100 means automatic ordering (system decides). Use another number to enforce manual priority.",
|
"priorityDescription": "Higher priority routes are evaluated first. Priority = 100 means automatic ordering (system decides). Use another number to enforce manual priority.",
|
||||||
"instanceName": "Instance Name",
|
"instanceName": "Instance Name",
|
||||||
@@ -2157,6 +2447,8 @@
|
|||||||
"logRetentionAccessDescription": "How long to retain access logs",
|
"logRetentionAccessDescription": "How long to retain access logs",
|
||||||
"logRetentionActionLabel": "Action Log Retention",
|
"logRetentionActionLabel": "Action Log Retention",
|
||||||
"logRetentionActionDescription": "How long to retain action logs",
|
"logRetentionActionDescription": "How long to retain action logs",
|
||||||
|
"logRetentionConnectionLabel": "Connection Log Retention",
|
||||||
|
"logRetentionConnectionDescription": "How long to retain connection logs",
|
||||||
"logRetentionDisabled": "Disabled",
|
"logRetentionDisabled": "Disabled",
|
||||||
"logRetention3Days": "3 days",
|
"logRetention3Days": "3 days",
|
||||||
"logRetention7Days": "7 days",
|
"logRetention7Days": "7 days",
|
||||||
@@ -2167,7 +2459,15 @@
|
|||||||
"logRetentionEndOfFollowingYear": "End of following year",
|
"logRetentionEndOfFollowingYear": "End of following year",
|
||||||
"actionLogsDescription": "View a history of actions performed in this organization",
|
"actionLogsDescription": "View a history of actions performed in this organization",
|
||||||
"accessLogsDescription": "View access auth requests for resources in this organization",
|
"accessLogsDescription": "View access auth requests for resources in this organization",
|
||||||
"licenseRequiredToUse": "An Enterprise license is required to use this feature.",
|
"connectionLogs": "Connection Logs",
|
||||||
|
"connectionLogsDescription": "View connection logs for tunnels in this organization",
|
||||||
|
"sidebarLogsConnection": "Connection Logs",
|
||||||
|
"sidebarLogsStreaming": "Streaming",
|
||||||
|
"sourceAddress": "Source Address",
|
||||||
|
"destinationAddress": "Destination Address",
|
||||||
|
"duration": "Duration",
|
||||||
|
"licenseRequiredToUse": "An <enterpriseLicenseLink>Enterprise Edition</enterpriseLicenseLink> license or <pangolinCloudLink>Pangolin Cloud</pangolinCloudLink> is required to use this feature. <bookADemoLink>Book a free demo or POC trial to learn more</bookADemoLink>.",
|
||||||
|
"ossEnterpriseEditionRequired": "The <enterpriseEditionLink>Enterprise Edition</enterpriseEditionLink> is required to use this feature. This feature is also available in <pangolinCloudLink>Pangolin Cloud</pangolinCloudLink>. <bookADemoLink>Book a free demo or POC trial to learn more</bookADemoLink>.",
|
||||||
"certResolver": "Certificate Resolver",
|
"certResolver": "Certificate Resolver",
|
||||||
"certResolverDescription": "Select the certificate resolver to use for this resource.",
|
"certResolverDescription": "Select the certificate resolver to use for this resource.",
|
||||||
"selectCertResolver": "Select Certificate Resolver",
|
"selectCertResolver": "Select Certificate Resolver",
|
||||||
@@ -2228,6 +2528,8 @@
|
|||||||
"deviceCodeInvalidFormat": "Code must be 9 characters (e.g., A1AJ-N5JD)",
|
"deviceCodeInvalidFormat": "Code must be 9 characters (e.g., A1AJ-N5JD)",
|
||||||
"deviceCodeInvalidOrExpired": "Invalid or expired code",
|
"deviceCodeInvalidOrExpired": "Invalid or expired code",
|
||||||
"deviceCodeVerifyFailed": "Failed to verify device code",
|
"deviceCodeVerifyFailed": "Failed to verify device code",
|
||||||
|
"deviceCodeValidating": "Validating device code...",
|
||||||
|
"deviceCodeVerifying": "Verifying device authorization...",
|
||||||
"signedInAs": "Signed in as",
|
"signedInAs": "Signed in as",
|
||||||
"deviceCodeEnterPrompt": "Enter the code displayed on the device",
|
"deviceCodeEnterPrompt": "Enter the code displayed on the device",
|
||||||
"continue": "Continue",
|
"continue": "Continue",
|
||||||
@@ -2240,7 +2542,7 @@
|
|||||||
"deviceOrganizationsAccess": "Access to all organizations your account has access to",
|
"deviceOrganizationsAccess": "Access to all organizations your account has access to",
|
||||||
"deviceAuthorize": "Authorize {applicationName}",
|
"deviceAuthorize": "Authorize {applicationName}",
|
||||||
"deviceConnected": "Device Connected!",
|
"deviceConnected": "Device Connected!",
|
||||||
"deviceAuthorizedMessage": "Device is authorized to access your account.",
|
"deviceAuthorizedMessage": "Device is authorized to access your account. Please return to the client application.",
|
||||||
"pangolinCloud": "Pangolin Cloud",
|
"pangolinCloud": "Pangolin Cloud",
|
||||||
"viewDevices": "View Devices",
|
"viewDevices": "View Devices",
|
||||||
"viewDevicesDescription": "Manage your connected devices",
|
"viewDevicesDescription": "Manage your connected devices",
|
||||||
@@ -2302,10 +2604,14 @@
|
|||||||
"identifier": "Identifier",
|
"identifier": "Identifier",
|
||||||
"deviceLoginUseDifferentAccount": "Not you? Use a different account.",
|
"deviceLoginUseDifferentAccount": "Not you? Use a different account.",
|
||||||
"deviceLoginDeviceRequestingAccessToAccount": "A device is requesting access to this account.",
|
"deviceLoginDeviceRequestingAccessToAccount": "A device is requesting access to this account.",
|
||||||
|
"loginSelectAuthenticationMethod": "Select an authentication method to continue.",
|
||||||
"noData": "No Data",
|
"noData": "No Data",
|
||||||
"machineClients": "Machine Clients",
|
"machineClients": "Machine Clients",
|
||||||
"install": "Install",
|
"install": "Install",
|
||||||
"run": "Run",
|
"run": "Run",
|
||||||
|
"envFile": "Environment File",
|
||||||
|
"serviceFile": "Service File",
|
||||||
|
"enableAndStart": "Enable and Start",
|
||||||
"clientNameDescription": "The display name of the client that can be changed later.",
|
"clientNameDescription": "The display name of the client that can be changed later.",
|
||||||
"clientAddress": "Client Address (Advanced)",
|
"clientAddress": "Client Address (Advanced)",
|
||||||
"setupFailedToFetchSubnet": "Failed to fetch default subnet",
|
"setupFailedToFetchSubnet": "Failed to fetch default subnet",
|
||||||
@@ -2346,6 +2652,7 @@
|
|||||||
"enterConfirmation": "Enter confirmation",
|
"enterConfirmation": "Enter confirmation",
|
||||||
"blueprintViewDetails": "Details",
|
"blueprintViewDetails": "Details",
|
||||||
"defaultIdentityProvider": "Default Identity Provider",
|
"defaultIdentityProvider": "Default Identity Provider",
|
||||||
|
"defaultIdentityProviderDescription": "When a default identity provider is selected, the user will be automatically redirected to the provider for authentication.",
|
||||||
"editInternalResourceDialogNetworkSettings": "Network Settings",
|
"editInternalResourceDialogNetworkSettings": "Network Settings",
|
||||||
"editInternalResourceDialogAccessPolicy": "Access Policy",
|
"editInternalResourceDialogAccessPolicy": "Access Policy",
|
||||||
"editInternalResourceDialogAddRoles": "Add Roles",
|
"editInternalResourceDialogAddRoles": "Add Roles",
|
||||||
@@ -2360,8 +2667,231 @@
|
|||||||
"editInternalResourceDialogAccessControl": "Access Control",
|
"editInternalResourceDialogAccessControl": "Access Control",
|
||||||
"editInternalResourceDialogAccessControlDescription": "Control which roles, users, and machine clients have access to this resource when connected. Admins always have access.",
|
"editInternalResourceDialogAccessControlDescription": "Control which roles, users, and machine clients have access to this resource when connected. Admins always have access.",
|
||||||
"editInternalResourceDialogPortRangeValidationError": "Port range must be \"*\" for all ports, or a comma-separated list of ports and ranges (e.g., \"80,443,8000-9000\"). Ports must be between 1 and 65535.",
|
"editInternalResourceDialogPortRangeValidationError": "Port range must be \"*\" for all ports, or a comma-separated list of ports and ranges (e.g., \"80,443,8000-9000\"). Ports must be between 1 and 65535.",
|
||||||
|
"internalResourceAuthDaemonStrategy": "SSH Auth Daemon Location",
|
||||||
|
"internalResourceAuthDaemonStrategyDescription": "Choose where the SSH authentication daemon runs: on the site (Newt) or on a remote host.",
|
||||||
|
"internalResourceAuthDaemonDescription": "The SSH authentication daemon handles SSH key signing and PAM authentication for this resource. Choose whether it runs on the site (Newt) or on a separate remote host. See <docsLink>the documentation</docsLink> for more.",
|
||||||
|
"internalResourceAuthDaemonDocsUrl": "https://docs.pangolin.net",
|
||||||
|
"internalResourceAuthDaemonStrategyPlaceholder": "Select Strategy",
|
||||||
|
"internalResourceAuthDaemonStrategyLabel": "Location",
|
||||||
|
"internalResourceAuthDaemonSite": "On Site",
|
||||||
|
"internalResourceAuthDaemonSiteDescription": "Auth daemon runs on the site (Newt).",
|
||||||
|
"internalResourceAuthDaemonRemote": "Remote Host",
|
||||||
|
"internalResourceAuthDaemonRemoteDescription": "Auth daemon runs on this resource's destination - not the site.",
|
||||||
|
"internalResourceAuthDaemonPort": "Daemon Port (optional)",
|
||||||
"orgAuthWhatsThis": "Where can I find my organization ID?",
|
"orgAuthWhatsThis": "Where can I find my organization ID?",
|
||||||
"learnMore": "Learn more",
|
"learnMore": "Learn more",
|
||||||
"backToHome": "Go back to home",
|
"backToHome": "Go back to home",
|
||||||
"needToSignInToOrg": "Need to use your organization's identity provider?"
|
"needToSignInToOrg": "Need to use your organization's identity provider?",
|
||||||
|
"maintenanceMode": "Maintenance Mode",
|
||||||
|
"maintenanceModeDescription": "Display a maintenance page to visitors",
|
||||||
|
"maintenanceModeType": "Maintenance Mode Type",
|
||||||
|
"showMaintenancePage": "Show a maintenance page to visitors",
|
||||||
|
"enableMaintenanceMode": "Enable Maintenance Mode",
|
||||||
|
"automatic": "Automatic",
|
||||||
|
"automaticModeDescription": " Show maintenance page only when all backend targets are down or unhealthy. Your resource continues working normally as long as at least one target is healthy.",
|
||||||
|
"forced": "Forced",
|
||||||
|
"forcedModeDescription": "Always show the maintenance page regardless of backend health. Use this for planned maintenance when you want to prevent all access.",
|
||||||
|
"warning:": "Warning:",
|
||||||
|
"forcedeModeWarning": "All traffic will be directed to the maintenance page. Your backend resources will not receive any requests.",
|
||||||
|
"pageTitle": "Page Title",
|
||||||
|
"pageTitleDescription": "The main heading displayed on the maintenance page",
|
||||||
|
"maintenancePageMessage": "Maintenance Message",
|
||||||
|
"maintenancePageMessagePlaceholder": "We'll be back soon! Our site is currently undergoing scheduled maintenance.",
|
||||||
|
"maintenancePageMessageDescription": "Detailed message explaining the maintenance",
|
||||||
|
"maintenancePageTimeTitle": "Estimated Completion Time (Optional)",
|
||||||
|
"maintenanceTime": "e.g., 2 hours, Nov 1 at 5:00 PM",
|
||||||
|
"maintenanceEstimatedTimeDescription": "When you expect maintenance to be completed",
|
||||||
|
"editDomain": "Edit Domain",
|
||||||
|
"editDomainDescription": "Select a domain for your resource",
|
||||||
|
"maintenanceModeDisabledTooltip": "This feature requires a valid license to enable.",
|
||||||
|
"maintenanceScreenTitle": "Service Temporarily Unavailable",
|
||||||
|
"maintenanceScreenMessage": "We are currently experiencing technical difficulties. Please check back soon.",
|
||||||
|
"maintenanceScreenEstimatedCompletion": "Estimated Completion:",
|
||||||
|
"createInternalResourceDialogDestinationRequired": "Destination is required",
|
||||||
|
"available": "Available",
|
||||||
|
"archived": "Archived",
|
||||||
|
"noArchivedDevices": "No archived devices found",
|
||||||
|
"deviceArchived": "Device archived",
|
||||||
|
"deviceArchivedDescription": "The device has been successfully archived.",
|
||||||
|
"errorArchivingDevice": "Error archiving device",
|
||||||
|
"failedToArchiveDevice": "Failed to archive device",
|
||||||
|
"deviceQuestionArchive": "Are you sure you want to archive this device?",
|
||||||
|
"deviceMessageArchive": "The device will be archived and removed from your active devices list.",
|
||||||
|
"deviceArchiveConfirm": "Archive Device",
|
||||||
|
"archiveDevice": "Archive Device",
|
||||||
|
"archive": "Archive",
|
||||||
|
"deviceUnarchived": "Device unarchived",
|
||||||
|
"deviceUnarchivedDescription": "The device has been successfully unarchived.",
|
||||||
|
"errorUnarchivingDevice": "Error unarchiving device",
|
||||||
|
"failedToUnarchiveDevice": "Failed to unarchive device",
|
||||||
|
"unarchive": "Unarchive",
|
||||||
|
"archiveClient": "Archive Client",
|
||||||
|
"archiveClientQuestion": "Are you sure you want to archive this client?",
|
||||||
|
"archiveClientMessage": "The client will be archived and removed from your active clients list.",
|
||||||
|
"archiveClientConfirm": "Archive Client",
|
||||||
|
"blockClient": "Block Client",
|
||||||
|
"blockClientQuestion": "Are you sure you want to block this client?",
|
||||||
|
"blockClientMessage": "The device will be forced to disconnect if currently connected. You can unblock the device later.",
|
||||||
|
"blockClientConfirm": "Block Client",
|
||||||
|
"active": "Active",
|
||||||
|
"usernameOrEmail": "Username or Email",
|
||||||
|
"selectYourOrganization": "Select your organization",
|
||||||
|
"signInTo": "Log in in to",
|
||||||
|
"signInWithPassword": "Continue with Password",
|
||||||
|
"noAuthMethodsAvailable": "No authentication methods available for this organization.",
|
||||||
|
"enterPassword": "Enter your password",
|
||||||
|
"enterMfaCode": "Enter the code from your authenticator app",
|
||||||
|
"securityKeyRequired": "Please use your security key to sign in.",
|
||||||
|
"needToUseAnotherAccount": "Need to use a different account?",
|
||||||
|
"loginLegalDisclaimer": "By clicking the buttons below, you acknowledge you have read, understand, and agree to the <termsOfService>Terms of Service</termsOfService> and <privacyPolicy>Privacy Policy</privacyPolicy>.",
|
||||||
|
"termsOfService": "Terms of Service",
|
||||||
|
"privacyPolicy": "Privacy Policy",
|
||||||
|
"userNotFoundWithUsername": "No user found with that username.",
|
||||||
|
"verify": "Verify",
|
||||||
|
"signIn": "Sign In",
|
||||||
|
"forgotPassword": "Forgot password?",
|
||||||
|
"orgSignInTip": "If you've logged in before, you can enter your username or email above to authenticate with your organization's identity provider instead. It's easier!",
|
||||||
|
"continueAnyway": "Continue anyway",
|
||||||
|
"dontShowAgain": "Don't show again",
|
||||||
|
"orgSignInNotice": "Did you know?",
|
||||||
|
"signupOrgNotice": "Trying to sign in?",
|
||||||
|
"signupOrgTip": "Are you trying to sign in through your organization's identity provider?",
|
||||||
|
"signupOrgLink": "Sign in or sign up with your organization instead",
|
||||||
|
"verifyEmailLogInWithDifferentAccount": "Use a Different Account",
|
||||||
|
"logIn": "Log In",
|
||||||
|
"deviceInformation": "Device Information",
|
||||||
|
"deviceInformationDescription": "Information about the device and agent",
|
||||||
|
"deviceSecurity": "Device Security",
|
||||||
|
"deviceSecurityDescription": "Device security posture information",
|
||||||
|
"platform": "Platform",
|
||||||
|
"macosVersion": "macOS Version",
|
||||||
|
"windowsVersion": "Windows Version",
|
||||||
|
"iosVersion": "iOS Version",
|
||||||
|
"androidVersion": "Android Version",
|
||||||
|
"osVersion": "OS Version",
|
||||||
|
"kernelVersion": "Kernel Version",
|
||||||
|
"deviceModel": "Device Model",
|
||||||
|
"serialNumber": "Serial Number",
|
||||||
|
"hostname": "Hostname",
|
||||||
|
"firstSeen": "First Seen",
|
||||||
|
"lastSeen": "Last Seen",
|
||||||
|
"biometricsEnabled": "Biometrics Enabled",
|
||||||
|
"diskEncrypted": "Disk Encrypted",
|
||||||
|
"firewallEnabled": "Firewall Enabled",
|
||||||
|
"autoUpdatesEnabled": "Auto Updates Enabled",
|
||||||
|
"tpmAvailable": "TPM Available",
|
||||||
|
"windowsAntivirusEnabled": "Antivirus Enabled",
|
||||||
|
"macosSipEnabled": "System Integrity Protection (SIP)",
|
||||||
|
"macosGatekeeperEnabled": "Gatekeeper",
|
||||||
|
"macosFirewallStealthMode": "Firewall Stealth Mode",
|
||||||
|
"linuxAppArmorEnabled": "AppArmor",
|
||||||
|
"linuxSELinuxEnabled": "SELinux",
|
||||||
|
"deviceSettingsDescription": "View device information and settings",
|
||||||
|
"devicePendingApprovalDescription": "This device is waiting for approval",
|
||||||
|
"deviceBlockedDescription": "This device is currently blocked. It won't be able to connect to any resources unless unblocked.",
|
||||||
|
"unblockClient": "Unblock Client",
|
||||||
|
"unblockClientDescription": "The device has been unblocked",
|
||||||
|
"unarchiveClient": "Unarchive Client",
|
||||||
|
"unarchiveClientDescription": "The device has been unarchived",
|
||||||
|
"block": "Block",
|
||||||
|
"unblock": "Unblock",
|
||||||
|
"deviceActions": "Device Actions",
|
||||||
|
"deviceActionsDescription": "Manage device status and access",
|
||||||
|
"devicePendingApprovalBannerDescription": "This device is pending approval. It won't be able to connect to resources until approved.",
|
||||||
|
"connected": "Connected",
|
||||||
|
"disconnected": "Disconnected",
|
||||||
|
"approvalsEmptyStateTitle": "Device Approvals Not Enabled",
|
||||||
|
"approvalsEmptyStateDescription": "Enable device approvals for roles to require admin approval before users can connect new devices.",
|
||||||
|
"approvalsEmptyStateStep1Title": "Go to Roles",
|
||||||
|
"approvalsEmptyStateStep1Description": "Navigate to your organization's roles settings to configure device approvals.",
|
||||||
|
"approvalsEmptyStateStep2Title": "Enable Device Approvals",
|
||||||
|
"approvalsEmptyStateStep2Description": "Edit a role and enable the 'Require Device Approvals' option. Users with this role will need admin approval for new devices.",
|
||||||
|
"approvalsEmptyStatePreviewDescription": "Preview: When enabled, pending device requests will appear here for review",
|
||||||
|
"approvalsEmptyStateButtonText": "Manage Roles",
|
||||||
|
"domainErrorTitle": "We are having trouble verifying your domain",
|
||||||
|
"idpAdminAutoProvisionPoliciesTabHint": "Configure role mapping and organization policies on the <policiesTabLink>Auto Provision Settings</policiesTabLink> tab.",
|
||||||
|
"streamingTitle": "Event Streaming",
|
||||||
|
"streamingDescription": "Stream events from your organization to external destinations in real time.",
|
||||||
|
"streamingUnnamedDestination": "Unnamed destination",
|
||||||
|
"streamingNoUrlConfigured": "No URL configured",
|
||||||
|
"streamingAddDestination": "Add Destination",
|
||||||
|
"streamingHttpWebhookTitle": "HTTP Webhook",
|
||||||
|
"streamingHttpWebhookDescription": "Send events to any HTTP endpoint with flexible authentication and templating.",
|
||||||
|
"streamingS3Title": "Amazon S3",
|
||||||
|
"streamingS3Description": "Stream events to an S3-compatible object storage bucket. Coming soon.",
|
||||||
|
"streamingDatadogTitle": "Datadog",
|
||||||
|
"streamingDatadogDescription": "Forward events directly to your Datadog account. Coming soon.",
|
||||||
|
"streamingTypePickerDescription": "Choose a destination type to get started.",
|
||||||
|
"streamingFailedToLoad": "Failed to load destinations",
|
||||||
|
"streamingUnexpectedError": "An unexpected error occurred.",
|
||||||
|
"streamingFailedToUpdate": "Failed to update destination",
|
||||||
|
"streamingDeletedSuccess": "Destination deleted successfully",
|
||||||
|
"streamingFailedToDelete": "Failed to delete destination",
|
||||||
|
"streamingDeleteTitle": "Delete Destination",
|
||||||
|
"streamingDeleteButtonText": "Delete Destination",
|
||||||
|
"streamingDeleteDialogAreYouSure": "Are you sure you want to delete",
|
||||||
|
"streamingDeleteDialogThisDestination": "this destination",
|
||||||
|
"streamingDeleteDialogPermanentlyRemoved": "? All configuration will be permanently removed.",
|
||||||
|
"httpDestEditTitle": "Edit Destination",
|
||||||
|
"httpDestAddTitle": "Add HTTP Destination",
|
||||||
|
"httpDestEditDescription": "Update the configuration for this HTTP event streaming destination.",
|
||||||
|
"httpDestAddDescription": "Configure a new HTTP endpoint to receive your organization's events.",
|
||||||
|
"httpDestTabSettings": "Settings",
|
||||||
|
"httpDestTabHeaders": "Headers",
|
||||||
|
"httpDestTabBody": "Body",
|
||||||
|
"httpDestTabLogs": "Logs",
|
||||||
|
"httpDestNamePlaceholder": "My HTTP destination",
|
||||||
|
"httpDestUrlLabel": "Destination URL",
|
||||||
|
"httpDestUrlErrorHttpRequired": "URL must use http or https",
|
||||||
|
"httpDestUrlErrorHttpsRequired": "HTTPS is required on cloud deployments",
|
||||||
|
"httpDestUrlErrorInvalid": "Enter a valid URL (e.g. https://example.com/webhook)",
|
||||||
|
"httpDestAuthTitle": "Authentication",
|
||||||
|
"httpDestAuthDescription": "Choose how requests to your endpoint are authenticated.",
|
||||||
|
"httpDestAuthNoneTitle": "No Authentication",
|
||||||
|
"httpDestAuthNoneDescription": "Sends requests without an Authorization header.",
|
||||||
|
"httpDestAuthBearerTitle": "Bearer Token",
|
||||||
|
"httpDestAuthBearerDescription": "Adds an Authorization: Bearer '<token>' header to each request.",
|
||||||
|
"httpDestAuthBearerPlaceholder": "Your API key or token",
|
||||||
|
"httpDestAuthBasicTitle": "Basic Auth",
|
||||||
|
"httpDestAuthBasicDescription": "Adds an Authorization: Basic '<credentials>' header. Provide credentials as username:password.",
|
||||||
|
"httpDestAuthBasicPlaceholder": "username:password",
|
||||||
|
"httpDestAuthCustomTitle": "Custom Header",
|
||||||
|
"httpDestAuthCustomDescription": "Specify a custom HTTP header name and value for authentication (e.g. X-API-Key).",
|
||||||
|
"httpDestAuthCustomHeaderNamePlaceholder": "Header name (e.g. X-API-Key)",
|
||||||
|
"httpDestAuthCustomHeaderValuePlaceholder": "Header value",
|
||||||
|
"httpDestCustomHeadersTitle": "Custom HTTP Headers",
|
||||||
|
"httpDestCustomHeadersDescription": "Add custom headers to every outgoing request. Useful for static tokens or a custom Content-Type. By default, Content-Type: application/json is sent.",
|
||||||
|
"httpDestNoHeadersConfigured": "No custom headers configured. Click \"Add Header\" to add one.",
|
||||||
|
"httpDestHeaderNamePlaceholder": "Header name",
|
||||||
|
"httpDestHeaderValuePlaceholder": "Value",
|
||||||
|
"httpDestAddHeader": "Add Header",
|
||||||
|
"httpDestBodyTemplateTitle": "Custom Body Template",
|
||||||
|
"httpDestBodyTemplateDescription": "Control the JSON payload structure sent to your endpoint. If disabled, a default JSON object is sent for each event.",
|
||||||
|
"httpDestEnableBodyTemplate": "Enable custom body template",
|
||||||
|
"httpDestBodyTemplateLabel": "Body Template (JSON)",
|
||||||
|
"httpDestBodyTemplateHint": "Use template variables to reference event fields in your payload.",
|
||||||
|
"httpDestPayloadFormatTitle": "Payload Format",
|
||||||
|
"httpDestPayloadFormatDescription": "How events are serialised into each request body.",
|
||||||
|
"httpDestFormatJsonArrayTitle": "JSON Array",
|
||||||
|
"httpDestFormatJsonArrayDescription": "One request per batch, body is a JSON array. Compatible with most generic webhooks and Datadog.",
|
||||||
|
"httpDestFormatNdjsonTitle": "NDJSON",
|
||||||
|
"httpDestFormatNdjsonDescription": "One request per batch, body is newline-delimited JSON — one object per line, no outer array. Required by Splunk HEC, Elastic / OpenSearch, and Grafana Loki.",
|
||||||
|
"httpDestFormatSingleTitle": "One Event Per Request",
|
||||||
|
"httpDestFormatSingleDescription": "Sends a separate HTTP POST for each individual event. Use only for endpoints that cannot handle batches.",
|
||||||
|
"httpDestLogTypesTitle": "Log Types",
|
||||||
|
"httpDestLogTypesDescription": "Choose which log types are forwarded to this destination. Only enabled log types will be streamed.",
|
||||||
|
"httpDestAccessLogsTitle": "Access Logs",
|
||||||
|
"httpDestAccessLogsDescription": "Resource access attempts, including authenticated and denied requests.",
|
||||||
|
"httpDestActionLogsTitle": "Action Logs",
|
||||||
|
"httpDestActionLogsDescription": "Administrative actions performed by users within the organization.",
|
||||||
|
"httpDestConnectionLogsTitle": "Connection Logs",
|
||||||
|
"httpDestConnectionLogsDescription": "Site and tunnel connection events, including connects and disconnects.",
|
||||||
|
"httpDestRequestLogsTitle": "Request Logs",
|
||||||
|
"httpDestRequestLogsDescription": "HTTP request logs for proxied resources, including method, path, and response code.",
|
||||||
|
"httpDestSaveChanges": "Save Changes",
|
||||||
|
"httpDestCreateDestination": "Create Destination",
|
||||||
|
"httpDestUpdatedSuccess": "Destination updated successfully",
|
||||||
|
"httpDestCreatedSuccess": "Destination created successfully",
|
||||||
|
"httpDestUpdateFailed": "Failed to update destination",
|
||||||
|
"httpDestCreateFailed": "Failed to create destination"
|
||||||
}
|
}
|
||||||
|
|||||||
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
File diff suppressed because it is too large
Load Diff
@@ -1,5 +1,7 @@
|
|||||||
{
|
{
|
||||||
"setupCreate": "創建您的第一個組織、網站和資源",
|
"setupCreate": "創建您的第一個組織、網站和資源",
|
||||||
|
"headerAuthCompatibilityInfo": "啟用此選項以在缺少驗證令牌時強制回傳 401 未授權回應。這對於不會在沒有伺服器挑戰的情況下發送憑證的瀏覽器或特定 HTTP 函式庫是必需的。",
|
||||||
|
"headerAuthCompatibility": "擴展相容性",
|
||||||
"setupNewOrg": "新建組織",
|
"setupNewOrg": "新建組織",
|
||||||
"setupCreateOrg": "創建組織",
|
"setupCreateOrg": "創建組織",
|
||||||
"setupCreateResources": "創建資源",
|
"setupCreateResources": "創建資源",
|
||||||
@@ -51,6 +53,9 @@
|
|||||||
"siteQuestionRemove": "您確定要從組織中刪除該站點嗎?",
|
"siteQuestionRemove": "您確定要從組織中刪除該站點嗎?",
|
||||||
"siteManageSites": "管理站點",
|
"siteManageSites": "管理站點",
|
||||||
"siteDescription": "允許通過安全隧道連接到您的網路",
|
"siteDescription": "允許通過安全隧道連接到您的網路",
|
||||||
|
"sitesBannerTitle": "連接任何網路",
|
||||||
|
"sitesBannerDescription": "站點是與遠端網路的連接,使 Pangolin 能夠為任何地方的使用者提供對公共或私有資源的存取。在任何可以執行二進位檔案或容器的地方安裝站點網路連接器 (Newt) 以建立連接。",
|
||||||
|
"sitesBannerButtonText": "安裝站點",
|
||||||
"siteCreate": "創建站點",
|
"siteCreate": "創建站點",
|
||||||
"siteCreateDescription2": "按照下面的步驟創建和連接一個新站點",
|
"siteCreateDescription2": "按照下面的步驟創建和連接一個新站點",
|
||||||
"siteCreateDescription": "創建一個新站點開始連接您的資源",
|
"siteCreateDescription": "創建一個新站點開始連接您的資源",
|
||||||
@@ -65,8 +70,8 @@
|
|||||||
"siteLoadWGConfig": "正在載入 WireGuard 配置...",
|
"siteLoadWGConfig": "正在載入 WireGuard 配置...",
|
||||||
"siteDocker": "擴展 Docker 部署詳細資訊",
|
"siteDocker": "擴展 Docker 部署詳細資訊",
|
||||||
"toggle": "切換",
|
"toggle": "切換",
|
||||||
"dockerCompose": "Docker 配置",
|
"dockerCompose": "Docker Compose",
|
||||||
"dockerRun": "停靠欄",
|
"dockerRun": "Docker Run",
|
||||||
"siteLearnLocal": "本地站點不需要隧道連接,點擊了解更多",
|
"siteLearnLocal": "本地站點不需要隧道連接,點擊了解更多",
|
||||||
"siteConfirmCopy": "我已經複製了配置資訊",
|
"siteConfirmCopy": "我已經複製了配置資訊",
|
||||||
"searchSitesProgress": "搜索站點...",
|
"searchSitesProgress": "搜索站點...",
|
||||||
@@ -98,9 +103,10 @@
|
|||||||
"siteLocalDescriptionSaas": "僅本地資源。沒有隧道。僅在遠程節點上可用。",
|
"siteLocalDescriptionSaas": "僅本地資源。沒有隧道。僅在遠程節點上可用。",
|
||||||
"siteSeeAll": "查看所有站點",
|
"siteSeeAll": "查看所有站點",
|
||||||
"siteTunnelDescription": "確定如何連接到您的網站",
|
"siteTunnelDescription": "確定如何連接到您的網站",
|
||||||
"siteNewtCredentials": "Newt 憑據",
|
"siteNewtCredentials": "Newt 憑證",
|
||||||
"siteNewtCredentialsDescription": "這是 Newt 伺服器的身份驗證憑據",
|
"siteNewtCredentialsDescription": "這是 Newt 伺服器的身份驗證憑證",
|
||||||
"siteCredentialsSave": "保存您的憑據",
|
"remoteNodeCredentialsDescription": "這是遠端節點與伺服器進行驗證的方式",
|
||||||
|
"siteCredentialsSave": "保存您的憑證",
|
||||||
"siteCredentialsSaveDescription": "您只能看到一次。請確保將其複製並保存到一個安全的地方。",
|
"siteCredentialsSaveDescription": "您只能看到一次。請確保將其複製並保存到一個安全的地方。",
|
||||||
"siteInfo": "站點資訊",
|
"siteInfo": "站點資訊",
|
||||||
"status": "狀態",
|
"status": "狀態",
|
||||||
@@ -144,8 +150,14 @@
|
|||||||
"expires": "過期時間",
|
"expires": "過期時間",
|
||||||
"never": "永不過期",
|
"never": "永不過期",
|
||||||
"shareErrorSelectResource": "請選擇一個資源",
|
"shareErrorSelectResource": "請選擇一個資源",
|
||||||
"resourceTitle": "管理資源",
|
"proxyResourceTitle": "管理公開資源",
|
||||||
"resourceDescription": "為您的私人應用程式創建安全代理",
|
"proxyResourceDescription": "建立和管理可透過網頁瀏覽器公開存取的資源",
|
||||||
|
"proxyResourcesBannerTitle": "基於網頁的公開存取",
|
||||||
|
"proxyResourcesBannerDescription": "公開資源是任何人都可以透過網頁瀏覽器存取的 HTTPS 或 TCP/UDP 代理。與私有資源不同,它們不需要客戶端軟體,並且可以包含基於身份和情境感知的存取策略。",
|
||||||
|
"clientResourceTitle": "管理私有資源",
|
||||||
|
"clientResourceDescription": "建立和管理只能透過已連接的客戶端存取的資源",
|
||||||
|
"privateResourcesBannerTitle": "零信任私有存取",
|
||||||
|
"privateResourcesBannerDescription": "私有資源使用零信任安全性,確保使用者和機器只能存取您明確授權的資源。連接使用者裝置或機器客戶端以透過安全的虛擬私人網路存取這些資源。",
|
||||||
"resourcesSearch": "搜索資源...",
|
"resourcesSearch": "搜索資源...",
|
||||||
"resourceAdd": "添加資源",
|
"resourceAdd": "添加資源",
|
||||||
"resourceErrorDelte": "刪除資源時出錯",
|
"resourceErrorDelte": "刪除資源時出錯",
|
||||||
@@ -179,7 +191,7 @@
|
|||||||
"baseDomain": "根域名",
|
"baseDomain": "根域名",
|
||||||
"subdomnainDescription": "您的資源可以訪問的子域名。",
|
"subdomnainDescription": "您的資源可以訪問的子域名。",
|
||||||
"resourceRawSettings": "TCP/UDP 設置",
|
"resourceRawSettings": "TCP/UDP 設置",
|
||||||
"resourceRawSettingsDescription": "配置如何通過 TCP/UDP 訪問您的資源。 您映射資源到主機Pangolin伺服器上的埠,這樣您就可以訪問伺服器-公共-ip:mapped埠的資源。",
|
"resourceRawSettingsDescription": "設定如何透過 TCP/UDP 存取資源",
|
||||||
"protocol": "協議",
|
"protocol": "協議",
|
||||||
"protocolSelect": "選擇協議",
|
"protocolSelect": "選擇協議",
|
||||||
"resourcePortNumber": "埠號",
|
"resourcePortNumber": "埠號",
|
||||||
@@ -436,6 +448,16 @@
|
|||||||
"inviteEmailSent": "發送邀請郵件給用戶",
|
"inviteEmailSent": "發送邀請郵件給用戶",
|
||||||
"inviteValid": "有效",
|
"inviteValid": "有效",
|
||||||
"selectDuration": "選擇持續時間",
|
"selectDuration": "選擇持續時間",
|
||||||
|
"selectResource": "選擇資源",
|
||||||
|
"filterByResource": "依資源篩選",
|
||||||
|
"resetFilters": "重設篩選條件",
|
||||||
|
"totalBlocked": "被 Pangolin 阻擋的請求",
|
||||||
|
"totalRequests": "總請求數",
|
||||||
|
"requestsByCountry": "依國家/地區的請求",
|
||||||
|
"requestsByDay": "依日期的請求",
|
||||||
|
"blocked": "已阻擋",
|
||||||
|
"allowed": "已允許",
|
||||||
|
"topCountries": "熱門國家/地區",
|
||||||
"accessRoleSelect": "選擇角色",
|
"accessRoleSelect": "選擇角色",
|
||||||
"inviteEmailSentDescription": "一封電子郵件已經發送給用戶,帶有下面的訪問連結。他們必須訪問該連結才能接受邀請。",
|
"inviteEmailSentDescription": "一封電子郵件已經發送給用戶,帶有下面的訪問連結。他們必須訪問該連結才能接受邀請。",
|
||||||
"inviteSentDescription": "用戶已被邀請。他們必須訪問下面的連結才能接受邀請。",
|
"inviteSentDescription": "用戶已被邀請。他們必須訪問下面的連結才能接受邀請。",
|
||||||
@@ -465,7 +487,7 @@
|
|||||||
"proxyErrorTls": "無效的 TLS 伺服器名稱。使用域名格式,或保存空以刪除 TLS 伺服器名稱。",
|
"proxyErrorTls": "無效的 TLS 伺服器名稱。使用域名格式,或保存空以刪除 TLS 伺服器名稱。",
|
||||||
"proxyEnableSSL": "啟用 SSL",
|
"proxyEnableSSL": "啟用 SSL",
|
||||||
"proxyEnableSSLDescription": "啟用 SSL/TLS 加密以確保您目標的 HTTPS 連接。",
|
"proxyEnableSSLDescription": "啟用 SSL/TLS 加密以確保您目標的 HTTPS 連接。",
|
||||||
"target": "Target",
|
"target": "目標",
|
||||||
"configureTarget": "配置目標",
|
"configureTarget": "配置目標",
|
||||||
"targetErrorFetch": "獲取目標失敗",
|
"targetErrorFetch": "獲取目標失敗",
|
||||||
"targetErrorFetchDescription": "獲取目標時出錯",
|
"targetErrorFetchDescription": "獲取目標時出錯",
|
||||||
@@ -516,6 +538,8 @@
|
|||||||
"targetCreatedDescription": "目標已成功創建",
|
"targetCreatedDescription": "目標已成功創建",
|
||||||
"targetErrorCreate": "創建目標失敗",
|
"targetErrorCreate": "創建目標失敗",
|
||||||
"targetErrorCreateDescription": "創建目標時出錯",
|
"targetErrorCreateDescription": "創建目標時出錯",
|
||||||
|
"tlsServerName": "TLS 伺服器名稱",
|
||||||
|
"tlsServerNameDescription": "用於 SNI 的 TLS 伺服器名稱",
|
||||||
"save": "保存",
|
"save": "保存",
|
||||||
"proxyAdditional": "附加代理設置",
|
"proxyAdditional": "附加代理設置",
|
||||||
"proxyAdditionalDescription": "配置你的資源如何處理代理設置",
|
"proxyAdditionalDescription": "配置你的資源如何處理代理設置",
|
||||||
@@ -702,6 +726,7 @@
|
|||||||
"resourceTransferSubmit": "轉移資源",
|
"resourceTransferSubmit": "轉移資源",
|
||||||
"siteDestination": "目標站點",
|
"siteDestination": "目標站點",
|
||||||
"searchSites": "搜索站點",
|
"searchSites": "搜索站點",
|
||||||
|
"countries": "國家/地區",
|
||||||
"accessRoleCreate": "創建角色",
|
"accessRoleCreate": "創建角色",
|
||||||
"accessRoleCreateDescription": "創建一個新角色來分組用戶並管理他們的權限。",
|
"accessRoleCreateDescription": "創建一個新角色來分組用戶並管理他們的權限。",
|
||||||
"accessRoleCreateSubmit": "創建角色",
|
"accessRoleCreateSubmit": "創建角色",
|
||||||
@@ -825,6 +850,7 @@
|
|||||||
"orgPolicyConfig": "配置組織訪問權限",
|
"orgPolicyConfig": "配置組織訪問權限",
|
||||||
"idpUpdatedDescription": "身份提供商更新成功",
|
"idpUpdatedDescription": "身份提供商更新成功",
|
||||||
"redirectUrl": "重定向網址",
|
"redirectUrl": "重定向網址",
|
||||||
|
"orgIdpRedirectUrls": "重新導向網址",
|
||||||
"redirectUrlAbout": "關於重定向網址",
|
"redirectUrlAbout": "關於重定向網址",
|
||||||
"redirectUrlAboutDescription": "這是用戶在驗證後將被重定向到的URL。您需要在身份提供商設置中配置此URL。",
|
"redirectUrlAboutDescription": "這是用戶在驗證後將被重定向到的URL。您需要在身份提供商設置中配置此URL。",
|
||||||
"pangolinAuth": "認證 - Pangolin",
|
"pangolinAuth": "認證 - Pangolin",
|
||||||
@@ -909,6 +935,10 @@
|
|||||||
"passwordResetSent": "我們將發送一個驗證碼到這個電子郵件地址。",
|
"passwordResetSent": "我們將發送一個驗證碼到這個電子郵件地址。",
|
||||||
"passwordResetCode": "驗證碼",
|
"passwordResetCode": "驗證碼",
|
||||||
"passwordResetCodeDescription": "請檢查您的電子郵件以獲取驗證碼。",
|
"passwordResetCodeDescription": "請檢查您的電子郵件以獲取驗證碼。",
|
||||||
|
"generatePasswordResetCode": "產生密碼重設代碼",
|
||||||
|
"passwordResetCodeGenerated": "密碼重設代碼已產生",
|
||||||
|
"passwordResetCodeGeneratedDescription": "請將此代碼分享給使用者。他們可以用它來重設密碼。",
|
||||||
|
"passwordResetUrl": "重設網址",
|
||||||
"passwordNew": "新密碼",
|
"passwordNew": "新密碼",
|
||||||
"passwordNewConfirm": "確認新密碼",
|
"passwordNewConfirm": "確認新密碼",
|
||||||
"changePassword": "更改密碼",
|
"changePassword": "更改密碼",
|
||||||
@@ -926,6 +956,9 @@
|
|||||||
"pincodeAuth": "驗證器代碼",
|
"pincodeAuth": "驗證器代碼",
|
||||||
"pincodeSubmit2": "提交代碼",
|
"pincodeSubmit2": "提交代碼",
|
||||||
"passwordResetSubmit": "請求重設",
|
"passwordResetSubmit": "請求重設",
|
||||||
|
"passwordResetAlreadyHaveCode": "輸入代碼",
|
||||||
|
"passwordResetSmtpRequired": "請聯絡您的管理員",
|
||||||
|
"passwordResetSmtpRequiredDescription": "需要密碼重設代碼才能重設您的密碼。請聯絡您的管理員尋求協助。",
|
||||||
"passwordBack": "回到密碼",
|
"passwordBack": "回到密碼",
|
||||||
"loginBack": "返回登錄",
|
"loginBack": "返回登錄",
|
||||||
"signup": "註冊",
|
"signup": "註冊",
|
||||||
@@ -1013,6 +1046,7 @@
|
|||||||
"updateOrgUser": "更新組織用戶",
|
"updateOrgUser": "更新組織用戶",
|
||||||
"createOrgUser": "創建組織用戶",
|
"createOrgUser": "創建組織用戶",
|
||||||
"actionUpdateOrg": "更新組織",
|
"actionUpdateOrg": "更新組織",
|
||||||
|
"actionRemoveInvitation": "移除邀請",
|
||||||
"actionUpdateUser": "更新用戶",
|
"actionUpdateUser": "更新用戶",
|
||||||
"actionGetUser": "獲取用戶",
|
"actionGetUser": "獲取用戶",
|
||||||
"actionGetOrgUser": "獲取組織用戶",
|
"actionGetOrgUser": "獲取組織用戶",
|
||||||
@@ -1057,6 +1091,7 @@
|
|||||||
"actionRemoveUser": "刪除用戶",
|
"actionRemoveUser": "刪除用戶",
|
||||||
"actionListUsers": "列出用戶",
|
"actionListUsers": "列出用戶",
|
||||||
"actionAddUserRole": "添加用戶角色",
|
"actionAddUserRole": "添加用戶角色",
|
||||||
|
"actionSetUserOrgRoles": "Set User Roles",
|
||||||
"actionGenerateAccessToken": "生成訪問令牌",
|
"actionGenerateAccessToken": "生成訪問令牌",
|
||||||
"actionDeleteAccessToken": "刪除訪問令牌",
|
"actionDeleteAccessToken": "刪除訪問令牌",
|
||||||
"actionListAccessTokens": "訪問令牌",
|
"actionListAccessTokens": "訪問令牌",
|
||||||
@@ -1093,12 +1128,15 @@
|
|||||||
"actionListSiteResources": "列出站點資源",
|
"actionListSiteResources": "列出站點資源",
|
||||||
"actionUpdateSiteResource": "更新站點資源",
|
"actionUpdateSiteResource": "更新站點資源",
|
||||||
"actionListInvitations": "邀請列表",
|
"actionListInvitations": "邀請列表",
|
||||||
|
"actionExportLogs": "匯出日誌",
|
||||||
|
"actionViewLogs": "查看日誌",
|
||||||
"noneSelected": "未選擇",
|
"noneSelected": "未選擇",
|
||||||
"orgNotFound2": "未找到組織。",
|
"orgNotFound2": "未找到組織。",
|
||||||
"searchProgress": "搜索中...",
|
"searchProgress": "搜索中...",
|
||||||
"create": "創建",
|
"create": "創建",
|
||||||
"orgs": "組織",
|
"orgs": "組織",
|
||||||
"loginError": "登錄時出錯",
|
"loginError": "登錄時出錯",
|
||||||
|
"loginRequiredForDevice": "需要登入以驗證您的裝置。",
|
||||||
"passwordForgot": "忘記密碼?",
|
"passwordForgot": "忘記密碼?",
|
||||||
"otpAuth": "兩步驗證",
|
"otpAuth": "兩步驗證",
|
||||||
"otpAuthDescription": "從您的身份驗證程序中輸入代碼或您的單次備份代碼。",
|
"otpAuthDescription": "從您的身份驗證程序中輸入代碼或您的單次備份代碼。",
|
||||||
@@ -1153,8 +1191,12 @@
|
|||||||
"sidebarHome": "首頁",
|
"sidebarHome": "首頁",
|
||||||
"sidebarSites": "站點",
|
"sidebarSites": "站點",
|
||||||
"sidebarResources": "資源",
|
"sidebarResources": "資源",
|
||||||
|
"sidebarProxyResources": "公開",
|
||||||
|
"sidebarClientResources": "私有",
|
||||||
"sidebarAccessControl": "訪問控制",
|
"sidebarAccessControl": "訪問控制",
|
||||||
|
"sidebarLogsAndAnalytics": "日誌與分析",
|
||||||
"sidebarUsers": "用戶",
|
"sidebarUsers": "用戶",
|
||||||
|
"sidebarAdmin": "管理員",
|
||||||
"sidebarInvitations": "邀請",
|
"sidebarInvitations": "邀請",
|
||||||
"sidebarRoles": "角色",
|
"sidebarRoles": "角色",
|
||||||
"sidebarShareableLinks": "分享連結",
|
"sidebarShareableLinks": "分享連結",
|
||||||
@@ -1164,8 +1206,14 @@
|
|||||||
"sidebarIdentityProviders": "身份提供商",
|
"sidebarIdentityProviders": "身份提供商",
|
||||||
"sidebarLicense": "證書",
|
"sidebarLicense": "證書",
|
||||||
"sidebarClients": "用戶端",
|
"sidebarClients": "用戶端",
|
||||||
|
"sidebarUserDevices": "使用者",
|
||||||
|
"sidebarMachineClients": "機器",
|
||||||
"sidebarDomains": "域",
|
"sidebarDomains": "域",
|
||||||
|
"sidebarGeneral": "管理",
|
||||||
|
"sidebarLogAndAnalytics": "日誌與分析",
|
||||||
"sidebarBluePrints": "藍圖",
|
"sidebarBluePrints": "藍圖",
|
||||||
|
"sidebarOrganization": "組織",
|
||||||
|
"sidebarLogsAnalytics": "分析",
|
||||||
"blueprints": "藍圖",
|
"blueprints": "藍圖",
|
||||||
"blueprintsDescription": "應用聲明配置並查看先前運行的",
|
"blueprintsDescription": "應用聲明配置並查看先前運行的",
|
||||||
"blueprintAdd": "添加藍圖",
|
"blueprintAdd": "添加藍圖",
|
||||||
@@ -1275,12 +1323,24 @@
|
|||||||
"accountSetupSuccess": "帳號設定完成!歡迎來到 Pangolin!",
|
"accountSetupSuccess": "帳號設定完成!歡迎來到 Pangolin!",
|
||||||
"documentation": "文件",
|
"documentation": "文件",
|
||||||
"saveAllSettings": "保存所有設置",
|
"saveAllSettings": "保存所有設置",
|
||||||
|
"saveResourceTargets": "儲存目標",
|
||||||
|
"saveResourceHttp": "儲存代理設定",
|
||||||
|
"saveProxyProtocol": "儲存代理協定設定",
|
||||||
"settingsUpdated": "設置已更新",
|
"settingsUpdated": "設置已更新",
|
||||||
"settingsUpdatedDescription": "所有設置已成功更新",
|
"settingsUpdatedDescription": "所有設置已成功更新",
|
||||||
"settingsErrorUpdate": "設置更新失敗",
|
"settingsErrorUpdate": "設置更新失敗",
|
||||||
"settingsErrorUpdateDescription": "更新設置時發生錯誤",
|
"settingsErrorUpdateDescription": "更新設置時發生錯誤",
|
||||||
"sidebarCollapse": "摺疊",
|
"sidebarCollapse": "摺疊",
|
||||||
"sidebarExpand": "展開",
|
"sidebarExpand": "展開",
|
||||||
|
"productUpdateMoreInfo": "還有 {noOfUpdates} 項更新",
|
||||||
|
"productUpdateInfo": "{noOfUpdates} 項更新",
|
||||||
|
"productUpdateWhatsNew": "新功能",
|
||||||
|
"productUpdateTitle": "產品更新",
|
||||||
|
"productUpdateEmpty": "沒有更新",
|
||||||
|
"dismissAll": "全部關閉",
|
||||||
|
"pangolinUpdateAvailable": "有可用更新",
|
||||||
|
"pangolinUpdateAvailableInfo": "版本 {version} 已準備好安裝",
|
||||||
|
"pangolinUpdateAvailableReleaseNotes": "查看發行說明",
|
||||||
"newtUpdateAvailable": "更新可用",
|
"newtUpdateAvailable": "更新可用",
|
||||||
"newtUpdateAvailableInfo": "新版本的 Newt 已可用。請更新到最新版本以獲得最佳體驗。",
|
"newtUpdateAvailableInfo": "新版本的 Newt 已可用。請更新到最新版本以獲得最佳體驗。",
|
||||||
"domainPickerEnterDomain": "域名",
|
"domainPickerEnterDomain": "域名",
|
||||||
@@ -1423,6 +1483,9 @@
|
|||||||
"and": "和",
|
"and": "和",
|
||||||
"privacyPolicy": "隱私政策"
|
"privacyPolicy": "隱私政策"
|
||||||
},
|
},
|
||||||
|
"signUpMarketing": {
|
||||||
|
"keepMeInTheLoop": "透過電子郵件接收新聞、更新和新功能通知。"
|
||||||
|
},
|
||||||
"siteRequired": "需要站點。",
|
"siteRequired": "需要站點。",
|
||||||
"olmTunnel": "Olm 隧道",
|
"olmTunnel": "Olm 隧道",
|
||||||
"olmTunnelDescription": "使用 Olm 進行用戶端連接",
|
"olmTunnelDescription": "使用 Olm 進行用戶端連接",
|
||||||
@@ -1456,15 +1519,14 @@
|
|||||||
"sitesFetchError": "獲取站點時出錯。",
|
"sitesFetchError": "獲取站點時出錯。",
|
||||||
"olmErrorFetchReleases": "獲取 Olm 發布版本時出錯。",
|
"olmErrorFetchReleases": "獲取 Olm 發布版本時出錯。",
|
||||||
"olmErrorFetchLatest": "獲取最新 Olm 發布版本時出錯。",
|
"olmErrorFetchLatest": "獲取最新 Olm 發布版本時出錯。",
|
||||||
"remoteSubnets": "遠程子網",
|
|
||||||
"enterCidrRange": "輸入 CIDR 範圍",
|
"enterCidrRange": "輸入 CIDR 範圍",
|
||||||
"remoteSubnetsDescription": "添加可以通過用戶端遠端存取該站點的 CIDR 範圍。使用類似 10.0.0.0/24 的格式。這僅適用於 VPN 用戶端連接。",
|
|
||||||
"resourceEnableProxy": "啟用公共代理",
|
"resourceEnableProxy": "啟用公共代理",
|
||||||
"resourceEnableProxyDescription": "啟用到此資源的公共代理。這允許外部網路通過開放埠訪問資源。需要 Traefik 配置。",
|
"resourceEnableProxyDescription": "啟用到此資源的公共代理。這允許外部網路通過開放埠訪問資源。需要 Traefik 配置。",
|
||||||
"externalProxyEnabled": "外部代理已啟用",
|
"externalProxyEnabled": "外部代理已啟用",
|
||||||
"addNewTarget": "添加新目標",
|
"addNewTarget": "添加新目標",
|
||||||
"targetsList": "目標列表",
|
"targetsList": "目標列表",
|
||||||
"advancedMode": "高級模式",
|
"advancedMode": "高級模式",
|
||||||
|
"advancedSettings": "進階設定",
|
||||||
"targetErrorDuplicateTargetFound": "找到重複的目標",
|
"targetErrorDuplicateTargetFound": "找到重複的目標",
|
||||||
"healthCheckHealthy": "正常",
|
"healthCheckHealthy": "正常",
|
||||||
"healthCheckUnhealthy": "不正常",
|
"healthCheckUnhealthy": "不正常",
|
||||||
@@ -1476,6 +1538,7 @@
|
|||||||
"enableHealthChecksDescription": "監視此目標的健康狀況。如果需要,您可以監視一個不同的終點。",
|
"enableHealthChecksDescription": "監視此目標的健康狀況。如果需要,您可以監視一個不同的終點。",
|
||||||
"healthScheme": "方法",
|
"healthScheme": "方法",
|
||||||
"healthSelectScheme": "選擇方法",
|
"healthSelectScheme": "選擇方法",
|
||||||
|
"healthCheckPortInvalid": "健康檢查連接埠必須介於 1 到 65535 之間",
|
||||||
"healthCheckPath": "路徑",
|
"healthCheckPath": "路徑",
|
||||||
"healthHostname": "IP / 主機",
|
"healthHostname": "IP / 主機",
|
||||||
"healthPort": "埠",
|
"healthPort": "埠",
|
||||||
@@ -1524,9 +1587,15 @@
|
|||||||
"resourcesTableNoProxyResourcesFound": "未找到代理資源。",
|
"resourcesTableNoProxyResourcesFound": "未找到代理資源。",
|
||||||
"resourcesTableNoInternalResourcesFound": "未找到內部資源。",
|
"resourcesTableNoInternalResourcesFound": "未找到內部資源。",
|
||||||
"resourcesTableDestination": "目標",
|
"resourcesTableDestination": "目標",
|
||||||
"resourcesTableTheseResourcesForUseWith": "這些資源供...使用",
|
"resourcesTableAlias": "別名",
|
||||||
"resourcesTableClients": "用戶端",
|
"resourcesTableClients": "用戶端",
|
||||||
"resourcesTableAndOnlyAccessibleInternally": "且僅在與用戶端連接時可內部訪問。",
|
"resourcesTableAndOnlyAccessibleInternally": "且僅在與用戶端連接時可內部訪問。",
|
||||||
|
"resourcesTableNoTargets": "無目標",
|
||||||
|
"resourcesTableHealthy": "健康",
|
||||||
|
"resourcesTableDegraded": "降級",
|
||||||
|
"resourcesTableOffline": "離線",
|
||||||
|
"resourcesTableUnknown": "未知",
|
||||||
|
"resourcesTableNotMonitored": "未監控",
|
||||||
"editInternalResourceDialogEditClientResource": "編輯用戶端資源",
|
"editInternalResourceDialogEditClientResource": "編輯用戶端資源",
|
||||||
"editInternalResourceDialogUpdateResourceProperties": "更新 {resourceName} 的資源屬性和目標配置。",
|
"editInternalResourceDialogUpdateResourceProperties": "更新 {resourceName} 的資源屬性和目標配置。",
|
||||||
"editInternalResourceDialogResourceProperties": "資源屬性",
|
"editInternalResourceDialogResourceProperties": "資源屬性",
|
||||||
@@ -1547,6 +1616,17 @@
|
|||||||
"editInternalResourceDialogInvalidIPAddressFormat": "無效的 IP 位址格式",
|
"editInternalResourceDialogInvalidIPAddressFormat": "無效的 IP 位址格式",
|
||||||
"editInternalResourceDialogDestinationPortMin": "目標埠必須至少為 1",
|
"editInternalResourceDialogDestinationPortMin": "目標埠必須至少為 1",
|
||||||
"editInternalResourceDialogDestinationPortMax": "目標埠必須小於 65536",
|
"editInternalResourceDialogDestinationPortMax": "目標埠必須小於 65536",
|
||||||
|
"editInternalResourceDialogPortModeRequired": "連接埠模式需要協定、代理連接埠和目標連接埠",
|
||||||
|
"editInternalResourceDialogMode": "模式",
|
||||||
|
"editInternalResourceDialogModePort": "連接埠",
|
||||||
|
"editInternalResourceDialogModeHost": "主機",
|
||||||
|
"editInternalResourceDialogModeCidr": "CIDR",
|
||||||
|
"editInternalResourceDialogDestination": "目的地",
|
||||||
|
"editInternalResourceDialogDestinationHostDescription": "站點網路上資源的 IP 位址或主機名稱。",
|
||||||
|
"editInternalResourceDialogDestinationIPDescription": "站點網路上資源的 IP 或主機名稱位址。",
|
||||||
|
"editInternalResourceDialogDestinationCidrDescription": "站點網路上資源的 CIDR 範圍。",
|
||||||
|
"editInternalResourceDialogAlias": "別名",
|
||||||
|
"editInternalResourceDialogAliasDescription": "此資源的可選內部 DNS 別名。",
|
||||||
"createInternalResourceDialogNoSitesAvailable": "暫無可用站點",
|
"createInternalResourceDialogNoSitesAvailable": "暫無可用站點",
|
||||||
"createInternalResourceDialogNoSitesAvailableDescription": "您需要至少配置一個子網的 Newt 站點來創建內部資源。",
|
"createInternalResourceDialogNoSitesAvailableDescription": "您需要至少配置一個子網的 Newt 站點來創建內部資源。",
|
||||||
"createInternalResourceDialogClose": "關閉",
|
"createInternalResourceDialogClose": "關閉",
|
||||||
@@ -1555,9 +1635,8 @@
|
|||||||
"createInternalResourceDialogResourceProperties": "資源屬性",
|
"createInternalResourceDialogResourceProperties": "資源屬性",
|
||||||
"createInternalResourceDialogName": "名稱",
|
"createInternalResourceDialogName": "名稱",
|
||||||
"createInternalResourceDialogSite": "站點",
|
"createInternalResourceDialogSite": "站點",
|
||||||
"createInternalResourceDialogSelectSite": "選擇站點...",
|
"selectSite": "選擇站點...",
|
||||||
"createInternalResourceDialogSearchSites": "搜索站點...",
|
"noSitesFound": "找不到站點。",
|
||||||
"createInternalResourceDialogNoSitesFound": "未找到站點。",
|
|
||||||
"createInternalResourceDialogProtocol": "協議",
|
"createInternalResourceDialogProtocol": "協議",
|
||||||
"createInternalResourceDialogTcp": "TCP",
|
"createInternalResourceDialogTcp": "TCP",
|
||||||
"createInternalResourceDialogUdp": "UDP",
|
"createInternalResourceDialogUdp": "UDP",
|
||||||
@@ -1580,11 +1659,22 @@
|
|||||||
"createInternalResourceDialogInvalidIPAddressFormat": "無效的 IP 位址格式",
|
"createInternalResourceDialogInvalidIPAddressFormat": "無效的 IP 位址格式",
|
||||||
"createInternalResourceDialogDestinationPortMin": "目標埠必須至少為 1",
|
"createInternalResourceDialogDestinationPortMin": "目標埠必須至少為 1",
|
||||||
"createInternalResourceDialogDestinationPortMax": "目標埠必須小於 65536",
|
"createInternalResourceDialogDestinationPortMax": "目標埠必須小於 65536",
|
||||||
|
"createInternalResourceDialogPortModeRequired": "連接埠模式需要協定、代理連接埠和目標連接埠",
|
||||||
|
"createInternalResourceDialogMode": "模式",
|
||||||
|
"createInternalResourceDialogModePort": "連接埠",
|
||||||
|
"createInternalResourceDialogModeHost": "主機",
|
||||||
|
"createInternalResourceDialogModeCidr": "CIDR",
|
||||||
|
"createInternalResourceDialogDestination": "目的地",
|
||||||
|
"createInternalResourceDialogDestinationHostDescription": "站點網路上資源的 IP 位址或主機名稱。",
|
||||||
|
"createInternalResourceDialogDestinationCidrDescription": "站點網路上資源的 CIDR 範圍。",
|
||||||
|
"createInternalResourceDialogAlias": "別名",
|
||||||
|
"createInternalResourceDialogAliasDescription": "此資源的可選內部 DNS 別名。",
|
||||||
"siteConfiguration": "配置",
|
"siteConfiguration": "配置",
|
||||||
"siteAcceptClientConnections": "接受用戶端連接",
|
"siteAcceptClientConnections": "接受用戶端連接",
|
||||||
"siteAcceptClientConnectionsDescription": "允許其他設備透過此 Newt 實例使用用戶端作為閘道器連接。",
|
"siteAcceptClientConnectionsDescription": "允許其他設備透過此 Newt 實例使用用戶端作為閘道器連接。",
|
||||||
"siteAddress": "站點地址",
|
"siteAddress": "站點地址",
|
||||||
"siteAddressDescription": "指定主機的 IP 位址以供用戶端連接。這是 Pangolin 網路中站點的內部地址,供用戶端訪問。必須在 Org 子網內。",
|
"siteAddressDescription": "指定主機的 IP 位址以供用戶端連接。這是 Pangolin 網路中站點的內部地址,供用戶端訪問。必須在 Org 子網內。",
|
||||||
|
"siteNameDescription": "站點的顯示名稱,可以稍後更改。",
|
||||||
"autoLoginExternalIdp": "自動使用外部 IDP 登錄",
|
"autoLoginExternalIdp": "自動使用外部 IDP 登錄",
|
||||||
"autoLoginExternalIdpDescription": "立即將用戶重定向到外部 IDP 進行身份驗證。",
|
"autoLoginExternalIdpDescription": "立即將用戶重定向到外部 IDP 進行身份驗證。",
|
||||||
"selectIdp": "選擇 IDP",
|
"selectIdp": "選擇 IDP",
|
||||||
@@ -1608,6 +1698,8 @@
|
|||||||
"remoteExitNodeConfirmDelete": "確認刪除節點",
|
"remoteExitNodeConfirmDelete": "確認刪除節點",
|
||||||
"remoteExitNodeDelete": "刪除節點",
|
"remoteExitNodeDelete": "刪除節點",
|
||||||
"sidebarRemoteExitNodes": "遠程節點",
|
"sidebarRemoteExitNodes": "遠程節點",
|
||||||
|
"remoteExitNodeId": "ID",
|
||||||
|
"remoteExitNodeSecretKey": "密鑰",
|
||||||
"remoteExitNodeCreate": {
|
"remoteExitNodeCreate": {
|
||||||
"title": "創建節點",
|
"title": "創建節點",
|
||||||
"description": "創建一個新節點來擴展您的網路連接",
|
"description": "創建一個新節點來擴展您的網路連接",
|
||||||
@@ -1731,12 +1823,33 @@
|
|||||||
"idpAzureClientIdDescription2": "您的 Azure 應用程式註冊用戶端 ID",
|
"idpAzureClientIdDescription2": "您的 Azure 應用程式註冊用戶端 ID",
|
||||||
"idpAzureClientSecretDescription2": "您的 Azure 應用程式註冊用戶端金鑰",
|
"idpAzureClientSecretDescription2": "您的 Azure 應用程式註冊用戶端金鑰",
|
||||||
"idpGoogleDescription": "Google OAuth2/OIDC 提供商",
|
"idpGoogleDescription": "Google OAuth2/OIDC 提供商",
|
||||||
"idpAzureDescription": "Microsoft Azure OAuth2/OIDC provider",
|
"idpAzureDescription": "Microsoft Azure OAuth2/OIDC 提供者",
|
||||||
"subnet": "子網",
|
"subnet": "子網",
|
||||||
"subnetDescription": "此組織網路配置的子網。",
|
"subnetDescription": "此組織網路配置的子網。",
|
||||||
|
"customDomain": "自訂網域",
|
||||||
"authPage": "認證頁面",
|
"authPage": "認證頁面",
|
||||||
"authPageDescription": "配置您的組織認證頁面",
|
"authPageDescription": "配置您的組織認證頁面",
|
||||||
"authPageDomain": "認證頁面域",
|
"authPageDomain": "認證頁面域",
|
||||||
|
"authPageBranding": "自訂品牌",
|
||||||
|
"authPageBrandingDescription": "設定此組織驗證頁面上顯示的品牌",
|
||||||
|
"authPageBrandingUpdated": "驗證頁面品牌更新成功",
|
||||||
|
"authPageBrandingRemoved": "驗證頁面品牌移除成功",
|
||||||
|
"authPageBrandingRemoveTitle": "移除驗證頁面品牌",
|
||||||
|
"authPageBrandingQuestionRemove": "您確定要移除驗證頁面的品牌嗎?",
|
||||||
|
"authPageBrandingDeleteConfirm": "確認刪除品牌",
|
||||||
|
"brandingLogoURL": "Logo 網址",
|
||||||
|
"brandingPrimaryColor": "主要顏色",
|
||||||
|
"brandingLogoWidth": "寬度 (px)",
|
||||||
|
"brandingLogoHeight": "高度 (px)",
|
||||||
|
"brandingOrgTitle": "組織驗證頁面標題",
|
||||||
|
"brandingOrgDescription": "{orgName} 將被替換為組織名稱",
|
||||||
|
"brandingOrgSubtitle": "組織驗證頁面副標題",
|
||||||
|
"brandingResourceTitle": "資源驗證頁面標題",
|
||||||
|
"brandingResourceSubtitle": "資源驗證頁面副標題",
|
||||||
|
"brandingResourceDescription": "{resourceName} 將被替換為組織名稱",
|
||||||
|
"saveAuthPageDomain": "儲存網域",
|
||||||
|
"saveAuthPageBranding": "儲存品牌",
|
||||||
|
"removeAuthPageBranding": "移除品牌",
|
||||||
"noDomainSet": "沒有域設置",
|
"noDomainSet": "沒有域設置",
|
||||||
"changeDomain": "更改域",
|
"changeDomain": "更改域",
|
||||||
"selectDomain": "選擇域",
|
"selectDomain": "選擇域",
|
||||||
@@ -1764,6 +1877,15 @@
|
|||||||
"orgAuthChooseIdpDescription": "選擇您的身份提供商以繼續",
|
"orgAuthChooseIdpDescription": "選擇您的身份提供商以繼續",
|
||||||
"orgAuthNoIdpConfigured": "此機構沒有配置任何身份提供者。您可以使用您的 Pangolin 身份登錄。",
|
"orgAuthNoIdpConfigured": "此機構沒有配置任何身份提供者。您可以使用您的 Pangolin 身份登錄。",
|
||||||
"orgAuthSignInWithPangolin": "使用 Pangolin 登錄",
|
"orgAuthSignInWithPangolin": "使用 Pangolin 登錄",
|
||||||
|
"orgAuthSignInToOrg": "登入組織",
|
||||||
|
"orgAuthSelectOrgTitle": "組織登入",
|
||||||
|
"orgAuthSelectOrgDescription": "輸入您的組織 ID 以繼續",
|
||||||
|
"orgAuthOrgIdPlaceholder": "your-organization",
|
||||||
|
"orgAuthOrgIdHelp": "輸入您組織的唯一識別碼",
|
||||||
|
"orgAuthSelectOrgHelp": "輸入組織 ID 後,您將被導向到組織的登入頁面,在那裡您可以使用 SSO 或組織憑證。",
|
||||||
|
"orgAuthRememberOrgId": "記住此組織 ID",
|
||||||
|
"orgAuthBackToSignIn": "返回標準登入",
|
||||||
|
"orgAuthNoAccount": "沒有帳戶?",
|
||||||
"subscriptionRequiredToUse": "需要訂閱才能使用此功能。",
|
"subscriptionRequiredToUse": "需要訂閱才能使用此功能。",
|
||||||
"idpDisabled": "身份提供者已禁用。",
|
"idpDisabled": "身份提供者已禁用。",
|
||||||
"orgAuthPageDisabled": "組織認證頁面已禁用。",
|
"orgAuthPageDisabled": "組織認證頁面已禁用。",
|
||||||
@@ -1778,6 +1900,8 @@
|
|||||||
"enableTwoFactorAuthentication": "啟用兩步驗證",
|
"enableTwoFactorAuthentication": "啟用兩步驗證",
|
||||||
"completeSecuritySteps": "完成安全步驟",
|
"completeSecuritySteps": "完成安全步驟",
|
||||||
"securitySettings": "安全設定",
|
"securitySettings": "安全設定",
|
||||||
|
"dangerSection": "危險區域",
|
||||||
|
"dangerSectionDescription": "永久刪除與此組織相關的所有資料",
|
||||||
"securitySettingsDescription": "配置您組織的安全策略",
|
"securitySettingsDescription": "配置您組織的安全策略",
|
||||||
"requireTwoFactorForAllUsers": "所有用戶需要兩步驗證",
|
"requireTwoFactorForAllUsers": "所有用戶需要兩步驗證",
|
||||||
"requireTwoFactorDescription": "如果啟用,此組織的所有內部用戶必須啟用雙重身份驗證才能訪問組織。",
|
"requireTwoFactorDescription": "如果啟用,此組織的所有內部用戶必須啟用雙重身份驗證才能訪問組織。",
|
||||||
@@ -1815,7 +1939,7 @@
|
|||||||
"securityPolicyChangeWarningText": "這將影響組織中的所有用戶",
|
"securityPolicyChangeWarningText": "這將影響組織中的所有用戶",
|
||||||
"authPageErrorUpdateMessage": "更新身份驗證頁面設置時出錯",
|
"authPageErrorUpdateMessage": "更新身份驗證頁面設置時出錯",
|
||||||
"authPageErrorUpdate": "無法更新認證頁面",
|
"authPageErrorUpdate": "無法更新認證頁面",
|
||||||
"authPageUpdated": "身份驗證頁面更新成功",
|
"authPageDomainUpdated": "驗證頁面網域更新成功",
|
||||||
"healthCheckNotAvailable": "本地的",
|
"healthCheckNotAvailable": "本地的",
|
||||||
"rewritePath": "重寫路徑",
|
"rewritePath": "重寫路徑",
|
||||||
"rewritePathDescription": "在轉發到目標之前,可以選擇重寫路徑。",
|
"rewritePathDescription": "在轉發到目標之前,可以選擇重寫路徑。",
|
||||||
@@ -1841,8 +1965,19 @@
|
|||||||
"enterpriseEdition": "企業版",
|
"enterpriseEdition": "企業版",
|
||||||
"unlicensed": "未授權",
|
"unlicensed": "未授權",
|
||||||
"beta": "測試版",
|
"beta": "測試版",
|
||||||
"manageClients": "管理用戶端",
|
"manageUserDevices": "使用者裝置",
|
||||||
"manageClientsDescription": "用戶端是可以連接到您的站點的設備",
|
"manageUserDevicesDescription": "查看和管理使用者用於私密連接資源的裝置",
|
||||||
|
"downloadClientBannerTitle": "下載 Pangolin 客戶端",
|
||||||
|
"downloadClientBannerDescription": "下載適用於您系統的 Pangolin 客戶端,以連接到 Pangolin 網路並私密存取資源。",
|
||||||
|
"manageMachineClients": "管理機器客戶端",
|
||||||
|
"manageMachineClientsDescription": "建立和管理伺服器和系統用於私密連接資源的客戶端",
|
||||||
|
"machineClientsBannerTitle": "伺服器與自動化系統",
|
||||||
|
"machineClientsBannerDescription": "機器客戶端適用於與特定使用者無關的伺服器和自動化系統。它們使用 ID 和密鑰進行驗證,可以透過 Pangolin CLI、Olm CLI 或 Olm 容器執行。",
|
||||||
|
"machineClientsBannerPangolinCLI": "Pangolin CLI",
|
||||||
|
"machineClientsBannerOlmCLI": "Olm CLI",
|
||||||
|
"machineClientsBannerOlmContainer": "Olm 容器",
|
||||||
|
"clientsTableUserClients": "使用者",
|
||||||
|
"clientsTableMachineClients": "機器",
|
||||||
"licenseTableValidUntil": "有效期至",
|
"licenseTableValidUntil": "有效期至",
|
||||||
"saasLicenseKeysSettingsTitle": "企業許可證",
|
"saasLicenseKeysSettingsTitle": "企業許可證",
|
||||||
"saasLicenseKeysSettingsDescription": "為自我託管的 Pangolin 實例生成和管理企業許可證金鑰",
|
"saasLicenseKeysSettingsDescription": "為自我託管的 Pangolin 實例生成和管理企業許可證金鑰",
|
||||||
@@ -1982,6 +2117,7 @@
|
|||||||
"clientMessageRemove": "一旦刪除,用戶端將無法連接到站點。",
|
"clientMessageRemove": "一旦刪除,用戶端將無法連接到站點。",
|
||||||
"sidebarLogs": "日誌",
|
"sidebarLogs": "日誌",
|
||||||
"request": "請求",
|
"request": "請求",
|
||||||
|
"requests": "請求",
|
||||||
"logs": "日誌",
|
"logs": "日誌",
|
||||||
"logsSettingsDescription": "監視從此 orginization 中收集的日誌",
|
"logsSettingsDescription": "監視從此 orginization 中收集的日誌",
|
||||||
"searchLogs": "搜索日誌...",
|
"searchLogs": "搜索日誌...",
|
||||||
@@ -1990,6 +2126,8 @@
|
|||||||
"timestamp": "時間戳",
|
"timestamp": "時間戳",
|
||||||
"accessLogs": "訪問日誌",
|
"accessLogs": "訪問日誌",
|
||||||
"exportCsv": "導出 CSV",
|
"exportCsv": "導出 CSV",
|
||||||
|
"exportError": "匯出 CSV 時發生未知錯誤",
|
||||||
|
"exportCsvTooltip": "在時間範圍內",
|
||||||
"actorId": "執行者 ID",
|
"actorId": "執行者 ID",
|
||||||
"allowedByRule": "根據規則允許",
|
"allowedByRule": "根據規則允許",
|
||||||
"allowedNoAuth": "無認證",
|
"allowedNoAuth": "無認證",
|
||||||
@@ -2007,6 +2145,7 @@
|
|||||||
"ip": "IP",
|
"ip": "IP",
|
||||||
"reason": "原因",
|
"reason": "原因",
|
||||||
"requestLogs": "請求日誌",
|
"requestLogs": "請求日誌",
|
||||||
|
"requestAnalytics": "請求分析",
|
||||||
"host": "主機",
|
"host": "主機",
|
||||||
"location": "地點",
|
"location": "地點",
|
||||||
"actionLogs": "操作日誌",
|
"actionLogs": "操作日誌",
|
||||||
@@ -2016,6 +2155,7 @@
|
|||||||
"logRetention": "日誌保留",
|
"logRetention": "日誌保留",
|
||||||
"logRetentionDescription": "管理不同類型的日誌為這個機構保留多長時間或禁用這些日誌",
|
"logRetentionDescription": "管理不同類型的日誌為這個機構保留多長時間或禁用這些日誌",
|
||||||
"requestLogsDescription": "查看此機構資源的詳細請求日誌",
|
"requestLogsDescription": "查看此機構資源的詳細請求日誌",
|
||||||
|
"requestAnalyticsDescription": "查看此組織資源的詳細請求分析",
|
||||||
"logRetentionRequestLabel": "請求日誌保留",
|
"logRetentionRequestLabel": "請求日誌保留",
|
||||||
"logRetentionRequestDescription": "保留請求日誌的時間",
|
"logRetentionRequestDescription": "保留請求日誌的時間",
|
||||||
"logRetentionAccessLabel": "訪問日誌保留",
|
"logRetentionAccessLabel": "訪問日誌保留",
|
||||||
@@ -2029,6 +2169,7 @@
|
|||||||
"logRetention30Days": "30 天",
|
"logRetention30Days": "30 天",
|
||||||
"logRetention90Days": "90 天",
|
"logRetention90Days": "90 天",
|
||||||
"logRetentionForever": "永遠的",
|
"logRetentionForever": "永遠的",
|
||||||
|
"logRetentionEndOfFollowingYear": "次年年底",
|
||||||
"actionLogsDescription": "查看此機構執行的操作歷史",
|
"actionLogsDescription": "查看此機構執行的操作歷史",
|
||||||
"accessLogsDescription": "查看此機構資源的訪問認證請求",
|
"accessLogsDescription": "查看此機構資源的訪問認證請求",
|
||||||
"licenseRequiredToUse": "需要企業許可證才能使用此功能。",
|
"licenseRequiredToUse": "需要企業許可證才能使用此功能。",
|
||||||
@@ -2084,6 +2225,43 @@
|
|||||||
"supportMessageSent": "消息已發送!",
|
"supportMessageSent": "消息已發送!",
|
||||||
"supportWillContact": "我們很快就會聯繫起來!",
|
"supportWillContact": "我們很快就會聯繫起來!",
|
||||||
"selectLogRetention": "選擇保留日誌",
|
"selectLogRetention": "選擇保留日誌",
|
||||||
|
"terms": "條款",
|
||||||
|
"privacy": "隱私權",
|
||||||
|
"security": "安全性",
|
||||||
|
"docs": "文件",
|
||||||
|
"deviceActivation": "裝置啟用",
|
||||||
|
"deviceCodeInvalidFormat": "代碼必須為 9 個字元(例如:A1AJ-N5JD)",
|
||||||
|
"deviceCodeInvalidOrExpired": "代碼無效或已過期",
|
||||||
|
"deviceCodeVerifyFailed": "驗證裝置代碼失敗",
|
||||||
|
"signedInAs": "已登入為",
|
||||||
|
"deviceCodeEnterPrompt": "輸入裝置上顯示的代碼",
|
||||||
|
"continue": "繼續",
|
||||||
|
"deviceUnknownLocation": "未知位置",
|
||||||
|
"deviceAuthorizationRequested": "此授權請求來自 {location},時間為 {date}。請確保您信任此裝置,因為它將獲得帳戶存取權限。",
|
||||||
|
"deviceLabel": "裝置:{deviceName}",
|
||||||
|
"deviceWantsAccess": "想要存取您的帳戶",
|
||||||
|
"deviceExistingAccess": "現有存取權限:",
|
||||||
|
"deviceFullAccess": "完整帳戶存取權限",
|
||||||
|
"deviceOrganizationsAccess": "存取您帳戶有權限的所有組織",
|
||||||
|
"deviceAuthorize": "授權 {applicationName}",
|
||||||
|
"deviceConnected": "裝置已連接!",
|
||||||
|
"deviceAuthorizedMessage": "裝置已獲授權存取您的帳戶。請返回客戶端應用程式。",
|
||||||
|
"pangolinCloud": "Pangolin 雲端",
|
||||||
|
"viewDevices": "查看裝置",
|
||||||
|
"viewDevicesDescription": "管理您已連接的裝置",
|
||||||
|
"noDevices": "找不到裝置",
|
||||||
|
"dateCreated": "建立日期",
|
||||||
|
"unnamedDevice": "未命名裝置",
|
||||||
|
"deviceQuestionRemove": "您確定要刪除此裝置嗎?",
|
||||||
|
"deviceMessageRemove": "此操作無法復原。",
|
||||||
|
"deviceDeleteConfirm": "刪除裝置",
|
||||||
|
"deleteDevice": "刪除裝置",
|
||||||
|
"errorLoadingDevices": "載入裝置時發生錯誤",
|
||||||
|
"failedToLoadDevices": "載入裝置失敗",
|
||||||
|
"deviceDeleted": "裝置已刪除",
|
||||||
|
"deviceDeletedDescription": "裝置已成功刪除。",
|
||||||
|
"errorDeletingDevice": "刪除裝置時發生錯誤",
|
||||||
|
"failedToDeleteDevice": "刪除裝置失敗",
|
||||||
"showColumns": "顯示列",
|
"showColumns": "顯示列",
|
||||||
"hideColumns": "隱藏列",
|
"hideColumns": "隱藏列",
|
||||||
"columnVisibility": "列可見性",
|
"columnVisibility": "列可見性",
|
||||||
@@ -2097,5 +2275,125 @@
|
|||||||
"selectedResources": "選定的資源",
|
"selectedResources": "選定的資源",
|
||||||
"enableSelected": "啟用選中的",
|
"enableSelected": "啟用選中的",
|
||||||
"disableSelected": "禁用選中的",
|
"disableSelected": "禁用選中的",
|
||||||
"checkSelectedStatus": "檢查選中的狀態"
|
"checkSelectedStatus": "檢查選中的狀態",
|
||||||
|
"clients": "客戶端",
|
||||||
|
"accessClientSelect": "選擇機器客戶端",
|
||||||
|
"resourceClientDescription": "可以存取此資源的機器客戶端",
|
||||||
|
"regenerate": "重新產生",
|
||||||
|
"credentials": "憑證",
|
||||||
|
"savecredentials": "儲存憑證",
|
||||||
|
"regenerateCredentialsButton": "重新產生憑證",
|
||||||
|
"regenerateCredentials": "重新產生憑證",
|
||||||
|
"generatedcredentials": "已產生的憑證",
|
||||||
|
"copyandsavethesecredentials": "複製並儲存這些憑證",
|
||||||
|
"copyandsavethesecredentialsdescription": "離開此頁面後將不會再顯示這些憑證。請立即安全儲存。",
|
||||||
|
"credentialsSaved": "憑證已儲存",
|
||||||
|
"credentialsSavedDescription": "憑證已成功重新產生並儲存。",
|
||||||
|
"credentialsSaveError": "憑證儲存錯誤",
|
||||||
|
"credentialsSaveErrorDescription": "重新產生和儲存憑證時發生錯誤。",
|
||||||
|
"regenerateCredentialsWarning": "重新產生憑證將使先前的憑證失效並導致斷線。請確保更新任何使用這些憑證的設定。",
|
||||||
|
"confirm": "確認",
|
||||||
|
"regenerateCredentialsConfirmation": "您確定要重新產生憑證嗎?",
|
||||||
|
"endpoint": "端點",
|
||||||
|
"Id": "ID",
|
||||||
|
"SecretKey": "密鑰",
|
||||||
|
"niceId": "友善 ID",
|
||||||
|
"niceIdUpdated": "友善 ID 已更新",
|
||||||
|
"niceIdUpdatedSuccessfully": "友善 ID 更新成功",
|
||||||
|
"niceIdUpdateError": "更新友善 ID 時發生錯誤",
|
||||||
|
"niceIdUpdateErrorDescription": "更新友善 ID 時發生錯誤。",
|
||||||
|
"niceIdCannotBeEmpty": "友善 ID 不能為空",
|
||||||
|
"enterIdentifier": "輸入識別碼",
|
||||||
|
"identifier": "識別碼",
|
||||||
|
"deviceLoginUseDifferentAccount": "不是您嗎?使用其他帳戶。",
|
||||||
|
"deviceLoginDeviceRequestingAccessToAccount": "有裝置正在請求存取此帳戶。",
|
||||||
|
"noData": "無資料",
|
||||||
|
"machineClients": "機器客戶端",
|
||||||
|
"install": "安裝",
|
||||||
|
"run": "執行",
|
||||||
|
"clientNameDescription": "客戶端的顯示名稱,可以稍後更改。",
|
||||||
|
"clientAddress": "客戶端位址(進階)",
|
||||||
|
"setupFailedToFetchSubnet": "取得預設子網路失敗",
|
||||||
|
"setupSubnetAdvanced": "子網路(進階)",
|
||||||
|
"setupSubnetDescription": "此組織內部網路的子網路。",
|
||||||
|
"setupUtilitySubnet": "工具子網路(進階)",
|
||||||
|
"setupUtilitySubnetDescription": "此組織別名位址和 DNS 伺服器的子網路。",
|
||||||
|
"siteRegenerateAndDisconnect": "重新產生並斷開連接",
|
||||||
|
"siteRegenerateAndDisconnectConfirmation": "您確定要重新產生憑證並斷開此站點的連接嗎?",
|
||||||
|
"siteRegenerateAndDisconnectWarning": "這將重新產生憑證並立即斷開站點連接。站點需要使用新憑證重新啟動。",
|
||||||
|
"siteRegenerateCredentialsConfirmation": "您確定要重新產生此站點的憑證嗎?",
|
||||||
|
"siteRegenerateCredentialsWarning": "這將重新產生憑證。站點將保持連接,直到您手動重新啟動並使用新憑證。",
|
||||||
|
"clientRegenerateAndDisconnect": "重新產生並斷開連接",
|
||||||
|
"clientRegenerateAndDisconnectConfirmation": "您確定要重新產生憑證並斷開此客戶端的連接嗎?",
|
||||||
|
"clientRegenerateAndDisconnectWarning": "這將重新產生憑證並立即斷開客戶端連接。客戶端需要使用新憑證重新啟動。",
|
||||||
|
"clientRegenerateCredentialsConfirmation": "您確定要重新產生此客戶端的憑證嗎?",
|
||||||
|
"clientRegenerateCredentialsWarning": "這將重新產生憑證。客戶端將保持連接,直到您手動重新啟動並使用新憑證。",
|
||||||
|
"remoteExitNodeRegenerateAndDisconnect": "重新產生並斷開連接",
|
||||||
|
"remoteExitNodeRegenerateAndDisconnectConfirmation": "您確定要重新產生憑證並斷開此遠端出口節點的連接嗎?",
|
||||||
|
"remoteExitNodeRegenerateAndDisconnectWarning": "這將重新產生憑證並立即斷開遠端出口節點連接。遠端出口節點需要使用新憑證重新啟動。",
|
||||||
|
"remoteExitNodeRegenerateCredentialsConfirmation": "您確定要重新產生此遠端出口節點的憑證嗎?",
|
||||||
|
"remoteExitNodeRegenerateCredentialsWarning": "這將重新產生憑證。遠端出口節點將保持連接,直到您手動重新啟動並使用新憑證。",
|
||||||
|
"agent": "代理",
|
||||||
|
"personalUseOnly": "僅限個人使用",
|
||||||
|
"loginPageLicenseWatermark": "此實例僅授權個人使用。",
|
||||||
|
"instanceIsUnlicensed": "此實例未授權。",
|
||||||
|
"portRestrictions": "連接埠限制",
|
||||||
|
"allPorts": "全部",
|
||||||
|
"custom": "自訂",
|
||||||
|
"allPortsAllowed": "允許所有連接埠",
|
||||||
|
"allPortsBlocked": "阻擋所有連接埠",
|
||||||
|
"tcpPortsDescription": "指定此資源允許的 TCP 連接埠。使用「*」表示所有連接埠,留空表示阻擋全部,或輸入以逗號分隔的連接埠和範圍(例如:80,443,8000-9000)。",
|
||||||
|
"udpPortsDescription": "指定此資源允許的 UDP 連接埠。使用「*」表示所有連接埠,留空表示阻擋全部,或輸入以逗號分隔的連接埠和範圍(例如:53,123,500-600)。",
|
||||||
|
"organizationLoginPageTitle": "組織登入頁面",
|
||||||
|
"organizationLoginPageDescription": "自訂此組織的登入頁面",
|
||||||
|
"resourceLoginPageTitle": "資源登入頁面",
|
||||||
|
"resourceLoginPageDescription": "自訂個別資源的登入頁面",
|
||||||
|
"enterConfirmation": "輸入確認",
|
||||||
|
"blueprintViewDetails": "詳細資訊",
|
||||||
|
"defaultIdentityProvider": "預設身份提供者",
|
||||||
|
"defaultIdentityProviderDescription": "當選擇預設身份提供者時,使用者將自動被重新導向到該提供者進行驗證。",
|
||||||
|
"editInternalResourceDialogNetworkSettings": "網路設定",
|
||||||
|
"editInternalResourceDialogAccessPolicy": "存取策略",
|
||||||
|
"editInternalResourceDialogAddRoles": "新增角色",
|
||||||
|
"editInternalResourceDialogAddUsers": "新增使用者",
|
||||||
|
"editInternalResourceDialogAddClients": "新增客戶端",
|
||||||
|
"editInternalResourceDialogDestinationLabel": "目的地",
|
||||||
|
"editInternalResourceDialogDestinationDescription": "指定內部資源的目的地位址。根據所選模式,這可以是主機名稱、IP 位址或 CIDR 範圍。可選擇設定內部 DNS 別名以便識別。",
|
||||||
|
"editInternalResourceDialogPortRestrictionsDescription": "限制對特定 TCP/UDP 連接埠的存取,或允許/阻擋所有連接埠。",
|
||||||
|
"editInternalResourceDialogTcp": "TCP",
|
||||||
|
"editInternalResourceDialogUdp": "UDP",
|
||||||
|
"editInternalResourceDialogIcmp": "ICMP",
|
||||||
|
"editInternalResourceDialogAccessControl": "存取控制",
|
||||||
|
"editInternalResourceDialogAccessControlDescription": "控制哪些角色、使用者和機器客戶端在連接時可以存取此資源。管理員始終擁有存取權限。",
|
||||||
|
"editInternalResourceDialogPortRangeValidationError": "連接埠範圍必須是「*」表示所有連接埠,或以逗號分隔的連接埠和範圍列表(例如:「80,443,8000-9000」)。連接埠必須介於 1 到 65535 之間。",
|
||||||
|
"orgAuthWhatsThis": "我在哪裡可以找到我的組織 ID?",
|
||||||
|
"learnMore": "了解更多",
|
||||||
|
"backToHome": "返回首頁",
|
||||||
|
"needToSignInToOrg": "需要使用您組織的身份提供者嗎?",
|
||||||
|
"maintenanceMode": "維護模式",
|
||||||
|
"maintenanceModeDescription": "向訪客顯示維護頁面",
|
||||||
|
"maintenanceModeType": "維護模式類型",
|
||||||
|
"showMaintenancePage": "向訪客顯示維護頁面",
|
||||||
|
"enableMaintenanceMode": "啟用維護模式",
|
||||||
|
"automatic": "自動",
|
||||||
|
"automaticModeDescription": "僅在所有後端目標都關閉或不健康時顯示維護頁面。只要至少有一個目標健康,您的資源就會正常運作。",
|
||||||
|
"forced": "強制",
|
||||||
|
"forcedModeDescription": "無論後端健康狀況如何,始終顯示維護頁面。當您想要阻止所有存取時,用於計劃維護。",
|
||||||
|
"warning:": "警告:",
|
||||||
|
"forcedeModeWarning": "所有流量將被導向維護頁面。您的後端資源將不會收到任何請求。",
|
||||||
|
"pageTitle": "頁面標題",
|
||||||
|
"pageTitleDescription": "維護頁面上顯示的主標題",
|
||||||
|
"maintenancePageMessage": "維護訊息",
|
||||||
|
"maintenancePageMessagePlaceholder": "我們很快就會回來!我們的網站目前正在進行預定維護。",
|
||||||
|
"maintenancePageMessageDescription": "說明維護的詳細訊息",
|
||||||
|
"maintenancePageTimeTitle": "預計完成時間(可選)",
|
||||||
|
"maintenanceTime": "例如:2 小時、11 月 1 日下午 5:00",
|
||||||
|
"maintenanceEstimatedTimeDescription": "您預計何時完成維護",
|
||||||
|
"editDomain": "編輯網域",
|
||||||
|
"editDomainDescription": "為您的資源選擇網域",
|
||||||
|
"maintenanceModeDisabledTooltip": "此功能需要有效的授權才能啟用。",
|
||||||
|
"maintenanceScreenTitle": "服務暫時無法使用",
|
||||||
|
"maintenanceScreenMessage": "我們目前遇到技術問題。請稍後再試。",
|
||||||
|
"maintenanceScreenEstimatedCompletion": "預計完成時間:",
|
||||||
|
"createInternalResourceDialogDestinationRequired": "目的地為必填欄位"
|
||||||
}
|
}
|
||||||
9450
package-lock.json
generated
9450
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
141
package.json
141
package.json
@@ -3,7 +3,7 @@
|
|||||||
"version": "0.0.0",
|
"version": "0.0.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"description": "Tunneled Reverse Proxy Management Server with Identity and Access Control and Dashboard UI",
|
"description": "Identity-aware VPN and proxy for remote access to anything, anywhere and Dashboard UI",
|
||||||
"homepage": "https://github.com/fosrl/pangolin",
|
"homepage": "https://github.com/fosrl/pangolin",
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
@@ -12,30 +12,29 @@
|
|||||||
"license": "SEE LICENSE IN LICENSE AND README.md",
|
"license": "SEE LICENSE IN LICENSE AND README.md",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "NODE_ENV=development ENVIRONMENT=dev tsx watch server/index.ts",
|
"dev": "NODE_ENV=development ENVIRONMENT=dev tsx watch server/index.ts",
|
||||||
"db:pg:generate": "drizzle-kit generate --config=./drizzle.pg.config.ts",
|
"dev:check": "npx tsc --noEmit && npm run format:check",
|
||||||
"db:sqlite:generate": "drizzle-kit generate --config=./drizzle.sqlite.config.ts",
|
"dev:setup": "cp config/config.example.yml config/config.yml && npm run set:oss && npm run set:sqlite && npm run db:sqlite:generate && npm run db:sqlite:push",
|
||||||
"db:pg:push": "npx tsx server/db/pg/migrate.ts",
|
"db:generate": "drizzle-kit generate --config=./drizzle.config.ts",
|
||||||
"db:sqlite:push": "npx tsx server/db/sqlite/migrate.ts",
|
"db:push": "npx tsx server/db/migrate.ts",
|
||||||
"db:sqlite:studio": "drizzle-kit studio --config=./drizzle.sqlite.config.ts",
|
"db:studio": "drizzle-kit studio --config=./drizzle.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 \"saas\" | \"enterprise\" | \"oss\";' > server/build.ts && cp tsconfig.oss.json tsconfig.json",
|
"set:oss": "echo 'export const build = \"oss\" as \"saas\" | \"enterprise\" | \"oss\";' > server/build.ts && cp tsconfig.oss.json tsconfig.json",
|
||||||
"set:saas": "echo 'export const build = \"saas\" as \"saas\" | \"enterprise\" | \"oss\";' > server/build.ts && cp tsconfig.saas.json tsconfig.json",
|
"set:saas": "echo 'export const build = \"saas\" as \"saas\" | \"enterprise\" | \"oss\";' > server/build.ts && cp tsconfig.saas.json tsconfig.json",
|
||||||
"set:enterprise": "echo 'export const build = \"enterprise\" as \"saas\" | \"enterprise\" | \"oss\";' > server/build.ts && cp tsconfig.enterprise.json tsconfig.json",
|
"set:enterprise": "echo 'export const build = \"enterprise\" as \"saas\" | \"enterprise\" | \"oss\";' > server/build.ts && cp tsconfig.enterprise.json tsconfig.json",
|
||||||
"set:sqlite": "echo 'export * from \"./sqlite\";\nexport const driver: \"pg\" | \"sqlite\" = \"sqlite\";' > server/db/index.ts",
|
"set:sqlite": "echo 'export * from \"./sqlite\";\nexport const driver: \"pg\" | \"sqlite\" = \"sqlite\";' > server/db/index.ts && cp drizzle.sqlite.config.ts drizzle.config.ts && cp server/setup/migrationsSqlite.ts server/setup/migrations.ts",
|
||||||
"set:pg": "echo 'export * from \"./pg\";\nexport const driver: \"pg\" | \"sqlite\" = \"pg\";' > server/db/index.ts",
|
"set:pg": "echo 'export * from \"./pg\";\nexport const driver: \"pg\" | \"sqlite\" = \"pg\";' > server/db/index.ts && cp drizzle.pg.config.ts drizzle.config.ts && cp server/setup/migrationsPg.ts server/setup/migrations.ts",
|
||||||
"next:build": "next build",
|
"build:next": "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": "mkdir -p dist && next build && node esbuild.mjs -e server/index.ts -o dist/server.mjs && node esbuild.mjs -e server/setup/migrations.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",
|
||||||
|
"format:check": "prettier --check .",
|
||||||
"format": "prettier --write ."
|
"format": "prettier --write ."
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@asteasolutions/zod-to-openapi": "8.2.0",
|
"@asteasolutions/zod-to-openapi": "8.4.1",
|
||||||
"@aws-sdk/client-s3": "3.955.0",
|
"@aws-sdk/client-s3": "3.1011.0",
|
||||||
"@faker-js/faker": "10.1.0",
|
"@faker-js/faker": "10.3.0",
|
||||||
"@headlessui/react": "2.2.9",
|
"@headlessui/react": "2.2.9",
|
||||||
"@hookform/resolvers": "5.2.2",
|
"@hookform/resolvers": "5.2.2",
|
||||||
"@monaco-editor/react": "4.7.0",
|
"@monaco-editor/react": "4.7.0",
|
||||||
@@ -60,90 +59,83 @@
|
|||||||
"@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": "1.0.2",
|
"@react-email/components": "1.0.8",
|
||||||
"@react-email/render": "2.0.0",
|
"@react-email/render": "2.0.4",
|
||||||
"@react-email/tailwind": "2.0.2",
|
"@react-email/tailwind": "2.0.5",
|
||||||
"@simplewebauthn/browser": "13.2.2",
|
"@simplewebauthn/browser": "13.3.0",
|
||||||
"@simplewebauthn/server": "13.2.2",
|
"@simplewebauthn/server": "13.3.0",
|
||||||
"@tailwindcss/forms": "0.5.11",
|
"@tailwindcss/forms": "0.5.11",
|
||||||
"@tanstack/react-query": "5.90.12",
|
"@tanstack/react-query": "5.90.21",
|
||||||
"@tanstack/react-table": "8.21.3",
|
"@tanstack/react-table": "8.21.3",
|
||||||
"arctic": "3.7.0",
|
"arctic": "3.7.0",
|
||||||
"axios": "1.13.2",
|
"axios": "1.13.5",
|
||||||
"better-sqlite3": "11.9.1",
|
"better-sqlite3": "11.9.1",
|
||||||
"canvas-confetti": "1.9.4",
|
"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",
|
||||||
"cookie": "1.1.1",
|
|
||||||
"cookie-parser": "1.4.7",
|
"cookie-parser": "1.4.7",
|
||||||
"cookies": "0.9.1",
|
"cors": "2.8.6",
|
||||||
"cors": "2.8.5",
|
|
||||||
"crypto-js": "4.2.0",
|
"crypto-js": "4.2.0",
|
||||||
"d3": "7.9.0",
|
"d3": "7.9.0",
|
||||||
"date-fns": "4.1.0",
|
|
||||||
"drizzle-orm": "0.45.1",
|
"drizzle-orm": "0.45.1",
|
||||||
"eslint": "9.39.2",
|
|
||||||
"eslint-config-next": "16.1.0",
|
|
||||||
"express": "5.2.1",
|
"express": "5.2.1",
|
||||||
"express-rate-limit": "8.2.1",
|
"express-rate-limit": "8.3.0",
|
||||||
"glob": "13.0.0",
|
"glob": "13.0.6",
|
||||||
"helmet": "8.1.0",
|
"helmet": "8.1.0",
|
||||||
"http-errors": "2.0.1",
|
"http-errors": "2.0.1",
|
||||||
"i": "0.3.7",
|
|
||||||
"input-otp": "1.4.2",
|
"input-otp": "1.4.2",
|
||||||
"ioredis": "5.8.2",
|
"ioredis": "5.10.0",
|
||||||
"jmespath": "0.16.0",
|
"jmespath": "0.16.0",
|
||||||
"js-yaml": "4.1.1",
|
"js-yaml": "4.1.1",
|
||||||
"jsonwebtoken": "9.0.3",
|
"jsonwebtoken": "9.0.3",
|
||||||
"lucide-react": "0.562.0",
|
"lucide-react": "0.577.0",
|
||||||
"maxmind": "5.0.1",
|
"maxmind": "5.0.5",
|
||||||
"moment": "2.30.1",
|
"moment": "2.30.1",
|
||||||
"next": "15.5.9",
|
"next": "15.5.14",
|
||||||
"next-intl": "4.6.1",
|
"next-intl": "4.8.3",
|
||||||
"next-themes": "0.4.6",
|
"next-themes": "0.4.6",
|
||||||
"nextjs-toploader": "3.9.17",
|
"nextjs-toploader": "3.9.17",
|
||||||
"node-cache": "5.1.2",
|
"node-cache": "5.1.2",
|
||||||
"node-fetch": "3.3.2",
|
"nodemailer": "8.0.4",
|
||||||
"nodemailer": "7.0.11",
|
|
||||||
"npm": "11.7.0",
|
|
||||||
"nprogress": "0.2.0",
|
|
||||||
"oslo": "1.2.1",
|
"oslo": "1.2.1",
|
||||||
"pg": "8.16.3",
|
"pg": "8.20.0",
|
||||||
"posthog-node": "5.17.4",
|
"posthog-node": "5.28.0",
|
||||||
"qrcode.react": "4.2.0",
|
"qrcode.react": "4.2.0",
|
||||||
"react": "19.2.3",
|
"react": "19.2.4",
|
||||||
"react-day-picker": "9.13.0",
|
"react-day-picker": "9.14.0",
|
||||||
"react-dom": "19.2.3",
|
"react-dom": "19.2.4",
|
||||||
"react-easy-sort": "1.8.0",
|
"react-easy-sort": "1.8.0",
|
||||||
"react-hook-form": "7.68.0",
|
"react-hook-form": "7.71.2",
|
||||||
"react-icons": "5.5.0",
|
"react-icons": "5.6.0",
|
||||||
"rebuild": "0.1.2",
|
|
||||||
"recharts": "2.15.4",
|
"recharts": "2.15.4",
|
||||||
"reodotdev": "1.0.0",
|
"reodotdev": "1.1.0",
|
||||||
"resend": "6.6.0",
|
"resend": "6.9.2",
|
||||||
"semver": "7.7.3",
|
"semver": "7.7.4",
|
||||||
"stripe": "20.1.0",
|
"sshpk": "1.18.0",
|
||||||
|
"stripe": "20.4.1",
|
||||||
"swagger-ui-express": "5.0.1",
|
"swagger-ui-express": "5.0.1",
|
||||||
"tailwind-merge": "3.4.0",
|
"tailwind-merge": "3.5.0",
|
||||||
"topojson-client": "3.1.0",
|
"topojson-client": "3.1.0",
|
||||||
"tw-animate-css": "1.4.0",
|
"tw-animate-css": "1.4.0",
|
||||||
|
"use-debounce": "10.1.0",
|
||||||
"uuid": "13.0.0",
|
"uuid": "13.0.0",
|
||||||
"vaul": "1.1.2",
|
"vaul": "1.1.2",
|
||||||
"visionscarto-world-atlas": "1.0.0",
|
"visionscarto-world-atlas": "1.0.0",
|
||||||
"winston": "3.19.0",
|
"winston": "3.19.0",
|
||||||
"winston-daily-rotate-file": "5.0.0",
|
"winston-daily-rotate-file": "5.0.0",
|
||||||
"ws": "8.18.3",
|
"ws": "8.19.0",
|
||||||
"yaml": "2.8.2",
|
"yaml": "2.8.3",
|
||||||
"yargs": "18.0.0",
|
"yargs": "18.0.0",
|
||||||
"zod": "4.2.1",
|
"zod": "4.3.6",
|
||||||
"zod-validation-error": "5.0.0"
|
"zod-validation-error": "5.0.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@dotenvx/dotenvx": "1.51.2",
|
"@dotenvx/dotenvx": "1.54.1",
|
||||||
"@esbuild-plugins/tsconfig-paths": "0.1.2",
|
"@esbuild-plugins/tsconfig-paths": "0.1.2",
|
||||||
"@tailwindcss/postcss": "4.1.18",
|
"@react-email/preview-server": "5.2.10",
|
||||||
"@tanstack/react-query-devtools": "5.91.1",
|
"@tailwindcss/postcss": "4.2.2",
|
||||||
|
"@tanstack/react-query-devtools": "5.91.3",
|
||||||
"@types/better-sqlite3": "7.6.13",
|
"@types/better-sqlite3": "7.6.13",
|
||||||
"@types/cookie-parser": "1.4.10",
|
"@types/cookie-parser": "1.4.10",
|
||||||
"@types/cors": "2.8.19",
|
"@types/cors": "2.8.19",
|
||||||
@@ -152,30 +144,37 @@
|
|||||||
"@types/express": "5.0.6",
|
"@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/jsonwebtoken": "9.0.10",
|
"@types/jsonwebtoken": "9.0.10",
|
||||||
"@types/node": "24.10.2",
|
"@types/node": "25.3.5",
|
||||||
"@types/nodemailer": "7.0.4",
|
"@types/nodemailer": "7.0.11",
|
||||||
"@types/nprogress": "0.2.3",
|
"@types/nprogress": "0.2.3",
|
||||||
"@types/pg": "8.16.0",
|
"@types/pg": "8.18.0",
|
||||||
"@types/react": "19.2.7",
|
"@types/react": "19.2.14",
|
||||||
"@types/react-dom": "19.2.3",
|
"@types/react-dom": "19.2.3",
|
||||||
"@types/semver": "7.7.1",
|
"@types/semver": "7.7.1",
|
||||||
|
"@types/sshpk": "1.17.4",
|
||||||
"@types/swagger-ui-express": "4.1.8",
|
"@types/swagger-ui-express": "4.1.8",
|
||||||
"@types/topojson-client": "3.1.5",
|
"@types/topojson-client": "3.1.5",
|
||||||
"@types/ws": "8.18.1",
|
"@types/ws": "8.18.1",
|
||||||
"@types/yargs": "17.0.35",
|
"@types/yargs": "17.0.35",
|
||||||
"@types/js-yaml": "4.0.9",
|
|
||||||
"babel-plugin-react-compiler": "1.0.0",
|
"babel-plugin-react-compiler": "1.0.0",
|
||||||
"drizzle-kit": "0.31.8",
|
"drizzle-kit": "0.31.10",
|
||||||
"esbuild": "0.27.2",
|
"esbuild": "0.27.4",
|
||||||
"esbuild-node-externals": "1.20.1",
|
"esbuild-node-externals": "1.20.1",
|
||||||
"postcss": "8.5.6",
|
"eslint": "10.0.3",
|
||||||
"prettier": "3.7.4",
|
"eslint-config-next": "16.1.7",
|
||||||
"react-email": "5.0.7",
|
"postcss": "8.5.8",
|
||||||
"tailwindcss": "4.1.18",
|
"prettier": "3.8.1",
|
||||||
|
"react-email": "5.2.10",
|
||||||
|
"tailwindcss": "4.2.2",
|
||||||
"tsc-alias": "1.8.16",
|
"tsc-alias": "1.8.16",
|
||||||
"tsx": "4.21.0",
|
"tsx": "4.21.0",
|
||||||
"typescript": "5.9.3",
|
"typescript": "5.9.3",
|
||||||
"typescript-eslint": "8.49.0"
|
"typescript-eslint": "8.56.1"
|
||||||
|
},
|
||||||
|
"overrides": {
|
||||||
|
"esbuild": "0.27.4",
|
||||||
|
"dompurify": "3.3.2"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
BIN
public/third-party/dd.png
vendored
Normal file
BIN
public/third-party/dd.png
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 73 KiB |
BIN
public/third-party/s3.png
vendored
Normal file
BIN
public/third-party/s3.png
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 13 KiB |
@@ -1,9 +1,10 @@
|
|||||||
import { Request } from "express";
|
import { Request } from "express";
|
||||||
import { db } from "@server/db";
|
import { db } from "@server/db";
|
||||||
import { userActions, roleActions, userOrgs } from "@server/db";
|
import { userActions, roleActions } from "@server/db";
|
||||||
import { and, eq } from "drizzle-orm";
|
import { and, eq, inArray } 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 { getUserOrgRoleIds } from "@server/lib/userOrgRoles";
|
||||||
|
|
||||||
export enum ActionsEnum {
|
export enum ActionsEnum {
|
||||||
createOrgUser = "createOrgUser",
|
createOrgUser = "createOrgUser",
|
||||||
@@ -19,6 +20,7 @@ export enum ActionsEnum {
|
|||||||
getSite = "getSite",
|
getSite = "getSite",
|
||||||
listSites = "listSites",
|
listSites = "listSites",
|
||||||
updateSite = "updateSite",
|
updateSite = "updateSite",
|
||||||
|
resetSiteBandwidth = "resetSiteBandwidth",
|
||||||
reGenerateSecret = "reGenerateSecret",
|
reGenerateSecret = "reGenerateSecret",
|
||||||
createResource = "createResource",
|
createResource = "createResource",
|
||||||
deleteResource = "deleteResource",
|
deleteResource = "deleteResource",
|
||||||
@@ -52,6 +54,8 @@ export enum ActionsEnum {
|
|||||||
listRoleResources = "listRoleResources",
|
listRoleResources = "listRoleResources",
|
||||||
// listRoleActions = "listRoleActions",
|
// listRoleActions = "listRoleActions",
|
||||||
addUserRole = "addUserRole",
|
addUserRole = "addUserRole",
|
||||||
|
removeUserRole = "removeUserRole",
|
||||||
|
setUserOrgRoles = "setUserOrgRoles",
|
||||||
// addUserSite = "addUserSite",
|
// addUserSite = "addUserSite",
|
||||||
// addUserAction = "addUserAction",
|
// addUserAction = "addUserAction",
|
||||||
// removeUserAction = "removeUserAction",
|
// removeUserAction = "removeUserAction",
|
||||||
@@ -78,6 +82,10 @@ export enum ActionsEnum {
|
|||||||
updateSiteResource = "updateSiteResource",
|
updateSiteResource = "updateSiteResource",
|
||||||
createClient = "createClient",
|
createClient = "createClient",
|
||||||
deleteClient = "deleteClient",
|
deleteClient = "deleteClient",
|
||||||
|
archiveClient = "archiveClient",
|
||||||
|
unarchiveClient = "unarchiveClient",
|
||||||
|
blockClient = "blockClient",
|
||||||
|
unblockClient = "unblockClient",
|
||||||
updateClient = "updateClient",
|
updateClient = "updateClient",
|
||||||
listClients = "listClients",
|
listClients = "listClients",
|
||||||
getClient = "getClient",
|
getClient = "getClient",
|
||||||
@@ -104,6 +112,10 @@ export enum ActionsEnum {
|
|||||||
listApiKeyActions = "listApiKeyActions",
|
listApiKeyActions = "listApiKeyActions",
|
||||||
listApiKeys = "listApiKeys",
|
listApiKeys = "listApiKeys",
|
||||||
getApiKey = "getApiKey",
|
getApiKey = "getApiKey",
|
||||||
|
createSiteProvisioningKey = "createSiteProvisioningKey",
|
||||||
|
listSiteProvisioningKeys = "listSiteProvisioningKeys",
|
||||||
|
updateSiteProvisioningKey = "updateSiteProvisioningKey",
|
||||||
|
deleteSiteProvisioningKey = "deleteSiteProvisioningKey",
|
||||||
getCertificate = "getCertificate",
|
getCertificate = "getCertificate",
|
||||||
restartCertificate = "restartCertificate",
|
restartCertificate = "restartCertificate",
|
||||||
billing = "billing",
|
billing = "billing",
|
||||||
@@ -125,7 +137,14 @@ export enum ActionsEnum {
|
|||||||
getBlueprint = "getBlueprint",
|
getBlueprint = "getBlueprint",
|
||||||
applyBlueprint = "applyBlueprint",
|
applyBlueprint = "applyBlueprint",
|
||||||
viewLogs = "viewLogs",
|
viewLogs = "viewLogs",
|
||||||
exportLogs = "exportLogs"
|
exportLogs = "exportLogs",
|
||||||
|
listApprovals = "listApprovals",
|
||||||
|
updateApprovals = "updateApprovals",
|
||||||
|
signSshKey = "signSshKey",
|
||||||
|
createEventStreamingDestination = "createEventStreamingDestination",
|
||||||
|
updateEventStreamingDestination = "updateEventStreamingDestination",
|
||||||
|
deleteEventStreamingDestination = "deleteEventStreamingDestination",
|
||||||
|
listEventStreamingDestinations = "listEventStreamingDestinations"
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function checkUserActionPermission(
|
export async function checkUserActionPermission(
|
||||||
@@ -146,29 +165,16 @@ export async function checkUserActionPermission(
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
let userOrgRoleId = req.userOrgRoleId;
|
let userOrgRoleIds = req.userOrgRoleIds;
|
||||||
|
|
||||||
// If userOrgRoleId is not available on the request, fetch it
|
if (userOrgRoleIds === undefined) {
|
||||||
if (userOrgRoleId === undefined) {
|
userOrgRoleIds = await getUserOrgRoleIds(userId, req.userOrgId!);
|
||||||
const userOrgRole = await db
|
if (userOrgRoleIds.length === 0) {
|
||||||
.select()
|
|
||||||
.from(userOrgs)
|
|
||||||
.where(
|
|
||||||
and(
|
|
||||||
eq(userOrgs.userId, userId),
|
|
||||||
eq(userOrgs.orgId, req.userOrgId!)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
.limit(1);
|
|
||||||
|
|
||||||
if (userOrgRole.length === 0) {
|
|
||||||
throw createHttpError(
|
throw createHttpError(
|
||||||
HttpCode.FORBIDDEN,
|
HttpCode.FORBIDDEN,
|
||||||
"User does not have access to this organization"
|
"User does not have access to this organization"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
userOrgRoleId = userOrgRole[0].roleId;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if the user has direct permission for the action in the current org
|
// Check if the user has direct permission for the action in the current org
|
||||||
@@ -179,7 +185,7 @@ export async function checkUserActionPermission(
|
|||||||
and(
|
and(
|
||||||
eq(userActions.userId, userId),
|
eq(userActions.userId, userId),
|
||||||
eq(userActions.actionId, actionId),
|
eq(userActions.actionId, actionId),
|
||||||
eq(userActions.orgId, req.userOrgId!) // TODO: we cant pass the org id if we are not checking the org
|
eq(userActions.orgId, req.userOrgId!)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
.limit(1);
|
.limit(1);
|
||||||
@@ -188,14 +194,14 @@ export async function checkUserActionPermission(
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// If no direct permission, check role-based permission
|
// If no direct permission, check role-based permission (any of user's roles)
|
||||||
const roleActionPermission = await db
|
const roleActionPermission = await db
|
||||||
.select()
|
.select()
|
||||||
.from(roleActions)
|
.from(roleActions)
|
||||||
.where(
|
.where(
|
||||||
and(
|
and(
|
||||||
eq(roleActions.actionId, actionId),
|
eq(roleActions.actionId, actionId),
|
||||||
eq(roleActions.roleId, userOrgRoleId!),
|
inArray(roleActions.roleId, userOrgRoleIds),
|
||||||
eq(roleActions.orgId, req.userOrgId!)
|
eq(roleActions.orgId, req.userOrgId!)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,26 +1,29 @@
|
|||||||
import { db } from "@server/db";
|
import { db } from "@server/db";
|
||||||
import { and, eq } from "drizzle-orm";
|
import { and, eq, inArray } from "drizzle-orm";
|
||||||
import { roleResources, userResources } from "@server/db";
|
import { roleResources, userResources } from "@server/db";
|
||||||
|
|
||||||
export async function canUserAccessResource({
|
export async function canUserAccessResource({
|
||||||
userId,
|
userId,
|
||||||
resourceId,
|
resourceId,
|
||||||
roleId
|
roleIds
|
||||||
}: {
|
}: {
|
||||||
userId: string;
|
userId: string;
|
||||||
resourceId: number;
|
resourceId: number;
|
||||||
roleId: number;
|
roleIds: number[];
|
||||||
}): Promise<boolean> {
|
}): Promise<boolean> {
|
||||||
const roleResourceAccess = await db
|
const roleResourceAccess =
|
||||||
|
roleIds.length > 0
|
||||||
|
? await db
|
||||||
.select()
|
.select()
|
||||||
.from(roleResources)
|
.from(roleResources)
|
||||||
.where(
|
.where(
|
||||||
and(
|
and(
|
||||||
eq(roleResources.resourceId, resourceId),
|
eq(roleResources.resourceId, resourceId),
|
||||||
eq(roleResources.roleId, roleId)
|
inArray(roleResources.roleId, roleIds)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
.limit(1);
|
.limit(1)
|
||||||
|
: [];
|
||||||
|
|
||||||
if (roleResourceAccess.length > 0) {
|
if (roleResourceAccess.length > 0) {
|
||||||
return true;
|
return true;
|
||||||
|
|||||||
48
server/auth/canUserAccessSiteResource.ts
Normal file
48
server/auth/canUserAccessSiteResource.ts
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
import { db } from "@server/db";
|
||||||
|
import { and, eq, inArray } from "drizzle-orm";
|
||||||
|
import { roleSiteResources, userSiteResources } from "@server/db";
|
||||||
|
|
||||||
|
export async function canUserAccessSiteResource({
|
||||||
|
userId,
|
||||||
|
resourceId,
|
||||||
|
roleIds
|
||||||
|
}: {
|
||||||
|
userId: string;
|
||||||
|
resourceId: number;
|
||||||
|
roleIds: number[];
|
||||||
|
}): Promise<boolean> {
|
||||||
|
const roleResourceAccess =
|
||||||
|
roleIds.length > 0
|
||||||
|
? await db
|
||||||
|
.select()
|
||||||
|
.from(roleSiteResources)
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
eq(roleSiteResources.siteResourceId, resourceId),
|
||||||
|
inArray(roleSiteResources.roleId, roleIds)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.limit(1)
|
||||||
|
: [];
|
||||||
|
|
||||||
|
if (roleResourceAccess.length > 0) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const userResourceAccess = await db
|
||||||
|
.select()
|
||||||
|
.from(userSiteResources)
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
eq(userSiteResources.userId, userId),
|
||||||
|
eq(userSiteResources.siteResourceId, resourceId)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.limit(1);
|
||||||
|
|
||||||
|
if (userResourceAccess.length > 0) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
@@ -3,7 +3,14 @@ import {
|
|||||||
encodeHexLowerCase
|
encodeHexLowerCase
|
||||||
} from "@oslojs/encoding";
|
} from "@oslojs/encoding";
|
||||||
import { sha256 } from "@oslojs/crypto/sha2";
|
import { sha256 } from "@oslojs/crypto/sha2";
|
||||||
import { resourceSessions, Session, sessions, User, users } from "@server/db";
|
import {
|
||||||
|
resourceSessions,
|
||||||
|
safeRead,
|
||||||
|
Session,
|
||||||
|
sessions,
|
||||||
|
User,
|
||||||
|
users
|
||||||
|
} from "@server/db";
|
||||||
import { db } from "@server/db";
|
import { db } from "@server/db";
|
||||||
import { eq, inArray } from "drizzle-orm";
|
import { eq, inArray } from "drizzle-orm";
|
||||||
import config from "@server/lib/config";
|
import config from "@server/lib/config";
|
||||||
@@ -54,11 +61,15 @@ export async function validateSessionToken(
|
|||||||
const sessionId = encodeHexLowerCase(
|
const sessionId = encodeHexLowerCase(
|
||||||
sha256(new TextEncoder().encode(token))
|
sha256(new TextEncoder().encode(token))
|
||||||
);
|
);
|
||||||
const result = await db
|
|
||||||
|
const result = await safeRead((db) =>
|
||||||
|
db
|
||||||
.select({ user: users, session: sessions })
|
.select({ user: users, session: sessions })
|
||||||
.from(sessions)
|
.from(sessions)
|
||||||
.innerJoin(users, eq(sessions.userId, users.userId))
|
.innerJoin(users, eq(sessions.userId, users.userId))
|
||||||
.where(eq(sessions.sessionId, sessionId));
|
.where(eq(sessions.sessionId, sessionId))
|
||||||
|
);
|
||||||
|
|
||||||
if (result.length < 1) {
|
if (result.length < 1) {
|
||||||
return { session: null, user: null };
|
return { session: null, user: null };
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { encodeHexLowerCase } from "@oslojs/encoding";
|
import { encodeHexLowerCase } from "@oslojs/encoding";
|
||||||
import { sha256 } from "@oslojs/crypto/sha2";
|
import { sha256 } from "@oslojs/crypto/sha2";
|
||||||
import { resourceSessions, ResourceSession } from "@server/db";
|
import { resourceSessions, ResourceSession } from "@server/db";
|
||||||
import { db } from "@server/db";
|
import { db, safeRead } 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";
|
||||||
|
|
||||||
@@ -66,7 +66,8 @@ export async function validateResourceSessionToken(
|
|||||||
const sessionId = encodeHexLowerCase(
|
const sessionId = encodeHexLowerCase(
|
||||||
sha256(new TextEncoder().encode(token))
|
sha256(new TextEncoder().encode(token))
|
||||||
);
|
);
|
||||||
const result = await db
|
const result = await safeRead((db) =>
|
||||||
|
db
|
||||||
.select()
|
.select()
|
||||||
.from(resourceSessions)
|
.from(resourceSessions)
|
||||||
.where(
|
.where(
|
||||||
@@ -74,6 +75,7 @@ export async function validateResourceSessionToken(
|
|||||||
eq(resourceSessions.sessionId, sessionId),
|
eq(resourceSessions.sessionId, sessionId),
|
||||||
eq(resourceSessions.resourceId, resourceId)
|
eq(resourceSessions.resourceId, resourceId)
|
||||||
)
|
)
|
||||||
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
if (result.length < 1) {
|
if (result.length < 1) {
|
||||||
@@ -85,7 +87,7 @@ export async function validateResourceSessionToken(
|
|||||||
if (Date.now() >= resourceSession.expiresAt) {
|
if (Date.now() >= resourceSession.expiresAt) {
|
||||||
await db
|
await db
|
||||||
.delete(resourceSessions)
|
.delete(resourceSessions)
|
||||||
.where(eq(resourceSessions.sessionId, resourceSessions.sessionId));
|
.where(eq(resourceSessions.sessionId, sessionId));
|
||||||
return { resourceSession: null };
|
return { resourceSession: null };
|
||||||
} else if (
|
} else if (
|
||||||
Date.now() >=
|
Date.now() >=
|
||||||
@@ -179,7 +181,7 @@ export function serializeResourceSessionCookie(
|
|||||||
return `${cookieName}_s.${now}=${token}; HttpOnly; SameSite=Lax; Expires=${expiresAt.toUTCString()}; Path=/; Secure; Domain=${domain}`;
|
return `${cookieName}_s.${now}=${token}; HttpOnly; SameSite=Lax; Expires=${expiresAt.toUTCString()}; Path=/; Secure; Domain=${domain}`;
|
||||||
} else {
|
} else {
|
||||||
if (expiresAt === undefined) {
|
if (expiresAt === undefined) {
|
||||||
return `${cookieName}.${now}=${token}; HttpOnly; SameSite=Lax; Path=/; Domain=$domain}`;
|
return `${cookieName}.${now}=${token}; HttpOnly; SameSite=Lax; Path=/; Domain=${domain}`;
|
||||||
}
|
}
|
||||||
return `${cookieName}.${now}=${token}; HttpOnly; SameSite=Lax; Expires=${expiresAt.toUTCString()}; Path=/; Domain=${domain}`;
|
return `${cookieName}.${now}=${token}; HttpOnly; SameSite=Lax; Expires=${expiresAt.toUTCString()}; Path=/; Domain=${domain}`;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,14 @@
|
|||||||
|
import { flushBandwidthToDb } from "@server/routers/newt/handleReceiveBandwidthMessage";
|
||||||
|
import { flushConnectionLogToDb } from "#dynamic/routers/newt";
|
||||||
|
import { flushSiteBandwidthToDb } from "@server/routers/gerbil/receiveBandwidth";
|
||||||
|
import { stopPingAccumulator } from "@server/routers/newt/pingAccumulator";
|
||||||
import { cleanup as wsCleanup } from "#dynamic/routers/ws";
|
import { cleanup as wsCleanup } from "#dynamic/routers/ws";
|
||||||
|
|
||||||
async function cleanup() {
|
async function cleanup() {
|
||||||
|
await stopPingAccumulator();
|
||||||
|
await flushBandwidthToDb();
|
||||||
|
await flushConnectionLogToDb();
|
||||||
|
await flushSiteBandwidthToDb();
|
||||||
await wsCleanup();
|
await wsCleanup();
|
||||||
|
|
||||||
process.exit(0);
|
process.exit(0);
|
||||||
|
|||||||
@@ -56,15 +56,15 @@ Ensure drizzle-kit is installed.
|
|||||||
You must have a connection string in your config file, as shown above.
|
You must have a connection string in your config file, as shown above.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
npm run db:pg:generate
|
npm run db:generate
|
||||||
npm run db:pg:push
|
npm run db:push
|
||||||
```
|
```
|
||||||
|
|
||||||
### SQLite
|
### SQLite
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
npm run db:sqlite:generate
|
npm run db:generate
|
||||||
npm run db:sqlite:push
|
npm run db:push
|
||||||
```
|
```
|
||||||
|
|
||||||
## Build Time
|
## Build Time
|
||||||
|
|||||||
150
server/db/ios_models.json
Normal file
150
server/db/ios_models.json
Normal file
@@ -0,0 +1,150 @@
|
|||||||
|
{
|
||||||
|
"iPad1,1": "iPad",
|
||||||
|
"iPad2,1": "iPad 2",
|
||||||
|
"iPad2,2": "iPad 2",
|
||||||
|
"iPad2,3": "iPad 2",
|
||||||
|
"iPad2,4": "iPad 2",
|
||||||
|
"iPad3,1": "iPad 3rd Gen",
|
||||||
|
"iPad3,3": "iPad 3rd Gen",
|
||||||
|
"iPad3,2": "iPad 3rd Gen",
|
||||||
|
"iPad3,4": "iPad 4th Gen",
|
||||||
|
"iPad3,5": "iPad 4th Gen",
|
||||||
|
"iPad3,6": "iPad 4th Gen",
|
||||||
|
"iPad6,11": "iPad 9.7 5th Gen",
|
||||||
|
"iPad6,12": "iPad 9.7 5th Gen",
|
||||||
|
"iPad7,5": "iPad 9.7 6th Gen",
|
||||||
|
"iPad7,6": "iPad 9.7 6th Gen",
|
||||||
|
"iPad7,11": "iPad 10.2 7th Gen",
|
||||||
|
"iPad7,12": "iPad 10.2 7th Gen",
|
||||||
|
"iPad11,6": "iPad 10.2 8th Gen",
|
||||||
|
"iPad11,7": "iPad 10.2 8th Gen",
|
||||||
|
"iPad12,1": "iPad 10.2 9th Gen",
|
||||||
|
"iPad12,2": "iPad 10.2 9th Gen",
|
||||||
|
"iPad13,18": "iPad 10.9 10th Gen",
|
||||||
|
"iPad13,19": "iPad 10.9 10th Gen",
|
||||||
|
"iPad4,1": "iPad Air",
|
||||||
|
"iPad4,2": "iPad Air",
|
||||||
|
"iPad4,3": "iPad Air",
|
||||||
|
"iPad5,3": "iPad Air 2",
|
||||||
|
"iPad5,4": "iPad Air 2",
|
||||||
|
"iPad11,3": "iPad Air 3rd Gen",
|
||||||
|
"iPad11,4": "iPad Air 3rd Gen",
|
||||||
|
"iPad13,1": "iPad Air 4th Gen",
|
||||||
|
"iPad13,2": "iPad Air 4th Gen",
|
||||||
|
"iPad13,16": "iPad Air 5th Gen",
|
||||||
|
"iPad13,17": "iPad Air 5th Gen",
|
||||||
|
"iPad14,8": "iPad Air M2 11",
|
||||||
|
"iPad14,9": "iPad Air M2 11",
|
||||||
|
"iPad14,10": "iPad Air M2 13",
|
||||||
|
"iPad14,11": "iPad Air M2 13",
|
||||||
|
"iPad2,5": "iPad mini",
|
||||||
|
"iPad2,6": "iPad mini",
|
||||||
|
"iPad2,7": "iPad mini",
|
||||||
|
"iPad4,4": "iPad mini 2",
|
||||||
|
"iPad4,5": "iPad mini 2",
|
||||||
|
"iPad4,6": "iPad mini 2",
|
||||||
|
"iPad4,7": "iPad mini 3",
|
||||||
|
"iPad4,8": "iPad mini 3",
|
||||||
|
"iPad4,9": "iPad mini 3",
|
||||||
|
"iPad5,1": "iPad mini 4",
|
||||||
|
"iPad5,2": "iPad mini 4",
|
||||||
|
"iPad11,1": "iPad mini 5th Gen",
|
||||||
|
"iPad11,2": "iPad mini 5th Gen",
|
||||||
|
"iPad14,1": "iPad mini 6th Gen",
|
||||||
|
"iPad14,2": "iPad mini 6th Gen",
|
||||||
|
"iPad6,7": "iPad Pro 12.9",
|
||||||
|
"iPad6,8": "iPad Pro 12.9",
|
||||||
|
"iPad6,3": "iPad Pro 9.7",
|
||||||
|
"iPad6,4": "iPad Pro 9.7",
|
||||||
|
"iPad7,3": "iPad Pro 10.5",
|
||||||
|
"iPad7,4": "iPad Pro 10.5",
|
||||||
|
"iPad7,1": "iPad Pro 12.9",
|
||||||
|
"iPad7,2": "iPad Pro 12.9",
|
||||||
|
"iPad8,1": "iPad Pro 11",
|
||||||
|
"iPad8,2": "iPad Pro 11",
|
||||||
|
"iPad8,3": "iPad Pro 11",
|
||||||
|
"iPad8,4": "iPad Pro 11",
|
||||||
|
"iPad8,5": "iPad Pro 12.9",
|
||||||
|
"iPad8,6": "iPad Pro 12.9",
|
||||||
|
"iPad8,7": "iPad Pro 12.9",
|
||||||
|
"iPad8,8": "iPad Pro 12.9",
|
||||||
|
"iPad8,9": "iPad Pro 11",
|
||||||
|
"iPad8,10": "iPad Pro 11",
|
||||||
|
"iPad8,11": "iPad Pro 12.9",
|
||||||
|
"iPad8,12": "iPad Pro 12.9",
|
||||||
|
"iPad13,4": "iPad Pro 11",
|
||||||
|
"iPad13,5": "iPad Pro 11",
|
||||||
|
"iPad13,6": "iPad Pro 11",
|
||||||
|
"iPad13,7": "iPad Pro 11",
|
||||||
|
"iPad13,8": "iPad Pro 12.9",
|
||||||
|
"iPad13,9": "iPad Pro 12.9",
|
||||||
|
"iPad13,10": "iPad Pro 12.9",
|
||||||
|
"iPad13,11": "iPad Pro 12.9",
|
||||||
|
"iPad14,3": "iPad Pro 11",
|
||||||
|
"iPad14,4": "iPad Pro 11",
|
||||||
|
"iPad14,5": "iPad Pro 12.9",
|
||||||
|
"iPad14,6": "iPad Pro 12.9",
|
||||||
|
"iPad16,3": "iPad Pro M4 11",
|
||||||
|
"iPad16,4": "iPad Pro M4 11",
|
||||||
|
"iPad16,5": "iPad Pro M4 13",
|
||||||
|
"iPad16,6": "iPad Pro M4 13",
|
||||||
|
"iPhone1,1": "iPhone",
|
||||||
|
"iPhone1,2": "iPhone 3G",
|
||||||
|
"iPhone2,1": "iPhone 3GS",
|
||||||
|
"iPhone3,1": "iPhone 4",
|
||||||
|
"iPhone3,2": "iPhone 4",
|
||||||
|
"iPhone3,3": "iPhone 4",
|
||||||
|
"iPhone4,1": "iPhone 4S",
|
||||||
|
"iPhone5,1": "iPhone 5",
|
||||||
|
"iPhone5,2": "iPhone 5",
|
||||||
|
"iPhone5,3": "iPhone 5c",
|
||||||
|
"iPhone5,4": "iPhone 5c",
|
||||||
|
"iPhone6,1": "iPhone 5s",
|
||||||
|
"iPhone6,2": "iPhone 5s",
|
||||||
|
"iPhone7,2": "iPhone 6",
|
||||||
|
"iPhone7,1": "iPhone 6 Plus",
|
||||||
|
"iPhone8,1": "iPhone 6s",
|
||||||
|
"iPhone8,2": "iPhone 6s Plus",
|
||||||
|
"iPhone8,4": "iPhone SE",
|
||||||
|
"iPhone9,1": "iPhone 7",
|
||||||
|
"iPhone9,3": "iPhone 7",
|
||||||
|
"iPhone9,2": "iPhone 7 Plus",
|
||||||
|
"iPhone9,4": "iPhone 7 Plus",
|
||||||
|
"iPhone10,1": "iPhone 8",
|
||||||
|
"iPhone10,4": "iPhone 8",
|
||||||
|
"iPhone10,2": "iPhone 8 Plus",
|
||||||
|
"iPhone10,5": "iPhone 8 Plus",
|
||||||
|
"iPhone10,3": "iPhone X",
|
||||||
|
"iPhone10,6": "iPhone X",
|
||||||
|
"iPhone11,2": "iPhone Xs",
|
||||||
|
"iPhone11,6": "iPhone Xs Max",
|
||||||
|
"iPhone11,8": "iPhone XR",
|
||||||
|
"iPhone12,1": "iPhone 11",
|
||||||
|
"iPhone12,3": "iPhone 11 Pro",
|
||||||
|
"iPhone12,5": "iPhone 11 Pro Max",
|
||||||
|
"iPhone12,8": "iPhone SE",
|
||||||
|
"iPhone13,1": "iPhone 12 mini",
|
||||||
|
"iPhone13,2": "iPhone 12",
|
||||||
|
"iPhone13,3": "iPhone 12 Pro",
|
||||||
|
"iPhone13,4": "iPhone 12 Pro Max",
|
||||||
|
"iPhone14,4": "iPhone 13 mini",
|
||||||
|
"iPhone14,5": "iPhone 13",
|
||||||
|
"iPhone14,2": "iPhone 13 Pro",
|
||||||
|
"iPhone14,3": "iPhone 13 Pro Max",
|
||||||
|
"iPhone14,6": "iPhone SE",
|
||||||
|
"iPhone14,7": "iPhone 14",
|
||||||
|
"iPhone14,8": "iPhone 14 Plus",
|
||||||
|
"iPhone15,2": "iPhone 14 Pro",
|
||||||
|
"iPhone15,3": "iPhone 14 Pro Max",
|
||||||
|
"iPhone15,4": "iPhone 15",
|
||||||
|
"iPhone15,5": "iPhone 15 Plus",
|
||||||
|
"iPhone16,1": "iPhone 15 Pro",
|
||||||
|
"iPhone16,2": "iPhone 15 Pro Max",
|
||||||
|
"iPod1,1": "iPod touch Original",
|
||||||
|
"iPod2,1": "iPod touch 2nd",
|
||||||
|
"iPod3,1": "iPod touch 3rd Gen",
|
||||||
|
"iPod4,1": "iPod touch 4th",
|
||||||
|
"iPod5,1": "iPod touch 5th",
|
||||||
|
"iPod7,1": "iPod touch 6th Gen",
|
||||||
|
"iPod9,1": "iPod touch 7th Gen"
|
||||||
|
}
|
||||||
201
server/db/mac_models.json
Normal file
201
server/db/mac_models.json
Normal file
@@ -0,0 +1,201 @@
|
|||||||
|
{
|
||||||
|
"PowerMac4,4": "eMac",
|
||||||
|
"PowerMac6,4": "eMac",
|
||||||
|
"PowerBook2,1": "iBook",
|
||||||
|
"PowerBook2,2": "iBook",
|
||||||
|
"PowerBook4,1": "iBook",
|
||||||
|
"PowerBook4,2": "iBook",
|
||||||
|
"PowerBook4,3": "iBook",
|
||||||
|
"PowerBook6,3": "iBook",
|
||||||
|
"PowerBook6,5": "iBook",
|
||||||
|
"PowerBook6,7": "iBook",
|
||||||
|
"iMac,1": "iMac",
|
||||||
|
"PowerMac2,1": "iMac",
|
||||||
|
"PowerMac2,2": "iMac",
|
||||||
|
"PowerMac4,1": "iMac",
|
||||||
|
"PowerMac4,2": "iMac",
|
||||||
|
"PowerMac4,5": "iMac",
|
||||||
|
"PowerMac6,1": "iMac",
|
||||||
|
"PowerMac6,3*": "iMac",
|
||||||
|
"PowerMac6,3": "iMac",
|
||||||
|
"PowerMac8,1": "iMac",
|
||||||
|
"PowerMac8,2": "iMac",
|
||||||
|
"PowerMac12,1": "iMac",
|
||||||
|
"iMac4,1": "iMac",
|
||||||
|
"iMac4,2": "iMac",
|
||||||
|
"iMac5,2": "iMac",
|
||||||
|
"iMac5,1": "iMac",
|
||||||
|
"iMac6,1": "iMac",
|
||||||
|
"iMac7,1": "iMac",
|
||||||
|
"iMac8,1": "iMac",
|
||||||
|
"iMac9,1": "iMac",
|
||||||
|
"iMac10,1": "iMac",
|
||||||
|
"iMac11,1": "iMac",
|
||||||
|
"iMac11,2": "iMac",
|
||||||
|
"iMac11,3": "iMac",
|
||||||
|
"iMac12,1": "iMac",
|
||||||
|
"iMac12,2": "iMac",
|
||||||
|
"iMac13,1": "iMac",
|
||||||
|
"iMac13,2": "iMac",
|
||||||
|
"iMac14,1": "iMac",
|
||||||
|
"iMac14,3": "iMac",
|
||||||
|
"iMac14,2": "iMac",
|
||||||
|
"iMac14,4": "iMac",
|
||||||
|
"iMac15,1": "iMac",
|
||||||
|
"iMac16,1": "iMac",
|
||||||
|
"iMac16,2": "iMac",
|
||||||
|
"iMac17,1": "iMac",
|
||||||
|
"iMac18,1": "iMac",
|
||||||
|
"iMac18,2": "iMac",
|
||||||
|
"iMac18,3": "iMac",
|
||||||
|
"iMac19,2": "iMac",
|
||||||
|
"iMac19,1": "iMac",
|
||||||
|
"iMac20,1": "iMac",
|
||||||
|
"iMac20,2": "iMac",
|
||||||
|
"iMac21,2": "iMac",
|
||||||
|
"iMac21,1": "iMac",
|
||||||
|
"iMacPro1,1": "iMac Pro",
|
||||||
|
"PowerMac10,1": "Mac mini",
|
||||||
|
"PowerMac10,2": "Mac mini",
|
||||||
|
"Macmini1,1": "Mac mini",
|
||||||
|
"Macmini2,1": "Mac mini",
|
||||||
|
"Macmini3,1": "Mac mini",
|
||||||
|
"Macmini4,1": "Mac mini",
|
||||||
|
"Macmini5,1": "Mac mini",
|
||||||
|
"Macmini5,2": "Mac mini",
|
||||||
|
"Macmini5,3": "Mac mini",
|
||||||
|
"Macmini6,1": "Mac mini",
|
||||||
|
"Macmini6,2": "Mac mini",
|
||||||
|
"Macmini7,1": "Mac mini",
|
||||||
|
"Macmini8,1": "Mac mini",
|
||||||
|
"ADP3,2": "Mac mini",
|
||||||
|
"Macmini9,1": "Mac mini",
|
||||||
|
"Mac14,3": "Mac mini",
|
||||||
|
"Mac14,12": "Mac mini",
|
||||||
|
"MacPro1,1*": "Mac Pro",
|
||||||
|
"MacPro2,1": "Mac Pro",
|
||||||
|
"MacPro3,1": "Mac Pro",
|
||||||
|
"MacPro4,1": "Mac Pro",
|
||||||
|
"MacPro5,1": "Mac Pro",
|
||||||
|
"MacPro6,1": "Mac Pro",
|
||||||
|
"MacPro7,1": "Mac Pro",
|
||||||
|
"N/A*": "Power Macintosh",
|
||||||
|
"PowerMac1,1": "Power Macintosh",
|
||||||
|
"PowerMac3,1": "Power Macintosh",
|
||||||
|
"PowerMac3,3": "Power Macintosh",
|
||||||
|
"PowerMac3,4": "Power Macintosh",
|
||||||
|
"PowerMac3,5": "Power Macintosh",
|
||||||
|
"PowerMac3,6": "Power Macintosh",
|
||||||
|
"Mac13,1": "Mac Studio",
|
||||||
|
"Mac13,2": "Mac Studio",
|
||||||
|
"MacBook1,1": "MacBook",
|
||||||
|
"MacBook2,1": "MacBook",
|
||||||
|
"MacBook3,1": "MacBook",
|
||||||
|
"MacBook4,1": "MacBook",
|
||||||
|
"MacBook5,1": "MacBook",
|
||||||
|
"MacBook5,2": "MacBook",
|
||||||
|
"MacBook6,1": "MacBook",
|
||||||
|
"MacBook7,1": "MacBook",
|
||||||
|
"MacBook8,1": "MacBook",
|
||||||
|
"MacBook9,1": "MacBook",
|
||||||
|
"MacBook10,1": "MacBook",
|
||||||
|
"MacBookAir1,1": "MacBook Air",
|
||||||
|
"MacBookAir2,1": "MacBook Air",
|
||||||
|
"MacBookAir3,1": "MacBook Air",
|
||||||
|
"MacBookAir3,2": "MacBook Air",
|
||||||
|
"MacBookAir4,1": "MacBook Air",
|
||||||
|
"MacBookAir4,2": "MacBook Air",
|
||||||
|
"MacBookAir5,1": "MacBook Air",
|
||||||
|
"MacBookAir5,2": "MacBook Air",
|
||||||
|
"MacBookAir6,1": "MacBook Air",
|
||||||
|
"MacBookAir6,2": "MacBook Air",
|
||||||
|
"MacBookAir7,1": "MacBook Air",
|
||||||
|
"MacBookAir7,2": "MacBook Air",
|
||||||
|
"MacBookAir8,1": "MacBook Air",
|
||||||
|
"MacBookAir8,2": "MacBook Air",
|
||||||
|
"MacBookAir9,1": "MacBook Air",
|
||||||
|
"MacBookAir10,1": "MacBook Air",
|
||||||
|
"Mac14,2": "MacBook Air",
|
||||||
|
"MacBookPro1,1": "MacBook Pro",
|
||||||
|
"MacBookPro1,2": "MacBook Pro",
|
||||||
|
"MacBookPro2,2": "MacBook Pro",
|
||||||
|
"MacBookPro2,1": "MacBook Pro",
|
||||||
|
"MacBookPro3,1": "MacBook Pro",
|
||||||
|
"MacBookPro4,1": "MacBook Pro",
|
||||||
|
"MacBookPro5,1": "MacBook Pro",
|
||||||
|
"MacBookPro5,2": "MacBook Pro",
|
||||||
|
"MacBookPro5,5": "MacBook Pro",
|
||||||
|
"MacBookPro5,4": "MacBook Pro",
|
||||||
|
"MacBookPro5,3": "MacBook Pro",
|
||||||
|
"MacBookPro7,1": "MacBook Pro",
|
||||||
|
"MacBookPro6,2": "MacBook Pro",
|
||||||
|
"MacBookPro6,1": "MacBook Pro",
|
||||||
|
"MacBookPro8,1": "MacBook Pro",
|
||||||
|
"MacBookPro8,2": "MacBook Pro",
|
||||||
|
"MacBookPro8,3": "MacBook Pro",
|
||||||
|
"MacBookPro9,2": "MacBook Pro",
|
||||||
|
"MacBookPro9,1": "MacBook Pro",
|
||||||
|
"MacBookPro10,1": "MacBook Pro",
|
||||||
|
"MacBookPro10,2": "MacBook Pro",
|
||||||
|
"MacBookPro11,1": "MacBook Pro",
|
||||||
|
"MacBookPro11,2": "MacBook Pro",
|
||||||
|
"MacBookPro11,3": "MacBook Pro",
|
||||||
|
"MacBookPro12,1": "MacBook Pro",
|
||||||
|
"MacBookPro11,4": "MacBook Pro",
|
||||||
|
"MacBookPro11,5": "MacBook Pro",
|
||||||
|
"MacBookPro13,1": "MacBook Pro",
|
||||||
|
"MacBookPro13,2": "MacBook Pro",
|
||||||
|
"MacBookPro13,3": "MacBook Pro",
|
||||||
|
"MacBookPro14,1": "MacBook Pro",
|
||||||
|
"MacBookPro14,2": "MacBook Pro",
|
||||||
|
"MacBookPro14,3": "MacBook Pro",
|
||||||
|
"MacBookPro15,2": "MacBook Pro",
|
||||||
|
"MacBookPro15,1": "MacBook Pro",
|
||||||
|
"MacBookPro15,3": "MacBook Pro",
|
||||||
|
"MacBookPro15,4": "MacBook Pro",
|
||||||
|
"MacBookPro16,1": "MacBook Pro",
|
||||||
|
"MacBookPro16,3": "MacBook Pro",
|
||||||
|
"MacBookPro16,2": "MacBook Pro",
|
||||||
|
"MacBookPro16,4": "MacBook Pro",
|
||||||
|
"MacBookPro17,1": "MacBook Pro",
|
||||||
|
"MacBookPro18,3": "MacBook Pro",
|
||||||
|
"MacBookPro18,4": "MacBook Pro",
|
||||||
|
"MacBookPro18,1": "MacBook Pro",
|
||||||
|
"MacBookPro18,2": "MacBook Pro",
|
||||||
|
"Mac14,7": "MacBook Pro",
|
||||||
|
"Mac14,9": "MacBook Pro",
|
||||||
|
"Mac14,5": "MacBook Pro",
|
||||||
|
"Mac14,10": "MacBook Pro",
|
||||||
|
"Mac14,6": "MacBook Pro",
|
||||||
|
"PowerMac1,2": "Power Macintosh",
|
||||||
|
"PowerMac5,1": "Power Macintosh",
|
||||||
|
"PowerMac7,2": "Power Macintosh",
|
||||||
|
"PowerMac7,3": "Power Macintosh",
|
||||||
|
"PowerMac9,1": "Power Macintosh",
|
||||||
|
"PowerMac11,2": "Power Macintosh",
|
||||||
|
"PowerBook1,1": "PowerBook",
|
||||||
|
"PowerBook3,1": "PowerBook",
|
||||||
|
"PowerBook3,2": "PowerBook",
|
||||||
|
"PowerBook3,3": "PowerBook",
|
||||||
|
"PowerBook3,4": "PowerBook",
|
||||||
|
"PowerBook3,5": "PowerBook",
|
||||||
|
"PowerBook6,1": "PowerBook",
|
||||||
|
"PowerBook5,1": "PowerBook",
|
||||||
|
"PowerBook6,2": "PowerBook",
|
||||||
|
"PowerBook5,2": "PowerBook",
|
||||||
|
"PowerBook5,3": "PowerBook",
|
||||||
|
"PowerBook6,4": "PowerBook",
|
||||||
|
"PowerBook5,4": "PowerBook",
|
||||||
|
"PowerBook5,5": "PowerBook",
|
||||||
|
"PowerBook6,8": "PowerBook",
|
||||||
|
"PowerBook5,6": "PowerBook",
|
||||||
|
"PowerBook5,7": "PowerBook",
|
||||||
|
"PowerBook5,8": "PowerBook",
|
||||||
|
"PowerBook5,9": "PowerBook",
|
||||||
|
"RackMac1,1": "Xserve",
|
||||||
|
"RackMac1,2": "Xserve",
|
||||||
|
"RackMac3,1": "Xserve",
|
||||||
|
"Xserve1,1": "Xserve",
|
||||||
|
"Xserve2,1": "Xserve",
|
||||||
|
"Xserve3,1": "Xserve"
|
||||||
|
}
|
||||||
3
server/db/migrate.ts
Normal file
3
server/db/migrate.ts
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
import { runMigrations } from "./";
|
||||||
|
|
||||||
|
await runMigrations();
|
||||||
@@ -16,6 +16,24 @@ if (!dev) {
|
|||||||
}
|
}
|
||||||
export const names = JSON.parse(readFileSync(file, "utf-8"));
|
export const names = JSON.parse(readFileSync(file, "utf-8"));
|
||||||
|
|
||||||
|
// Load iOS and Mac model mappings
|
||||||
|
let iosModelsFile: string;
|
||||||
|
let macModelsFile: string;
|
||||||
|
if (!dev) {
|
||||||
|
iosModelsFile = join(__DIRNAME, "ios_models.json");
|
||||||
|
macModelsFile = join(__DIRNAME, "mac_models.json");
|
||||||
|
} else {
|
||||||
|
iosModelsFile = join("server/db/ios_models.json");
|
||||||
|
macModelsFile = join("server/db/mac_models.json");
|
||||||
|
}
|
||||||
|
|
||||||
|
const iosModels: Record<string, string> = JSON.parse(
|
||||||
|
readFileSync(iosModelsFile, "utf-8")
|
||||||
|
);
|
||||||
|
const macModels: Record<string, string> = JSON.parse(
|
||||||
|
readFileSync(macModelsFile, "utf-8")
|
||||||
|
);
|
||||||
|
|
||||||
export async function getUniqueClientName(orgId: string): Promise<string> {
|
export async function getUniqueClientName(orgId: string): Promise<string> {
|
||||||
let loops = 0;
|
let loops = 0;
|
||||||
while (true) {
|
while (true) {
|
||||||
@@ -159,3 +177,29 @@ export function generateName(): string {
|
|||||||
// clean out any non-alphanumeric characters except for dashes
|
// clean out any non-alphanumeric characters except for dashes
|
||||||
return name.replace(/[^a-z0-9-]/g, "");
|
return name.replace(/[^a-z0-9-]/g, "");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function getMacDeviceName(macIdentifier?: string | null): string | null {
|
||||||
|
if (macIdentifier && macModels[macIdentifier]) {
|
||||||
|
return macModels[macIdentifier];
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getIosDeviceName(iosIdentifier?: string | null): string | null {
|
||||||
|
if (iosIdentifier && iosModels[iosIdentifier]) {
|
||||||
|
return iosModels[iosIdentifier];
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getUserDeviceName(
|
||||||
|
model: string | null,
|
||||||
|
fallBack: string | null
|
||||||
|
): string {
|
||||||
|
return (
|
||||||
|
getMacDeviceName(model) ||
|
||||||
|
getIosDeviceName(model) ||
|
||||||
|
fallBack ||
|
||||||
|
"Unknown Device"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { drizzle as DrizzlePostgres } from "drizzle-orm/node-postgres";
|
import { drizzle as DrizzlePostgres } from "drizzle-orm/node-postgres";
|
||||||
import { Pool } from "pg";
|
|
||||||
import { readConfigFile } from "@server/lib/readConfigFile";
|
import { readConfigFile } from "@server/lib/readConfigFile";
|
||||||
import { withReplicas } from "drizzle-orm/pg-core";
|
import { withReplicas } from "drizzle-orm/pg-core";
|
||||||
|
import { createPool } from "./poolConfig";
|
||||||
|
|
||||||
function createDb() {
|
function createDb() {
|
||||||
const config = readConfigFile();
|
const config = readConfigFile();
|
||||||
@@ -39,12 +39,17 @@ function createDb() {
|
|||||||
|
|
||||||
// Create connection pools instead of individual connections
|
// Create connection pools instead of individual connections
|
||||||
const poolConfig = config.postgres.pool;
|
const poolConfig = config.postgres.pool;
|
||||||
const primaryPool = new Pool({
|
const maxConnections = poolConfig?.max_connections || 20;
|
||||||
|
const idleTimeoutMs = poolConfig?.idle_timeout_ms || 30000;
|
||||||
|
const connectionTimeoutMs = poolConfig?.connection_timeout_ms || 5000;
|
||||||
|
|
||||||
|
const primaryPool = createPool(
|
||||||
connectionString,
|
connectionString,
|
||||||
max: poolConfig?.max_connections || 20,
|
maxConnections,
|
||||||
idleTimeoutMillis: poolConfig?.idle_timeout_ms || 30000,
|
idleTimeoutMs,
|
||||||
connectionTimeoutMillis: poolConfig?.connection_timeout_ms || 5000
|
connectionTimeoutMs,
|
||||||
});
|
"primary"
|
||||||
|
);
|
||||||
|
|
||||||
const replicas = [];
|
const replicas = [];
|
||||||
|
|
||||||
@@ -55,14 +60,15 @@ function createDb() {
|
|||||||
})
|
})
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
|
const maxReplicaConnections = poolConfig?.max_replica_connections || 20;
|
||||||
for (const conn of replicaConnections) {
|
for (const conn of replicaConnections) {
|
||||||
const replicaPool = new Pool({
|
const replicaPool = createPool(
|
||||||
connectionString: conn.connection_string,
|
conn.connection_string,
|
||||||
max: poolConfig?.max_replica_connections || 20,
|
maxReplicaConnections,
|
||||||
idleTimeoutMillis: poolConfig?.idle_timeout_ms || 30000,
|
idleTimeoutMs,
|
||||||
connectionTimeoutMillis:
|
connectionTimeoutMs,
|
||||||
poolConfig?.connection_timeout_ms || 5000
|
"replica"
|
||||||
});
|
);
|
||||||
replicas.push(
|
replicas.push(
|
||||||
DrizzlePostgres(replicaPool, {
|
DrizzlePostgres(replicaPool, {
|
||||||
logger: process.env.QUERY_LOGGING == "true"
|
logger: process.env.QUERY_LOGGING == "true"
|
||||||
@@ -85,3 +91,4 @@ export const primaryDb = db.$primary;
|
|||||||
export type Transaction = Parameters<
|
export type Transaction = Parameters<
|
||||||
Parameters<(typeof db)["transaction"]>[0]
|
Parameters<(typeof db)["transaction"]>[0]
|
||||||
>[0];
|
>[0];
|
||||||
|
export const DB_TYPE: "pg" | "sqlite" = "pg";
|
||||||
|
|||||||
@@ -1,3 +1,6 @@
|
|||||||
export * from "./driver";
|
export * from "./driver";
|
||||||
|
export * from "./logsDriver";
|
||||||
|
export * from "./safeRead";
|
||||||
export * from "./schema/schema";
|
export * from "./schema/schema";
|
||||||
export * from "./schema/privateSchema";
|
export * from "./schema/privateSchema";
|
||||||
|
export * from "./migrate";
|
||||||
|
|||||||
94
server/db/pg/logsDriver.ts
Normal file
94
server/db/pg/logsDriver.ts
Normal file
@@ -0,0 +1,94 @@
|
|||||||
|
import { drizzle as DrizzlePostgres } from "drizzle-orm/node-postgres";
|
||||||
|
import { readConfigFile } from "@server/lib/readConfigFile";
|
||||||
|
import { withReplicas } from "drizzle-orm/pg-core";
|
||||||
|
import { build } from "@server/build";
|
||||||
|
import { db as mainDb, primaryDb as mainPrimaryDb } from "./driver";
|
||||||
|
import { createPool } from "./poolConfig";
|
||||||
|
|
||||||
|
function createLogsDb() {
|
||||||
|
// Only use separate logs database in SaaS builds
|
||||||
|
if (build !== "saas") {
|
||||||
|
return mainDb;
|
||||||
|
}
|
||||||
|
|
||||||
|
const config = readConfigFile();
|
||||||
|
|
||||||
|
// Merge configs, prioritizing private config
|
||||||
|
const logsConfig = config.postgres_logs;
|
||||||
|
|
||||||
|
// Check environment variable first
|
||||||
|
let connectionString = process.env.POSTGRES_LOGS_CONNECTION_STRING;
|
||||||
|
let replicaConnections: Array<{ connection_string: string }> = [];
|
||||||
|
|
||||||
|
if (!connectionString && logsConfig) {
|
||||||
|
connectionString = logsConfig.connection_string;
|
||||||
|
replicaConnections = logsConfig.replicas || [];
|
||||||
|
}
|
||||||
|
|
||||||
|
// If POSTGRES_LOGS_REPLICA_CONNECTION_STRINGS is set, use it
|
||||||
|
if (process.env.POSTGRES_LOGS_REPLICA_CONNECTION_STRINGS) {
|
||||||
|
replicaConnections =
|
||||||
|
process.env.POSTGRES_LOGS_REPLICA_CONNECTION_STRINGS.split(",").map(
|
||||||
|
(conn) => ({
|
||||||
|
connection_string: conn.trim()
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// If no logs database is configured, fall back to main database
|
||||||
|
if (!connectionString) {
|
||||||
|
return mainDb;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create separate connection pool for logs database
|
||||||
|
const poolConfig = logsConfig?.pool || config.postgres?.pool;
|
||||||
|
const maxConnections = poolConfig?.max_connections || 20;
|
||||||
|
const idleTimeoutMs = poolConfig?.idle_timeout_ms || 30000;
|
||||||
|
const connectionTimeoutMs = poolConfig?.connection_timeout_ms || 5000;
|
||||||
|
|
||||||
|
const primaryPool = createPool(
|
||||||
|
connectionString,
|
||||||
|
maxConnections,
|
||||||
|
idleTimeoutMs,
|
||||||
|
connectionTimeoutMs,
|
||||||
|
"logs-primary"
|
||||||
|
);
|
||||||
|
|
||||||
|
const replicas = [];
|
||||||
|
|
||||||
|
if (!replicaConnections.length) {
|
||||||
|
replicas.push(
|
||||||
|
DrizzlePostgres(primaryPool, {
|
||||||
|
logger: process.env.QUERY_LOGGING == "true"
|
||||||
|
})
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
const maxReplicaConnections =
|
||||||
|
poolConfig?.max_replica_connections || 20;
|
||||||
|
for (const conn of replicaConnections) {
|
||||||
|
const replicaPool = createPool(
|
||||||
|
conn.connection_string,
|
||||||
|
maxReplicaConnections,
|
||||||
|
idleTimeoutMs,
|
||||||
|
connectionTimeoutMs,
|
||||||
|
"logs-replica"
|
||||||
|
);
|
||||||
|
replicas.push(
|
||||||
|
DrizzlePostgres(replicaPool, {
|
||||||
|
logger: process.env.QUERY_LOGGING == "true"
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return withReplicas(
|
||||||
|
DrizzlePostgres(primaryPool, {
|
||||||
|
logger: process.env.QUERY_LOGGING == "true"
|
||||||
|
}),
|
||||||
|
replicas as any
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export const logsDb = createLogsDb();
|
||||||
|
export default logsDb;
|
||||||
|
export const primaryLogsDb = logsDb.$primary;
|
||||||
@@ -4,7 +4,7 @@ import path from "path";
|
|||||||
|
|
||||||
const migrationsFolder = path.join("server/migrations");
|
const migrationsFolder = path.join("server/migrations");
|
||||||
|
|
||||||
const runMigrations = async () => {
|
export const runMigrations = async () => {
|
||||||
console.log("Running migrations...");
|
console.log("Running migrations...");
|
||||||
try {
|
try {
|
||||||
await migrate(db as any, {
|
await migrate(db as any, {
|
||||||
@@ -17,5 +17,3 @@ const runMigrations = async () => {
|
|||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
runMigrations();
|
|
||||||
|
|||||||
63
server/db/pg/poolConfig.ts
Normal file
63
server/db/pg/poolConfig.ts
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
import { Pool, PoolConfig } from "pg";
|
||||||
|
import logger from "@server/logger";
|
||||||
|
|
||||||
|
export function createPoolConfig(
|
||||||
|
connectionString: string,
|
||||||
|
maxConnections: number,
|
||||||
|
idleTimeoutMs: number,
|
||||||
|
connectionTimeoutMs: number
|
||||||
|
): PoolConfig {
|
||||||
|
return {
|
||||||
|
connectionString,
|
||||||
|
max: maxConnections,
|
||||||
|
idleTimeoutMillis: idleTimeoutMs,
|
||||||
|
connectionTimeoutMillis: connectionTimeoutMs,
|
||||||
|
// TCP keepalive to prevent silent connection drops by NAT gateways,
|
||||||
|
// load balancers, and other intermediate network devices (e.g. AWS
|
||||||
|
// NAT Gateway drops idle TCP connections after ~350s)
|
||||||
|
keepAlive: true,
|
||||||
|
keepAliveInitialDelayMillis: 10000, // send first keepalive after 10s of idle
|
||||||
|
// Allow connections to be released and recreated more aggressively
|
||||||
|
// to avoid stale connections building up
|
||||||
|
allowExitOnIdle: false
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function attachPoolErrorHandlers(pool: Pool, label: string): void {
|
||||||
|
pool.on("error", (err) => {
|
||||||
|
// This catches errors on idle clients in the pool. Without this
|
||||||
|
// handler an unexpected disconnect would crash the process.
|
||||||
|
logger.error(
|
||||||
|
`Unexpected error on idle ${label} database client: ${err.message}`
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
pool.on("connect", (client) => {
|
||||||
|
// Set a statement timeout on every new connection so a single slow
|
||||||
|
// query can't block the pool forever
|
||||||
|
client.query("SET statement_timeout = '30s'").catch((err: Error) => {
|
||||||
|
logger.warn(
|
||||||
|
`Failed to set statement_timeout on ${label} client: ${err.message}`
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createPool(
|
||||||
|
connectionString: string,
|
||||||
|
maxConnections: number,
|
||||||
|
idleTimeoutMs: number,
|
||||||
|
connectionTimeoutMs: number,
|
||||||
|
label: string
|
||||||
|
): Pool {
|
||||||
|
const pool = new Pool(
|
||||||
|
createPoolConfig(
|
||||||
|
connectionString,
|
||||||
|
maxConnections,
|
||||||
|
idleTimeoutMs,
|
||||||
|
connectionTimeoutMs
|
||||||
|
)
|
||||||
|
);
|
||||||
|
attachPoolErrorHandlers(pool, label);
|
||||||
|
return pool;
|
||||||
|
}
|
||||||
24
server/db/pg/safeRead.ts
Normal file
24
server/db/pg/safeRead.ts
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
import { db, primaryDb } from "./driver";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Runs a read query with replica fallback for Postgres.
|
||||||
|
* Executes the query against the replica first (when replicas exist).
|
||||||
|
* If the query throws or returns no data (null, undefined, or empty array),
|
||||||
|
* runs the same query against the primary.
|
||||||
|
*/
|
||||||
|
export async function safeRead<T>(
|
||||||
|
query: (d: typeof db | typeof primaryDb) => Promise<T>
|
||||||
|
): Promise<T> {
|
||||||
|
try {
|
||||||
|
const result = await query(db);
|
||||||
|
if (result === undefined || result === null) {
|
||||||
|
return query(primaryDb);
|
||||||
|
}
|
||||||
|
if (Array.isArray(result) && result.length === 0) {
|
||||||
|
return query(primaryDb);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
} catch {
|
||||||
|
return query(primaryDb);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -7,10 +7,22 @@ import {
|
|||||||
bigint,
|
bigint,
|
||||||
real,
|
real,
|
||||||
text,
|
text,
|
||||||
index
|
index,
|
||||||
|
primaryKey,
|
||||||
|
uniqueIndex
|
||||||
} 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,
|
||||||
|
clients,
|
||||||
|
siteResources,
|
||||||
|
sites
|
||||||
|
} from "./schema";
|
||||||
|
|
||||||
export const certificates = pgTable("certificates", {
|
export const certificates = pgTable("certificates", {
|
||||||
certId: serial("certId").primaryKey(),
|
certId: serial("certId").primaryKey(),
|
||||||
@@ -74,11 +86,16 @@ export const subscriptions = pgTable("subscriptions", {
|
|||||||
canceledAt: bigint("canceledAt", { mode: "number" }),
|
canceledAt: bigint("canceledAt", { mode: "number" }),
|
||||||
createdAt: bigint("createdAt", { mode: "number" }).notNull(),
|
createdAt: bigint("createdAt", { mode: "number" }).notNull(),
|
||||||
updatedAt: bigint("updatedAt", { mode: "number" }),
|
updatedAt: bigint("updatedAt", { mode: "number" }),
|
||||||
billingCycleAnchor: bigint("billingCycleAnchor", { mode: "number" })
|
version: integer("version"),
|
||||||
|
billingCycleAnchor: bigint("billingCycleAnchor", { mode: "number" }),
|
||||||
|
type: varchar("type", { length: 50 }) // tier1, tier2, tier3, or license
|
||||||
});
|
});
|
||||||
|
|
||||||
export const subscriptionItems = pgTable("subscriptionItems", {
|
export const subscriptionItems = pgTable("subscriptionItems", {
|
||||||
subscriptionItemId: serial("subscriptionItemId").primaryKey(),
|
subscriptionItemId: serial("subscriptionItemId").primaryKey(),
|
||||||
|
stripeSubscriptionItemId: varchar("stripeSubscriptionItemId", {
|
||||||
|
length: 255
|
||||||
|
}),
|
||||||
subscriptionId: varchar("subscriptionId", { length: 255 })
|
subscriptionId: varchar("subscriptionId", { length: 255 })
|
||||||
.notNull()
|
.notNull()
|
||||||
.references(() => subscriptions.subscriptionId, {
|
.references(() => subscriptions.subscriptionId, {
|
||||||
@@ -86,6 +103,7 @@ export const subscriptionItems = pgTable("subscriptionItems", {
|
|||||||
}),
|
}),
|
||||||
planId: varchar("planId", { length: 255 }).notNull(),
|
planId: varchar("planId", { length: 255 }).notNull(),
|
||||||
priceId: varchar("priceId", { length: 255 }),
|
priceId: varchar("priceId", { length: 255 }),
|
||||||
|
featureId: varchar("featureId", { length: 255 }),
|
||||||
meterId: varchar("meterId", { length: 255 }),
|
meterId: varchar("meterId", { length: 255 }),
|
||||||
unitAmount: real("unitAmount"),
|
unitAmount: real("unitAmount"),
|
||||||
tiers: text("tiers"),
|
tiers: text("tiers"),
|
||||||
@@ -128,6 +146,7 @@ export const limits = pgTable("limits", {
|
|||||||
})
|
})
|
||||||
.notNull(),
|
.notNull(),
|
||||||
value: real("value"),
|
value: real("value"),
|
||||||
|
override: boolean("override").default(false),
|
||||||
description: text("description")
|
description: text("description")
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -206,7 +225,7 @@ export const loginPageOrg = pgTable("loginPageOrg", {
|
|||||||
|
|
||||||
export const loginPageBranding = pgTable("loginPageBranding", {
|
export const loginPageBranding = pgTable("loginPageBranding", {
|
||||||
loginPageBrandingId: serial("loginPageBrandingId").primaryKey(),
|
loginPageBrandingId: serial("loginPageBrandingId").primaryKey(),
|
||||||
logoUrl: text("logoUrl").notNull(),
|
logoUrl: text("logoUrl"),
|
||||||
logoWidth: integer("logoWidth").notNull(),
|
logoWidth: integer("logoWidth").notNull(),
|
||||||
logoHeight: integer("logoHeight").notNull(),
|
logoHeight: integer("logoHeight").notNull(),
|
||||||
primaryColor: text("primaryColor"),
|
primaryColor: text("primaryColor"),
|
||||||
@@ -273,6 +292,7 @@ export const accessAuditLog = pgTable(
|
|||||||
actor: varchar("actor", { length: 255 }),
|
actor: varchar("actor", { length: 255 }),
|
||||||
actorId: varchar("actorId", { length: 255 }),
|
actorId: varchar("actorId", { length: 255 }),
|
||||||
resourceId: integer("resourceId"),
|
resourceId: integer("resourceId"),
|
||||||
|
siteResourceId: integer("siteResourceId"),
|
||||||
ip: varchar("ip", { length: 45 }),
|
ip: varchar("ip", { length: 45 }),
|
||||||
type: varchar("type", { length: 100 }).notNull(),
|
type: varchar("type", { length: 100 }).notNull(),
|
||||||
action: boolean("action").notNull(),
|
action: boolean("action").notNull(),
|
||||||
@@ -289,6 +309,156 @@ export const accessAuditLog = pgTable(
|
|||||||
]
|
]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
export const connectionAuditLog = pgTable(
|
||||||
|
"connectionAuditLog",
|
||||||
|
{
|
||||||
|
id: serial("id").primaryKey(),
|
||||||
|
sessionId: text("sessionId").notNull(),
|
||||||
|
siteResourceId: integer("siteResourceId").references(
|
||||||
|
() => siteResources.siteResourceId,
|
||||||
|
{ onDelete: "cascade" }
|
||||||
|
),
|
||||||
|
orgId: text("orgId").references(() => orgs.orgId, {
|
||||||
|
onDelete: "cascade"
|
||||||
|
}),
|
||||||
|
siteId: integer("siteId").references(() => sites.siteId, {
|
||||||
|
onDelete: "cascade"
|
||||||
|
}),
|
||||||
|
clientId: integer("clientId").references(() => clients.clientId, {
|
||||||
|
onDelete: "cascade"
|
||||||
|
}),
|
||||||
|
userId: text("userId").references(() => users.userId, {
|
||||||
|
onDelete: "cascade"
|
||||||
|
}),
|
||||||
|
sourceAddr: text("sourceAddr").notNull(),
|
||||||
|
destAddr: text("destAddr").notNull(),
|
||||||
|
protocol: text("protocol").notNull(),
|
||||||
|
startedAt: integer("startedAt").notNull(),
|
||||||
|
endedAt: integer("endedAt"),
|
||||||
|
bytesTx: integer("bytesTx"),
|
||||||
|
bytesRx: integer("bytesRx")
|
||||||
|
},
|
||||||
|
(table) => [
|
||||||
|
index("idx_accessAuditLog_startedAt").on(table.startedAt),
|
||||||
|
index("idx_accessAuditLog_org_startedAt").on(
|
||||||
|
table.orgId,
|
||||||
|
table.startedAt
|
||||||
|
),
|
||||||
|
index("idx_accessAuditLog_siteResourceId").on(table.siteResourceId)
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
export const approvals = pgTable("approvals", {
|
||||||
|
approvalId: serial("approvalId").primaryKey(),
|
||||||
|
timestamp: integer("timestamp").notNull(), // this is EPOCH time in seconds
|
||||||
|
orgId: varchar("orgId")
|
||||||
|
.references(() => orgs.orgId, {
|
||||||
|
onDelete: "cascade"
|
||||||
|
})
|
||||||
|
.notNull(),
|
||||||
|
clientId: integer("clientId").references(() => clients.clientId, {
|
||||||
|
onDelete: "cascade"
|
||||||
|
}), // clients reference user devices (in this case)
|
||||||
|
userId: varchar("userId")
|
||||||
|
.references(() => users.userId, {
|
||||||
|
// optionally tied to a user and in this case delete when the user deletes
|
||||||
|
onDelete: "cascade"
|
||||||
|
})
|
||||||
|
.notNull(),
|
||||||
|
decision: varchar("decision")
|
||||||
|
.$type<"approved" | "denied" | "pending">()
|
||||||
|
.default("pending")
|
||||||
|
.notNull(),
|
||||||
|
type: varchar("type")
|
||||||
|
.$type<"user_device" /*| 'proxy' // for later */>()
|
||||||
|
.notNull()
|
||||||
|
});
|
||||||
|
|
||||||
|
export const bannedEmails = pgTable("bannedEmails", {
|
||||||
|
email: varchar("email", { length: 255 }).primaryKey()
|
||||||
|
});
|
||||||
|
|
||||||
|
export const bannedIps = pgTable("bannedIps", {
|
||||||
|
ip: varchar("ip", { length: 255 }).primaryKey()
|
||||||
|
});
|
||||||
|
|
||||||
|
export const siteProvisioningKeys = pgTable("siteProvisioningKeys", {
|
||||||
|
siteProvisioningKeyId: varchar("siteProvisioningKeyId", {
|
||||||
|
length: 255
|
||||||
|
}).primaryKey(),
|
||||||
|
name: varchar("name", { length: 255 }).notNull(),
|
||||||
|
siteProvisioningKeyHash: text("siteProvisioningKeyHash").notNull(),
|
||||||
|
lastChars: varchar("lastChars", { length: 4 }).notNull(),
|
||||||
|
createdAt: varchar("dateCreated", { length: 255 }).notNull(),
|
||||||
|
lastUsed: varchar("lastUsed", { length: 255 }),
|
||||||
|
maxBatchSize: integer("maxBatchSize"), // null = no limit
|
||||||
|
numUsed: integer("numUsed").notNull().default(0),
|
||||||
|
validUntil: varchar("validUntil", { length: 255 }),
|
||||||
|
approveNewSites: boolean("approveNewSites").notNull().default(true)
|
||||||
|
});
|
||||||
|
|
||||||
|
export const siteProvisioningKeyOrg = pgTable(
|
||||||
|
"siteProvisioningKeyOrg",
|
||||||
|
{
|
||||||
|
siteProvisioningKeyId: varchar("siteProvisioningKeyId", {
|
||||||
|
length: 255
|
||||||
|
})
|
||||||
|
.notNull()
|
||||||
|
.references(() => siteProvisioningKeys.siteProvisioningKeyId, {
|
||||||
|
onDelete: "cascade"
|
||||||
|
}),
|
||||||
|
orgId: varchar("orgId", { length: 255 })
|
||||||
|
.notNull()
|
||||||
|
.references(() => orgs.orgId, { onDelete: "cascade" })
|
||||||
|
},
|
||||||
|
(table) => [
|
||||||
|
primaryKey({
|
||||||
|
columns: [table.siteProvisioningKeyId, table.orgId]
|
||||||
|
})
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
export const eventStreamingDestinations = pgTable(
|
||||||
|
"eventStreamingDestinations",
|
||||||
|
{
|
||||||
|
destinationId: serial("destinationId").primaryKey(),
|
||||||
|
orgId: varchar("orgId", { length: 255 })
|
||||||
|
.notNull()
|
||||||
|
.references(() => orgs.orgId, { onDelete: "cascade" }),
|
||||||
|
sendConnectionLogs: boolean("sendConnectionLogs").notNull().default(false),
|
||||||
|
sendRequestLogs: boolean("sendRequestLogs").notNull().default(false),
|
||||||
|
sendActionLogs: boolean("sendActionLogs").notNull().default(false),
|
||||||
|
sendAccessLogs: boolean("sendAccessLogs").notNull().default(false),
|
||||||
|
type: varchar("type", { length: 50 }).notNull(), // e.g. "http", "kafka", etc.
|
||||||
|
config: text("config").notNull(), // JSON string with the configuration for the destination
|
||||||
|
enabled: boolean("enabled").notNull().default(true),
|
||||||
|
createdAt: bigint("createdAt", { mode: "number" }).notNull(),
|
||||||
|
updatedAt: bigint("updatedAt", { mode: "number" }).notNull()
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
export const eventStreamingCursors = pgTable(
|
||||||
|
"eventStreamingCursors",
|
||||||
|
{
|
||||||
|
cursorId: serial("cursorId").primaryKey(),
|
||||||
|
destinationId: integer("destinationId")
|
||||||
|
.notNull()
|
||||||
|
.references(() => eventStreamingDestinations.destinationId, {
|
||||||
|
onDelete: "cascade"
|
||||||
|
}),
|
||||||
|
logType: varchar("logType", { length: 50 }).notNull(), // "request" | "action" | "access" | "connection"
|
||||||
|
lastSentId: bigint("lastSentId", { mode: "number" }).notNull().default(0),
|
||||||
|
lastSentAt: bigint("lastSentAt", { mode: "number" }) // epoch milliseconds, null if never sent
|
||||||
|
},
|
||||||
|
(table) => [
|
||||||
|
uniqueIndex("idx_eventStreamingCursors_dest_type").on(
|
||||||
|
table.destinationId,
|
||||||
|
table.logType
|
||||||
|
)
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
export type Approval = InferSelectModel<typeof approvals>;
|
||||||
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>;
|
||||||
@@ -309,3 +479,19 @@ export type LoginPage = InferSelectModel<typeof loginPage>;
|
|||||||
export type LoginPageBranding = InferSelectModel<typeof loginPageBranding>;
|
export type LoginPageBranding = InferSelectModel<typeof loginPageBranding>;
|
||||||
export type ActionAuditLog = InferSelectModel<typeof actionAuditLog>;
|
export type ActionAuditLog = InferSelectModel<typeof actionAuditLog>;
|
||||||
export type AccessAuditLog = InferSelectModel<typeof accessAuditLog>;
|
export type AccessAuditLog = InferSelectModel<typeof accessAuditLog>;
|
||||||
|
export type ConnectionAuditLog = InferSelectModel<typeof connectionAuditLog>;
|
||||||
|
export type SessionTransferToken = InferSelectModel<
|
||||||
|
typeof sessionTransferToken
|
||||||
|
>;
|
||||||
|
export type BannedEmail = InferSelectModel<typeof bannedEmails>;
|
||||||
|
export type BannedIp = InferSelectModel<typeof bannedIps>;
|
||||||
|
export type SiteProvisioningKey = InferSelectModel<typeof siteProvisioningKeys>;
|
||||||
|
export type SiteProvisioningKeyOrg = InferSelectModel<
|
||||||
|
typeof siteProvisioningKeyOrg
|
||||||
|
>;
|
||||||
|
export type EventStreamingDestination = InferSelectModel<
|
||||||
|
typeof eventStreamingDestinations
|
||||||
|
>;
|
||||||
|
export type EventStreamingCursor = InferSelectModel<
|
||||||
|
typeof eventStreamingCursors
|
||||||
|
>;
|
||||||
|
|||||||
@@ -1,18 +1,18 @@
|
|||||||
import {
|
|
||||||
pgTable,
|
|
||||||
serial,
|
|
||||||
varchar,
|
|
||||||
boolean,
|
|
||||||
integer,
|
|
||||||
bigint,
|
|
||||||
real,
|
|
||||||
text,
|
|
||||||
index,
|
|
||||||
uniqueIndex
|
|
||||||
} from "drizzle-orm/pg-core";
|
|
||||||
import { InferSelectModel } from "drizzle-orm";
|
|
||||||
import { randomUUID } from "crypto";
|
import { randomUUID } from "crypto";
|
||||||
import { alias } from "yargs";
|
import { InferSelectModel } from "drizzle-orm";
|
||||||
|
import {
|
||||||
|
bigint,
|
||||||
|
boolean,
|
||||||
|
index,
|
||||||
|
integer,
|
||||||
|
pgTable,
|
||||||
|
primaryKey,
|
||||||
|
real,
|
||||||
|
serial,
|
||||||
|
text,
|
||||||
|
unique,
|
||||||
|
varchar
|
||||||
|
} from "drizzle-orm/pg-core";
|
||||||
|
|
||||||
export const domains = pgTable("domains", {
|
export const domains = pgTable("domains", {
|
||||||
domainId: varchar("domainId").primaryKey(),
|
domainId: varchar("domainId").primaryKey(),
|
||||||
@@ -24,7 +24,8 @@ export const domains = pgTable("domains", {
|
|||||||
tries: integer("tries").notNull().default(0),
|
tries: integer("tries").notNull().default(0),
|
||||||
certResolver: varchar("certResolver"),
|
certResolver: varchar("certResolver"),
|
||||||
customCertResolver: varchar("customCertResolver"),
|
customCertResolver: varchar("customCertResolver"),
|
||||||
preferWildcardCert: boolean("preferWildcardCert")
|
preferWildcardCert: boolean("preferWildcardCert"),
|
||||||
|
errorMessage: text("errorMessage")
|
||||||
});
|
});
|
||||||
|
|
||||||
export const dnsRecords = pgTable("dnsRecords", {
|
export const dnsRecords = pgTable("dnsRecords", {
|
||||||
@@ -55,7 +56,14 @@ export const orgs = pgTable("orgs", {
|
|||||||
.default(0),
|
.default(0),
|
||||||
settingsLogRetentionDaysAction: integer("settingsLogRetentionDaysAction") // where 0 = dont keep logs and -1 = keep forever and 9001 = end of the following year
|
settingsLogRetentionDaysAction: integer("settingsLogRetentionDaysAction") // where 0 = dont keep logs and -1 = keep forever and 9001 = end of the following year
|
||||||
.notNull()
|
.notNull()
|
||||||
.default(0)
|
.default(0),
|
||||||
|
settingsLogRetentionDaysConnection: integer("settingsLogRetentionDaysConnection") // where 0 = dont keep logs and -1 = keep forever and 9001 = end of the following year
|
||||||
|
.notNull()
|
||||||
|
.default(0),
|
||||||
|
sshCaPrivateKey: text("sshCaPrivateKey"), // Encrypted SSH CA private key (PEM format)
|
||||||
|
sshCaPublicKey: text("sshCaPublicKey"), // SSH CA public key (OpenSSH format)
|
||||||
|
isBillingOrg: boolean("isBillingOrg"),
|
||||||
|
billingOrgId: varchar("billingOrgId")
|
||||||
});
|
});
|
||||||
|
|
||||||
export const orgDomains = pgTable("orgDomains", {
|
export const orgDomains = pgTable("orgDomains", {
|
||||||
@@ -86,12 +94,14 @@ export const sites = pgTable("sites", {
|
|||||||
lastBandwidthUpdate: varchar("lastBandwidthUpdate"),
|
lastBandwidthUpdate: varchar("lastBandwidthUpdate"),
|
||||||
type: varchar("type").notNull(), // "newt" or "wireguard"
|
type: varchar("type").notNull(), // "newt" or "wireguard"
|
||||||
online: boolean("online").notNull().default(false),
|
online: boolean("online").notNull().default(false),
|
||||||
|
lastPing: integer("lastPing"),
|
||||||
address: varchar("address"),
|
address: varchar("address"),
|
||||||
endpoint: varchar("endpoint"),
|
endpoint: varchar("endpoint"),
|
||||||
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),
|
||||||
|
status: varchar("status").$type<"pending" | "approved">().default("approved")
|
||||||
});
|
});
|
||||||
|
|
||||||
export const resources = pgTable("resources", {
|
export const resources = pgTable("resources", {
|
||||||
@@ -132,7 +142,18 @@ export const resources = pgTable("resources", {
|
|||||||
}),
|
}),
|
||||||
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),
|
proxyProtocol: boolean("proxyProtocol").notNull().default(false),
|
||||||
proxyProtocolVersion: integer("proxyProtocolVersion").default(1)
|
proxyProtocolVersion: integer("proxyProtocolVersion").default(1),
|
||||||
|
|
||||||
|
maintenanceModeEnabled: boolean("maintenanceModeEnabled")
|
||||||
|
.notNull()
|
||||||
|
.default(false),
|
||||||
|
maintenanceModeType: text("maintenanceModeType", {
|
||||||
|
enum: ["forced", "automatic"]
|
||||||
|
}).default("forced"), // "forced" = always show, "automatic" = only when down
|
||||||
|
maintenanceTitle: text("maintenanceTitle"),
|
||||||
|
maintenanceMessage: text("maintenanceMessage"),
|
||||||
|
maintenanceEstimatedTime: text("maintenanceEstimatedTime"),
|
||||||
|
postAuthPath: text("postAuthPath")
|
||||||
});
|
});
|
||||||
|
|
||||||
export const targets = pgTable("targets", {
|
export const targets = pgTable("targets", {
|
||||||
@@ -177,7 +198,9 @@ 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")
|
||||||
|
.$type<"unknown" | "healthy" | "unhealthy">()
|
||||||
|
.default("unknown"), // "unknown", "healthy", "unhealthy"
|
||||||
hcTlsServerName: text("hcTlsServerName")
|
hcTlsServerName: text("hcTlsServerName")
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -207,7 +230,7 @@ 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(),
|
||||||
mode: varchar("mode").notNull(), // "host" | "cidr" | "port"
|
mode: varchar("mode").$type<"host" | "cidr">().notNull(), // "host" | "cidr" | "port"
|
||||||
protocol: varchar("protocol"), // only for port mode
|
protocol: varchar("protocol"), // only for port mode
|
||||||
proxyPort: integer("proxyPort"), // only for port mode
|
proxyPort: integer("proxyPort"), // only for port mode
|
||||||
destinationPort: integer("destinationPort"), // only for port mode
|
destinationPort: integer("destinationPort"), // only for port mode
|
||||||
@@ -215,9 +238,13 @@ export const siteResources = pgTable("siteResources", {
|
|||||||
enabled: boolean("enabled").notNull().default(true),
|
enabled: boolean("enabled").notNull().default(true),
|
||||||
alias: varchar("alias"),
|
alias: varchar("alias"),
|
||||||
aliasAddress: varchar("aliasAddress"),
|
aliasAddress: varchar("aliasAddress"),
|
||||||
tcpPortRangeString: varchar("tcpPortRangeString"),
|
tcpPortRangeString: varchar("tcpPortRangeString").notNull().default("*"),
|
||||||
udpPortRangeString: varchar("udpPortRangeString"),
|
udpPortRangeString: varchar("udpPortRangeString").notNull().default("*"),
|
||||||
disableIcmp: boolean("disableIcmp").notNull().default(false)
|
disableIcmp: boolean("disableIcmp").notNull().default(false),
|
||||||
|
authDaemonPort: integer("authDaemonPort").default(22123),
|
||||||
|
authDaemonMode: varchar("authDaemonMode", { length: 32 })
|
||||||
|
.$type<"site" | "remote">()
|
||||||
|
.default("site")
|
||||||
});
|
});
|
||||||
|
|
||||||
export const clientSiteResources = pgTable("clientSiteResources", {
|
export const clientSiteResources = pgTable("clientSiteResources", {
|
||||||
@@ -264,8 +291,10 @@ 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"),
|
||||||
|
marketingEmailConsent: boolean("marketingEmailConsent").default(false),
|
||||||
serverAdmin: boolean("serverAdmin").notNull().default(false),
|
serverAdmin: boolean("serverAdmin").notNull().default(false),
|
||||||
lastPasswordChange: bigint("lastPasswordChange", { mode: "number" })
|
lastPasswordChange: bigint("lastPasswordChange", { mode: "number" }),
|
||||||
|
locale: varchar("locale")
|
||||||
});
|
});
|
||||||
|
|
||||||
export const newts = pgTable("newt", {
|
export const newts = pgTable("newt", {
|
||||||
@@ -313,11 +342,9 @@ export const userOrgs = pgTable("userOrgs", {
|
|||||||
onDelete: "cascade"
|
onDelete: "cascade"
|
||||||
})
|
})
|
||||||
.notNull(),
|
.notNull(),
|
||||||
roleId: integer("roleId")
|
|
||||||
.notNull()
|
|
||||||
.references(() => roles.roleId),
|
|
||||||
isOwner: boolean("isOwner").notNull().default(false),
|
isOwner: boolean("isOwner").notNull().default(false),
|
||||||
autoProvisioned: boolean("autoProvisioned").default(false)
|
autoProvisioned: boolean("autoProvisioned").default(false),
|
||||||
|
pamUsername: varchar("pamUsername") // cleaned username for ssh and such
|
||||||
});
|
});
|
||||||
|
|
||||||
export const emailVerificationCodes = pgTable("emailVerificationCodes", {
|
export const emailVerificationCodes = pgTable("emailVerificationCodes", {
|
||||||
@@ -355,9 +382,30 @@ export const roles = pgTable("roles", {
|
|||||||
.notNull(),
|
.notNull(),
|
||||||
isAdmin: boolean("isAdmin"),
|
isAdmin: boolean("isAdmin"),
|
||||||
name: varchar("name").notNull(),
|
name: varchar("name").notNull(),
|
||||||
description: varchar("description")
|
description: varchar("description"),
|
||||||
|
requireDeviceApproval: boolean("requireDeviceApproval").default(false),
|
||||||
|
sshSudoMode: varchar("sshSudoMode", { length: 32 }).default("none"), // "none" | "full" | "commands"
|
||||||
|
sshSudoCommands: text("sshSudoCommands").default("[]"),
|
||||||
|
sshCreateHomeDir: boolean("sshCreateHomeDir").default(true),
|
||||||
|
sshUnixGroups: text("sshUnixGroups").default("[]")
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const userOrgRoles = pgTable(
|
||||||
|
"userOrgRoles",
|
||||||
|
{
|
||||||
|
userId: varchar("userId")
|
||||||
|
.notNull()
|
||||||
|
.references(() => users.userId, { onDelete: "cascade" }),
|
||||||
|
orgId: varchar("orgId")
|
||||||
|
.notNull()
|
||||||
|
.references(() => orgs.orgId, { onDelete: "cascade" }),
|
||||||
|
roleId: integer("roleId")
|
||||||
|
.notNull()
|
||||||
|
.references(() => roles.roleId, { onDelete: "cascade" })
|
||||||
|
},
|
||||||
|
(t) => [unique().on(t.userId, t.orgId, t.roleId)]
|
||||||
|
);
|
||||||
|
|
||||||
export const roleActions = pgTable("roleActions", {
|
export const roleActions = pgTable("roleActions", {
|
||||||
roleId: integer("roleId")
|
roleId: integer("roleId")
|
||||||
.notNull()
|
.notNull()
|
||||||
@@ -425,11 +473,21 @@ export const userInvites = pgTable("userInvites", {
|
|||||||
.references(() => orgs.orgId, { onDelete: "cascade" }),
|
.references(() => orgs.orgId, { onDelete: "cascade" }),
|
||||||
email: varchar("email").notNull(),
|
email: varchar("email").notNull(),
|
||||||
expiresAt: bigint("expiresAt", { mode: "number" }).notNull(),
|
expiresAt: bigint("expiresAt", { mode: "number" }).notNull(),
|
||||||
tokenHash: varchar("token").notNull(),
|
tokenHash: varchar("token").notNull()
|
||||||
|
});
|
||||||
|
|
||||||
|
export const userInviteRoles = pgTable(
|
||||||
|
"userInviteRoles",
|
||||||
|
{
|
||||||
|
inviteId: varchar("inviteId")
|
||||||
|
.notNull()
|
||||||
|
.references(() => userInvites.inviteId, { onDelete: "cascade" }),
|
||||||
roleId: integer("roleId")
|
roleId: integer("roleId")
|
||||||
.notNull()
|
.notNull()
|
||||||
.references(() => roles.roleId, { onDelete: "cascade" })
|
.references(() => roles.roleId, { onDelete: "cascade" })
|
||||||
});
|
},
|
||||||
|
(t) => [primaryKey({ columns: [t.inviteId, t.roleId] })]
|
||||||
|
);
|
||||||
|
|
||||||
export const resourcePincode = pgTable("resourcePincode", {
|
export const resourcePincode = pgTable("resourcePincode", {
|
||||||
pincodeId: serial("pincodeId").primaryKey(),
|
pincodeId: serial("pincodeId").primaryKey(),
|
||||||
@@ -456,6 +514,23 @@ export const resourceHeaderAuth = pgTable("resourceHeaderAuth", {
|
|||||||
headerAuthHash: varchar("headerAuthHash").notNull()
|
headerAuthHash: varchar("headerAuthHash").notNull()
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const resourceHeaderAuthExtendedCompatibility = pgTable(
|
||||||
|
"resourceHeaderAuthExtendedCompatibility",
|
||||||
|
{
|
||||||
|
headerAuthExtendedCompatibilityId: serial(
|
||||||
|
"headerAuthExtendedCompatibilityId"
|
||||||
|
).primaryKey(),
|
||||||
|
resourceId: integer("resourceId")
|
||||||
|
.notNull()
|
||||||
|
.references(() => resources.resourceId, { onDelete: "cascade" }),
|
||||||
|
extendedCompatibilityIsActivated: boolean(
|
||||||
|
"extendedCompatibilityIsActivated"
|
||||||
|
)
|
||||||
|
.notNull()
|
||||||
|
.default(true)
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
export const resourceAccessToken = pgTable("resourceAccessToken", {
|
export const resourceAccessToken = pgTable("resourceAccessToken", {
|
||||||
accessTokenId: varchar("accessTokenId").primaryKey(),
|
accessTokenId: varchar("accessTokenId").primaryKey(),
|
||||||
orgId: varchar("orgId")
|
orgId: varchar("orgId")
|
||||||
@@ -564,7 +639,8 @@ export const idp = pgTable("idp", {
|
|||||||
type: varchar("type").notNull(),
|
type: varchar("type").notNull(),
|
||||||
defaultRoleMapping: varchar("defaultRoleMapping"),
|
defaultRoleMapping: varchar("defaultRoleMapping"),
|
||||||
defaultOrgMapping: varchar("defaultOrgMapping"),
|
defaultOrgMapping: varchar("defaultOrgMapping"),
|
||||||
autoProvision: boolean("autoProvision").notNull().default(false)
|
autoProvision: boolean("autoProvision").notNull().default(false),
|
||||||
|
tags: text("tags")
|
||||||
});
|
});
|
||||||
|
|
||||||
export const idpOidcConfig = pgTable("idpOidcConfig", {
|
export const idpOidcConfig = pgTable("idpOidcConfig", {
|
||||||
@@ -661,7 +737,12 @@ export const clients = pgTable("clients", {
|
|||||||
online: boolean("online").notNull().default(false),
|
online: boolean("online").notNull().default(false),
|
||||||
// endpoint: varchar("endpoint"),
|
// endpoint: varchar("endpoint"),
|
||||||
lastHolePunch: integer("lastHolePunch"),
|
lastHolePunch: integer("lastHolePunch"),
|
||||||
maxConnections: integer("maxConnections")
|
maxConnections: integer("maxConnections"),
|
||||||
|
archived: boolean("archived").notNull().default(false),
|
||||||
|
blocked: boolean("blocked").notNull().default(false),
|
||||||
|
approvalState: varchar("approvalState").$type<
|
||||||
|
"pending" | "approved" | "denied"
|
||||||
|
>()
|
||||||
});
|
});
|
||||||
|
|
||||||
export const clientSitesAssociationsCache = pgTable(
|
export const clientSitesAssociationsCache = pgTable(
|
||||||
@@ -671,6 +752,7 @@ export const clientSitesAssociationsCache = pgTable(
|
|||||||
.notNull(),
|
.notNull(),
|
||||||
siteId: integer("siteId").notNull(),
|
siteId: integer("siteId").notNull(),
|
||||||
isRelayed: boolean("isRelayed").notNull().default(false),
|
isRelayed: boolean("isRelayed").notNull().default(false),
|
||||||
|
isJitMode: boolean("isJitMode").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
|
publicKey: varchar("publicKey") // this will act as the session's public key for hole punching so we can track when it changes
|
||||||
}
|
}
|
||||||
@@ -685,6 +767,16 @@ export const clientSiteResourcesAssociationsCache = pgTable(
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
export const clientPostureSnapshots = pgTable("clientPostureSnapshots", {
|
||||||
|
snapshotId: serial("snapshotId").primaryKey(),
|
||||||
|
|
||||||
|
clientId: integer("clientId").references(() => clients.clientId, {
|
||||||
|
onDelete: "cascade"
|
||||||
|
}),
|
||||||
|
|
||||||
|
collectedAt: integer("collectedAt").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(),
|
||||||
@@ -699,7 +791,118 @@ export const olms = pgTable("olms", {
|
|||||||
userId: text("userId").references(() => users.userId, {
|
userId: text("userId").references(() => users.userId, {
|
||||||
// optionally tied to a user and in this case delete when the user deletes
|
// optionally tied to a user and in this case delete when the user deletes
|
||||||
onDelete: "cascade"
|
onDelete: "cascade"
|
||||||
})
|
}),
|
||||||
|
archived: boolean("archived").notNull().default(false)
|
||||||
|
});
|
||||||
|
|
||||||
|
export const currentFingerprint = pgTable("currentFingerprint", {
|
||||||
|
fingerprintId: serial("id").primaryKey(),
|
||||||
|
|
||||||
|
olmId: text("olmId")
|
||||||
|
.references(() => olms.olmId, { onDelete: "cascade" })
|
||||||
|
.notNull(),
|
||||||
|
|
||||||
|
firstSeen: integer("firstSeen").notNull(),
|
||||||
|
lastSeen: integer("lastSeen").notNull(),
|
||||||
|
lastCollectedAt: integer("lastCollectedAt").notNull(),
|
||||||
|
|
||||||
|
username: text("username"),
|
||||||
|
hostname: text("hostname"),
|
||||||
|
platform: text("platform"),
|
||||||
|
osVersion: text("osVersion"),
|
||||||
|
kernelVersion: text("kernelVersion"),
|
||||||
|
arch: text("arch"),
|
||||||
|
deviceModel: text("deviceModel"),
|
||||||
|
serialNumber: text("serialNumber"),
|
||||||
|
platformFingerprint: varchar("platformFingerprint"),
|
||||||
|
|
||||||
|
// Platform-agnostic checks
|
||||||
|
|
||||||
|
biometricsEnabled: boolean("biometricsEnabled").notNull().default(false),
|
||||||
|
diskEncrypted: boolean("diskEncrypted").notNull().default(false),
|
||||||
|
firewallEnabled: boolean("firewallEnabled").notNull().default(false),
|
||||||
|
autoUpdatesEnabled: boolean("autoUpdatesEnabled").notNull().default(false),
|
||||||
|
tpmAvailable: boolean("tpmAvailable").notNull().default(false),
|
||||||
|
|
||||||
|
// Windows-specific posture check information
|
||||||
|
|
||||||
|
windowsAntivirusEnabled: boolean("windowsAntivirusEnabled")
|
||||||
|
.notNull()
|
||||||
|
.default(false),
|
||||||
|
|
||||||
|
// macOS-specific posture check information
|
||||||
|
|
||||||
|
macosSipEnabled: boolean("macosSipEnabled").notNull().default(false),
|
||||||
|
macosGatekeeperEnabled: boolean("macosGatekeeperEnabled")
|
||||||
|
.notNull()
|
||||||
|
.default(false),
|
||||||
|
macosFirewallStealthMode: boolean("macosFirewallStealthMode")
|
||||||
|
.notNull()
|
||||||
|
.default(false),
|
||||||
|
|
||||||
|
// Linux-specific posture check information
|
||||||
|
|
||||||
|
linuxAppArmorEnabled: boolean("linuxAppArmorEnabled")
|
||||||
|
.notNull()
|
||||||
|
.default(false),
|
||||||
|
linuxSELinuxEnabled: boolean("linuxSELinuxEnabled").notNull().default(false)
|
||||||
|
});
|
||||||
|
|
||||||
|
export const fingerprintSnapshots = pgTable("fingerprintSnapshots", {
|
||||||
|
snapshotId: serial("id").primaryKey(),
|
||||||
|
|
||||||
|
fingerprintId: integer("fingerprintId").references(
|
||||||
|
() => currentFingerprint.fingerprintId,
|
||||||
|
{
|
||||||
|
onDelete: "set null"
|
||||||
|
}
|
||||||
|
),
|
||||||
|
|
||||||
|
username: text("username"),
|
||||||
|
hostname: text("hostname"),
|
||||||
|
platform: text("platform"),
|
||||||
|
osVersion: text("osVersion"),
|
||||||
|
kernelVersion: text("kernelVersion"),
|
||||||
|
arch: text("arch"),
|
||||||
|
deviceModel: text("deviceModel"),
|
||||||
|
serialNumber: text("serialNumber"),
|
||||||
|
platformFingerprint: varchar("platformFingerprint"),
|
||||||
|
|
||||||
|
// Platform-agnostic checks
|
||||||
|
|
||||||
|
biometricsEnabled: boolean("biometricsEnabled").notNull().default(false),
|
||||||
|
diskEncrypted: boolean("diskEncrypted").notNull().default(false),
|
||||||
|
firewallEnabled: boolean("firewallEnabled").notNull().default(false),
|
||||||
|
autoUpdatesEnabled: boolean("autoUpdatesEnabled").notNull().default(false),
|
||||||
|
tpmAvailable: boolean("tpmAvailable").notNull().default(false),
|
||||||
|
|
||||||
|
// Windows-specific posture check information
|
||||||
|
|
||||||
|
windowsAntivirusEnabled: boolean("windowsAntivirusEnabled")
|
||||||
|
.notNull()
|
||||||
|
.default(false),
|
||||||
|
|
||||||
|
// macOS-specific posture check information
|
||||||
|
|
||||||
|
macosSipEnabled: boolean("macosSipEnabled").notNull().default(false),
|
||||||
|
macosGatekeeperEnabled: boolean("macosGatekeeperEnabled")
|
||||||
|
.notNull()
|
||||||
|
.default(false),
|
||||||
|
macosFirewallStealthMode: boolean("macosFirewallStealthMode")
|
||||||
|
.notNull()
|
||||||
|
.default(false),
|
||||||
|
|
||||||
|
// Linux-specific posture check information
|
||||||
|
|
||||||
|
linuxAppArmorEnabled: boolean("linuxAppArmorEnabled")
|
||||||
|
.notNull()
|
||||||
|
.default(false),
|
||||||
|
linuxSELinuxEnabled: boolean("linuxSELinuxEnabled")
|
||||||
|
.notNull()
|
||||||
|
.default(false),
|
||||||
|
|
||||||
|
hash: text("hash").notNull(),
|
||||||
|
collectedAt: integer("collectedAt").notNull()
|
||||||
});
|
});
|
||||||
|
|
||||||
export const olmSessions = pgTable("clientSession", {
|
export const olmSessions = pgTable("clientSession", {
|
||||||
@@ -828,6 +1031,16 @@ export const deviceWebAuthCodes = pgTable("deviceWebAuthCodes", {
|
|||||||
})
|
})
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const roundTripMessageTracker = pgTable("roundTripMessageTracker", {
|
||||||
|
messageId: serial("messageId").primaryKey(),
|
||||||
|
wsClientId: varchar("clientId"),
|
||||||
|
messageType: varchar("messageType"),
|
||||||
|
sentAt: bigint("sentAt", { mode: "number" }).notNull(),
|
||||||
|
receivedAt: bigint("receivedAt", { mode: "number" }),
|
||||||
|
error: text("error"),
|
||||||
|
complete: boolean("complete").notNull().default(false)
|
||||||
|
});
|
||||||
|
|
||||||
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>;
|
||||||
@@ -851,11 +1064,16 @@ export type UserSite = InferSelectModel<typeof userSites>;
|
|||||||
export type RoleResource = InferSelectModel<typeof roleResources>;
|
export type RoleResource = InferSelectModel<typeof roleResources>;
|
||||||
export type UserResource = InferSelectModel<typeof userResources>;
|
export type UserResource = InferSelectModel<typeof userResources>;
|
||||||
export type UserInvite = InferSelectModel<typeof userInvites>;
|
export type UserInvite = InferSelectModel<typeof userInvites>;
|
||||||
|
export type UserInviteRole = InferSelectModel<typeof userInviteRoles>;
|
||||||
export type UserOrg = InferSelectModel<typeof userOrgs>;
|
export type UserOrg = InferSelectModel<typeof userOrgs>;
|
||||||
|
export type UserOrgRole = InferSelectModel<typeof userOrgRoles>;
|
||||||
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 ResourceHeaderAuth = InferSelectModel<typeof resourceHeaderAuth>;
|
||||||
|
export type ResourceHeaderAuthExtendedCompatibility = InferSelectModel<
|
||||||
|
typeof resourceHeaderAuthExtendedCompatibility
|
||||||
|
>;
|
||||||
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>;
|
||||||
@@ -885,3 +1103,6 @@ export type SecurityKey = InferSelectModel<typeof securityKeys>;
|
|||||||
export type WebauthnChallenge = InferSelectModel<typeof webauthnChallenge>;
|
export type WebauthnChallenge = InferSelectModel<typeof webauthnChallenge>;
|
||||||
export type DeviceWebAuthCode = InferSelectModel<typeof deviceWebAuthCodes>;
|
export type DeviceWebAuthCode = InferSelectModel<typeof deviceWebAuthCodes>;
|
||||||
export type RequestAuditLog = InferSelectModel<typeof requestAuditLog>;
|
export type RequestAuditLog = InferSelectModel<typeof requestAuditLog>;
|
||||||
|
export type RoundTripMessageTracker = InferSelectModel<
|
||||||
|
typeof roundTripMessageTracker
|
||||||
|
>;
|
||||||
|
|||||||
@@ -1,4 +1,12 @@
|
|||||||
import { db, loginPage, LoginPage, loginPageOrg, Org, orgs } from "@server/db";
|
import {
|
||||||
|
db,
|
||||||
|
loginPage,
|
||||||
|
LoginPage,
|
||||||
|
loginPageOrg,
|
||||||
|
Org,
|
||||||
|
orgs,
|
||||||
|
roles
|
||||||
|
} from "@server/db";
|
||||||
import {
|
import {
|
||||||
Resource,
|
Resource,
|
||||||
ResourcePassword,
|
ResourcePassword,
|
||||||
@@ -12,17 +20,19 @@ import {
|
|||||||
resources,
|
resources,
|
||||||
roleResources,
|
roleResources,
|
||||||
sessions,
|
sessions,
|
||||||
userOrgs,
|
|
||||||
userResources,
|
userResources,
|
||||||
users
|
users,
|
||||||
|
ResourceHeaderAuthExtendedCompatibility,
|
||||||
|
resourceHeaderAuthExtendedCompatibility
|
||||||
} from "@server/db";
|
} from "@server/db";
|
||||||
import { and, eq } from "drizzle-orm";
|
import { and, eq, inArray } from "drizzle-orm";
|
||||||
|
|
||||||
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;
|
headerAuth: ResourceHeaderAuth | null;
|
||||||
|
headerAuthExtendedCompatibility: ResourceHeaderAuthExtendedCompatibility | null;
|
||||||
org: Org;
|
org: Org;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -52,6 +62,13 @@ export async function getResourceByDomain(
|
|||||||
resourceHeaderAuth,
|
resourceHeaderAuth,
|
||||||
eq(resourceHeaderAuth.resourceId, resources.resourceId)
|
eq(resourceHeaderAuth.resourceId, resources.resourceId)
|
||||||
)
|
)
|
||||||
|
.leftJoin(
|
||||||
|
resourceHeaderAuthExtendedCompatibility,
|
||||||
|
eq(
|
||||||
|
resourceHeaderAuthExtendedCompatibility.resourceId,
|
||||||
|
resources.resourceId
|
||||||
|
)
|
||||||
|
)
|
||||||
.innerJoin(orgs, eq(orgs.orgId, resources.orgId))
|
.innerJoin(orgs, eq(orgs.orgId, resources.orgId))
|
||||||
.where(eq(resources.fullDomain, domain))
|
.where(eq(resources.fullDomain, domain))
|
||||||
.limit(1);
|
.limit(1);
|
||||||
@@ -65,6 +82,8 @@ export async function getResourceByDomain(
|
|||||||
pincode: result.resourcePincode,
|
pincode: result.resourcePincode,
|
||||||
password: result.resourcePassword,
|
password: result.resourcePassword,
|
||||||
headerAuth: result.resourceHeaderAuth,
|
headerAuth: result.resourceHeaderAuth,
|
||||||
|
headerAuthExtendedCompatibility:
|
||||||
|
result.resourceHeaderAuthExtendedCompatibility,
|
||||||
org: result.orgs
|
org: result.orgs
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -92,16 +111,15 @@ export async function getUserSessionWithUser(
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get user organization role
|
* Get role name by role ID (for display).
|
||||||
*/
|
*/
|
||||||
export async function getUserOrgRole(userId: string, orgId: string) {
|
export async function getRoleName(roleId: number): Promise<string | null> {
|
||||||
const userOrgRole = await db
|
const [row] = await db
|
||||||
.select()
|
.select({ name: roles.name })
|
||||||
.from(userOrgs)
|
.from(roles)
|
||||||
.where(and(eq(userOrgs.userId, userId), eq(userOrgs.orgId, orgId)))
|
.where(eq(roles.roleId, roleId))
|
||||||
.limit(1);
|
.limit(1);
|
||||||
|
return row?.name ?? null;
|
||||||
return userOrgRole.length > 0 ? userOrgRole[0] : null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -109,7 +127,7 @@ export async function getUserOrgRole(userId: string, orgId: string) {
|
|||||||
*/
|
*/
|
||||||
export async function getRoleResourceAccess(
|
export async function getRoleResourceAccess(
|
||||||
resourceId: number,
|
resourceId: number,
|
||||||
roleId: number
|
roleIds: number[]
|
||||||
) {
|
) {
|
||||||
const roleResourceAccess = await db
|
const roleResourceAccess = await db
|
||||||
.select()
|
.select()
|
||||||
@@ -117,12 +135,11 @@ export async function getRoleResourceAccess(
|
|||||||
.where(
|
.where(
|
||||||
and(
|
and(
|
||||||
eq(roleResources.resourceId, resourceId),
|
eq(roleResources.resourceId, resourceId),
|
||||||
eq(roleResources.roleId, roleId)
|
inArray(roleResources.roleId, roleIds)
|
||||||
)
|
)
|
||||||
)
|
);
|
||||||
.limit(1);
|
|
||||||
|
|
||||||
return roleResourceAccess.length > 0 ? roleResourceAccess[0] : null;
|
return roleResourceAccess.length > 0 ? roleResourceAccess : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
196
server/db/regions.ts
Normal file
196
server/db/regions.ts
Normal file
@@ -0,0 +1,196 @@
|
|||||||
|
// Regions of the World
|
||||||
|
// as of 2025-10-25
|
||||||
|
//
|
||||||
|
// Adapted according to the United Nations Geoscheme
|
||||||
|
// see https://www.unicode.org/cldr/charts/48/supplemental/territory_containment_un_m_49.html
|
||||||
|
// see https://unstats.un.org/unsd/methodology/m49
|
||||||
|
|
||||||
|
export const REGIONS = [
|
||||||
|
{
|
||||||
|
name: "regionAfrica",
|
||||||
|
id: "002",
|
||||||
|
includes: [
|
||||||
|
{
|
||||||
|
name: "regionNorthernAfrica",
|
||||||
|
id: "015",
|
||||||
|
countries: ["DZ", "EG", "LY", "MA", "SD", "TN", "EH"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "regionEasternAfrica",
|
||||||
|
id: "014",
|
||||||
|
countries: ["IO", "BI", "KM", "DJ", "ER", "ET", "TF", "KE", "MG", "MW", "MU", "YT", "MZ", "RE", "RW", "SC", "SO", "SS", "UG", "ZM", "ZW"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "regionMiddleAfrica",
|
||||||
|
id: "017",
|
||||||
|
countries: ["AO", "CM", "CF", "TD", "CG", "CD", "GQ", "GA", "ST"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "regionSouthernAfrica",
|
||||||
|
id: "018",
|
||||||
|
countries: ["BW", "SZ", "LS", "NA", "ZA"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "regionWesternAfrica",
|
||||||
|
id: "011",
|
||||||
|
countries: ["BJ", "BF", "CV", "CI", "GM", "GH", "GN", "GW", "LR", "ML", "MR", "NE", "NG", "SH", "SN", "SL", "TG"]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "regionAmericas",
|
||||||
|
id: "019",
|
||||||
|
includes: [
|
||||||
|
{
|
||||||
|
name: "regionCaribbean",
|
||||||
|
id: "029",
|
||||||
|
countries: ["AI", "AG", "AW", "BS", "BB", "BQ", "VG", "KY", "CU", "CW", "DM", "DO", "GD", "GP", "HT", "JM", "MQ", "MS", "PR", "BL", "KN", "LC", "MF", "VC", "SX", "TT", "TC", "VI"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "regionCentralAmerica",
|
||||||
|
id: "013",
|
||||||
|
countries: ["BZ", "CR", "SV", "GT", "HN", "MX", "NI", "PA"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "regionSouthAmerica",
|
||||||
|
id: "005",
|
||||||
|
countries: ["AR", "BO", "BV", "BR", "CL", "CO", "EC", "FK", "GF", "GY", "PY", "PE", "GS", "SR", "UY", "VE"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "regionNorthernAmerica",
|
||||||
|
id: "021",
|
||||||
|
countries: ["BM", "CA", "GL", "PM", "US"]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "regionAsia",
|
||||||
|
id: "142",
|
||||||
|
includes: [
|
||||||
|
{
|
||||||
|
name: "regionCentralAsia",
|
||||||
|
id: "143",
|
||||||
|
countries: ["KZ", "KG", "TJ", "TM", "UZ"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "regionEasternAsia",
|
||||||
|
id: "030",
|
||||||
|
countries: ["CN", "HK", "MO", "KP", "JP", "MN", "KR"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "regionSouthEasternAsia",
|
||||||
|
id: "035",
|
||||||
|
countries: ["BN", "KH", "ID", "LA", "MY", "MM", "PH", "SG", "TH", "TL", "VN"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "regionSouthernAsia",
|
||||||
|
id: "034",
|
||||||
|
countries: ["AF", "BD", "BT", "IN", "IR", "MV", "NP", "PK", "LK"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "regionWesternAsia",
|
||||||
|
id: "145",
|
||||||
|
countries: ["AM", "AZ", "BH", "CY", "GE", "IQ", "IL", "JO", "KW", "LB", "OM", "QA", "SA", "PS", "SY", "TR", "AE", "YE"]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "regionEurope",
|
||||||
|
id: "150",
|
||||||
|
includes: [
|
||||||
|
{
|
||||||
|
name: "regionEasternEurope",
|
||||||
|
id: "151",
|
||||||
|
countries: ["BY", "BG", "CZ", "HU", "PL", "MD", "RO", "RU", "SK", "UA"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "regionNorthernEurope",
|
||||||
|
id: "154",
|
||||||
|
countries: ["AX", "DK", "EE", "FO", "FI", "GG", "IS", "IE", "IM", "JE", "LV", "LT", "NO", "SJ", "SE", "GB"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "regionSouthernEurope",
|
||||||
|
id: "039",
|
||||||
|
countries: ["AL", "AD", "BA", "HR", "GI", "GR", "VA", "IT", "MT", "ME", "MK", "PT", "SM", "RS", "SI", "ES"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "regionWesternEurope",
|
||||||
|
id: "155",
|
||||||
|
countries: ["AT", "BE", "FR", "DE", "LI", "LU", "MC", "NL", "CH"]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "regionOceania",
|
||||||
|
id: "009",
|
||||||
|
includes: [
|
||||||
|
{
|
||||||
|
name: "regionAustraliaAndNewZealand",
|
||||||
|
id: "053",
|
||||||
|
countries: ["AU", "CX", "CC", "HM", "NZ", "NF"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "regionMelanesia",
|
||||||
|
id: "054",
|
||||||
|
countries: ["FJ", "NC", "PG", "SB", "VU"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "regionMicronesia",
|
||||||
|
id: "057",
|
||||||
|
countries: ["GU", "KI", "MH", "FM", "NR", "MP", "PW", "UM"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "regionPolynesia",
|
||||||
|
id: "061",
|
||||||
|
countries: ["AS", "CK", "PF", "NU", "PN", "WS", "TK", "TO", "TV", "WF"]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
type Subregion = {
|
||||||
|
name: string;
|
||||||
|
id: string;
|
||||||
|
countries: string[];
|
||||||
|
};
|
||||||
|
|
||||||
|
type Region = {
|
||||||
|
name: string;
|
||||||
|
id: string;
|
||||||
|
includes: Subregion[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export function getRegionNameById(regionId: string): string | undefined {
|
||||||
|
// Check top-level regions
|
||||||
|
const region = REGIONS.find((r) => r.id === regionId);
|
||||||
|
if (region) {
|
||||||
|
return region.name;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check subregions
|
||||||
|
for (const region of REGIONS) {
|
||||||
|
for (const subregion of region.includes) {
|
||||||
|
if (subregion.id === regionId) {
|
||||||
|
return subregion.name;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isValidRegionId(regionId: string): boolean {
|
||||||
|
// Check top-level regions
|
||||||
|
if (REGIONS.find((r) => r.id === regionId)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check subregions
|
||||||
|
for (const region of REGIONS) {
|
||||||
|
if (region.includes.find((s) => s.id === regionId)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
@@ -24,6 +24,7 @@ export const primaryDb = db;
|
|||||||
export type Transaction = Parameters<
|
export type Transaction = Parameters<
|
||||||
Parameters<(typeof db)["transaction"]>[0]
|
Parameters<(typeof db)["transaction"]>[0]
|
||||||
>[0];
|
>[0];
|
||||||
|
export const DB_TYPE: "pg" | "sqlite" = "sqlite";
|
||||||
|
|
||||||
function checkFileExists(filePath: string): boolean {
|
function checkFileExists(filePath: string): boolean {
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -1,3 +1,6 @@
|
|||||||
export * from "./driver";
|
export * from "./driver";
|
||||||
|
export * from "./logsDriver";
|
||||||
|
export * from "./safeRead";
|
||||||
export * from "./schema/schema";
|
export * from "./schema/schema";
|
||||||
export * from "./schema/privateSchema";
|
export * from "./schema/privateSchema";
|
||||||
|
export * from "./migrate";
|
||||||
|
|||||||
7
server/db/sqlite/logsDriver.ts
Normal file
7
server/db/sqlite/logsDriver.ts
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
import { db as mainDb } from "./driver";
|
||||||
|
|
||||||
|
// SQLite doesn't support separate databases for logs in the same way as Postgres
|
||||||
|
// Always use the main database connection for SQLite
|
||||||
|
export const logsDb = mainDb;
|
||||||
|
export default logsDb;
|
||||||
|
export const primaryLogsDb = logsDb;
|
||||||
@@ -4,7 +4,7 @@ import path from "path";
|
|||||||
|
|
||||||
const migrationsFolder = path.join("server/migrations");
|
const migrationsFolder = path.join("server/migrations");
|
||||||
|
|
||||||
const runMigrations = async () => {
|
export const runMigrations = async () => {
|
||||||
console.log("Running migrations...");
|
console.log("Running migrations...");
|
||||||
try {
|
try {
|
||||||
migrate(db as any, {
|
migrate(db as any, {
|
||||||
@@ -16,5 +16,3 @@ const runMigrations = async () => {
|
|||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
runMigrations();
|
|
||||||
|
|||||||
11
server/db/sqlite/safeRead.ts
Normal file
11
server/db/sqlite/safeRead.ts
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
import { db } from "./driver";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Runs a read query. For SQLite there is no replica/primary distinction,
|
||||||
|
* so the query is executed once against the database.
|
||||||
|
*/
|
||||||
|
export async function safeRead<T>(
|
||||||
|
query: (d: typeof db) => Promise<T>
|
||||||
|
): Promise<T> {
|
||||||
|
return query(db);
|
||||||
|
}
|
||||||
@@ -2,11 +2,22 @@ import { InferSelectModel } from "drizzle-orm";
|
|||||||
import {
|
import {
|
||||||
index,
|
index,
|
||||||
integer,
|
integer,
|
||||||
|
primaryKey,
|
||||||
real,
|
real,
|
||||||
sqliteTable,
|
sqliteTable,
|
||||||
text
|
text,
|
||||||
|
uniqueIndex
|
||||||
} from "drizzle-orm/sqlite-core";
|
} from "drizzle-orm/sqlite-core";
|
||||||
import { domains, exitNodes, orgs, sessions, users } from "./schema";
|
import {
|
||||||
|
clients,
|
||||||
|
domains,
|
||||||
|
exitNodes,
|
||||||
|
orgs,
|
||||||
|
sessions,
|
||||||
|
siteResources,
|
||||||
|
sites,
|
||||||
|
users
|
||||||
|
} from "./schema";
|
||||||
|
|
||||||
export const certificates = sqliteTable("certificates", {
|
export const certificates = sqliteTable("certificates", {
|
||||||
certId: integer("certId").primaryKey({ autoIncrement: true }),
|
certId: integer("certId").primaryKey({ autoIncrement: true }),
|
||||||
@@ -70,13 +81,16 @@ export const subscriptions = sqliteTable("subscriptions", {
|
|||||||
canceledAt: integer("canceledAt"),
|
canceledAt: integer("canceledAt"),
|
||||||
createdAt: integer("createdAt").notNull(),
|
createdAt: integer("createdAt").notNull(),
|
||||||
updatedAt: integer("updatedAt"),
|
updatedAt: integer("updatedAt"),
|
||||||
billingCycleAnchor: integer("billingCycleAnchor")
|
version: integer("version"),
|
||||||
|
billingCycleAnchor: integer("billingCycleAnchor"),
|
||||||
|
type: text("type") // tier1, tier2, tier3, or license
|
||||||
});
|
});
|
||||||
|
|
||||||
export const subscriptionItems = sqliteTable("subscriptionItems", {
|
export const subscriptionItems = sqliteTable("subscriptionItems", {
|
||||||
subscriptionItemId: integer("subscriptionItemId").primaryKey({
|
subscriptionItemId: integer("subscriptionItemId").primaryKey({
|
||||||
autoIncrement: true
|
autoIncrement: true
|
||||||
}),
|
}),
|
||||||
|
stripeSubscriptionItemId: text("stripeSubscriptionItemId"),
|
||||||
subscriptionId: text("subscriptionId")
|
subscriptionId: text("subscriptionId")
|
||||||
.notNull()
|
.notNull()
|
||||||
.references(() => subscriptions.subscriptionId, {
|
.references(() => subscriptions.subscriptionId, {
|
||||||
@@ -84,6 +98,7 @@ export const subscriptionItems = sqliteTable("subscriptionItems", {
|
|||||||
}),
|
}),
|
||||||
planId: text("planId").notNull(),
|
planId: text("planId").notNull(),
|
||||||
priceId: text("priceId"),
|
priceId: text("priceId"),
|
||||||
|
featureId: text("featureId"),
|
||||||
meterId: text("meterId"),
|
meterId: text("meterId"),
|
||||||
unitAmount: real("unitAmount"),
|
unitAmount: real("unitAmount"),
|
||||||
tiers: text("tiers"),
|
tiers: text("tiers"),
|
||||||
@@ -126,6 +141,7 @@ export const limits = sqliteTable("limits", {
|
|||||||
})
|
})
|
||||||
.notNull(),
|
.notNull(),
|
||||||
value: real("value"),
|
value: real("value"),
|
||||||
|
override: integer("override", { mode: "boolean" }).default(false),
|
||||||
description: text("description")
|
description: text("description")
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -206,7 +222,7 @@ export const loginPageBranding = sqliteTable("loginPageBranding", {
|
|||||||
loginPageBrandingId: integer("loginPageBrandingId").primaryKey({
|
loginPageBrandingId: integer("loginPageBrandingId").primaryKey({
|
||||||
autoIncrement: true
|
autoIncrement: true
|
||||||
}),
|
}),
|
||||||
logoUrl: text("logoUrl").notNull(),
|
logoUrl: text("logoUrl"),
|
||||||
logoWidth: integer("logoWidth").notNull(),
|
logoWidth: integer("logoWidth").notNull(),
|
||||||
logoHeight: integer("logoHeight").notNull(),
|
logoHeight: integer("logoHeight").notNull(),
|
||||||
primaryColor: text("primaryColor"),
|
primaryColor: text("primaryColor"),
|
||||||
@@ -273,6 +289,7 @@ export const accessAuditLog = sqliteTable(
|
|||||||
actor: text("actor"),
|
actor: text("actor"),
|
||||||
actorId: text("actorId"),
|
actorId: text("actorId"),
|
||||||
resourceId: integer("resourceId"),
|
resourceId: integer("resourceId"),
|
||||||
|
siteResourceId: integer("siteResourceId"),
|
||||||
ip: text("ip"),
|
ip: text("ip"),
|
||||||
location: text("location"),
|
location: text("location"),
|
||||||
type: text("type").notNull(),
|
type: text("type").notNull(),
|
||||||
@@ -289,6 +306,156 @@ export const accessAuditLog = sqliteTable(
|
|||||||
]
|
]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
export const connectionAuditLog = sqliteTable(
|
||||||
|
"connectionAuditLog",
|
||||||
|
{
|
||||||
|
id: integer("id").primaryKey({ autoIncrement: true }),
|
||||||
|
sessionId: text("sessionId").notNull(),
|
||||||
|
siteResourceId: integer("siteResourceId").references(
|
||||||
|
() => siteResources.siteResourceId,
|
||||||
|
{ onDelete: "cascade" }
|
||||||
|
),
|
||||||
|
orgId: text("orgId").references(() => orgs.orgId, {
|
||||||
|
onDelete: "cascade"
|
||||||
|
}),
|
||||||
|
siteId: integer("siteId").references(() => sites.siteId, {
|
||||||
|
onDelete: "cascade"
|
||||||
|
}),
|
||||||
|
clientId: integer("clientId").references(() => clients.clientId, {
|
||||||
|
onDelete: "cascade"
|
||||||
|
}),
|
||||||
|
userId: text("userId").references(() => users.userId, {
|
||||||
|
onDelete: "cascade"
|
||||||
|
}),
|
||||||
|
sourceAddr: text("sourceAddr").notNull(),
|
||||||
|
destAddr: text("destAddr").notNull(),
|
||||||
|
protocol: text("protocol").notNull(),
|
||||||
|
startedAt: integer("startedAt").notNull(),
|
||||||
|
endedAt: integer("endedAt"),
|
||||||
|
bytesTx: integer("bytesTx"),
|
||||||
|
bytesRx: integer("bytesRx")
|
||||||
|
},
|
||||||
|
(table) => [
|
||||||
|
index("idx_accessAuditLog_startedAt").on(table.startedAt),
|
||||||
|
index("idx_accessAuditLog_org_startedAt").on(
|
||||||
|
table.orgId,
|
||||||
|
table.startedAt
|
||||||
|
),
|
||||||
|
index("idx_accessAuditLog_siteResourceId").on(table.siteResourceId)
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
export const approvals = sqliteTable("approvals", {
|
||||||
|
approvalId: integer("approvalId").primaryKey({ autoIncrement: true }),
|
||||||
|
timestamp: integer("timestamp").notNull(), // this is EPOCH time in seconds
|
||||||
|
orgId: text("orgId")
|
||||||
|
.references(() => orgs.orgId, {
|
||||||
|
onDelete: "cascade"
|
||||||
|
})
|
||||||
|
.notNull(),
|
||||||
|
clientId: integer("clientId").references(() => clients.clientId, {
|
||||||
|
onDelete: "cascade"
|
||||||
|
}), // olms reference user devices clients
|
||||||
|
userId: text("userId").references(() => users.userId, {
|
||||||
|
// optionally tied to a user and in this case delete when the user deletes
|
||||||
|
onDelete: "cascade"
|
||||||
|
}),
|
||||||
|
decision: text("decision")
|
||||||
|
.$type<"approved" | "denied" | "pending">()
|
||||||
|
.default("pending")
|
||||||
|
.notNull(),
|
||||||
|
type: text("type")
|
||||||
|
.$type<"user_device" /*| 'proxy' // for later */>()
|
||||||
|
.notNull()
|
||||||
|
});
|
||||||
|
|
||||||
|
export const bannedEmails = sqliteTable("bannedEmails", {
|
||||||
|
email: text("email").primaryKey()
|
||||||
|
});
|
||||||
|
|
||||||
|
export const bannedIps = sqliteTable("bannedIps", {
|
||||||
|
ip: text("ip").primaryKey()
|
||||||
|
});
|
||||||
|
|
||||||
|
export const siteProvisioningKeys = sqliteTable("siteProvisioningKeys", {
|
||||||
|
siteProvisioningKeyId: text("siteProvisioningKeyId").primaryKey(),
|
||||||
|
name: text("name").notNull(),
|
||||||
|
siteProvisioningKeyHash: text("siteProvisioningKeyHash").notNull(),
|
||||||
|
lastChars: text("lastChars").notNull(),
|
||||||
|
createdAt: text("dateCreated").notNull(),
|
||||||
|
lastUsed: text("lastUsed"),
|
||||||
|
maxBatchSize: integer("maxBatchSize"), // null = no limit
|
||||||
|
numUsed: integer("numUsed").notNull().default(0),
|
||||||
|
validUntil: text("validUntil"),
|
||||||
|
approveNewSites: integer("approveNewSites", { mode: "boolean" })
|
||||||
|
.notNull()
|
||||||
|
.default(true)
|
||||||
|
});
|
||||||
|
|
||||||
|
export const siteProvisioningKeyOrg = sqliteTable(
|
||||||
|
"siteProvisioningKeyOrg",
|
||||||
|
{
|
||||||
|
siteProvisioningKeyId: text("siteProvisioningKeyId")
|
||||||
|
.notNull()
|
||||||
|
.references(() => siteProvisioningKeys.siteProvisioningKeyId, {
|
||||||
|
onDelete: "cascade"
|
||||||
|
}),
|
||||||
|
orgId: text("orgId")
|
||||||
|
.notNull()
|
||||||
|
.references(() => orgs.orgId, { onDelete: "cascade" })
|
||||||
|
},
|
||||||
|
(table) => [
|
||||||
|
primaryKey({
|
||||||
|
columns: [table.siteProvisioningKeyId, table.orgId]
|
||||||
|
})
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
export const eventStreamingDestinations = sqliteTable(
|
||||||
|
"eventStreamingDestinations",
|
||||||
|
{
|
||||||
|
destinationId: integer("destinationId").primaryKey({
|
||||||
|
autoIncrement: true
|
||||||
|
}),
|
||||||
|
orgId: text("orgId")
|
||||||
|
.notNull()
|
||||||
|
.references(() => orgs.orgId, { onDelete: "cascade" }),
|
||||||
|
sendConnectionLogs: integer("sendConnectionLogs", { mode: "boolean" }).notNull().default(false),
|
||||||
|
sendRequestLogs: integer("sendRequestLogs", { mode: "boolean" }).notNull().default(false),
|
||||||
|
sendActionLogs: integer("sendActionLogs", { mode: "boolean" }).notNull().default(false),
|
||||||
|
sendAccessLogs: integer("sendAccessLogs", { mode: "boolean" }).notNull().default(false),
|
||||||
|
type: text("type").notNull(), // e.g. "http", "kafka", etc.
|
||||||
|
config: text("config").notNull(), // JSON string with the configuration for the destination
|
||||||
|
enabled: integer("enabled", { mode: "boolean" })
|
||||||
|
.notNull()
|
||||||
|
.default(true),
|
||||||
|
createdAt: integer("createdAt").notNull(),
|
||||||
|
updatedAt: integer("updatedAt").notNull()
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
export const eventStreamingCursors = sqliteTable(
|
||||||
|
"eventStreamingCursors",
|
||||||
|
{
|
||||||
|
cursorId: integer("cursorId").primaryKey({ autoIncrement: true }),
|
||||||
|
destinationId: integer("destinationId")
|
||||||
|
.notNull()
|
||||||
|
.references(() => eventStreamingDestinations.destinationId, {
|
||||||
|
onDelete: "cascade"
|
||||||
|
}),
|
||||||
|
logType: text("logType").notNull(), // "request" | "action" | "access" | "connection"
|
||||||
|
lastSentId: integer("lastSentId").notNull().default(0),
|
||||||
|
lastSentAt: integer("lastSentAt") // epoch milliseconds, null if never sent
|
||||||
|
},
|
||||||
|
(table) => [
|
||||||
|
uniqueIndex("idx_eventStreamingCursors_dest_type").on(
|
||||||
|
table.destinationId,
|
||||||
|
table.logType
|
||||||
|
)
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
export type Approval = InferSelectModel<typeof approvals>;
|
||||||
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>;
|
||||||
@@ -309,3 +476,13 @@ export type LoginPage = InferSelectModel<typeof loginPage>;
|
|||||||
export type LoginPageBranding = InferSelectModel<typeof loginPageBranding>;
|
export type LoginPageBranding = InferSelectModel<typeof loginPageBranding>;
|
||||||
export type ActionAuditLog = InferSelectModel<typeof actionAuditLog>;
|
export type ActionAuditLog = InferSelectModel<typeof actionAuditLog>;
|
||||||
export type AccessAuditLog = InferSelectModel<typeof accessAuditLog>;
|
export type AccessAuditLog = InferSelectModel<typeof accessAuditLog>;
|
||||||
|
export type ConnectionAuditLog = InferSelectModel<typeof connectionAuditLog>;
|
||||||
|
export type BannedEmail = InferSelectModel<typeof bannedEmails>;
|
||||||
|
export type BannedIp = InferSelectModel<typeof bannedIps>;
|
||||||
|
export type SiteProvisioningKey = InferSelectModel<typeof siteProvisioningKeys>;
|
||||||
|
export type EventStreamingDestination = InferSelectModel<
|
||||||
|
typeof eventStreamingDestinations
|
||||||
|
>;
|
||||||
|
export type EventStreamingCursor = InferSelectModel<
|
||||||
|
typeof eventStreamingCursors
|
||||||
|
>;
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
import { randomUUID } from "crypto";
|
import { randomUUID } from "crypto";
|
||||||
import { InferSelectModel } from "drizzle-orm";
|
import { InferSelectModel } from "drizzle-orm";
|
||||||
import {
|
import {
|
||||||
|
index,
|
||||||
|
integer,
|
||||||
|
primaryKey,
|
||||||
sqliteTable,
|
sqliteTable,
|
||||||
text,
|
text,
|
||||||
integer,
|
unique
|
||||||
index,
|
|
||||||
uniqueIndex
|
|
||||||
} from "drizzle-orm/sqlite-core";
|
} 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(),
|
||||||
@@ -20,7 +20,8 @@ export const domains = sqliteTable("domains", {
|
|||||||
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"),
|
certResolver: text("certResolver"),
|
||||||
preferWildcardCert: integer("preferWildcardCert", { mode: "boolean" })
|
preferWildcardCert: integer("preferWildcardCert", { mode: "boolean" }),
|
||||||
|
errorMessage: text("errorMessage")
|
||||||
});
|
});
|
||||||
|
|
||||||
export const dnsRecords = sqliteTable("dnsRecords", {
|
export const dnsRecords = sqliteTable("dnsRecords", {
|
||||||
@@ -52,7 +53,14 @@ export const orgs = sqliteTable("orgs", {
|
|||||||
.default(0),
|
.default(0),
|
||||||
settingsLogRetentionDaysAction: integer("settingsLogRetentionDaysAction") // where 0 = dont keep logs and -1 = keep forever and 9001 = end of the following year
|
settingsLogRetentionDaysAction: integer("settingsLogRetentionDaysAction") // where 0 = dont keep logs and -1 = keep forever and 9001 = end of the following year
|
||||||
.notNull()
|
.notNull()
|
||||||
.default(0)
|
.default(0),
|
||||||
|
settingsLogRetentionDaysConnection: integer("settingsLogRetentionDaysConnection") // where 0 = dont keep logs and -1 = keep forever and 9001 = end of the following year
|
||||||
|
.notNull()
|
||||||
|
.default(0),
|
||||||
|
sshCaPrivateKey: text("sshCaPrivateKey"), // Encrypted SSH CA private key (PEM format)
|
||||||
|
sshCaPublicKey: text("sshCaPublicKey"), // SSH CA public key (OpenSSH format)
|
||||||
|
isBillingOrg: integer("isBillingOrg", { mode: "boolean" }),
|
||||||
|
billingOrgId: text("billingOrgId")
|
||||||
});
|
});
|
||||||
|
|
||||||
export const userDomains = sqliteTable("userDomains", {
|
export const userDomains = sqliteTable("userDomains", {
|
||||||
@@ -92,6 +100,7 @@ export const sites = sqliteTable("sites", {
|
|||||||
lastBandwidthUpdate: text("lastBandwidthUpdate"),
|
lastBandwidthUpdate: text("lastBandwidthUpdate"),
|
||||||
type: text("type").notNull(), // "newt" or "wireguard"
|
type: text("type").notNull(), // "newt" or "wireguard"
|
||||||
online: integer("online", { mode: "boolean" }).notNull().default(false),
|
online: integer("online", { mode: "boolean" }).notNull().default(false),
|
||||||
|
lastPing: integer("lastPing"),
|
||||||
|
|
||||||
// exit node stuff that is how to connect to the site when it has a wg server
|
// exit node stuff that is how to connect to the site when it has a wg server
|
||||||
address: text("address"), // this is the address of the wireguard interface in newt
|
address: text("address"), // this is the address of the wireguard interface in newt
|
||||||
@@ -101,7 +110,8 @@ 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),
|
||||||
|
status: text("status").$type<"pending" | "approved">().default("approved")
|
||||||
});
|
});
|
||||||
|
|
||||||
export const resources = sqliteTable("resources", {
|
export const resources = sqliteTable("resources", {
|
||||||
@@ -150,7 +160,20 @@ export const resources = sqliteTable("resources", {
|
|||||||
proxyProtocol: integer("proxyProtocol", { mode: "boolean" })
|
proxyProtocol: integer("proxyProtocol", { mode: "boolean" })
|
||||||
.notNull()
|
.notNull()
|
||||||
.default(false),
|
.default(false),
|
||||||
proxyProtocolVersion: integer("proxyProtocolVersion").default(1)
|
proxyProtocolVersion: integer("proxyProtocolVersion").default(1),
|
||||||
|
|
||||||
|
maintenanceModeEnabled: integer("maintenanceModeEnabled", {
|
||||||
|
mode: "boolean"
|
||||||
|
})
|
||||||
|
.notNull()
|
||||||
|
.default(false),
|
||||||
|
maintenanceModeType: text("maintenanceModeType", {
|
||||||
|
enum: ["forced", "automatic"]
|
||||||
|
}).default("forced"), // "forced" = always show, "automatic" = only when down
|
||||||
|
maintenanceTitle: text("maintenanceTitle"),
|
||||||
|
maintenanceMessage: text("maintenanceMessage"),
|
||||||
|
maintenanceEstimatedTime: text("maintenanceEstimatedTime"),
|
||||||
|
postAuthPath: text("postAuthPath")
|
||||||
});
|
});
|
||||||
|
|
||||||
export const targets = sqliteTable("targets", {
|
export const targets = sqliteTable("targets", {
|
||||||
@@ -201,7 +224,9 @@ export const targetHealthCheck = sqliteTable("targetHealthCheck", {
|
|||||||
}).default(true),
|
}).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")
|
||||||
|
.$type<"unknown" | "healthy" | "unhealthy">()
|
||||||
|
.default("unknown"), // "unknown", "healthy", "unhealthy"
|
||||||
hcTlsServerName: text("hcTlsServerName")
|
hcTlsServerName: text("hcTlsServerName")
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -233,7 +258,7 @@ 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(),
|
||||||
mode: text("mode").notNull(), // "host" | "cidr" | "port"
|
mode: text("mode").$type<"host" | "cidr">().notNull(), // "host" | "cidr" | "port"
|
||||||
protocol: text("protocol"), // only for port mode
|
protocol: text("protocol"), // only for port mode
|
||||||
proxyPort: integer("proxyPort"), // only for port mode
|
proxyPort: integer("proxyPort"), // only for port mode
|
||||||
destinationPort: integer("destinationPort"), // only for port mode
|
destinationPort: integer("destinationPort"), // only for port mode
|
||||||
@@ -241,9 +266,15 @@ export const siteResources = sqliteTable("siteResources", {
|
|||||||
enabled: integer("enabled", { mode: "boolean" }).notNull().default(true),
|
enabled: integer("enabled", { mode: "boolean" }).notNull().default(true),
|
||||||
alias: text("alias"),
|
alias: text("alias"),
|
||||||
aliasAddress: text("aliasAddress"),
|
aliasAddress: text("aliasAddress"),
|
||||||
tcpPortRangeString: text("tcpPortRangeString"),
|
tcpPortRangeString: text("tcpPortRangeString").notNull().default("*"),
|
||||||
udpPortRangeString: text("udpPortRangeString"),
|
udpPortRangeString: text("udpPortRangeString").notNull().default("*"),
|
||||||
disableIcmp: integer("disableIcmp", { mode: "boolean" })
|
disableIcmp: integer("disableIcmp", { mode: "boolean" })
|
||||||
|
.notNull()
|
||||||
|
.default(false),
|
||||||
|
authDaemonPort: integer("authDaemonPort").default(22123),
|
||||||
|
authDaemonMode: text("authDaemonMode")
|
||||||
|
.$type<"site" | "remote">()
|
||||||
|
.default("site")
|
||||||
});
|
});
|
||||||
|
|
||||||
export const clientSiteResources = sqliteTable("clientSiteResources", {
|
export const clientSiteResources = sqliteTable("clientSiteResources", {
|
||||||
@@ -296,10 +327,14 @@ export const users = sqliteTable("user", {
|
|||||||
dateCreated: text("dateCreated").notNull(),
|
dateCreated: text("dateCreated").notNull(),
|
||||||
termsAcceptedTimestamp: text("termsAcceptedTimestamp"),
|
termsAcceptedTimestamp: text("termsAcceptedTimestamp"),
|
||||||
termsVersion: text("termsVersion"),
|
termsVersion: text("termsVersion"),
|
||||||
|
marketingEmailConsent: integer("marketingEmailConsent", {
|
||||||
|
mode: "boolean"
|
||||||
|
}).default(false),
|
||||||
serverAdmin: integer("serverAdmin", { mode: "boolean" })
|
serverAdmin: integer("serverAdmin", { mode: "boolean" })
|
||||||
.notNull()
|
.notNull()
|
||||||
.default(false),
|
.default(false),
|
||||||
lastPasswordChange: integer("lastPasswordChange")
|
lastPasswordChange: integer("lastPasswordChange"),
|
||||||
|
locale: text("locale")
|
||||||
});
|
});
|
||||||
|
|
||||||
export const securityKeys = sqliteTable("webauthnCredentials", {
|
export const securityKeys = sqliteTable("webauthnCredentials", {
|
||||||
@@ -371,7 +406,12 @@ export const clients = sqliteTable("clients", {
|
|||||||
type: text("type").notNull(), // "olm"
|
type: text("type").notNull(), // "olm"
|
||||||
online: integer("online", { mode: "boolean" }).notNull().default(false),
|
online: integer("online", { mode: "boolean" }).notNull().default(false),
|
||||||
// endpoint: text("endpoint"),
|
// endpoint: text("endpoint"),
|
||||||
lastHolePunch: integer("lastHolePunch")
|
lastHolePunch: integer("lastHolePunch"),
|
||||||
|
archived: integer("archived", { mode: "boolean" }).notNull().default(false),
|
||||||
|
blocked: integer("blocked", { mode: "boolean" }).notNull().default(false),
|
||||||
|
approvalState: text("approvalState").$type<
|
||||||
|
"pending" | "approved" | "denied"
|
||||||
|
>()
|
||||||
});
|
});
|
||||||
|
|
||||||
export const clientSitesAssociationsCache = sqliteTable(
|
export const clientSitesAssociationsCache = sqliteTable(
|
||||||
@@ -383,6 +423,9 @@ export const clientSitesAssociationsCache = sqliteTable(
|
|||||||
isRelayed: integer("isRelayed", { mode: "boolean" })
|
isRelayed: integer("isRelayed", { mode: "boolean" })
|
||||||
.notNull()
|
.notNull()
|
||||||
.default(false),
|
.default(false),
|
||||||
|
isJitMode: integer("isJitMode", { mode: "boolean" })
|
||||||
|
.notNull()
|
||||||
|
.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
|
publicKey: text("publicKey") // this will act as the session's public key for hole punching so we can track when it changes
|
||||||
}
|
}
|
||||||
@@ -411,7 +454,160 @@ export const olms = sqliteTable("olms", {
|
|||||||
userId: text("userId").references(() => users.userId, {
|
userId: text("userId").references(() => users.userId, {
|
||||||
// optionally tied to a user and in this case delete when the user deletes
|
// optionally tied to a user and in this case delete when the user deletes
|
||||||
onDelete: "cascade"
|
onDelete: "cascade"
|
||||||
|
}),
|
||||||
|
archived: integer("archived", { mode: "boolean" }).notNull().default(false)
|
||||||
|
});
|
||||||
|
|
||||||
|
export const currentFingerprint = sqliteTable("currentFingerprint", {
|
||||||
|
fingerprintId: integer("id").primaryKey({ autoIncrement: true }),
|
||||||
|
|
||||||
|
olmId: text("olmId")
|
||||||
|
.references(() => olms.olmId, { onDelete: "cascade" })
|
||||||
|
.notNull(),
|
||||||
|
|
||||||
|
firstSeen: integer("firstSeen").notNull(),
|
||||||
|
lastSeen: integer("lastSeen").notNull(),
|
||||||
|
lastCollectedAt: integer("lastCollectedAt").notNull(),
|
||||||
|
|
||||||
|
username: text("username"),
|
||||||
|
hostname: text("hostname"),
|
||||||
|
platform: text("platform"),
|
||||||
|
osVersion: text("osVersion"),
|
||||||
|
kernelVersion: text("kernelVersion"),
|
||||||
|
arch: text("arch"),
|
||||||
|
deviceModel: text("deviceModel"),
|
||||||
|
serialNumber: text("serialNumber"),
|
||||||
|
platformFingerprint: text("platformFingerprint"),
|
||||||
|
|
||||||
|
// Platform-agnostic checks
|
||||||
|
|
||||||
|
biometricsEnabled: integer("biometricsEnabled", { mode: "boolean" })
|
||||||
|
.notNull()
|
||||||
|
.default(false),
|
||||||
|
diskEncrypted: integer("diskEncrypted", { mode: "boolean" })
|
||||||
|
.notNull()
|
||||||
|
.default(false),
|
||||||
|
firewallEnabled: integer("firewallEnabled", { mode: "boolean" })
|
||||||
|
.notNull()
|
||||||
|
.default(false),
|
||||||
|
autoUpdatesEnabled: integer("autoUpdatesEnabled", { mode: "boolean" })
|
||||||
|
.notNull()
|
||||||
|
.default(false),
|
||||||
|
tpmAvailable: integer("tpmAvailable", { mode: "boolean" })
|
||||||
|
.notNull()
|
||||||
|
.default(false),
|
||||||
|
|
||||||
|
// Windows-specific posture check information
|
||||||
|
|
||||||
|
windowsAntivirusEnabled: integer("windowsAntivirusEnabled", {
|
||||||
|
mode: "boolean"
|
||||||
})
|
})
|
||||||
|
.notNull()
|
||||||
|
.default(false),
|
||||||
|
|
||||||
|
// macOS-specific posture check information
|
||||||
|
|
||||||
|
macosSipEnabled: integer("macosSipEnabled", { mode: "boolean" })
|
||||||
|
.notNull()
|
||||||
|
.default(false),
|
||||||
|
macosGatekeeperEnabled: integer("macosGatekeeperEnabled", {
|
||||||
|
mode: "boolean"
|
||||||
|
})
|
||||||
|
.notNull()
|
||||||
|
.default(false),
|
||||||
|
macosFirewallStealthMode: integer("macosFirewallStealthMode", {
|
||||||
|
mode: "boolean"
|
||||||
|
})
|
||||||
|
.notNull()
|
||||||
|
.default(false),
|
||||||
|
|
||||||
|
// Linux-specific posture check information
|
||||||
|
|
||||||
|
linuxAppArmorEnabled: integer("linuxAppArmorEnabled", { mode: "boolean" })
|
||||||
|
.notNull()
|
||||||
|
.default(false),
|
||||||
|
linuxSELinuxEnabled: integer("linuxSELinuxEnabled", {
|
||||||
|
mode: "boolean"
|
||||||
|
})
|
||||||
|
.notNull()
|
||||||
|
.default(false)
|
||||||
|
});
|
||||||
|
|
||||||
|
export const fingerprintSnapshots = sqliteTable("fingerprintSnapshots", {
|
||||||
|
snapshotId: integer("id").primaryKey({ autoIncrement: true }),
|
||||||
|
|
||||||
|
fingerprintId: integer("fingerprintId").references(
|
||||||
|
() => currentFingerprint.fingerprintId,
|
||||||
|
{
|
||||||
|
onDelete: "set null"
|
||||||
|
}
|
||||||
|
),
|
||||||
|
|
||||||
|
username: text("username"),
|
||||||
|
hostname: text("hostname"),
|
||||||
|
platform: text("platform"),
|
||||||
|
osVersion: text("osVersion"),
|
||||||
|
kernelVersion: text("kernelVersion"),
|
||||||
|
arch: text("arch"),
|
||||||
|
deviceModel: text("deviceModel"),
|
||||||
|
serialNumber: text("serialNumber"),
|
||||||
|
platformFingerprint: text("platformFingerprint"),
|
||||||
|
|
||||||
|
// Platform-agnostic checks
|
||||||
|
|
||||||
|
biometricsEnabled: integer("biometricsEnabled", { mode: "boolean" })
|
||||||
|
.notNull()
|
||||||
|
.default(false),
|
||||||
|
diskEncrypted: integer("diskEncrypted", { mode: "boolean" })
|
||||||
|
.notNull()
|
||||||
|
.default(false),
|
||||||
|
firewallEnabled: integer("firewallEnabled", { mode: "boolean" })
|
||||||
|
.notNull()
|
||||||
|
.default(false),
|
||||||
|
autoUpdatesEnabled: integer("autoUpdatesEnabled", { mode: "boolean" })
|
||||||
|
.notNull()
|
||||||
|
.default(false),
|
||||||
|
tpmAvailable: integer("tpmAvailable", { mode: "boolean" })
|
||||||
|
.notNull()
|
||||||
|
.default(false),
|
||||||
|
|
||||||
|
// Windows-specific posture check information
|
||||||
|
|
||||||
|
windowsAntivirusEnabled: integer("windowsAntivirusEnabled", {
|
||||||
|
mode: "boolean"
|
||||||
|
})
|
||||||
|
.notNull()
|
||||||
|
.default(false),
|
||||||
|
|
||||||
|
// macOS-specific posture check information
|
||||||
|
|
||||||
|
macosSipEnabled: integer("macosSipEnabled", { mode: "boolean" })
|
||||||
|
.notNull()
|
||||||
|
.default(false),
|
||||||
|
macosGatekeeperEnabled: integer("macosGatekeeperEnabled", {
|
||||||
|
mode: "boolean"
|
||||||
|
})
|
||||||
|
.notNull()
|
||||||
|
.default(false),
|
||||||
|
macosFirewallStealthMode: integer("macosFirewallStealthMode", {
|
||||||
|
mode: "boolean"
|
||||||
|
})
|
||||||
|
.notNull()
|
||||||
|
.default(false),
|
||||||
|
|
||||||
|
// Linux-specific posture check information
|
||||||
|
|
||||||
|
linuxAppArmorEnabled: integer("linuxAppArmorEnabled", { mode: "boolean" })
|
||||||
|
.notNull()
|
||||||
|
.default(false),
|
||||||
|
linuxSELinuxEnabled: integer("linuxSELinuxEnabled", {
|
||||||
|
mode: "boolean"
|
||||||
|
})
|
||||||
|
.notNull()
|
||||||
|
.default(false),
|
||||||
|
|
||||||
|
hash: text("hash").notNull(),
|
||||||
|
collectedAt: integer("collectedAt").notNull()
|
||||||
});
|
});
|
||||||
|
|
||||||
export const twoFactorBackupCodes = sqliteTable("twoFactorBackupCodes", {
|
export const twoFactorBackupCodes = sqliteTable("twoFactorBackupCodes", {
|
||||||
@@ -459,13 +655,11 @@ export const userOrgs = sqliteTable("userOrgs", {
|
|||||||
onDelete: "cascade"
|
onDelete: "cascade"
|
||||||
})
|
})
|
||||||
.notNull(),
|
.notNull(),
|
||||||
roleId: integer("roleId")
|
|
||||||
.notNull()
|
|
||||||
.references(() => roles.roleId),
|
|
||||||
isOwner: integer("isOwner", { mode: "boolean" }).notNull().default(false),
|
isOwner: integer("isOwner", { mode: "boolean" }).notNull().default(false),
|
||||||
autoProvisioned: integer("autoProvisioned", {
|
autoProvisioned: integer("autoProvisioned", {
|
||||||
mode: "boolean"
|
mode: "boolean"
|
||||||
}).default(false)
|
}).default(false),
|
||||||
|
pamUsername: text("pamUsername") // cleaned username for ssh and such
|
||||||
});
|
});
|
||||||
|
|
||||||
export const emailVerificationCodes = sqliteTable("emailVerificationCodes", {
|
export const emailVerificationCodes = sqliteTable("emailVerificationCodes", {
|
||||||
@@ -503,9 +697,34 @@ export const roles = sqliteTable("roles", {
|
|||||||
.notNull(),
|
.notNull(),
|
||||||
isAdmin: integer("isAdmin", { mode: "boolean" }),
|
isAdmin: integer("isAdmin", { mode: "boolean" }),
|
||||||
name: text("name").notNull(),
|
name: text("name").notNull(),
|
||||||
description: text("description")
|
description: text("description"),
|
||||||
|
requireDeviceApproval: integer("requireDeviceApproval", {
|
||||||
|
mode: "boolean"
|
||||||
|
}).default(false),
|
||||||
|
sshSudoMode: text("sshSudoMode").default("none"), // "none" | "full" | "commands"
|
||||||
|
sshSudoCommands: text("sshSudoCommands").default("[]"),
|
||||||
|
sshCreateHomeDir: integer("sshCreateHomeDir", { mode: "boolean" }).default(
|
||||||
|
true
|
||||||
|
),
|
||||||
|
sshUnixGroups: text("sshUnixGroups").default("[]")
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const userOrgRoles = sqliteTable(
|
||||||
|
"userOrgRoles",
|
||||||
|
{
|
||||||
|
userId: text("userId")
|
||||||
|
.notNull()
|
||||||
|
.references(() => users.userId, { onDelete: "cascade" }),
|
||||||
|
orgId: text("orgId")
|
||||||
|
.notNull()
|
||||||
|
.references(() => orgs.orgId, { onDelete: "cascade" }),
|
||||||
|
roleId: integer("roleId")
|
||||||
|
.notNull()
|
||||||
|
.references(() => roles.roleId, { onDelete: "cascade" })
|
||||||
|
},
|
||||||
|
(t) => [unique().on(t.userId, t.orgId, t.roleId)]
|
||||||
|
);
|
||||||
|
|
||||||
export const roleActions = sqliteTable("roleActions", {
|
export const roleActions = sqliteTable("roleActions", {
|
||||||
roleId: integer("roleId")
|
roleId: integer("roleId")
|
||||||
.notNull()
|
.notNull()
|
||||||
@@ -591,11 +810,21 @@ export const userInvites = sqliteTable("userInvites", {
|
|||||||
.references(() => orgs.orgId, { onDelete: "cascade" }),
|
.references(() => orgs.orgId, { onDelete: "cascade" }),
|
||||||
email: text("email").notNull(),
|
email: text("email").notNull(),
|
||||||
expiresAt: integer("expiresAt").notNull(),
|
expiresAt: integer("expiresAt").notNull(),
|
||||||
tokenHash: text("token").notNull(),
|
tokenHash: text("token").notNull()
|
||||||
|
});
|
||||||
|
|
||||||
|
export const userInviteRoles = sqliteTable(
|
||||||
|
"userInviteRoles",
|
||||||
|
{
|
||||||
|
inviteId: text("inviteId")
|
||||||
|
.notNull()
|
||||||
|
.references(() => userInvites.inviteId, { onDelete: "cascade" }),
|
||||||
roleId: integer("roleId")
|
roleId: integer("roleId")
|
||||||
.notNull()
|
.notNull()
|
||||||
.references(() => roles.roleId, { onDelete: "cascade" })
|
.references(() => roles.roleId, { onDelete: "cascade" })
|
||||||
});
|
},
|
||||||
|
(t) => [primaryKey({ columns: [t.inviteId, t.roleId] })]
|
||||||
|
);
|
||||||
|
|
||||||
export const resourcePincode = sqliteTable("resourcePincode", {
|
export const resourcePincode = sqliteTable("resourcePincode", {
|
||||||
pincodeId: integer("pincodeId").primaryKey({
|
pincodeId: integer("pincodeId").primaryKey({
|
||||||
@@ -628,6 +857,26 @@ export const resourceHeaderAuth = sqliteTable("resourceHeaderAuth", {
|
|||||||
headerAuthHash: text("headerAuthHash").notNull()
|
headerAuthHash: text("headerAuthHash").notNull()
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const resourceHeaderAuthExtendedCompatibility = sqliteTable(
|
||||||
|
"resourceHeaderAuthExtendedCompatibility",
|
||||||
|
{
|
||||||
|
headerAuthExtendedCompatibilityId: integer(
|
||||||
|
"headerAuthExtendedCompatibilityId"
|
||||||
|
).primaryKey({
|
||||||
|
autoIncrement: true
|
||||||
|
}),
|
||||||
|
resourceId: integer("resourceId")
|
||||||
|
.notNull()
|
||||||
|
.references(() => resources.resourceId, { onDelete: "cascade" }),
|
||||||
|
extendedCompatibilityIsActivated: integer(
|
||||||
|
"extendedCompatibilityIsActivated",
|
||||||
|
{ mode: "boolean" }
|
||||||
|
)
|
||||||
|
.notNull()
|
||||||
|
.default(true)
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
export const resourceAccessToken = sqliteTable("resourceAccessToken", {
|
export const resourceAccessToken = sqliteTable("resourceAccessToken", {
|
||||||
accessTokenId: text("accessTokenId").primaryKey(),
|
accessTokenId: text("accessTokenId").primaryKey(),
|
||||||
orgId: text("orgId")
|
orgId: text("orgId")
|
||||||
@@ -742,7 +991,8 @@ export const idp = sqliteTable("idp", {
|
|||||||
mode: "boolean"
|
mode: "boolean"
|
||||||
})
|
})
|
||||||
.notNull()
|
.notNull()
|
||||||
.default(false)
|
.default(false),
|
||||||
|
tags: text("tags")
|
||||||
});
|
});
|
||||||
|
|
||||||
// Identity Provider OAuth Configuration
|
// Identity Provider OAuth Configuration
|
||||||
@@ -883,6 +1133,16 @@ export const deviceWebAuthCodes = sqliteTable("deviceWebAuthCodes", {
|
|||||||
})
|
})
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const roundTripMessageTracker = sqliteTable("roundTripMessageTracker", {
|
||||||
|
messageId: integer("messageId").primaryKey({ autoIncrement: true }),
|
||||||
|
wsClientId: text("clientId"),
|
||||||
|
messageType: text("messageType"),
|
||||||
|
sentAt: integer("sentAt").notNull(),
|
||||||
|
receivedAt: integer("receivedAt"),
|
||||||
|
error: text("error"),
|
||||||
|
complete: integer("complete", { mode: "boolean" }).notNull().default(false)
|
||||||
|
});
|
||||||
|
|
||||||
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>;
|
||||||
@@ -908,11 +1168,16 @@ export type UserSite = InferSelectModel<typeof userSites>;
|
|||||||
export type RoleResource = InferSelectModel<typeof roleResources>;
|
export type RoleResource = InferSelectModel<typeof roleResources>;
|
||||||
export type UserResource = InferSelectModel<typeof userResources>;
|
export type UserResource = InferSelectModel<typeof userResources>;
|
||||||
export type UserInvite = InferSelectModel<typeof userInvites>;
|
export type UserInvite = InferSelectModel<typeof userInvites>;
|
||||||
|
export type UserInviteRole = InferSelectModel<typeof userInviteRoles>;
|
||||||
export type UserOrg = InferSelectModel<typeof userOrgs>;
|
export type UserOrg = InferSelectModel<typeof userOrgs>;
|
||||||
|
export type UserOrgRole = InferSelectModel<typeof userOrgRoles>;
|
||||||
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 ResourceHeaderAuth = InferSelectModel<typeof resourceHeaderAuth>;
|
||||||
|
export type ResourceHeaderAuthExtendedCompatibility = InferSelectModel<
|
||||||
|
typeof resourceHeaderAuthExtendedCompatibility
|
||||||
|
>;
|
||||||
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>;
|
||||||
@@ -941,3 +1206,6 @@ export type SecurityKey = InferSelectModel<typeof securityKeys>;
|
|||||||
export type WebauthnChallenge = InferSelectModel<typeof webauthnChallenge>;
|
export type WebauthnChallenge = InferSelectModel<typeof webauthnChallenge>;
|
||||||
export type RequestAuditLog = InferSelectModel<typeof requestAuditLog>;
|
export type RequestAuditLog = InferSelectModel<typeof requestAuditLog>;
|
||||||
export type DeviceWebAuthCode = InferSelectModel<typeof deviceWebAuthCodes>;
|
export type DeviceWebAuthCode = InferSelectModel<typeof deviceWebAuthCodes>;
|
||||||
|
export type RoundTripMessageTracker = InferSelectModel<
|
||||||
|
typeof roundTripMessageTracker
|
||||||
|
>;
|
||||||
|
|||||||
118
server/emails/templates/EnterpriseEditionKeyGenerated.tsx
Normal file
118
server/emails/templates/EnterpriseEditionKeyGenerated.tsx
Normal file
@@ -0,0 +1,118 @@
|
|||||||
|
import React from "react";
|
||||||
|
import { Body, Head, Html, Preview, Tailwind } from "@react-email/components";
|
||||||
|
import { themeColors } from "./lib/theme";
|
||||||
|
import {
|
||||||
|
EmailContainer,
|
||||||
|
EmailFooter,
|
||||||
|
EmailGreeting,
|
||||||
|
EmailHeading,
|
||||||
|
EmailInfoSection,
|
||||||
|
EmailLetterHead,
|
||||||
|
EmailSection,
|
||||||
|
EmailSignature,
|
||||||
|
EmailText
|
||||||
|
} from "./components/Email";
|
||||||
|
import CopyCodeBox from "./components/CopyCodeBox";
|
||||||
|
import ButtonLink from "./components/ButtonLink";
|
||||||
|
|
||||||
|
type EnterpriseEditionKeyGeneratedProps = {
|
||||||
|
keyValue: string;
|
||||||
|
personalUseOnly: boolean;
|
||||||
|
users: number;
|
||||||
|
sites: number;
|
||||||
|
modifySubscriptionLink?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const EnterpriseEditionKeyGenerated = ({
|
||||||
|
keyValue,
|
||||||
|
personalUseOnly,
|
||||||
|
users,
|
||||||
|
sites,
|
||||||
|
modifySubscriptionLink
|
||||||
|
}: EnterpriseEditionKeyGeneratedProps) => {
|
||||||
|
const previewText = personalUseOnly
|
||||||
|
? "Your Enterprise Edition key for personal use is ready"
|
||||||
|
: "Thank you for your purchase — your Enterprise Edition key is ready";
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Html>
|
||||||
|
<Head />
|
||||||
|
<Preview>{previewText}</Preview>
|
||||||
|
<Tailwind config={themeColors}>
|
||||||
|
<Body className="font-sans bg-gray-50">
|
||||||
|
<EmailContainer>
|
||||||
|
<EmailLetterHead />
|
||||||
|
|
||||||
|
<EmailGreeting>Hi there,</EmailGreeting>
|
||||||
|
|
||||||
|
{personalUseOnly ? (
|
||||||
|
<EmailText>
|
||||||
|
Your Enterprise Edition license key has been
|
||||||
|
generated. Qualifying users can use the
|
||||||
|
Enterprise Edition for free for{" "}
|
||||||
|
<strong>personal use only</strong>.
|
||||||
|
</EmailText>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<EmailText>
|
||||||
|
Thank you for your purchase. Your Enterprise
|
||||||
|
Edition license key is ready. Below are the
|
||||||
|
terms of your license.
|
||||||
|
</EmailText>
|
||||||
|
<EmailInfoSection
|
||||||
|
title="License details"
|
||||||
|
items={[
|
||||||
|
{
|
||||||
|
label: "Licensed users",
|
||||||
|
value: users
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Licensed sites",
|
||||||
|
value: sites
|
||||||
|
}
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
{modifySubscriptionLink && (
|
||||||
|
<EmailSection>
|
||||||
|
<ButtonLink
|
||||||
|
href={modifySubscriptionLink}
|
||||||
|
>
|
||||||
|
Modify subscription
|
||||||
|
</ButtonLink>
|
||||||
|
</EmailSection>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<EmailSection>
|
||||||
|
<EmailText>Your license key:</EmailText>
|
||||||
|
<CopyCodeBox
|
||||||
|
text={keyValue}
|
||||||
|
hint="Copy this key and use it when activating Enterprise Edition on your Pangolin host."
|
||||||
|
/>
|
||||||
|
</EmailSection>
|
||||||
|
|
||||||
|
<EmailText>
|
||||||
|
If you need to purchase additional license keys or
|
||||||
|
modify your existing license, please reach out to
|
||||||
|
our support team at{" "}
|
||||||
|
<a
|
||||||
|
href="mailto:support@pangolin.net"
|
||||||
|
className="text-primary font-medium"
|
||||||
|
>
|
||||||
|
support@pangolin.net
|
||||||
|
</a>
|
||||||
|
.
|
||||||
|
</EmailText>
|
||||||
|
|
||||||
|
<EmailFooter>
|
||||||
|
<EmailSignature />
|
||||||
|
</EmailFooter>
|
||||||
|
</EmailContainer>
|
||||||
|
</Body>
|
||||||
|
</Tailwind>
|
||||||
|
</Html>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default EnterpriseEditionKeyGenerated;
|
||||||
@@ -1,6 +1,14 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
|
|
||||||
export default function CopyCodeBox({ text }: { text: string }) {
|
const DEFAULT_HINT = "Copy and paste this code when prompted";
|
||||||
|
|
||||||
|
export default function CopyCodeBox({
|
||||||
|
text,
|
||||||
|
hint
|
||||||
|
}: {
|
||||||
|
text: string;
|
||||||
|
hint?: string;
|
||||||
|
}) {
|
||||||
return (
|
return (
|
||||||
<div className="inline-block">
|
<div className="inline-block">
|
||||||
<div className="bg-gray-50 border border-gray-200 rounded-lg px-6 py-4 mx-auto">
|
<div className="bg-gray-50 border border-gray-200 rounded-lg px-6 py-4 mx-auto">
|
||||||
@@ -8,9 +16,7 @@ export default function CopyCodeBox({ text }: { text: string }) {
|
|||||||
{text}
|
{text}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<p className="text-xs text-gray-500 mt-2">
|
<p className="text-xs text-gray-500 mt-2">{hint ?? DEFAULT_HINT}</p>
|
||||||
Copy and paste this code when prompted
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -74,7 +74,7 @@ declare global {
|
|||||||
session: Session;
|
session: Session;
|
||||||
userOrg?: UserOrg;
|
userOrg?: UserOrg;
|
||||||
apiKeyOrg?: ApiKeyOrg;
|
apiKeyOrg?: ApiKeyOrg;
|
||||||
userOrgRoleId?: number;
|
userOrgRoleIds?: number[];
|
||||||
userOrgId?: string;
|
userOrgId?: string;
|
||||||
userOrgIds?: string[];
|
userOrgIds?: string[];
|
||||||
remoteExitNode?: RemoteExitNode;
|
remoteExitNode?: RemoteExitNode;
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ import fs from "fs";
|
|||||||
import path from "path";
|
import path from "path";
|
||||||
import { APP_PATH } from "./lib/consts";
|
import { APP_PATH } from "./lib/consts";
|
||||||
import yaml from "js-yaml";
|
import yaml from "js-yaml";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
const dev = process.env.ENVIRONMENT !== "prod";
|
const dev = process.env.ENVIRONMENT !== "prod";
|
||||||
const externalPort = config.getRawConfig().server.integration_port;
|
const externalPort = config.getRawConfig().server.integration_port;
|
||||||
@@ -38,12 +39,24 @@ export function createIntegrationApiServer() {
|
|||||||
apiServer.use(cookieParser());
|
apiServer.use(cookieParser());
|
||||||
apiServer.use(express.json());
|
apiServer.use(express.json());
|
||||||
|
|
||||||
|
const openApiDocumentation = getOpenApiDocumentation();
|
||||||
|
|
||||||
apiServer.use(
|
apiServer.use(
|
||||||
"/v1/docs",
|
"/v1/docs",
|
||||||
swaggerUi.serve,
|
swaggerUi.serve,
|
||||||
swaggerUi.setup(getOpenApiDocumentation())
|
swaggerUi.setup(openApiDocumentation)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Unauthenticated OpenAPI spec endpoints
|
||||||
|
apiServer.get("/v1/openapi.json", (_req, res) => {
|
||||||
|
res.json(openApiDocumentation);
|
||||||
|
});
|
||||||
|
|
||||||
|
apiServer.get("/v1/openapi.yaml", (_req, res) => {
|
||||||
|
const yamlOutput = yaml.dump(openApiDocumentation);
|
||||||
|
res.type("application/yaml").send(yamlOutput);
|
||||||
|
});
|
||||||
|
|
||||||
// API routes
|
// API routes
|
||||||
const prefix = `/v1`;
|
const prefix = `/v1`;
|
||||||
apiServer.use(logIncomingMiddleware);
|
apiServer.use(logIncomingMiddleware);
|
||||||
@@ -75,16 +88,6 @@ function getOpenApiDocumentation() {
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
for (const def of registry.definitions) {
|
|
||||||
if (def.type === "route") {
|
|
||||||
def.route.security = [
|
|
||||||
{
|
|
||||||
[bearerAuth.name]: []
|
|
||||||
}
|
|
||||||
];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
registry.registerPath({
|
registry.registerPath({
|
||||||
method: "get",
|
method: "get",
|
||||||
path: "/",
|
path: "/",
|
||||||
@@ -94,6 +97,74 @@ function getOpenApiDocumentation() {
|
|||||||
responses: {}
|
responses: {}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
registry.registerPath({
|
||||||
|
method: "get",
|
||||||
|
path: "/openapi.json",
|
||||||
|
description: "Get OpenAPI specification as JSON",
|
||||||
|
tags: [],
|
||||||
|
request: {},
|
||||||
|
responses: {
|
||||||
|
"200": {
|
||||||
|
description: "OpenAPI specification as JSON",
|
||||||
|
content: {
|
||||||
|
"application/json": {
|
||||||
|
schema: {
|
||||||
|
type: "object"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
registry.registerPath({
|
||||||
|
method: "get",
|
||||||
|
path: "/openapi.yaml",
|
||||||
|
description: "Get OpenAPI specification as YAML",
|
||||||
|
tags: [],
|
||||||
|
request: {},
|
||||||
|
responses: {
|
||||||
|
"200": {
|
||||||
|
description: "OpenAPI specification as YAML",
|
||||||
|
content: {
|
||||||
|
"application/yaml": {
|
||||||
|
schema: {
|
||||||
|
type: "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
for (const def of registry.definitions) {
|
||||||
|
if (def.type === "route") {
|
||||||
|
def.route.security = [
|
||||||
|
{
|
||||||
|
[bearerAuth.name]: []
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
// Ensure every route has a generic JSON response schema so Swagger UI can render responses
|
||||||
|
const existingResponses = def.route.responses;
|
||||||
|
const hasExistingResponses =
|
||||||
|
existingResponses && Object.keys(existingResponses).length > 0;
|
||||||
|
|
||||||
|
if (!hasExistingResponses) {
|
||||||
|
def.route.responses = {
|
||||||
|
"*": {
|
||||||
|
description: "",
|
||||||
|
content: {
|
||||||
|
"application/json": {
|
||||||
|
schema: z.object({})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const generator = new OpenApiGeneratorV3(registry.definitions);
|
const generator = new OpenApiGeneratorV3(registry.definitions);
|
||||||
|
|
||||||
const generated = generator.generateDocument({
|
const generated = generator.generateDocument({
|
||||||
@@ -105,11 +176,13 @@ function getOpenApiDocumentation() {
|
|||||||
servers: [{ url: "/v1" }]
|
servers: [{ url: "/v1" }]
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (!process.env.DISABLE_GEN_OPENAPI) {
|
||||||
// convert to yaml and save to file
|
// convert to yaml and save to file
|
||||||
const outputPath = path.join(APP_PATH, "openapi.yaml");
|
const outputPath = path.join(APP_PATH, "openapi.yaml");
|
||||||
const yamlOutput = yaml.dump(generated);
|
const yamlOutput = yaml.dump(generated);
|
||||||
fs.writeFileSync(outputPath, yamlOutput, "utf8");
|
fs.writeFileSync(outputPath, yamlOutput, "utf8");
|
||||||
logger.info(`OpenAPI documentation saved to ${outputPath}`);
|
logger.info(`OpenAPI documentation saved to ${outputPath}`);
|
||||||
|
}
|
||||||
|
|
||||||
return generated;
|
return generated;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,6 +16,11 @@ const internalPort = config.getRawConfig().server.internal_port;
|
|||||||
export function createInternalServer() {
|
export function createInternalServer() {
|
||||||
const internalServer = express();
|
const internalServer = express();
|
||||||
|
|
||||||
|
const trustProxy = config.getRawConfig().server.trust_proxy;
|
||||||
|
if (trustProxy) {
|
||||||
|
internalServer.set("trust proxy", trustProxy);
|
||||||
|
}
|
||||||
|
|
||||||
internalServer.use(helmet());
|
internalServer.use(helmet());
|
||||||
internalServer.use(cors());
|
internalServer.use(cors());
|
||||||
internalServer.use(stripDuplicateSesions);
|
internalServer.use(stripDuplicateSesions);
|
||||||
|
|||||||
@@ -1,30 +1,44 @@
|
|||||||
import Stripe from "stripe";
|
|
||||||
|
|
||||||
export enum FeatureId {
|
export enum FeatureId {
|
||||||
SITE_UPTIME = "siteUptime",
|
|
||||||
USERS = "users",
|
USERS = "users",
|
||||||
|
SITES = "sites",
|
||||||
EGRESS_DATA_MB = "egressDataMb",
|
EGRESS_DATA_MB = "egressDataMb",
|
||||||
DOMAINS = "domains",
|
DOMAINS = "domains",
|
||||||
REMOTE_EXIT_NODES = "remoteExitNodes"
|
REMOTE_EXIT_NODES = "remoteExitNodes",
|
||||||
|
ORGINIZATIONS = "organizations",
|
||||||
|
TIER1 = "tier1"
|
||||||
}
|
}
|
||||||
|
|
||||||
export const FeatureMeterIds: Record<FeatureId, string> = {
|
export async function getFeatureDisplayName(featureId: FeatureId): Promise<string> {
|
||||||
[FeatureId.SITE_UPTIME]: "mtr_61Srrej5wUJuiTWgo41D3Ee2Ir7WmDLU",
|
switch (featureId) {
|
||||||
[FeatureId.USERS]: "mtr_61SrreISyIWpwUNGR41D3Ee2Ir7WmQro",
|
case FeatureId.USERS:
|
||||||
[FeatureId.EGRESS_DATA_MB]: "mtr_61Srreh9eWrExDSCe41D3Ee2Ir7Wm5YW",
|
return "Users";
|
||||||
[FeatureId.DOMAINS]: "mtr_61Ss9nIKDNMw0LDRU41D3Ee2Ir7WmRPU",
|
case FeatureId.SITES:
|
||||||
[FeatureId.REMOTE_EXIT_NODES]: "mtr_61T86UXnfxTVXy9sD41D3Ee2Ir7WmFTE"
|
return "Sites";
|
||||||
|
case FeatureId.EGRESS_DATA_MB:
|
||||||
|
return "Egress Data (MB)";
|
||||||
|
case FeatureId.DOMAINS:
|
||||||
|
return "Domains";
|
||||||
|
case FeatureId.REMOTE_EXIT_NODES:
|
||||||
|
return "Remote Exit Nodes";
|
||||||
|
case FeatureId.ORGINIZATIONS:
|
||||||
|
return "Organizations";
|
||||||
|
case FeatureId.TIER1:
|
||||||
|
return "Home Lab";
|
||||||
|
default:
|
||||||
|
return featureId;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// this is from the old system
|
||||||
|
export const FeatureMeterIds: Partial<Record<FeatureId, string>> = { // right now we are not charging for any data
|
||||||
|
// [FeatureId.EGRESS_DATA_MB]: "mtr_61Srreh9eWrExDSCe41D3Ee2Ir7Wm5YW"
|
||||||
};
|
};
|
||||||
|
|
||||||
export const FeatureMeterIdsSandbox: Record<FeatureId, string> = {
|
export const FeatureMeterIdsSandbox: Partial<Record<FeatureId, string>> = {
|
||||||
[FeatureId.SITE_UPTIME]: "mtr_test_61Snh3cees4w60gv841DCpkOb237BDEu",
|
// [FeatureId.EGRESS_DATA_MB]: "mtr_test_61Snh2a2m6qome5Kv41DCpkOb237B3dQ"
|
||||||
[FeatureId.USERS]: "mtr_test_61Sn5fLtq1gSfRkyA41DCpkOb237B6au",
|
|
||||||
[FeatureId.EGRESS_DATA_MB]: "mtr_test_61Snh2a2m6qome5Kv41DCpkOb237B3dQ",
|
|
||||||
[FeatureId.DOMAINS]: "mtr_test_61SsA8qrdAlgPpFRQ41DCpkOb237BGts",
|
|
||||||
[FeatureId.REMOTE_EXIT_NODES]: "mtr_test_61T86Vqmwa3D9ra3341DCpkOb237B94K"
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export function getFeatureMeterId(featureId: FeatureId): string {
|
export function getFeatureMeterId(featureId: FeatureId): string | undefined {
|
||||||
if (
|
if (
|
||||||
process.env.ENVIRONMENT == "prod" &&
|
process.env.ENVIRONMENT == "prod" &&
|
||||||
process.env.SANDBOX_MODE !== "true"
|
process.env.SANDBOX_MODE !== "true"
|
||||||
@@ -43,45 +57,81 @@ export function getFeatureIdByMetricId(
|
|||||||
)?.[0];
|
)?.[0];
|
||||||
}
|
}
|
||||||
|
|
||||||
export type FeaturePriceSet = {
|
export type FeaturePriceSet = Partial<Record<FeatureId, string>>;
|
||||||
[key in Exclude<FeatureId, FeatureId.DOMAINS>]: string;
|
|
||||||
} & {
|
export const tier1FeaturePriceSet: FeaturePriceSet = {
|
||||||
[FeatureId.DOMAINS]?: string; // Optional since domains are not billed
|
[FeatureId.TIER1]: "price_1SzVE3D3Ee2Ir7Wm6wT5Dl3G"
|
||||||
};
|
};
|
||||||
|
|
||||||
export const standardFeaturePriceSet: FeaturePriceSet = {
|
export const tier1FeaturePriceSetSandbox: FeaturePriceSet = {
|
||||||
// Free tier matches the freeLimitSet
|
[FeatureId.TIER1]: "price_1SxgpPDCpkOb237Bfo4rIsoT"
|
||||||
[FeatureId.SITE_UPTIME]: "price_1RrQc4D3Ee2Ir7WmaJGZ3MtF",
|
|
||||||
[FeatureId.USERS]: "price_1RrQeJD3Ee2Ir7WmgveP3xea",
|
|
||||||
[FeatureId.EGRESS_DATA_MB]: "price_1RrQXFD3Ee2Ir7WmvGDlgxQk",
|
|
||||||
// [FeatureId.DOMAINS]: "price_1Rz3tMD3Ee2Ir7Wm5qLeASzC",
|
|
||||||
[FeatureId.REMOTE_EXIT_NODES]: "price_1S46weD3Ee2Ir7Wm94KEHI4h"
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const standardFeaturePriceSetSandbox: FeaturePriceSet = {
|
export function getTier1FeaturePriceSet(): FeaturePriceSet {
|
||||||
// Free tier matches the freeLimitSet
|
|
||||||
[FeatureId.SITE_UPTIME]: "price_1RefFBDCpkOb237BPrKZ8IEU",
|
|
||||||
[FeatureId.USERS]: "price_1ReNa4DCpkOb237Bc67G5muF",
|
|
||||||
[FeatureId.EGRESS_DATA_MB]: "price_1Rfp9LDCpkOb237BwuN5Oiu0",
|
|
||||||
// [FeatureId.DOMAINS]: "price_1Ryi88DCpkOb237B2D6DM80b",
|
|
||||||
[FeatureId.REMOTE_EXIT_NODES]: "price_1RyiZvDCpkOb237BXpmoIYJL"
|
|
||||||
};
|
|
||||||
|
|
||||||
export function getStandardFeaturePriceSet(): FeaturePriceSet {
|
|
||||||
if (
|
if (
|
||||||
process.env.ENVIRONMENT == "prod" &&
|
process.env.ENVIRONMENT == "prod" &&
|
||||||
process.env.SANDBOX_MODE !== "true"
|
process.env.SANDBOX_MODE !== "true"
|
||||||
) {
|
) {
|
||||||
return standardFeaturePriceSet;
|
return tier1FeaturePriceSet;
|
||||||
} else {
|
} else {
|
||||||
return standardFeaturePriceSetSandbox;
|
return tier1FeaturePriceSetSandbox;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getLineItems(
|
export const tier2FeaturePriceSet: FeaturePriceSet = {
|
||||||
featurePriceSet: FeaturePriceSet
|
[FeatureId.USERS]: "price_1SzVCcD3Ee2Ir7Wmn6U3KvPN"
|
||||||
): Stripe.Checkout.SessionCreateParams.LineItem[] {
|
};
|
||||||
return Object.entries(featurePriceSet).map(([featureId, priceId]) => ({
|
|
||||||
price: priceId
|
export const tier2FeaturePriceSetSandbox: FeaturePriceSet = {
|
||||||
}));
|
[FeatureId.USERS]: "price_1SxaEHDCpkOb237BD9lBkPiR"
|
||||||
|
};
|
||||||
|
|
||||||
|
export function getTier2FeaturePriceSet(): FeaturePriceSet {
|
||||||
|
if (
|
||||||
|
process.env.ENVIRONMENT == "prod" &&
|
||||||
|
process.env.SANDBOX_MODE !== "true"
|
||||||
|
) {
|
||||||
|
return tier2FeaturePriceSet;
|
||||||
|
} else {
|
||||||
|
return tier2FeaturePriceSetSandbox;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const tier3FeaturePriceSet: FeaturePriceSet = {
|
||||||
|
[FeatureId.USERS]: "price_1SzVDKD3Ee2Ir7WmPtOKNusv"
|
||||||
|
};
|
||||||
|
|
||||||
|
export const tier3FeaturePriceSetSandbox: FeaturePriceSet = {
|
||||||
|
[FeatureId.USERS]: "price_1SxaEODCpkOb237BiXdCBSfs"
|
||||||
|
};
|
||||||
|
|
||||||
|
export function getTier3FeaturePriceSet(): FeaturePriceSet {
|
||||||
|
if (
|
||||||
|
process.env.ENVIRONMENT == "prod" &&
|
||||||
|
process.env.SANDBOX_MODE !== "true"
|
||||||
|
) {
|
||||||
|
return tier3FeaturePriceSet;
|
||||||
|
} else {
|
||||||
|
return tier3FeaturePriceSetSandbox;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getFeatureIdByPriceId(priceId: string): FeatureId | undefined {
|
||||||
|
// Check all feature price sets
|
||||||
|
const allPriceSets = [
|
||||||
|
getTier1FeaturePriceSet(),
|
||||||
|
getTier2FeaturePriceSet(),
|
||||||
|
getTier3FeaturePriceSet()
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const priceSet of allPriceSets) {
|
||||||
|
const entry = (Object.entries(priceSet) as [FeatureId, string][]).find(
|
||||||
|
([_, price]) => price === priceId
|
||||||
|
);
|
||||||
|
if (entry) {
|
||||||
|
return entry[0];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|||||||
25
server/lib/billing/getLineItems.ts
Normal file
25
server/lib/billing/getLineItems.ts
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
import Stripe from "stripe";
|
||||||
|
import { FeatureId, FeaturePriceSet } from "./features";
|
||||||
|
import { usageService } from "./usageService";
|
||||||
|
|
||||||
|
export async function getLineItems(
|
||||||
|
featurePriceSet: FeaturePriceSet,
|
||||||
|
orgId: string,
|
||||||
|
): Promise<Stripe.Checkout.SessionCreateParams.LineItem[]> {
|
||||||
|
const users = await usageService.getUsage(orgId, FeatureId.USERS);
|
||||||
|
|
||||||
|
return Object.entries(featurePriceSet).map(([featureId, priceId]) => {
|
||||||
|
let quantity: number | undefined;
|
||||||
|
|
||||||
|
if (featureId === FeatureId.USERS) {
|
||||||
|
quantity = users?.instantaneousValue || 1;
|
||||||
|
} else if (featureId === FeatureId.TIER1) {
|
||||||
|
quantity = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
price: priceId,
|
||||||
|
quantity: quantity
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
37
server/lib/billing/licenses.ts
Normal file
37
server/lib/billing/licenses.ts
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
export enum LicenseId {
|
||||||
|
SMALL_LICENSE = "small_license",
|
||||||
|
BIG_LICENSE = "big_license"
|
||||||
|
}
|
||||||
|
|
||||||
|
export type LicensePriceSet = {
|
||||||
|
[key in LicenseId]: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const licensePriceSet: LicensePriceSet = {
|
||||||
|
// Free license matches the freeLimitSet
|
||||||
|
[LicenseId.SMALL_LICENSE]: "price_1SxKHiD3Ee2Ir7WmvtEh17A8",
|
||||||
|
[LicenseId.BIG_LICENSE]: "price_1SxKHiD3Ee2Ir7WmMUiP0H6Y"
|
||||||
|
};
|
||||||
|
|
||||||
|
export const licensePriceSetSandbox: LicensePriceSet = {
|
||||||
|
// Free license matches the freeLimitSet
|
||||||
|
// when matching license the keys closer to 0 index are matched first so list the licenses in descending order of value
|
||||||
|
[LicenseId.SMALL_LICENSE]: "price_1SxDwuDCpkOb237Bz0yTiOgN",
|
||||||
|
[LicenseId.BIG_LICENSE]: "price_1SxDy0DCpkOb237BWJxrxYkl"
|
||||||
|
};
|
||||||
|
|
||||||
|
export function getLicensePriceSet(
|
||||||
|
environment?: string,
|
||||||
|
sandbox_mode?: boolean
|
||||||
|
): LicensePriceSet {
|
||||||
|
if (
|
||||||
|
(process.env.ENVIRONMENT == "prod" &&
|
||||||
|
process.env.SANDBOX_MODE !== "true") ||
|
||||||
|
(environment === "prod" && sandbox_mode !== true)
|
||||||
|
) {
|
||||||
|
// THIS GETS LOADED CLIENT SIDE AND SERVER SIDE
|
||||||
|
return licensePriceSet;
|
||||||
|
} else {
|
||||||
|
return licensePriceSetSandbox;
|
||||||
|
}
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user