Compare commits
946 commits
Author | SHA1 | Date | |
---|---|---|---|
|
07b86521a5 | ||
|
961008bbe1 | ||
|
92e69f561f | ||
|
69b28b9b02 | ||
|
35a68703c2 | ||
|
c49fe04750 | ||
|
31feabbec7 | ||
|
bc3cb2c3c9 | ||
|
5ec4481c92 | ||
|
be5cb48dfe | ||
|
48ff1ece16 | ||
|
ed20ed592f | ||
|
4fb3435c29 | ||
|
37eb14a01a | ||
|
d403bc86e3 | ||
|
0e2f0f2a4d | ||
|
1a4d34a802 | ||
|
bb15af9954 | ||
|
8a250d1011 | ||
|
2f9994f600 | ||
|
1cca06a274 | ||
|
8fdb3ea631 | ||
|
35823d5751 | ||
|
66f90a542a | ||
|
49981c4bee | ||
|
d732c1a845 | ||
|
4d7e25f97b | ||
|
80656f48e0 | ||
|
ebde149980 | ||
|
adc0a81592 | ||
|
b596f00ce5 | ||
|
448442f92b | ||
|
8518201562 | ||
|
17586c4559 | ||
|
f8622da7d4 | ||
|
b1a27e9060 | ||
|
3c6423d444 | ||
|
91b03160ea | ||
|
0c1e20ba48 | ||
|
1dcac85c0d | ||
|
3fc72dbec2 | ||
|
494329f568 | ||
|
a1e8211ba7 | ||
|
80aa7502af | ||
|
67bae76048 | ||
|
bda2aa46b6 | ||
|
6d9ba8dd2f | ||
|
2ca8febff7 | ||
|
e105a523e4 | ||
|
28f8b05dbc | ||
|
d95286db0e | ||
|
8e45c34e8e | ||
|
9e87c42d0c | ||
|
0b52cd002e | ||
|
15adfcca8c | ||
|
1466788f77 | ||
|
760fe3aca9 | ||
|
5f75813e84 | ||
|
59cb06acf4 | ||
|
6349406523 | ||
|
bcc2c59f08 | ||
|
52d46f9879 | ||
|
0b50a5474d | ||
|
5c929badeb | ||
|
3f2de333fb | ||
|
7c12b8ae25 | ||
|
b54ccbfa2f | ||
|
114ce8997f | ||
|
f1bba3b958 | ||
|
053acef728 | ||
|
9f2710185b | ||
|
d000879c01 | ||
|
25ae169fee | ||
|
4443dda0f6 | ||
|
c484e989a9 | ||
|
86a4656651 | ||
|
f25aefeb11 | ||
|
228643c7d7 | ||
|
072d6d7094 | ||
|
de3ce672b8 | ||
|
6f5c191998 | ||
|
bbaea4def0 | ||
|
54f9282166 | ||
|
a39b1db266 | ||
|
2ddb4ec905 | ||
|
7a59e3acf7 | ||
|
b34c3db956 | ||
|
afea958aca | ||
|
dca2a29865 | ||
|
97b8e84143 | ||
|
23eb0da7d7 | ||
|
2edda471e7 | ||
|
676aa1358d | ||
|
87a36d6ae3 | ||
|
b67611094e | ||
|
2e986def78 | ||
|
d16a05959d | ||
|
7e58e0b490 | ||
|
9b01aecf3c | ||
|
86043fd5f8 | ||
|
372a1758e9 | ||
|
0a2b1d9e53 | ||
|
e562946308 | ||
|
398e15b3c6 | ||
|
c225a54dbe | ||
|
5148988dcc | ||
|
28b57ba652 | ||
|
9c7e74ef37 | ||
|
330b28ad9c | ||
|
da7166a7ea | ||
|
e8793c5d8d | ||
|
c63d45e344 | ||
|
1159a79410 | ||
|
5b5e65ac08 | ||
|
417811e94f | ||
|
9c40057c51 | ||
|
a6409c16c3 | ||
|
ba4667528c | ||
|
a2368d7c3f | ||
|
e90b203a7d | ||
|
b7389d74db | ||
|
24321521c7 | ||
|
f85497d446 | ||
|
1f9e92fdb7 | ||
|
b66d2b95c5 | ||
|
6f013b3bc4 | ||
|
cdf8a01c14 | ||
|
547923bbb4 | ||
|
a8c3c1bece | ||
|
0f47a5d51e | ||
|
9f9196427f | ||
|
341d688e79 | ||
|
92c5d1ed46 | ||
|
55d8579b1a | ||
|
51a74c5045 | ||
|
64f1af4d0b | ||
|
8dd5daf495 | ||
|
0ee87d736c | ||
|
ba822af355 | ||
|
78c1990b42 | ||
|
768179a664 | ||
|
1268c46f02 | ||
|
d870a49381 | ||
|
8e476cc07e | ||
|
ea54fd6e98 | ||
|
ea001cc0db | ||
|
2378343c74 | ||
|
018b3346bb | ||
|
d104829a6d | ||
|
0035460712 | ||
|
cae57ffcd8 | ||
|
b587ff663c | ||
|
531a888592 | ||
|
a13b167c41 | ||
|
c1f9195a38 | ||
|
e1ea7c6986 | ||
|
bd21927069 | ||
|
f1f4f7e5f3 | ||
|
ccdcda7f1d | ||
|
e9586b7336 | ||
|
da36a218df | ||
|
1723089844 | ||
|
e99eb04e2f | ||
|
5fddd71fc2 | ||
|
c682b03736 | ||
|
4008767e1b | ||
|
74c768e2fb | ||
|
03b883c320 | ||
|
807ddaa0bb | ||
|
da9d555168 | ||
|
05ba2c0103 | ||
|
de96b722d5 | ||
|
afde493040 | ||
|
cce440cf49 | ||
|
e96dbdb026 | ||
|
f4cd163c34 | ||
|
0af9bdc964 | ||
|
71bb1105f7 | ||
|
84c28645be | ||
|
665ef08733 | ||
|
b3e63a5f8a | ||
|
92c09b8843 | ||
|
7b726a41ef | ||
|
a1a0587a2b | ||
|
98be9cee83 | ||
|
0646934c9d | ||
|
f0f68632ff | ||
|
e557bda48e | ||
|
2992de5139 | ||
|
779532b1c9 | ||
|
300175ac67 | ||
|
a356147164 | ||
|
d1489a9a78 | ||
|
021bc073a2 | ||
|
4e571e1e4d | ||
|
1dff9baa61 | ||
|
19d54778f5 | ||
|
99d0fa974b | ||
|
84ca275f1e | ||
|
963bc5f0bc | ||
|
b446c2ce4b | ||
|
65a4f7af28 | ||
|
f6b2c5bbf3 | ||
|
e99905e3c9 | ||
|
5c0b42446d | ||
|
a3be0d4655 | ||
|
47729c2348 | ||
|
78bfcf5b1c | ||
|
f1291d4d7d | ||
|
ff809416f5 | ||
|
83a4677026 | ||
|
ffe8593a07 | ||
|
60e4db16ff | ||
|
2e510778be | ||
|
c492559010 | ||
|
a8dc36b6d2 | ||
|
379d31aac6 | ||
|
28fc1ab063 | ||
|
75e01018a5 | ||
|
b7df0b122d | ||
|
f9798a8d86 | ||
|
7691bdd181 | ||
|
3dc79da2fa | ||
|
83b00c1cfa | ||
|
d6fdb38c22 | ||
|
3505342a8d | ||
|
78661799f2 | ||
|
ec57996b01 | ||
|
0a97d91aed | ||
|
753027ffc7 | ||
|
d3383f0f1a | ||
|
9075ecb007 | ||
|
4b8cc7c4d3 | ||
|
2f20397c60 | ||
|
7f227932da | ||
|
69d253fba3 | ||
|
b75800c583 | ||
|
a35add3fc6 | ||
|
b17ff57582 | ||
|
915ccdc007 | ||
|
98a261e38c | ||
|
c9f5ffae42 | ||
|
342675276b | ||
|
590296e64d | ||
|
17b39d16a3 | ||
|
515a621eb4 | ||
|
ca0e950cdf | ||
|
cec9e4101d | ||
|
862fabf6ad | ||
|
128a9aae4e | ||
|
1983d0067d | ||
|
bdaf48da20 | ||
|
09024a93e9 | ||
|
064e7071b2 | ||
|
2b2fedb380 | ||
|
5ff7c216b9 | ||
|
c476ac7bc5 | ||
|
e83e8c2ee4 | ||
|
607b168b56 | ||
|
e0cf0916dd | ||
|
0f3a5ce8ba | ||
|
2a6298e9eb | ||
|
f97b133c8c | ||
|
f11fa4f32d | ||
|
f0a1c10ec5 | ||
|
08fed8fe93 | ||
|
813992141a | ||
|
f90129213e | ||
|
838af30a38 | ||
|
13ff15311a | ||
|
56e237c479 | ||
|
ff1e10a025 | ||
|
c259037dbf | ||
|
7b4878620f | ||
|
38fd58c173 | ||
|
d1185d43f7 | ||
|
a093c54b16 | ||
|
30cc85b793 | ||
|
5009906385 | ||
|
6ccc05b183 | ||
|
3994b25a71 | ||
|
5130071a60 | ||
|
d5e67835aa | ||
|
bf8078ed66 | ||
|
1559a2a943 | ||
|
e2d5301376 | ||
|
f31717145f | ||
|
db76558944 | ||
|
1bcb027e05 | ||
|
719d75f8a6 | ||
|
8c164c410d | ||
|
8c6a4a90aa | ||
|
c679875273 | ||
|
1e5141c27c | ||
|
e56330be47 | ||
|
764a63d784 | ||
|
6f280c4664 | ||
|
9b29f38d10 | ||
|
6b60410791 | ||
|
dffaceba6f | ||
|
4ffdd6f74f | ||
|
3499dd4f56 | ||
|
7004820326 | ||
|
24a17235ae | ||
|
66e8a4666c | ||
|
862dd6f0bc | ||
|
83afb23ac4 | ||
|
a0381eb2c6 | ||
|
3b6a44e683 | ||
|
baee745d3c | ||
|
cc930ebf53 | ||
|
809a135721 | ||
|
073c318f12 | ||
|
8f1cfd8037 | ||
|
7bf9cccbf6 | ||
|
d194e230de | ||
|
bd3d9d2da3 | ||
|
e694817b57 | ||
|
66191a9610 | ||
|
9bb4d8b2a3 | ||
|
2f5579b070 | ||
|
bc5d7f5f57 | ||
|
d729be0d71 | ||
|
7ccf7f6f15 | ||
|
918dc98f71 | ||
|
79a72528d7 | ||
|
0f2cc7d425 | ||
|
8f75725987 | ||
|
2918a3f767 | ||
|
598c206bbd | ||
|
34180ca454 | ||
|
fb5010a2b5 | ||
|
0e87b6e48b | ||
|
d45443258b | ||
|
f3b44a3085 | ||
|
9680260104 | ||
|
2ead5f4506 | ||
|
4f68a26049 | ||
|
49b88002fb | ||
|
ead5df0a8c | ||
|
9f38ad9b4d | ||
|
317a15b649 | ||
|
7748fb682d | ||
|
6f601c7814 | ||
|
c63dcd89b5 | ||
|
8c6e3be8ce | ||
|
13bfeae780 | ||
|
051162cd69 | ||
|
9065d21778 | ||
|
cafbad88f3 | ||
|
20965fc67b | ||
|
3efb04a603 | ||
|
d650d1e6eb | ||
|
2b23587200 | ||
|
a839f9146f | ||
|
a6138a02fd | ||
|
0e18fc4700 | ||
|
68015511c1 | ||
|
715d33fe90 | ||
|
85b99852bb | ||
|
a54c88eb32 | ||
|
6a5bdd40b6 | ||
|
d03f45279c | ||
|
e7b45df81f | ||
|
91df8c0556 | ||
|
2fd8134a57 | ||
|
494b54ac32 | ||
|
377eb2b851 | ||
|
bd7e96b8af | ||
|
acf25e8ad7 | ||
|
f300838f8e | ||
|
1bf2e23f5d | ||
|
58ba0d07b0 | ||
|
97ae76e4e7 | ||
|
4b24d722b6 | ||
|
09efed4331 | ||
|
a0ac757982 | ||
|
c86418dbbb | ||
|
c043912f94 | ||
|
b56ba3ee23 | ||
|
0b8bb5a974 | ||
|
3b3d7b134a | ||
|
b84b78a34d | ||
|
954b13ac60 | ||
|
bae540966b | ||
|
79d4ab1671 | ||
|
31104d3d04 | ||
|
f254d98712 | ||
|
b403f5018b | ||
|
fd219d5780 | ||
|
17ac5a5e81 | ||
|
a24431bc3b | ||
|
cdfeb2ff86 | ||
|
8199202dc3 | ||
|
7fd1fb89f1 | ||
|
32e54d0f94 | ||
|
21e9edd201 | ||
|
a0001aaa74 | ||
|
2b77c0fac8 | ||
|
81ab008d83 | ||
|
753307bb99 | ||
|
970feb75dd | ||
|
08556789f9 | ||
|
72d8ad3204 | ||
|
6cdf53e262 | ||
|
f6d81c3a23 | ||
|
4d7b905e98 | ||
|
8c42dee5de | ||
|
23d529bb31 | ||
|
7bbb687047 | ||
|
b39708700d | ||
|
d46b9eaf87 | ||
|
3e60a2bd6f | ||
|
4e2e434947 | ||
|
af32dfbbcd | ||
|
fc19d0ba8b | ||
|
83c10166e2 | ||
|
ddd292422b | ||
|
8bb0401c25 | ||
|
139c9d2ce3 | ||
|
21f4623e3e | ||
|
b86ef93211 | ||
|
6384bcd934 | ||
|
3d4177bd93 | ||
|
c6e1a9a171 | ||
|
13825568fe | ||
|
05c6a010e4 | ||
|
98178eaf24 | ||
|
8266c26ef1 | ||
|
5c682fe923 | ||
|
100dd80764 | ||
|
a59222cabb | ||
|
867f1bcc96 | ||
|
459bc32f9d | ||
|
78b38c91e7 | ||
|
2ee88d6a46 | ||
|
8d651cd44d | ||
|
8aa95db9bc | ||
|
31a41576d8 | ||
|
9a2022a4fe | ||
|
335c9b1fea | ||
|
b395b65b86 | ||
|
768e4745e1 | ||
|
ba33064852 | ||
|
94b5aadd76 | ||
|
a65ea9c360 | ||
|
a6afce5c0e | ||
|
d26ec69445 | ||
|
3c2ea1a75f | ||
|
0537992603 | ||
|
6acc2b6a17 | ||
|
83b4976305 | ||
|
b1cbb1b50f | ||
|
ff9e5a383b | ||
|
d66739f69e | ||
|
fc6c93a08a | ||
|
9897c53ed3 | ||
|
ced34dd2c6 | ||
|
92caac309a | ||
|
3c7a91a047 | ||
|
571db825ad | ||
|
0ae5ac9947 | ||
|
cd35148e48 | ||
|
19ccf098f0 | ||
|
2b64c0e84e | ||
|
d62a3c64cf | ||
|
3503cc3338 | ||
|
6d83e29ee2 | ||
|
5f9c9eed0a | ||
|
97ef363461 | ||
|
c67a2dfa73 | ||
|
f29a5ccc67 | ||
|
cbca88f76b | ||
|
3ee9051bc1 | ||
|
097dafb553 | ||
|
915581dfe7 | ||
|
a2cf4ffac1 | ||
|
6b4e52a725 | ||
|
454d7c4a88 | ||
|
cb85ad460e | ||
|
e41eafd497 | ||
|
d70396a664 | ||
|
3d59556bcd | ||
|
c7018e92b0 | ||
|
a575bace39 | ||
|
2047aa30e1 | ||
|
3b10453af3 | ||
|
363b8b52af | ||
|
3257edc2a0 | ||
|
cd54e7dd38 | ||
|
9177eaba22 | ||
|
33ae2e08cc | ||
|
4fc61386d3 | ||
|
c409266954 | ||
|
57315a36ee | ||
|
63637b91a8 | ||
|
09238cd98a | ||
|
67b149ce4b | ||
|
96151de814 | ||
|
f2e461a1ee | ||
|
8125622c98 | ||
|
1a6942ccc9 | ||
|
7b0e1df778 | ||
|
6f8c538086 | ||
|
b353a8f9b4 | ||
|
0eb35f2221 | ||
|
7d6b114d67 | ||
|
a169256770 | ||
|
2e54afd72f | ||
|
26207bd951 | ||
|
3ed681e277 | ||
|
c135b5e3cf | ||
|
e648307f0b | ||
|
0e4f35e87a | ||
|
553dffd4ee | ||
|
b4b19d2263 | ||
|
c7c39676d1 | ||
|
a6348a3e28 | ||
|
75212f1e05 | ||
|
c1fd38ac39 | ||
|
33e2798313 | ||
|
f0cb65f65c | ||
|
e885676ad8 | ||
|
b75d0a921e | ||
|
34fa5fe438 | ||
|
c2449ce795 | ||
|
9bb6cb14a6 | ||
|
b6f67e0f0b | ||
|
980545c636 | ||
|
92135ff9c1 | ||
|
dd7b91f770 | ||
|
ab843b1a43 | ||
|
4593edbb45 | ||
|
96b451843c | ||
|
54aa3ce7d8 | ||
|
45a70152ee | ||
|
8c5f00a446 | ||
|
af98610d0d | ||
|
875ec662ad | ||
|
8800ec9675 | ||
|
df4da75c57 | ||
|
717dfae26c | ||
|
58a2a9dcc9 | ||
|
27a0df4ed4 | ||
|
6fc6f325a7 | ||
|
b46e49922c | ||
|
2cca561e51 | ||
|
fbc1aa25a3 | ||
|
e8870cf174 | ||
|
17919192e0 | ||
|
d768bb163a | ||
|
dc6fafba41 | ||
|
9f979c5019 | ||
|
c3d2c34279 | ||
|
430f187fde | ||
|
f438d2ddbf | ||
|
6d519af198 | ||
|
a34e88257d | ||
|
ea0ab0e63c | ||
|
80375cd0dc | ||
|
f817ba7664 | ||
|
3398088e03 | ||
|
e586dd50f4 | ||
|
5a71c0ba65 | ||
|
f13b6abd78 | ||
|
34c6b590d7 | ||
|
ab797203eb | ||
|
30e8b1f0fe | ||
|
d03bee98f5 | ||
|
fa365fb7b8 | ||
|
ea1cd4b0d4 | ||
|
be0c7444e9 | ||
|
858c809514 | ||
|
10ff2c8a65 | ||
|
167d0b6867 | ||
|
8c121daf6c | ||
|
a23d437bd3 | ||
|
cd280d1396 | ||
|
d18200739a | ||
|
a62b2e8d10 | ||
|
c92069a1f4 | ||
|
c5e37c1608 | ||
|
948eb7f6d0 | ||
|
62a0104e70 | ||
|
6dd8db5cd1 | ||
|
9ea7275371 | ||
|
c997b8625f | ||
|
6f3514199a | ||
|
0cfc4d7dad | ||
|
56fd366a7d | ||
|
1c18b2bffb | ||
|
23b5dcfbed | ||
|
d523ebe0e0 | ||
|
30ebbaaef0 | ||
|
dba5a73e0e | ||
|
f07e8d08c3 | ||
|
ea24759bb3 | ||
|
b467d6afa1 | ||
|
373441b7ab | ||
|
af3694da34 | ||
|
ae4ef4eb99 | ||
|
547e777eb0 | ||
|
d9ee40c898 | ||
|
eff812eaa8 | ||
|
731ec1da69 | ||
|
b8ed5ac1c5 | ||
|
d2d84be99a | ||
|
96bfc3cf36 | ||
|
6f54e3da9e | ||
|
825730052b | ||
|
edc8716297 | ||
|
3ee4aaf194 | ||
|
b9a5d486b9 | ||
|
d3b9363392 | ||
|
dc66ebeed6 | ||
|
1f584bf3e8 | ||
|
5b0200154a | ||
|
1e55d96376 | ||
|
285b99f1b7 | ||
|
8ad8f98f48 | ||
|
160a7ff3db | ||
|
a512148348 | ||
|
d9eccd6c13 | ||
|
1f95d7161a | ||
|
3a1f4d7545 | ||
|
492669f68a | ||
|
caded23b51 | ||
|
e9cc48a3ae | ||
|
4ed98c227b | ||
|
f66fb7d4a3 | ||
|
f25990a9a7 | ||
|
21d5b67ef1 | ||
|
198810121c | ||
|
408822ab7f | ||
|
840d5c2b66 | ||
|
491b4e7b18 | ||
|
89729a451c | ||
|
0fd3271ef4 | ||
|
fa21934d5d | ||
|
f91a4e88d5 | ||
|
3e9dc4753b | ||
|
b03415a0eb | ||
|
a8e8676b0a | ||
|
8242a66b97 | ||
|
d994a8100d | ||
|
1ee8561e2a | ||
|
bb7421c54e | ||
|
99352aa2a9 | ||
|
31d54eb63c | ||
|
58c12996f1 | ||
|
3dba4aa36d | ||
|
d88fc132cc | ||
|
c6ff868be8 | ||
|
e8d2cde465 | ||
|
2bd06ff493 | ||
|
75dc6edd51 | ||
|
afc6ee596d | ||
|
d47c2f9dcf | ||
|
23f9d314df | ||
|
cae4f5d840 | ||
|
1e72b0f854 | ||
|
4dd9f4736d | ||
|
e38941adf1 | ||
|
b9c7c8c966 | ||
|
87b95986c3 | ||
|
e48a0fcabc | ||
|
e9e9478f6c | ||
|
bc050097c3 | ||
|
dde2f45669 | ||
|
f62f2e3b08 | ||
|
0b235f985f | ||
|
bb0c1c839b | ||
|
4e02a7712a | ||
|
8df01208e0 | ||
|
938cc31b8a | ||
|
08bd3cfd0b | ||
|
3bb4b44f19 | ||
|
a058f4acf3 | ||
|
55222450f3 | ||
|
17789ef1a5 | ||
|
dd24b4ad74 | ||
|
6b8fa28308 | ||
|
d9aab7b3ff | ||
|
5b44f3552d | ||
|
fa1997adc1 | ||
|
29375385c0 | ||
|
4f5c3a86ff | ||
|
6e5391cb8f | ||
|
3d4b9d48e3 | ||
|
aca1cc0518 | ||
|
2543bf356c | ||
|
95fed840d4 | ||
|
8a377d73fd | ||
|
576fda2357 | ||
|
230c08e541 | ||
|
9e572685ba | ||
|
7f4135e0cf | ||
|
9d68c5666f | ||
|
d460dd35c7 | ||
|
059081ad8b | ||
|
7eb08474ff | ||
|
83c0379c6b | ||
|
21f1326045 | ||
|
f62e32724c | ||
|
5e052a446a | ||
|
a76dd9c9d1 | ||
|
9a167b5acb | ||
|
5d2f3186cc | ||
|
e58d10fc53 | ||
|
4392bb604c | ||
|
5a4a6655a5 | ||
|
a20befd89f | ||
|
a9f0b9aa38 | ||
|
f8e0219b49 | ||
|
cb431f3574 | ||
|
1ff3a9b2f9 | ||
|
237960fc5b | ||
|
752c474983 | ||
|
02ccb029ae | ||
|
3ebc01df8c | ||
|
81adcd9234 | ||
|
cffc156cf6 | ||
|
e4af990bf2 | ||
|
e236364124 | ||
|
f5a3fd7202 | ||
|
b3026ba663 | ||
|
18e6f16ce7 | ||
|
599d0a52bf | ||
|
eed6081ade | ||
|
c4ae34383d | ||
|
c543376a0a | ||
|
a5b782b72a | ||
|
4819f410e6 | ||
|
4084849fdc | ||
|
35e5f39c71 | ||
|
80d76befc9 | ||
|
893244100e | ||
|
2a43b3ce4a | ||
|
b82754c7af | ||
|
8793d3976d | ||
|
6e833d4cee | ||
|
b3d0b69c04 | ||
|
28ac5e1237 | ||
|
8990de5618 | ||
|
6aeddde1cd | ||
|
c3dbc64a58 | ||
|
2a00c877ea | ||
|
91b4bb4683 | ||
|
f4fd33b47f | ||
|
d6d6a59eee | ||
|
4dba75f913 | ||
|
548a883e3f | ||
|
a6d6aaaadd | ||
|
566e66daa4 | ||
|
97af632c61 | ||
|
5d6e15b0d6 | ||
|
419bacf55f | ||
|
960eb34c7d | ||
|
6f59d0cd2d | ||
|
6fd1dbc638 | ||
|
87915f29f6 | ||
|
181071e4f6 | ||
|
b01fa82627 | ||
|
feb558cfa8 | ||
|
0c370e4299 | ||
|
9ea7c43212 | ||
|
38528ae8c5 | ||
|
c837899d82 | ||
|
d737fda8bc | ||
|
7938b419cc | ||
|
475a431859 | ||
|
bf8bb1a0df | ||
|
957fa67e24 | ||
|
490ddfcd88 | ||
|
fa6fc9e80d | ||
|
f960fb7d67 | ||
|
b4c6897850 | ||
|
e2f056e6ca | ||
|
f6a19631dc | ||
|
8fa719181a | ||
|
b4fda6a1f6 | ||
|
e2efd0e65a | ||
|
361f487384 | ||
|
99188233db | ||
|
dc49027b30 | ||
|
581fdd67b1 | ||
|
3bab90891f | ||
|
8c0e4d2d8c | ||
|
3e94384cde | ||
|
189b739997 | ||
|
d664aa204f | ||
|
db0328fa71 | ||
|
334fc55dd0 | ||
|
ab933d48de | ||
|
36b62a5fe4 | ||
|
08752820fc | ||
|
787ec50a9c | ||
|
65b29161a0 | ||
|
f60f15345f | ||
|
c286c28d46 | ||
|
8fb003d7ce | ||
|
35daf42a55 | ||
|
976aaca287 | ||
|
0454f09383 | ||
|
6b5674a107 | ||
|
45a75d0bee | ||
|
12f627711c | ||
|
442775ac90 | ||
|
01da3b3225 | ||
|
51ac815b23 | ||
|
285ad45a0e | ||
|
4707722e6e | ||
|
499f75edd1 | ||
|
57b96adcd0 | ||
|
eb9675c6cf | ||
|
b59c6e377a | ||
|
432f38333e | ||
|
e86640547e | ||
|
25c125b96d | ||
|
aa3b527f67 | ||
|
569635f3ed | ||
|
bacd5a4373 | ||
|
53be2739bb | ||
|
b7ae712b63 | ||
|
4a42aa385a | ||
|
ac8e315fbd | ||
|
7556a59e11 | ||
|
8b0c30f19f | ||
|
b731a50cc9 | ||
|
2398931cc1 | ||
|
419e576a3e | ||
|
1a750e8279 | ||
|
f14379a1c8 | ||
|
521bbbf1d6 | ||
|
cb775340a4 | ||
|
31bd42f964 | ||
|
e64e7d1d92 | ||
|
480a5f648d | ||
|
9cb215295a | ||
|
764c56c4a1 | ||
|
e057c5f3bf | ||
|
bc8cd5c941 | ||
|
6350edf8fd | ||
|
8e8fdabd03 | ||
|
8398f7b7c0 | ||
|
2883d8c544 | ||
|
dd8c426faa | ||
|
e57574ba9c | ||
|
64a2cc23c6 | ||
|
ec33fe5657 | ||
|
56b3b2ab3b | ||
|
a436dff4a0 | ||
|
cf80d67bf8 | ||
|
e24edc0803 | ||
|
d89ca10a82 | ||
|
d9e6d0c71a | ||
|
517bc7f632 | ||
|
674316aa46 | ||
|
7a55c9ad03 | ||
|
c7f3c9da92 | ||
|
be77b3e8f3 | ||
|
d7f50bac6a | ||
|
3ccfe60685 | ||
|
40040af957 | ||
|
1568b38eac | ||
|
7fd1652a71 | ||
|
787a172a7c | ||
|
23a68fbc10 | ||
|
f078ee6051 | ||
|
0450f62108 | ||
|
b2faeb3c17 | ||
|
9ea37789d6 | ||
|
aa45150c51 | ||
|
a708750fea | ||
|
68ebdda1ff | ||
|
d260450a84 | ||
|
a76e3e00f7 | ||
|
a33ebe5bc5 | ||
|
b75f848b90 | ||
|
c4e62a7aee | ||
|
c903c03979 | ||
|
d7b9755f3a | ||
|
e17bf0db13 | ||
|
410207f3ca | ||
|
7faf1fba8b | ||
|
8c267489c7 | ||
|
fb5c428147 | ||
|
5ff4215bde | ||
|
96d6ad8142 | ||
|
875fa215c5 | ||
|
bcd80e19d4 | ||
|
56e1684e2e | ||
|
1baa02de89 | ||
|
11cdfa7557 | ||
|
8fbd8a905f | ||
|
cd059728fd | ||
|
473b5bd3db | ||
|
81c7954e0c | ||
|
a665e3aae9 | ||
|
4b6985718a | ||
|
619cfef1c7 | ||
|
15eb666394 | ||
|
dac49f7fdc | ||
|
926ec831e2 | ||
|
87012c47ea | ||
|
fbe7e0a427 | ||
|
779a1c303f | ||
|
14e6136683 | ||
|
1f11a1df02 | ||
|
8e4bccffbf | ||
|
733e0e07c3 | ||
|
8ee6a3f134 | ||
|
bacc5e4213 | ||
|
afd87d07a3 | ||
|
bebe40c8e8 | ||
|
e4c5be4350 | ||
|
a9a9391b39 | ||
|
9f54f4d81a | ||
|
1a63669805 | ||
|
fcf6abd36e | ||
|
b9080a1ec1 | ||
|
450b0bf4fa | ||
|
a4d3a5ad4d | ||
|
6c22a2aeb4 | ||
|
2e36c97d1c | ||
|
f99efbb1e9 | ||
|
6cf3bf0255 | ||
|
a0abe41c8a | ||
|
3830ad65fc | ||
|
204403da67 | ||
|
098723b88d | ||
|
b21e758eb8 | ||
|
b1f4971f25 | ||
|
6e1bfdac58 | ||
|
11920ca997 | ||
|
255e29d9c8 | ||
|
35ccdd3014 | ||
|
757d628bc8 | ||
|
a57d32d05d | ||
|
ef69bf9256 | ||
|
bec303821b | ||
|
346f2db5fb | ||
|
b9de0f8e38 | ||
|
e112fcba29 | ||
|
41983ce356 | ||
|
fb49fb8ddd |
|
@ -26,3 +26,5 @@ install/
|
||||||
bruno/
|
bruno/
|
||||||
LICENSE
|
LICENSE
|
||||||
CONTRIBUTING.md
|
CONTRIBUTING.md
|
||||||
|
dist
|
||||||
|
.git
|
||||||
|
|
40
.github/dependabot.yml
vendored
Normal file
|
@ -0,0 +1,40 @@
|
||||||
|
version: 2
|
||||||
|
updates:
|
||||||
|
- package-ecosystem: "npm"
|
||||||
|
directory: "/"
|
||||||
|
schedule:
|
||||||
|
interval: "daily"
|
||||||
|
groups:
|
||||||
|
dev-patch-updates:
|
||||||
|
dependency-type: "development"
|
||||||
|
update-types:
|
||||||
|
- "patch"
|
||||||
|
dev-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:
|
||||||
|
- "minor"
|
||||||
|
|
||||||
|
- package-ecosystem: "docker"
|
||||||
|
directory: "/"
|
||||||
|
schedule:
|
||||||
|
interval: "daily"
|
||||||
|
groups:
|
||||||
|
patch-updates:
|
||||||
|
update-types:
|
||||||
|
- "patch"
|
||||||
|
minor-updates:
|
||||||
|
update-types:
|
||||||
|
- "minor"
|
||||||
|
|
||||||
|
- package-ecosystem: "github-actions"
|
||||||
|
directory: "/"
|
||||||
|
schedule:
|
||||||
|
interval: "weekly"
|
8
.github/workflows/cicd.yml
vendored
|
@ -12,13 +12,13 @@ jobs:
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@v3
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Set up Docker Buildx
|
- name: Set up Docker Buildx
|
||||||
uses: docker/setup-buildx-action@v2
|
uses: docker/setup-buildx-action@v3
|
||||||
|
|
||||||
- name: Log in to Docker Hub
|
- name: Log in to Docker Hub
|
||||||
uses: docker/login-action@v2
|
uses: docker/login-action@v3
|
||||||
with:
|
with:
|
||||||
username: ${{ secrets.DOCKER_HUB_USERNAME }}
|
username: ${{ secrets.DOCKER_HUB_USERNAME }}
|
||||||
password: ${{ secrets.DOCKER_HUB_ACCESS_TOKEN }}
|
password: ${{ secrets.DOCKER_HUB_ACCESS_TOKEN }}
|
||||||
|
@ -28,7 +28,7 @@ jobs:
|
||||||
run: echo "TAG=${GITHUB_REF#refs/tags/}" >> $GITHUB_ENV
|
run: echo "TAG=${GITHUB_REF#refs/tags/}" >> $GITHUB_ENV
|
||||||
|
|
||||||
- name: Install Go
|
- name: Install Go
|
||||||
uses: actions/setup-go@v4
|
uses: actions/setup-go@v5
|
||||||
with:
|
with:
|
||||||
go-version: 1.23.0
|
go-version: 1.23.0
|
||||||
|
|
||||||
|
|
34
.github/workflows/linting.yml
vendored
Normal file
|
@ -0,0 +1,34 @@
|
||||||
|
name: ESLint
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
paths:
|
||||||
|
- '**/*.js'
|
||||||
|
- '**/*.jsx'
|
||||||
|
- '**/*.ts'
|
||||||
|
- '**/*.tsx'
|
||||||
|
- '.eslintrc*'
|
||||||
|
- 'package.json'
|
||||||
|
- 'yarn.lock'
|
||||||
|
- 'pnpm-lock.yaml'
|
||||||
|
- 'package-lock.json'
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
Linter:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Set up Node.js
|
||||||
|
uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: '20'
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: |
|
||||||
|
npm ci
|
||||||
|
|
||||||
|
- name: Run ESLint
|
||||||
|
run: |
|
||||||
|
npx eslint . --ext .js,.jsx,.ts,.tsx
|
4
.github/workflows/stale-bot.yml
vendored
|
@ -16,9 +16,9 @@ jobs:
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/stale@v9
|
- uses: actions/stale@v9
|
||||||
with:
|
with:
|
||||||
days-before-stale: 30
|
days-before-stale: 14
|
||||||
days-before-close: 14
|
days-before-close: 14
|
||||||
stale-issue-message: 'This issue has been automatically marked as stale due to 30 days of inactivity. It will be closed in 14 days if no further activity occurs.'
|
stale-issue-message: 'This issue has been automatically marked as stale due to 14 days of inactivity. It will be closed in 14 days if no further activity occurs.'
|
||||||
close-issue-message: 'This issue has been automatically closed due to inactivity. If you believe this is still relevant, please open a new issue with up-to-date information.'
|
close-issue-message: 'This issue has been automatically closed due to inactivity. If you believe this is still relevant, please open a new issue with up-to-date information.'
|
||||||
stale-issue-label: 'stale'
|
stale-issue-label: 'stale'
|
||||||
|
|
||||||
|
|
55
.github/workflows/test.yml
vendored
Normal file
|
@ -0,0 +1,55 @@
|
||||||
|
name: Run Tests
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
- dev
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
test:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: '20'
|
||||||
|
|
||||||
|
- name: Copy config file
|
||||||
|
run: cp config/config.example.yml config/config.yml
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: npm ci
|
||||||
|
|
||||||
|
- name: Create database index.ts
|
||||||
|
run: echo 'export * from "./sqlite";' > server/db/index.ts
|
||||||
|
|
||||||
|
- name: Generate database migrations
|
||||||
|
run: npm run db:sqlite:generate
|
||||||
|
|
||||||
|
- name: Apply database migrations
|
||||||
|
run: npm run db:sqlite:push
|
||||||
|
|
||||||
|
- name: Start app in background
|
||||||
|
run: nohup npm run dev &
|
||||||
|
|
||||||
|
- name: Wait for app availability
|
||||||
|
run: |
|
||||||
|
for i in {1..5}; do
|
||||||
|
if curl --silent --fail http://localhost:3002/auth/login; then
|
||||||
|
echo "App is up"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
echo "Waiting for the app... attempt $i"
|
||||||
|
sleep 5
|
||||||
|
done
|
||||||
|
echo "App failed to start"
|
||||||
|
exit 1
|
||||||
|
|
||||||
|
- name: Build Docker image sqlite
|
||||||
|
run: make build-sqlite
|
||||||
|
|
||||||
|
- name: Build Docker image pg
|
||||||
|
run: make build-pg
|
3
.gitignore
vendored
|
@ -18,6 +18,7 @@ yarn-error.log*
|
||||||
next-env.d.ts
|
next-env.d.ts
|
||||||
*.db
|
*.db
|
||||||
*.sqlite
|
*.sqlite
|
||||||
|
!Dockerfile.sqlite
|
||||||
*.sqlite3
|
*.sqlite3
|
||||||
*.log
|
*.log
|
||||||
.machinelogs*.json
|
.machinelogs*.json
|
||||||
|
@ -32,3 +33,5 @@ installer
|
||||||
bin
|
bin
|
||||||
.secrets
|
.secrets
|
||||||
test_event.json
|
test_event.json
|
||||||
|
.idea/
|
||||||
|
server/db/index.ts
|
||||||
|
|
|
@ -6,10 +6,6 @@ Please see the contribution and local development guide on the docs page before
|
||||||
|
|
||||||
https://docs.fossorial.io/development
|
https://docs.fossorial.io/development
|
||||||
|
|
||||||
For ideas about what features to work on and our future plans, please see the roadmap:
|
|
||||||
|
|
||||||
https://docs.fossorial.io/roadmap
|
|
||||||
|
|
||||||
### Licensing Considerations
|
### Licensing Considerations
|
||||||
|
|
||||||
Please note that your contributions will be distributed under the AGPLv3 and the Fossorial Commercial license. For inquiries about commercial licensing, please contact us.
|
Please note that your contributions will be distributed under the AGPLv3 and the Fossorial Commercial license. For inquiries about commercial licensing, please contact us.
|
||||||
|
|
14
Dockerfile.dev
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
FROM node:20-alpine
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
COPY package*.json ./
|
||||||
|
|
||||||
|
# Install dependencies
|
||||||
|
RUN npm ci
|
||||||
|
|
||||||
|
# Copy source code
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
# Use tsx watch for development with hot reload
|
||||||
|
CMD ["npm", "run", "dev"]
|
41
Dockerfile.pg
Normal file
|
@ -0,0 +1,41 @@
|
||||||
|
FROM node:20-alpine AS builder
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# COPY package.json package-lock.json ./
|
||||||
|
COPY package*.json ./
|
||||||
|
RUN npm ci
|
||||||
|
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
RUN echo 'export * from "./pg";' > server/db/index.ts
|
||||||
|
|
||||||
|
RUN npx drizzle-kit generate --dialect postgresql --schema ./server/db/pg/schema.ts --out init
|
||||||
|
|
||||||
|
RUN npm run build:pg
|
||||||
|
RUN npm run build:cli
|
||||||
|
|
||||||
|
FROM node:20-alpine AS runner
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Curl used for the health checks
|
||||||
|
RUN apk add --no-cache curl
|
||||||
|
|
||||||
|
# COPY package.json package-lock.json ./
|
||||||
|
COPY package*.json ./
|
||||||
|
RUN npm ci --omit=dev && npm cache clean --force
|
||||||
|
|
||||||
|
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 ./cli/wrapper.sh /usr/local/bin/pangctl
|
||||||
|
RUN chmod +x /usr/local/bin/pangctl ./dist/cli.mjs
|
||||||
|
|
||||||
|
COPY server/db/names.json ./dist/names.json
|
||||||
|
|
||||||
|
COPY public ./public
|
||||||
|
|
||||||
|
CMD ["npm", "run", "start:pg"]
|
|
@ -2,14 +2,18 @@ FROM node:20-alpine AS builder
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
COPY package.json package-lock.json ./
|
# COPY package.json package-lock.json ./
|
||||||
|
COPY package*.json ./
|
||||||
RUN npm ci
|
RUN npm ci
|
||||||
|
|
||||||
COPY . .
|
COPY . .
|
||||||
|
|
||||||
RUN npx drizzle-kit generate --dialect sqlite --schema ./server/db/schemas/ --out init
|
RUN echo 'export * from "./sqlite";' > server/db/index.ts
|
||||||
|
|
||||||
RUN npm run build
|
RUN npx drizzle-kit generate --dialect sqlite --schema ./server/db/sqlite/schema.ts --out init
|
||||||
|
|
||||||
|
RUN npm run build:sqlite
|
||||||
|
RUN npm run build:cli
|
||||||
|
|
||||||
FROM node:20-alpine AS runner
|
FROM node:20-alpine AS runner
|
||||||
|
|
||||||
|
@ -18,16 +22,20 @@ WORKDIR /app
|
||||||
# Curl used for the health checks
|
# Curl used for the health checks
|
||||||
RUN apk add --no-cache curl
|
RUN apk add --no-cache curl
|
||||||
|
|
||||||
COPY package.json package-lock.json ./
|
# COPY package.json package-lock.json ./
|
||||||
RUN npm ci --only=production && npm cache clean --force
|
COPY package*.json ./
|
||||||
|
RUN npm ci --omit=dev && npm cache clean --force
|
||||||
|
|
||||||
COPY --from=builder /app/.next/standalone ./
|
COPY --from=builder /app/.next/standalone ./
|
||||||
COPY --from=builder /app/.next/static ./.next/static
|
COPY --from=builder /app/.next/static ./.next/static
|
||||||
COPY --from=builder /app/dist ./dist
|
COPY --from=builder /app/dist ./dist
|
||||||
COPY --from=builder /app/init ./dist/init
|
COPY --from=builder /app/init ./dist/init
|
||||||
|
|
||||||
|
COPY ./cli/wrapper.sh /usr/local/bin/pangctl
|
||||||
|
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 public ./public
|
COPY public ./public
|
||||||
|
|
||||||
CMD ["npm", "start"]
|
CMD ["npm", "run", "start:sqlite"]
|
17
Makefile
|
@ -1,10 +1,14 @@
|
||||||
|
.PHONY: build build-pg build-release build-arm build-x86 test clean
|
||||||
|
|
||||||
build-release:
|
build-release:
|
||||||
@if [ -z "$(tag)" ]; then \
|
@if [ -z "$(tag)" ]; then \
|
||||||
echo "Error: tag is required. Usage: make build-all tag=<tag>"; \
|
echo "Error: tag is required. Usage: make build-release tag=<tag>"; \
|
||||||
exit 1; \
|
exit 1; \
|
||||||
fi
|
fi
|
||||||
docker buildx build --platform linux/arm64,linux/amd64 -t fosrl/pangolin:latest -f Dockerfile --push .
|
docker buildx build --platform linux/arm64,linux/amd64 -t fosrl/pangolin:latest -f Dockerfile.sqlite --push .
|
||||||
docker buildx build --platform linux/arm64,linux/amd64 -t fosrl/pangolin:$(tag) -f Dockerfile --push .
|
docker buildx build --platform linux/arm64,linux/amd64 -t fosrl/pangolin:$(tag) -f Dockerfile.sqlite --push .
|
||||||
|
docker buildx build --platform linux/arm64,linux/amd64 -t fosrl/pangolin:postgresql-latest -f Dockerfile.pg --push .
|
||||||
|
docker buildx build --platform linux/arm64,linux/amd64 -t fosrl/pangolin:postgresql-$(tag) -f Dockerfile.pg --push .
|
||||||
|
|
||||||
build-arm:
|
build-arm:
|
||||||
docker buildx build --platform linux/arm64 -t fosrl/pangolin:latest .
|
docker buildx build --platform linux/arm64 -t fosrl/pangolin:latest .
|
||||||
|
@ -12,8 +16,11 @@ build-arm:
|
||||||
build-x86:
|
build-x86:
|
||||||
docker buildx build --platform linux/amd64 -t fosrl/pangolin:latest .
|
docker buildx build --platform linux/amd64 -t fosrl/pangolin:latest .
|
||||||
|
|
||||||
build:
|
build-sqlite:
|
||||||
docker build -t fosrl/pangolin:latest .
|
docker build -t fosrl/pangolin:latest -f Dockerfile.sqlite .
|
||||||
|
|
||||||
|
build-pg:
|
||||||
|
docker build -t fosrl/pangolin:postgresql-latest -f Dockerfile.pg .
|
||||||
|
|
||||||
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
|
||||||
|
|
122
README.md
|
@ -1,24 +1,22 @@
|
||||||
<div align="center">
|
<div align="center">
|
||||||
<h2 align="center"><a href="https://fossorial.io"><img alt="pangolin" src="public/logo//word_mark.png" width="400" /></a></h2>
|
<h2>
|
||||||
|
<picture>
|
||||||
[](https://docs.fossorial.io/)
|
<source media="(prefers-color-scheme: dark)" srcset="public/logo/word_mark_white.png">
|
||||||
[](https://hub.docker.com/r/fosrl/pangolin)
|
<img alt="Pangolin Logo" src="public/logo/word_mark_black.png" width="250">
|
||||||

|
</picture>
|
||||||
[](https://discord.gg/HCJR8Xhme4)
|
</h2>
|
||||||
[](https://www.youtube.com/@fossorial-app)
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<h3 align="center">Tunneled Mesh Reverse Proxy Server with Access Control</h3>
|
<h4 align="center">Secure gateway to your private networks</h4>
|
||||||
<div align="center">
|
<div align="center">
|
||||||
|
|
||||||
_Your own self-hosted zero trust tunnel._
|
_Pangolin tunnels your services to the internet so you can access anything from anywhere._
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div align="center">
|
<div align="center">
|
||||||
<h5>
|
<h5>
|
||||||
<a href="https://fossorial.io">
|
<a href="https://digpangolin.com">
|
||||||
Website
|
Website
|
||||||
</a>
|
</a>
|
||||||
<span> | </span>
|
<span> | </span>
|
||||||
|
@ -30,24 +28,39 @@ _Your own self-hosted zero trust tunnel._
|
||||||
Contact Us
|
Contact Us
|
||||||
</a>
|
</a>
|
||||||
</h5>
|
</h5>
|
||||||
|
|
||||||
|
[](https://hub.docker.com/r/fosrl/pangolin)
|
||||||
|

|
||||||
|
[](https://discord.gg/HCJR8Xhme4)
|
||||||
|
[](https://www.youtube.com/@fossorial-app)
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<p align="center">
|
||||||
|
<strong>
|
||||||
|
Start testing Pangolin at <a href="https://pangolin.fossorial.io/auth/signup">pangolin.fossorial.io</a>
|
||||||
|
</strong>
|
||||||
|
</p>
|
||||||
|
|
||||||
Pangolin is a self-hosted tunneled reverse proxy server with identity and access control, designed to securely expose private resources on distributed networks. Acting as a central hub, it connects isolated networks — even those behind restrictive firewalls — through encrypted tunnels, enabling easy access to remote services without opening ports.
|
Pangolin is a self-hosted tunneled reverse proxy server with identity and access control, designed to securely expose private resources on distributed networks. Acting as a central hub, it connects isolated networks — even those behind restrictive firewalls — through encrypted tunnels, enabling easy access to remote services without opening ports.
|
||||||
|
|
||||||
<img src="public/screenshots/sites.png" alt="Preview"/>
|
<img src="public/screenshots/hero.png" alt="Preview"/>
|
||||||
|
|
||||||
_Sites page of Pangolin dashboard (dark mode) showing multiple tunnels connected to the central server._
|

|
||||||
|
|
||||||
## Key Features
|
## Key Features
|
||||||
|
|
||||||
### Reverse Proxy Through WireGuard Tunnel
|
### Reverse Proxy Through WireGuard Tunnel
|
||||||
|
|
||||||
- Expose private resources on your network **without opening ports** (firewall punching).
|
- Expose private resources on your network **without opening ports** (firewall punching).
|
||||||
- Secure and easy to configure site-to-site connectivity via a custom **user space WireGuard client**, [Newt](https://github.com/fosrl/newt).
|
- Secure and easy to configure private connectivity via a custom **user space WireGuard client**, [Newt](https://github.com/fosrl/newt).
|
||||||
- Built-in support for any WireGuard client.
|
- Built-in support for any WireGuard client.
|
||||||
- Automated **SSL certificates** (https) via [LetsEncrypt](https://letsencrypt.org/).
|
- Automated **SSL certificates** (https) via [LetsEncrypt](https://letsencrypt.org/).
|
||||||
- Support for HTTP/HTTPS and **raw TCP/UDP services**.
|
- Support for HTTP/HTTPS and **raw TCP/UDP services**.
|
||||||
- Load balancing.
|
- Load balancing.
|
||||||
|
- Extend functionality with existing [Traefik](https://github.com/traefik/traefik) plugins, such as [CrowdSec](https://plugins.traefik.io/plugins/6335346ca4caa9ddeffda116/crowdsec-bouncer-traefik-plugin) and [Geoblock](https://github.com/PascalMinder/geoblock).
|
||||||
|
- **Automatically install and configure Crowdsec via Pangolin's installer script.**
|
||||||
|
- Attach as many sites to the central server as you wish.
|
||||||
|
|
||||||
### Identity & Access Management
|
### Identity & Access Management
|
||||||
|
|
||||||
|
@ -61,86 +74,73 @@ _Sites page of Pangolin dashboard (dark mode) showing multiple tunnels connected
|
||||||
- **Temporary, self-destructing share links.**
|
- **Temporary, self-destructing share links.**
|
||||||
- Resource specific pin codes.
|
- Resource specific pin codes.
|
||||||
- Resource specific passwords.
|
- Resource specific passwords.
|
||||||
|
- Passkeys
|
||||||
|
- External identity provider (IdP) support with OAuth2/OIDC, such as Authentik, Keycloak, Okta, and others.
|
||||||
|
- Auto-provision users and roles from your IdP.
|
||||||
|
|
||||||
### Simple Dashboard UI
|
<img src="public/auth-diagram1.png" alt="Auth and diagram"/>
|
||||||
|
|
||||||
- Manage sites, users, and roles with a clean and intuitive UI.
|
## Use Cases
|
||||||
- Monitor site usage and connectivity.
|
|
||||||
- Light and dark mode options.
|
|
||||||
- Mobile friendly.
|
|
||||||
|
|
||||||
### Easy Deployment
|
### Manage Access to Internal Apps
|
||||||
|
|
||||||
- Run on any cloud provider or on-premises.
|
- Grant users access to your apps from anywhere using just a web browser. No client software required.
|
||||||
- **Docker Compose based setup** for simplified deployment.
|
|
||||||
- Future-proof installation script for streamlined setup and feature additions.
|
|
||||||
- Use any WireGuard client to connect, or use **Newt, our custom user space client** for the best experience.
|
|
||||||
|
|
||||||
### Modular Design
|
### Developers and DevOps
|
||||||
|
|
||||||
- Extend functionality with existing [Traefik](https://github.com/traefik/traefik) plugins, such as [CrowdSec](https://plugins.traefik.io/plugins/6335346ca4caa9ddeffda116/crowdsec-bouncer-traefik-plugin) and [Geoblock](github.com/PascalMinder/geoblock).
|
- Expose and test internal tools and dashboards like **Grafana**. Bring localhost or private IPs online for easy access.
|
||||||
- **Automatically install and configure Crowdsec via Pangolin's installer script.**
|
|
||||||
- Attach as many sites to the central server as you wish.
|
|
||||||
|
|
||||||
<img src="public/screenshots/collage.png" alt="Collage"/>
|
### Secure API Gateway
|
||||||
|
|
||||||
## Deployment and Usage Example
|
- One application load balancer across multiple clouds and on-premises.
|
||||||
|
|
||||||
1. **Deploy the Central Server**:
|
### IoT and Edge Devices
|
||||||
|
|
||||||
- Deploy the Docker Compose stack onto a VPS hosted on a cloud platform like RackNerd, Amazon EC2, DigitalOcean Droplet, or similar. There are many cheap VPS hosting options available to suit your needs.
|
- Easily expose **IoT devices**, **edge servers**, or **Raspberry Pi** to the internet for field equipment monitoring.
|
||||||
|
|
||||||
> [!TIP]
|
<img src="public/screenshots/sites.png" alt="Sites"/>
|
||||||
> Many of our users have had a great experience with [RackNerd](https://my.racknerd.com/aff.php?aff=13788). Depending on promotions, you can likely get a **VPS with 1 vCPU, 1GB RAM, and ~20GB SSD for just around $12/year**. That's a great deal!
|
|
||||||
> We are part of the [RackNerd](https://my.racknerd.com/aff.php?aff=13788) affiliate program, so if you purchase through [our link](https://my.racknerd.com/aff.php?aff=13788), we receive a small commission which helps us maintain the project and keep it free for everyone.
|
|
||||||
|
|
||||||
2. **Domain Configuration**:
|
## Deployment Options
|
||||||
|
|
||||||
- Point your domain name to the VPS and configure Pangolin with your preferred settings.
|
### Fully Self Hosted
|
||||||
|
|
||||||
3. **Connect Private Sites**:
|
Host the full application on your own server or on the cloud with a VPS. Take a look at the [documentation](https://docs.fossorial.io/Getting%20Started/quick-install) to get started.
|
||||||
|
|
||||||
- Install Newt or use another WireGuard client on private sites.
|
> Many of our users have had a great experience with [RackNerd](https://my.racknerd.com/aff.php?aff=13788). Depending on promotions, you can get a [**VPS with 1 vCPU, 1GB RAM, and ~20GB SSD for just around $12/year**](https://my.racknerd.com/aff.php?aff=13788&pid=912). That's a great deal!
|
||||||
- Automatically establish a connection from these sites to the central server.
|
|
||||||
|
|
||||||
4. **Expose Resources**:
|
### Pangolin Cloud
|
||||||
|
|
||||||
- Add resources to the central server and configure access control rules.
|
Easy to use with simple [pay as you go pricing](https://digpangolin.com/pricing). [Check it out here](https://pangolin.fossorial.io/auth/signup).
|
||||||
- Access these resources securely from anywhere.
|
|
||||||
|
|
||||||
**Use Case Example - Bypassing Port Restrictions in Home Lab**:
|
- Everything you get with self hosted Pangolin, but fully managed for you.
|
||||||
Imagine private sites where the ISP restricts port forwarding. By connecting these sites to Pangolin via WireGuard, you can securely expose HTTP and HTTPS resources on the private network without any networking complexity.
|
|
||||||
|
|
||||||
**Use Case Example - IoT Networks**:
|
### Hybrid & High Availability
|
||||||
IoT networks are often fragmented and difficult to manage. By deploying Pangolin on a central server, you can connect all your IoT sites via Newt or another WireGuard client. This creates a simple, secure, and centralized way to access IoT resources without the need for intricate networking setups.
|
|
||||||
|
|
||||||
|
Managed control plane, your infrastructure
|
||||||
|
|
||||||
<img src="public/screenshots/resources.png" alt="Resources"/>
|
- We manage database and control plane.
|
||||||
|
- You self-host lightweight exit-node.
|
||||||
|
- Traffic flows through your infra.
|
||||||
|
- We coordinate failover between your nodes or to Cloud when things go bad.
|
||||||
|
|
||||||
_Resources page of Pangolin dashboard (dark mode) showing HTTPS and TCP resources with access control rules._
|
If interested, [contact us](mailto:numbat@fossorial.io).
|
||||||
|
|
||||||
## Similar Projects and Inspirations
|
### Full Enterprise On-Premises
|
||||||
|
|
||||||
**Cloudflare Tunnels**:
|
[Contact us](mailto:numbat@fossorial.io) for a full distributed and enterprise deployments on your infrastructure controlled by your team.
|
||||||
A similar approach to proxying private resources securely, but Pangolin is a self-hosted alternative, giving you full control over your infrastructure.
|
|
||||||
|
|
||||||
**Authentik and Authelia**:
|
|
||||||
These projects inspired Pangolin’s centralized authentication system for proxies, enabling robust user and role management.
|
|
||||||
|
|
||||||
## Project Development / Roadmap
|
## Project Development / Roadmap
|
||||||
|
|
||||||
> [!NOTE]
|
We want to hear your feature requests! Add them to the [discussion board](https://github.com/orgs/fosrl/discussions/categories/feature-requests).
|
||||||
> Pangolin is under heavy development. The roadmap is subject to change as we fix bugs, add new features, and make improvements.
|
|
||||||
|
|
||||||
View the [project board](https://github.com/orgs/fosrl/projects/1) for more detailed info.
|
|
||||||
|
|
||||||
## Licensing
|
## Licensing
|
||||||
|
|
||||||
Pangolin is dual licensed under the AGPL-3 and the Fossorial Commercial license. To see our commercial offerings, please see our [website](https://fossorial.io) for details. For inquiries about commercial licensing, please contact us at [numbat@fossorial.io](mailto:numbat@fossorial.io).
|
Pangolin is dual licensed under the AGPL-3 and the Fossorial Commercial license. For inquiries about commercial licensing, please contact us at [numbat@fossorial.io](mailto:numbat@fossorial.io).
|
||||||
|
|
||||||
## Contributions
|
## Contributions
|
||||||
|
|
||||||
|
Looking for something to contribute? Take a look at issues marked with [help wanted](https://github.com/fosrl/pangolin/issues?q=is%3Aissue%20state%3Aopen%20label%3A%22help%20wanted%22).
|
||||||
|
|
||||||
Please see [CONTRIBUTING](./CONTRIBUTING.md) in the repository for guidelines and best practices.
|
Please see [CONTRIBUTING](./CONTRIBUTING.md) in the repository for guidelines and best practices.
|
||||||
|
|
||||||
Please post bug reports and other functional issues in the [Issues](https://github.com/fosrl/pangolin/issues) section of the repository.
|
Please post bug reports and other functional issues in the [Issues](https://github.com/fosrl/pangolin/issues) section of the repository.
|
||||||
For all feature requests, or other ideas, please use the [Discussions](https://github.com/orgs/fosrl/discussions) section.
|
|
||||||
|
|
22
bruno/Clients/createClient.bru
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
meta {
|
||||||
|
name: createClient
|
||||||
|
type: http
|
||||||
|
seq: 1
|
||||||
|
}
|
||||||
|
|
||||||
|
put {
|
||||||
|
url: http://localhost:3000/api/v1/site/1/client
|
||||||
|
body: json
|
||||||
|
auth: none
|
||||||
|
}
|
||||||
|
|
||||||
|
body:json {
|
||||||
|
{
|
||||||
|
"siteId": 1,
|
||||||
|
"name": "test",
|
||||||
|
"type": "olm",
|
||||||
|
"subnet": "100.90.129.4/30",
|
||||||
|
"olmId": "029yzunhx6nh3y5",
|
||||||
|
"secret": "l0ymp075y3d4rccb25l6sqpgar52k09etunui970qq5gj7x6"
|
||||||
|
}
|
||||||
|
}
|
11
bruno/Clients/pickClientDefaults.bru
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
meta {
|
||||||
|
name: pickClientDefaults
|
||||||
|
type: http
|
||||||
|
seq: 2
|
||||||
|
}
|
||||||
|
|
||||||
|
get {
|
||||||
|
url: http://localhost:3000/api/v1/site/1/pick-client-defaults
|
||||||
|
body: none
|
||||||
|
auth: none
|
||||||
|
}
|
141
cli/commands/setAdminCredentials.ts
Normal file
|
@ -0,0 +1,141 @@
|
||||||
|
import { CommandModule } from "yargs";
|
||||||
|
import { hashPassword, verifyPassword } from "@server/auth/password";
|
||||||
|
import { db, resourceSessions, sessions } from "@server/db";
|
||||||
|
import { users } from "@server/db";
|
||||||
|
import { eq, inArray } from "drizzle-orm";
|
||||||
|
import moment from "moment";
|
||||||
|
import { fromError } from "zod-validation-error";
|
||||||
|
import { passwordSchema } from "@server/auth/passwordSchema";
|
||||||
|
import { UserType } from "@server/types/UserTypes";
|
||||||
|
import { generateRandomString, RandomReader } from "@oslojs/crypto/random";
|
||||||
|
|
||||||
|
type SetAdminCredentialsArgs = {
|
||||||
|
email: string;
|
||||||
|
password: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const setAdminCredentials: CommandModule<{}, SetAdminCredentialsArgs> = {
|
||||||
|
command: "set-admin-credentials",
|
||||||
|
describe: "Set the server admin credentials",
|
||||||
|
builder: (yargs) => {
|
||||||
|
return yargs
|
||||||
|
.option("email", {
|
||||||
|
type: "string",
|
||||||
|
demandOption: true,
|
||||||
|
describe: "Admin email address"
|
||||||
|
})
|
||||||
|
.option("password", {
|
||||||
|
type: "string",
|
||||||
|
demandOption: true,
|
||||||
|
describe: "Admin password"
|
||||||
|
});
|
||||||
|
},
|
||||||
|
handler: async (argv: { email: string; password: string }) => {
|
||||||
|
try {
|
||||||
|
const { email, password } = argv;
|
||||||
|
|
||||||
|
const parsed = passwordSchema.safeParse(password);
|
||||||
|
|
||||||
|
if (!parsed.success) {
|
||||||
|
throw Error(
|
||||||
|
`Invalid server admin password: ${fromError(parsed.error).toString()}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const passwordHash = await hashPassword(password);
|
||||||
|
|
||||||
|
await db.transaction(async (trx) => {
|
||||||
|
try {
|
||||||
|
const [existing] = await trx
|
||||||
|
.select()
|
||||||
|
.from(users)
|
||||||
|
.where(eq(users.serverAdmin, true));
|
||||||
|
|
||||||
|
if (existing) {
|
||||||
|
const passwordChanged = !(await verifyPassword(
|
||||||
|
password,
|
||||||
|
existing.passwordHash!
|
||||||
|
));
|
||||||
|
|
||||||
|
if (passwordChanged) {
|
||||||
|
await trx
|
||||||
|
.update(users)
|
||||||
|
.set({ passwordHash })
|
||||||
|
.where(eq(users.userId, existing.userId));
|
||||||
|
|
||||||
|
await invalidateAllSessions(existing.userId);
|
||||||
|
console.log("Server admin password updated");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (existing.email !== email) {
|
||||||
|
await trx
|
||||||
|
.update(users)
|
||||||
|
.set({ email, username: email })
|
||||||
|
.where(eq(users.userId, existing.userId));
|
||||||
|
|
||||||
|
console.log("Server admin email updated");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const userId = generateId(15);
|
||||||
|
|
||||||
|
await trx.update(users).set({ serverAdmin: false });
|
||||||
|
|
||||||
|
await db.insert(users).values({
|
||||||
|
userId: userId,
|
||||||
|
email: email,
|
||||||
|
type: UserType.Internal,
|
||||||
|
username: email,
|
||||||
|
passwordHash,
|
||||||
|
dateCreated: moment().toISOString(),
|
||||||
|
serverAdmin: true,
|
||||||
|
emailVerified: true
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log("Server admin created");
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error("Failed to set admin credentials", e);
|
||||||
|
trx.rollback();
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log("Admin credentials updated successfully");
|
||||||
|
process.exit(0);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error:", error);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export async function invalidateAllSessions(userId: string): Promise<void> {
|
||||||
|
try {
|
||||||
|
await db.transaction(async (trx) => {
|
||||||
|
const userSessions = await trx
|
||||||
|
.select()
|
||||||
|
.from(sessions)
|
||||||
|
.where(eq(sessions.userId, userId));
|
||||||
|
await trx.delete(resourceSessions).where(
|
||||||
|
inArray(
|
||||||
|
resourceSessions.userSessionId,
|
||||||
|
userSessions.map((s) => s.sessionId)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
await trx.delete(sessions).where(eq(sessions.userId, userId));
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
console.log("Failed to all invalidate user sessions", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const random: RandomReader = {
|
||||||
|
read(bytes: Uint8Array): void {
|
||||||
|
crypto.getRandomValues(bytes);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export function generateId(length: number): string {
|
||||||
|
const alphabet = "abcdefghijklmnopqrstuvwxyz0123456789";
|
||||||
|
return generateRandomString(random, alphabet, length);
|
||||||
|
}
|
11
cli/index.ts
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
#!/usr/bin/env node
|
||||||
|
|
||||||
|
import yargs from "yargs";
|
||||||
|
import { hideBin } from "yargs/helpers";
|
||||||
|
import { setAdminCredentials } from "@cli/commands/setAdminCredentials";
|
||||||
|
|
||||||
|
yargs(hideBin(process.argv))
|
||||||
|
.scriptName("pangctl")
|
||||||
|
.command(setAdminCredentials)
|
||||||
|
.demandCommand()
|
||||||
|
.help().argv;
|
3
cli/wrapper.sh
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
#!/bin/sh
|
||||||
|
cd /app/
|
||||||
|
./dist/cli.mjs "$@"
|
|
@ -18,6 +18,7 @@ server:
|
||||||
internal_hostname: "pangolin"
|
internal_hostname: "pangolin"
|
||||||
session_cookie_name: "p_session_token"
|
session_cookie_name: "p_session_token"
|
||||||
resource_access_token_param: "p_token"
|
resource_access_token_param: "p_token"
|
||||||
|
secret: "your_secret_key_here"
|
||||||
resource_access_token_headers:
|
resource_access_token_headers:
|
||||||
id: "P-Access-Token-Id"
|
id: "P-Access-Token-Id"
|
||||||
token: "P-Access-Token"
|
token: "P-Access-Token"
|
||||||
|
@ -40,14 +41,8 @@ rate_limits:
|
||||||
window_minutes: 1
|
window_minutes: 1
|
||||||
max_requests: 500
|
max_requests: 500
|
||||||
|
|
||||||
users:
|
|
||||||
server_admin:
|
|
||||||
email: "admin@example.com"
|
|
||||||
password: "Password123!"
|
|
||||||
|
|
||||||
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: true
|
||||||
allow_raw_resources: true
|
allow_raw_resources: true
|
||||||
allow_base_domain_resources: true
|
|
||||||
|
|
BIN
config/db/db.sqlite.bak
Normal file
3
crowdin.yml
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
files:
|
||||||
|
- source: /messages/en-US.json
|
||||||
|
translation: /messages/%locale%.json
|
|
@ -10,7 +10,7 @@ services:
|
||||||
test: ["CMD", "curl", "-f", "http://localhost:3001/api/v1/"]
|
test: ["CMD", "curl", "-f", "http://localhost:3001/api/v1/"]
|
||||||
interval: "3s"
|
interval: "3s"
|
||||||
timeout: "3s"
|
timeout: "3s"
|
||||||
retries: 5
|
retries: 15
|
||||||
|
|
||||||
gerbil:
|
gerbil:
|
||||||
image: fosrl/gerbil:latest
|
image: fosrl/gerbil:latest
|
||||||
|
@ -31,11 +31,12 @@ services:
|
||||||
- SYS_MODULE
|
- SYS_MODULE
|
||||||
ports:
|
ports:
|
||||||
- 51820:51820/udp
|
- 51820:51820/udp
|
||||||
|
- 21820:21820/udp
|
||||||
- 443:443 # Port for traefik because of the network_mode
|
- 443:443 # Port for traefik because of the network_mode
|
||||||
- 80:80 # Port for traefik because of the network_mode
|
- 80:80 # Port for traefik because of the network_mode
|
||||||
|
|
||||||
traefik:
|
traefik:
|
||||||
image: traefik:v3.3.3
|
image: traefik:v3.4.0
|
||||||
container_name: traefik
|
container_name: traefik
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
network_mode: service:gerbil # Ports appear on the gerbil service
|
network_mode: service:gerbil # Ports appear on the gerbil service
|
||||||
|
@ -52,3 +53,4 @@ networks:
|
||||||
default:
|
default:
|
||||||
driver: bridge
|
driver: bridge
|
||||||
name: pangolin
|
name: pangolin
|
||||||
|
enable_ipv6: true
|
12
docker-compose.pg.yml
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
services:
|
||||||
|
# PostgreSQL Service
|
||||||
|
db:
|
||||||
|
image: postgres:17 # Use the PostgreSQL 17 image
|
||||||
|
container_name: dev_postgres # Name your PostgreSQL container
|
||||||
|
environment:
|
||||||
|
POSTGRES_DB: postgres # Default database name
|
||||||
|
POSTGRES_USER: postgres # Default user
|
||||||
|
POSTGRES_PASSWORD: password # Default password (change for production!)
|
||||||
|
ports:
|
||||||
|
- "5432:5432" # Map host port 5432 to container port 5432
|
||||||
|
restart: no
|
29
docker-compose.yml
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
services:
|
||||||
|
# Development application service
|
||||||
|
app:
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
dockerfile: Dockerfile.dev
|
||||||
|
container_name: dev_pangolin
|
||||||
|
ports:
|
||||||
|
- "3000:3000"
|
||||||
|
- "3001:3001"
|
||||||
|
- "3002:3002"
|
||||||
|
environment:
|
||||||
|
- NODE_ENV=development
|
||||||
|
- ENVIRONMENT=dev
|
||||||
|
- DB_TYPE=pg
|
||||||
|
volumes:
|
||||||
|
# Mount source code for hot reload
|
||||||
|
- ./src:/app/src
|
||||||
|
- ./server:/app/server
|
||||||
|
- ./public:/app/public
|
||||||
|
- ./messages:/app/messages
|
||||||
|
- ./components.json:/app/components.json
|
||||||
|
- ./next.config.mjs:/app/next.config.mjs
|
||||||
|
- ./tsconfig.json:/app/tsconfig.json
|
||||||
|
- ./tailwind.config.js:/app/tailwind.config.js
|
||||||
|
- ./postcss.config.mjs:/app/postcss.config.mjs
|
||||||
|
- ./eslint.config.js:/app/eslint.config.js
|
||||||
|
- ./config:/app/config
|
||||||
|
restart: no
|
12
drizzle.pg.config.ts
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
import { defineConfig } from "drizzle-kit";
|
||||||
|
import path from "path";
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
dialect: "postgresql",
|
||||||
|
schema: [path.join("server", "db", "pg", "schema.ts")],
|
||||||
|
out: path.join("server", "migrations"),
|
||||||
|
verbose: true,
|
||||||
|
dbCredentials: {
|
||||||
|
url: process.env.DATABASE_URL as string
|
||||||
|
}
|
||||||
|
});
|
|
@ -4,7 +4,7 @@ import path from "path";
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
dialect: "sqlite",
|
dialect: "sqlite",
|
||||||
schema: path.join("server", "db", "schemas"),
|
schema: path.join("server", "db", "sqlite", "schema.ts"),
|
||||||
out: path.join("server", "migrations"),
|
out: path.join("server", "migrations"),
|
||||||
verbose: true,
|
verbose: true,
|
||||||
dbCredentials: {
|
dbCredentials: {
|
|
@ -52,6 +52,7 @@ esbuild
|
||||||
bundle: true,
|
bundle: true,
|
||||||
outfile: argv.out,
|
outfile: argv.out,
|
||||||
format: "esm",
|
format: "esm",
|
||||||
|
minify: true,
|
||||||
banner: {
|
banner: {
|
||||||
js: banner,
|
js: banner,
|
||||||
},
|
},
|
||||||
|
|
|
@ -1,9 +1,19 @@
|
||||||
// eslint.config.js
|
import tseslint from 'typescript-eslint';
|
||||||
export default [
|
|
||||||
{
|
export default tseslint.config({
|
||||||
|
files: ["**/*.{ts,tsx,js,jsx}"],
|
||||||
|
languageOptions: {
|
||||||
|
parser: tseslint.parser,
|
||||||
|
parserOptions: {
|
||||||
|
ecmaVersion: "latest",
|
||||||
|
sourceType: "module",
|
||||||
|
ecmaFeatures: {
|
||||||
|
jsx: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
rules: {
|
rules: {
|
||||||
semi: "error",
|
"semi": "error",
|
||||||
"prefer-const": "error"
|
"prefer-const": "warn"
|
||||||
}
|
}
|
||||||
}
|
});
|
||||||
];
|
|
|
@ -1,4 +1,5 @@
|
||||||
all: update-versions go-build-release put-back
|
all: update-versions go-build-release put-back
|
||||||
|
dev-all: dev-update-versions dev-build dev-clean
|
||||||
|
|
||||||
go-build-release:
|
go-build-release:
|
||||||
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o bin/installer_linux_amd64
|
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o bin/installer_linux_amd64
|
||||||
|
@ -11,6 +12,12 @@ clean:
|
||||||
update-versions:
|
update-versions:
|
||||||
@echo "Fetching latest versions..."
|
@echo "Fetching latest versions..."
|
||||||
cp main.go main.go.bak && \
|
cp main.go main.go.bak && \
|
||||||
|
$(MAKE) dev-update-versions
|
||||||
|
|
||||||
|
put-back:
|
||||||
|
mv main.go.bak main.go
|
||||||
|
|
||||||
|
dev-update-versions:
|
||||||
PANGOLIN_VERSION=$$(curl -s https://api.github.com/repos/fosrl/pangolin/tags | jq -r '.[0].name') && \
|
PANGOLIN_VERSION=$$(curl -s https://api.github.com/repos/fosrl/pangolin/tags | jq -r '.[0].name') && \
|
||||||
GERBIL_VERSION=$$(curl -s https://api.github.com/repos/fosrl/gerbil/tags | jq -r '.[0].name') && \
|
GERBIL_VERSION=$$(curl -s https://api.github.com/repos/fosrl/gerbil/tags | jq -r '.[0].name') && \
|
||||||
BADGER_VERSION=$$(curl -s https://api.github.com/repos/fosrl/badger/tags | jq -r '.[0].name') && \
|
BADGER_VERSION=$$(curl -s https://api.github.com/repos/fosrl/badger/tags | jq -r '.[0].name') && \
|
||||||
|
@ -20,5 +27,11 @@ update-versions:
|
||||||
sed -i "s/config.BadgerVersion = \".*\"/config.BadgerVersion = \"$$BADGER_VERSION\"/" main.go && \
|
sed -i "s/config.BadgerVersion = \".*\"/config.BadgerVersion = \"$$BADGER_VERSION\"/" main.go && \
|
||||||
echo "Updated main.go with latest versions"
|
echo "Updated main.go with latest versions"
|
||||||
|
|
||||||
put-back:
|
dev-build: go-build-release
|
||||||
mv main.go.bak main.go
|
|
||||||
|
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"
|
||||||
|
|
|
@ -4,7 +4,6 @@
|
||||||
app:
|
app:
|
||||||
dashboard_url: "https://{{.DashboardDomain}}"
|
dashboard_url: "https://{{.DashboardDomain}}"
|
||||||
log_level: "info"
|
log_level: "info"
|
||||||
save_logs: false
|
|
||||||
|
|
||||||
domains:
|
domains:
|
||||||
domain1:
|
domain1:
|
||||||
|
@ -12,39 +11,17 @@ domains:
|
||||||
cert_resolver: "letsencrypt"
|
cert_resolver: "letsencrypt"
|
||||||
|
|
||||||
server:
|
server:
|
||||||
external_port: 3000
|
secret: "{{.Secret}}"
|
||||||
internal_port: 3001
|
|
||||||
next_port: 3002
|
|
||||||
internal_hostname: "pangolin"
|
|
||||||
session_cookie_name: "p_session_token"
|
|
||||||
resource_access_token_param: "p_token"
|
|
||||||
resource_access_token_headers:
|
|
||||||
id: "P-Access-Token-Id"
|
|
||||||
token: "P-Access-Token"
|
|
||||||
resource_session_request_param: "p_session_request"
|
|
||||||
cors:
|
cors:
|
||||||
origins: ["https://{{.DashboardDomain}}"]
|
origins: ["https://{{.DashboardDomain}}"]
|
||||||
methods: ["GET", "POST", "PUT", "DELETE", "PATCH"]
|
methods: ["GET", "POST", "PUT", "DELETE", "PATCH"]
|
||||||
headers: ["X-CSRF-Token", "Content-Type"]
|
allowed_headers: ["X-CSRF-Token", "Content-Type"]
|
||||||
credentials: false
|
credentials: false
|
||||||
|
|
||||||
traefik:
|
|
||||||
cert_resolver: "letsencrypt"
|
|
||||||
http_entrypoint: "web"
|
|
||||||
https_entrypoint: "websecure"
|
|
||||||
|
|
||||||
gerbil:
|
gerbil:
|
||||||
start_port: 51820
|
start_port: 51820
|
||||||
base_endpoint: "{{.DashboardDomain}}"
|
base_endpoint: "{{.DashboardDomain}}"
|
||||||
use_subdomain: false
|
|
||||||
block_size: 24
|
|
||||||
site_block_size: 30
|
|
||||||
subnet_group: 100.89.137.0/20
|
|
||||||
|
|
||||||
rate_limits:
|
|
||||||
global:
|
|
||||||
window_minutes: 1
|
|
||||||
max_requests: 500
|
|
||||||
{{if .EnableEmail}}
|
{{if .EnableEmail}}
|
||||||
email:
|
email:
|
||||||
smtp_host: "{{.EmailSMTPHost}}"
|
smtp_host: "{{.EmailSMTPHost}}"
|
||||||
|
@ -53,14 +30,9 @@ email:
|
||||||
smtp_pass: "{{.EmailSMTPPass}}"
|
smtp_pass: "{{.EmailSMTPPass}}"
|
||||||
no_reply: "{{.EmailNoReply}}"
|
no_reply: "{{.EmailNoReply}}"
|
||||||
{{end}}
|
{{end}}
|
||||||
users:
|
|
||||||
server_admin:
|
|
||||||
email: "{{.AdminUserEmail}}"
|
|
||||||
password: "{{.AdminUserPassword}}"
|
|
||||||
|
|
||||||
flags:
|
flags:
|
||||||
require_email_verification: {{.EnableEmail}}
|
require_email_verification: {{.EnableEmail}}
|
||||||
disable_signup_without_invite: {{.DisableSignupWithoutInvite}}
|
disable_signup_without_invite: true
|
||||||
disable_user_create_org: {{.DisableUserCreateOrg}}
|
disable_user_create_org: false
|
||||||
allow_raw_resources: true
|
allow_raw_resources: true
|
||||||
allow_base_domain_resources: true
|
|
||||||
|
|
6
install/config/crowdsec/acquis.d/appsec.yaml
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
listen_addr: 0.0.0.0:7422
|
||||||
|
appsec_config: crowdsecurity/appsec-default
|
||||||
|
name: myAppSecComponent
|
||||||
|
source: appsec
|
||||||
|
labels:
|
||||||
|
type: appsec
|
5
install/config/crowdsec/acquis.d/traefik.yaml
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
poll_without_inotify: false
|
||||||
|
filenames:
|
||||||
|
- /var/log/traefik/*.log
|
||||||
|
labels:
|
||||||
|
type: traefik
|
|
@ -1,18 +0,0 @@
|
||||||
filenames:
|
|
||||||
- /var/log/auth.log
|
|
||||||
- /var/log/syslog
|
|
||||||
labels:
|
|
||||||
type: syslog
|
|
||||||
---
|
|
||||||
poll_without_inotify: false
|
|
||||||
filenames:
|
|
||||||
- /var/log/traefik/*.log
|
|
||||||
labels:
|
|
||||||
type: traefik
|
|
||||||
---
|
|
||||||
listen_addr: 0.0.0.0:7422
|
|
||||||
appsec_config: crowdsecurity/appsec-default
|
|
||||||
name: myAppSecComponent
|
|
||||||
source: appsec
|
|
||||||
labels:
|
|
||||||
type: appsec
|
|
|
@ -1,15 +1,17 @@
|
||||||
services:
|
services:
|
||||||
crowdsec:
|
crowdsec:
|
||||||
image: crowdsecurity/crowdsec:latest
|
image: docker.io/crowdsecurity/crowdsec:latest
|
||||||
container_name: crowdsec
|
container_name: crowdsec
|
||||||
environment:
|
environment:
|
||||||
GID: "1000"
|
GID: "1000"
|
||||||
COLLECTIONS: crowdsecurity/traefik crowdsecurity/appsec-virtual-patching crowdsecurity/appsec-generic-rules
|
COLLECTIONS: crowdsecurity/traefik crowdsecurity/appsec-virtual-patching crowdsecurity/appsec-generic-rules
|
||||||
ENROLL_INSTANCE_NAME: "pangolin-crowdsec"
|
ENROLL_INSTANCE_NAME: "pangolin-crowdsec"
|
||||||
PARSERS: crowdsecurity/whitelists
|
PARSERS: crowdsecurity/whitelists
|
||||||
ACQUIRE_FILES: "/var/log/traefik/*.log"
|
|
||||||
ENROLL_TAGS: docker
|
ENROLL_TAGS: docker
|
||||||
healthcheck:
|
healthcheck:
|
||||||
|
interval: 10s
|
||||||
|
retries: 15
|
||||||
|
timeout: 10s
|
||||||
test: ["CMD", "cscli", "capi", "status"]
|
test: ["CMD", "cscli", "capi", "status"]
|
||||||
labels:
|
labels:
|
||||||
- "traefik.enable=false" # Disable traefik for crowdsec
|
- "traefik.enable=false" # Disable traefik for crowdsec
|
||||||
|
@ -18,13 +20,8 @@ services:
|
||||||
- ./config/crowdsec:/etc/crowdsec # crowdsec config
|
- ./config/crowdsec:/etc/crowdsec # crowdsec config
|
||||||
- ./config/crowdsec/db:/var/lib/crowdsec/data # crowdsec db
|
- ./config/crowdsec/db:/var/lib/crowdsec/data # crowdsec db
|
||||||
# log bind mounts into crowdsec
|
# log bind mounts into crowdsec
|
||||||
- ./config/crowdsec_logs/auth.log:/var/log/auth.log:ro # auth.log
|
|
||||||
- ./config/crowdsec_logs/syslog:/var/log/syslog:ro # syslog
|
|
||||||
- ./config/crowdsec_logs:/var/log # crowdsec logs
|
|
||||||
- ./config/traefik/logs:/var/log/traefik # traefik logs
|
- ./config/traefik/logs:/var/log/traefik # traefik logs
|
||||||
ports:
|
ports:
|
||||||
- 6060:6060 # metrics endpoint for prometheus
|
- 6060:6060 # metrics endpoint for prometheus
|
||||||
expose:
|
|
||||||
- 6060 # metrics endpoint for prometheus
|
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
command: -t # Add test config flag to verify configuration
|
command: -t # Add test config flag to verify configuration
|
|
@ -42,6 +42,7 @@ http:
|
||||||
crowdsecAppsecHost: crowdsec:7422 # CrowdSec IP address which you noted down later
|
crowdsecAppsecHost: crowdsec:7422 # CrowdSec IP address which you noted down later
|
||||||
crowdsecAppsecFailureBlock: true # Block on failure
|
crowdsecAppsecFailureBlock: true # Block on failure
|
||||||
crowdsecAppsecUnreachableBlock: true # Block on unreachable
|
crowdsecAppsecUnreachableBlock: true # Block on unreachable
|
||||||
|
crowdsecAppsecBodyLimit: 10485760
|
||||||
crowdsecLapiKey: "PUT_YOUR_BOUNCER_KEY_HERE_OR_IT_WILL_NOT_WORK" # CrowdSec API key which you noted down later
|
crowdsecLapiKey: "PUT_YOUR_BOUNCER_KEY_HERE_OR_IT_WILL_NOT_WORK" # CrowdSec API key which you noted down later
|
||||||
crowdsecLapiHost: crowdsec:8080 # CrowdSec
|
crowdsecLapiHost: crowdsec:8080 # CrowdSec
|
||||||
crowdsecLapiScheme: http # CrowdSec API scheme
|
crowdsecLapiScheme: http # CrowdSec API scheme
|
||||||
|
|
|
@ -16,11 +16,15 @@ experimental:
|
||||||
version: "{{.BadgerVersion}}"
|
version: "{{.BadgerVersion}}"
|
||||||
crowdsec: # CrowdSec plugin configuration added
|
crowdsec: # CrowdSec plugin configuration added
|
||||||
moduleName: "github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin"
|
moduleName: "github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin"
|
||||||
version: "v1.3.5"
|
version: "v1.4.2"
|
||||||
|
|
||||||
log:
|
log:
|
||||||
level: "INFO"
|
level: "INFO"
|
||||||
format: "json" # Log format changed to json for better parsing
|
format: "json" # Log format changed to json for better parsing
|
||||||
|
maxSize: 100
|
||||||
|
maxBackups: 3
|
||||||
|
maxAge: 3
|
||||||
|
compress: true
|
||||||
|
|
||||||
accessLog: # We enable access logs as json
|
accessLog: # We enable access logs as json
|
||||||
filePath: "/var/log/traefik/access.log"
|
filePath: "/var/log/traefik/access.log"
|
||||||
|
|
|
@ -1,19 +1,19 @@
|
||||||
name: pangolin
|
name: pangolin
|
||||||
services:
|
services:
|
||||||
pangolin:
|
pangolin:
|
||||||
image: fosrl/pangolin:{{.PangolinVersion}}
|
image: docker.io/fosrl/pangolin:{{.PangolinVersion}}
|
||||||
container_name: pangolin
|
container_name: pangolin
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
volumes:
|
volumes:
|
||||||
- ./config:/app/config
|
- ./config:/app/config
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: ["CMD", "curl", "-f", "http://localhost:3001/api/v1/"]
|
test: ["CMD", "curl", "-f", "http://localhost:3001/api/v1/"]
|
||||||
interval: "3s"
|
interval: "10s"
|
||||||
timeout: "3s"
|
timeout: "10s"
|
||||||
retries: 5
|
retries: 15
|
||||||
{{if .InstallGerbil}}
|
{{if .InstallGerbil}}
|
||||||
gerbil:
|
gerbil:
|
||||||
image: fosrl/gerbil:{{.GerbilVersion}}
|
image: docker.io/fosrl/gerbil:{{.GerbilVersion}}
|
||||||
container_name: gerbil
|
container_name: gerbil
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
depends_on:
|
depends_on:
|
||||||
|
@ -31,11 +31,12 @@ services:
|
||||||
- SYS_MODULE
|
- SYS_MODULE
|
||||||
ports:
|
ports:
|
||||||
- 51820:51820/udp
|
- 51820:51820/udp
|
||||||
|
- 21820:21820/udp
|
||||||
- 443:443 # Port for traefik because of the network_mode
|
- 443:443 # Port for traefik because of the network_mode
|
||||||
- 80:80 # Port for traefik because of the network_mode
|
- 80:80 # Port for traefik because of the network_mode
|
||||||
{{end}}
|
{{end}}
|
||||||
traefik:
|
traefik:
|
||||||
image: traefik:v3.3.3
|
image: docker.io/traefik:v3.4.1
|
||||||
container_name: traefik
|
container_name: traefik
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
{{if .InstallGerbil}}
|
{{if .InstallGerbil}}
|
||||||
|
@ -59,3 +60,4 @@ networks:
|
||||||
default:
|
default:
|
||||||
driver: bridge
|
driver: bridge
|
||||||
name: pangolin
|
name: pangolin
|
||||||
|
enable_ipv6: true
|
||||||
|
|
|
@ -18,6 +18,10 @@ experimental:
|
||||||
log:
|
log:
|
||||||
level: "INFO"
|
level: "INFO"
|
||||||
format: "common"
|
format: "common"
|
||||||
|
maxSize: 100
|
||||||
|
maxBackups: 3
|
||||||
|
maxAge: 3
|
||||||
|
compress: true
|
||||||
|
|
||||||
certificatesResolvers:
|
certificatesResolvers:
|
||||||
letsencrypt:
|
letsencrypt:
|
||||||
|
|
|
@ -3,14 +3,17 @@ package main
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"log"
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"gopkg.in/yaml.v3"
|
||||||
)
|
)
|
||||||
|
|
||||||
func installCrowdsec(config Config) error {
|
func installCrowdsec(config Config) error {
|
||||||
|
|
||||||
if err := stopContainers(); err != nil {
|
if err := stopContainers(config.InstallationContainerType); err != nil {
|
||||||
return fmt.Errorf("failed to stop containers: %v", err)
|
return fmt.Errorf("failed to stop containers: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -25,7 +28,7 @@ func installCrowdsec(config Config) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
os.MkdirAll("config/crowdsec/db", 0755)
|
os.MkdirAll("config/crowdsec/db", 0755)
|
||||||
os.MkdirAll("config/crowdsec_logs/syslog", 0755)
|
os.MkdirAll("config/crowdsec/acquis.d", 0755)
|
||||||
os.MkdirAll("config/traefik/logs", 0755)
|
os.MkdirAll("config/traefik/logs", 0755)
|
||||||
|
|
||||||
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 {
|
||||||
|
@ -63,12 +66,18 @@ func installCrowdsec(config Config) error {
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := startContainers(); err != nil {
|
// check and add the service dependency of crowdsec to traefik
|
||||||
|
if err := CheckAndAddCrowdsecDependency("docker-compose.yml"); err != nil {
|
||||||
|
fmt.Printf("Error adding crowdsec dependency to traefik: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := startContainers(config.InstallationContainerType); err != nil {
|
||||||
return fmt.Errorf("failed to start containers: %v", err)
|
return fmt.Errorf("failed to start containers: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// get API key
|
// get API key
|
||||||
apiKey, err := GetCrowdSecAPIKey()
|
apiKey, err := GetCrowdSecAPIKey(config.InstallationContainerType)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to get API key: %v", err)
|
return fmt.Errorf("failed to get API key: %v", err)
|
||||||
}
|
}
|
||||||
|
@ -78,7 +87,7 @@ func installCrowdsec(config Config) error {
|
||||||
return fmt.Errorf("failed to replace bouncer key: %v", err)
|
return fmt.Errorf("failed to replace bouncer key: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := restartContainer("traefik"); err != nil {
|
if err := restartContainer("traefik", config.InstallationContainerType); err != nil {
|
||||||
return fmt.Errorf("failed to restart containers: %v", err)
|
return fmt.Errorf("failed to restart containers: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -101,9 +110,9 @@ func checkIsCrowdsecInstalledInCompose() bool {
|
||||||
return bytes.Contains(content, []byte("crowdsec:"))
|
return bytes.Contains(content, []byte("crowdsec:"))
|
||||||
}
|
}
|
||||||
|
|
||||||
func GetCrowdSecAPIKey() (string, error) {
|
func GetCrowdSecAPIKey(containerType SupportedContainer) (string, error) {
|
||||||
// First, ensure the container is running
|
// First, ensure the container is running
|
||||||
if err := waitForContainer("crowdsec"); err != nil {
|
if err := waitForContainer("crowdsec", containerType); err != nil {
|
||||||
return "", fmt.Errorf("waiting for container: %w", err)
|
return "", fmt.Errorf("waiting for container: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -135,3 +144,58 @@ func checkIfTextInFile(file, text string) bool {
|
||||||
// Check for text
|
// Check for text
|
||||||
return bytes.Contains(content, []byte(text))
|
return bytes.Contains(content, []byte(text))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func CheckAndAddCrowdsecDependency(composePath string) error {
|
||||||
|
// Read the docker-compose.yml file
|
||||||
|
data, err := os.ReadFile(composePath)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("error reading compose file: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse YAML into a generic map
|
||||||
|
var compose map[string]interface{}
|
||||||
|
if err := yaml.Unmarshal(data, &compose); err != nil {
|
||||||
|
return fmt.Errorf("error parsing compose file: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get services section
|
||||||
|
services, ok := compose["services"].(map[string]interface{})
|
||||||
|
if !ok {
|
||||||
|
return fmt.Errorf("services section not found or invalid")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get traefik service
|
||||||
|
traefik, ok := services["traefik"].(map[string]interface{})
|
||||||
|
if !ok {
|
||||||
|
return fmt.Errorf("traefik service not found or invalid")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get dependencies
|
||||||
|
dependsOn, ok := traefik["depends_on"].(map[string]interface{})
|
||||||
|
if ok {
|
||||||
|
// Append the new block for crowdsec
|
||||||
|
dependsOn["crowdsec"] = map[string]interface{}{
|
||||||
|
"condition": "service_healthy",
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// No dependencies exist, create it
|
||||||
|
traefik["depends_on"] = map[string]interface{}{
|
||||||
|
"crowdsec": map[string]interface{}{
|
||||||
|
"condition": "service_healthy",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Marshal the modified data back to YAML with indentation
|
||||||
|
modifiedData, err := MarshalYAMLWithIndent(compose, 2) // Set indentation to 2 spaces
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("error marshaling YAML: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := os.WriteFile(composePath, modifiedData, 0644); err != nil {
|
||||||
|
return fmt.Errorf("error writing updated compose file: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Println("Added dependency of crowdsec to traefik")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
|
@ -3,7 +3,8 @@ module installer
|
||||||
go 1.23.0
|
go 1.23.0
|
||||||
|
|
||||||
require (
|
require (
|
||||||
golang.org/x/sys v0.29.0 // indirect
|
golang.org/x/term v0.28.0
|
||||||
golang.org/x/term v0.28.0 // indirect
|
gopkg.in/yaml.v3 v3.0.1
|
||||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
require golang.org/x/sys v0.29.0 // indirect
|
||||||
|
|
|
@ -2,6 +2,7 @@ golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU=
|
||||||
golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
golang.org/x/term v0.28.0 h1:/Ts8HFuMR2E6IP/jlo7QVLZHggjKQbhu/7H0LJFr3Gg=
|
golang.org/x/term v0.28.0 h1:/Ts8HFuMR2E6IP/jlo7QVLZHggjKQbhu/7H0LJFr3Gg=
|
||||||
golang.org/x/term v0.28.0/go.mod h1:Sw/lC2IAUZ92udQNf3WodGtn4k/XoLyZoh8v/8uiwek=
|
golang.org/x/term v0.28.0/go.mod h1:Sw/lC2IAUZ92udQNf3WodGtn4k/XoLyZoh8v/8uiwek=
|
||||||
|
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=
|
||||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
docker
|
||||||
example.com
|
example.com
|
||||||
pangolin.example.com
|
pangolin.example.com
|
||||||
admin@example.com
|
admin@example.com
|
||||||
|
|
527
install/main.go
|
@ -7,15 +7,17 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"io/fs"
|
"io/fs"
|
||||||
|
"math/rand"
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
|
"os/user"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"runtime"
|
"runtime"
|
||||||
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"syscall"
|
"syscall"
|
||||||
"text/template"
|
"text/template"
|
||||||
"time"
|
"time"
|
||||||
"unicode"
|
|
||||||
|
|
||||||
"golang.org/x/term"
|
"golang.org/x/term"
|
||||||
)
|
)
|
||||||
|
@ -31,16 +33,13 @@ func loadVersions(config *Config) {
|
||||||
var configFiles embed.FS
|
var configFiles embed.FS
|
||||||
|
|
||||||
type Config struct {
|
type Config struct {
|
||||||
|
InstallationContainerType SupportedContainer
|
||||||
PangolinVersion string
|
PangolinVersion string
|
||||||
GerbilVersion string
|
GerbilVersion string
|
||||||
BadgerVersion string
|
BadgerVersion string
|
||||||
BaseDomain string
|
BaseDomain string
|
||||||
DashboardDomain string
|
DashboardDomain string
|
||||||
LetsEncryptEmail string
|
LetsEncryptEmail string
|
||||||
AdminUserEmail string
|
|
||||||
AdminUserPassword string
|
|
||||||
DisableSignupWithoutInvite bool
|
|
||||||
DisableUserCreateOrg bool
|
|
||||||
EnableEmail bool
|
EnableEmail bool
|
||||||
EmailSMTPHost string
|
EmailSMTPHost string
|
||||||
EmailSMTPPort int
|
EmailSMTPPort int
|
||||||
|
@ -50,25 +49,106 @@ type Config struct {
|
||||||
InstallGerbil bool
|
InstallGerbil bool
|
||||||
TraefikBouncerKey string
|
TraefikBouncerKey string
|
||||||
DoCrowdsecInstall bool
|
DoCrowdsecInstall bool
|
||||||
|
Secret string
|
||||||
}
|
}
|
||||||
|
|
||||||
func main() {
|
type SupportedContainer string
|
||||||
reader := bufio.NewReader(os.Stdin)
|
|
||||||
|
|
||||||
// check if the user is root
|
const (
|
||||||
|
Docker SupportedContainer = "docker"
|
||||||
|
Podman SupportedContainer = "podman"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
|
||||||
|
// print a banner about prerequisites - opening port 80, 443, 51820, and 21820 on the VPS and firewall and pointing your domain to the VPS IP with a records. Docs are at http://localhost:3000/Getting%20Started/dns-networking
|
||||||
|
|
||||||
|
fmt.Println("Welcome to the Pangolin installer!")
|
||||||
|
fmt.Println("This installer will help you set up Pangolin on your server.")
|
||||||
|
fmt.Println("")
|
||||||
|
fmt.Println("Please make sure you have the following prerequisites:")
|
||||||
|
fmt.Println("- Open TCP ports 80 and 443 and UDP ports 51820 and 21820 on your VPS and firewall.")
|
||||||
|
fmt.Println("- Point your domain to the VPS IP with A records.")
|
||||||
|
fmt.Println("")
|
||||||
|
fmt.Println("http://docs.fossorial.io/Getting%20Started/dns-networking")
|
||||||
|
fmt.Println("")
|
||||||
|
fmt.Println("Lets get started!")
|
||||||
|
fmt.Println("")
|
||||||
|
|
||||||
|
reader := bufio.NewReader(os.Stdin)
|
||||||
|
inputContainer := readString(reader, "Would you like to run Pangolin as Docker or Podman containers?", "docker")
|
||||||
|
|
||||||
|
chosenContainer := Docker
|
||||||
|
if strings.EqualFold(inputContainer, "docker") {
|
||||||
|
chosenContainer = Docker
|
||||||
|
} else if strings.EqualFold(inputContainer, "podman") {
|
||||||
|
chosenContainer = Podman
|
||||||
|
} else {
|
||||||
|
fmt.Printf("Unrecognized container type: %s. Valid options are 'docker' or 'podman'.\n", inputContainer)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
if chosenContainer == Podman {
|
||||||
|
if !isPodmanInstalled() {
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := exec.Command("bash", "-c", "cat /etc/sysctl.conf | 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("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)
|
||||||
|
if approved {
|
||||||
if os.Geteuid() != 0 {
|
if os.Geteuid() != 0 {
|
||||||
fmt.Println("This script must be run as root")
|
fmt.Println("You need to run the installer as root for such a configuration.")
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Podman containers are not able to listen on privileged ports. The official recommendation is to
|
||||||
|
// container low-range ports as unprivileged ports.
|
||||||
|
// Linux only.
|
||||||
|
|
||||||
|
if err := run("bash", "-c", "echo 'net.ipv4.ip_unprivileged_port_start=80' >> /etc/sysctl.conf && sysctl -p"); err != nil {
|
||||||
|
fmt.Sprintf("failed to configure unprivileged ports: %v.\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
fmt.Println("You need to configure port forwarding or adjust the listening ports before running pangolin.")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
fmt.Println("Unprivileged ports have been configured.")
|
||||||
|
}
|
||||||
|
|
||||||
|
} else if chosenContainer == Docker {
|
||||||
|
// check if docker is not installed and the user is root
|
||||||
|
if !isDockerInstalled() {
|
||||||
|
if os.Geteuid() != 0 {
|
||||||
|
fmt.Println("Docker is not installed. Please install Docker manually or run this installer as root.")
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// check if the user is in the docker group (linux only)
|
||||||
|
if !isUserInDockerGroup() {
|
||||||
|
fmt.Println("You are not in the docker group.")
|
||||||
|
fmt.Println("The installer will not be able to run docker commands without running it as root.")
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// This shouldn't happen unless there's a third container runtime.
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
var config Config
|
var config Config
|
||||||
config.DoCrowdsecInstall = false
|
config.InstallationContainerType = chosenContainer
|
||||||
|
|
||||||
// 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(reader)
|
||||||
|
|
||||||
loadVersions(&config)
|
loadVersions(&config)
|
||||||
|
config.DoCrowdsecInstall = false
|
||||||
|
config.Secret = generateRandomSecretKey()
|
||||||
|
|
||||||
if err := createConfigFiles(config); err != nil {
|
if err := createConfigFiles(config); err != nil {
|
||||||
fmt.Printf("Error creating config files: %v\n", err)
|
fmt.Printf("Error creating config files: %v\n", err)
|
||||||
|
@ -77,17 +157,47 @@ func main() {
|
||||||
|
|
||||||
moveFile("config/docker-compose.yml", "docker-compose.yml")
|
moveFile("config/docker-compose.yml", "docker-compose.yml")
|
||||||
|
|
||||||
if !isDockerInstalled() && runtime.GOOS == "linux" {
|
if !isDockerInstalled() && runtime.GOOS == "linux" && chosenContainer == Docker {
|
||||||
if readBool(reader, "Docker is not installed. Would you like to install it?", true) {
|
if readBool(reader, "Docker is not installed. Would you like to install it?", true) {
|
||||||
installDocker()
|
installDocker()
|
||||||
|
// try to start docker service but ignore errors
|
||||||
|
if err := startDockerService(); err != nil {
|
||||||
|
fmt.Println("Error starting Docker service:", err)
|
||||||
|
} else {
|
||||||
|
fmt.Println("Docker service started successfully!")
|
||||||
|
}
|
||||||
|
// wait 10 seconds for docker to start checking if docker is running every 2 seconds
|
||||||
|
fmt.Println("Waiting for Docker to start...")
|
||||||
|
for i := 0; i < 5; i++ {
|
||||||
|
if isDockerRunning() {
|
||||||
|
fmt.Println("Docker is running!")
|
||||||
|
break
|
||||||
|
}
|
||||||
|
fmt.Println("Docker is not running yet, waiting...")
|
||||||
|
time.Sleep(2 * time.Second)
|
||||||
|
}
|
||||||
|
if !isDockerRunning() {
|
||||||
|
fmt.Println("Docker is still not running after 10 seconds. Please check the installation.")
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
fmt.Println("Docker installed successfully!")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Println("\n=== Starting installation ===")
|
fmt.Println("\n=== Starting installation ===")
|
||||||
|
|
||||||
if isDockerInstalled() {
|
if (isDockerInstalled() && chosenContainer == Docker) ||
|
||||||
|
(isPodmanInstalled() && chosenContainer == Podman) {
|
||||||
if readBool(reader, "Would you like to install and start the containers?", true) {
|
if readBool(reader, "Would you like to install and start the containers?", true) {
|
||||||
pullAndStartContainers()
|
if err := pullContainers(chosenContainer); err != nil {
|
||||||
|
fmt.Println("Error: ", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := startContainers(chosenContainer); err != nil {
|
||||||
|
fmt.Println("Error: ", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
@ -99,6 +209,8 @@ func main() {
|
||||||
// check if crowdsec is installed
|
// check if crowdsec is installed
|
||||||
if readBool(reader, "Would you like to install CrowdSec?", false) {
|
if readBool(reader, "Would you like to install CrowdSec?", false) {
|
||||||
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.
|
||||||
if readBool(reader, "Are you willing to manage CrowdSec?", false) {
|
if readBool(reader, "Are you willing to manage CrowdSec?", false) {
|
||||||
if config.DashboardDomain == "" {
|
if config.DashboardDomain == "" {
|
||||||
traefikConfig, err := ReadTraefikConfig("config/traefik/traefik_config.yml", "config/traefik/dynamic_config.yml")
|
traefikConfig, err := ReadTraefikConfig("config/traefik/traefik_config.yml", "config/traefik/dynamic_config.yml")
|
||||||
|
@ -128,6 +240,7 @@ func main() {
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Println("Installation complete!")
|
fmt.Println("Installation complete!")
|
||||||
|
fmt.Printf("\nTo complete the initial setup, please visit:\nhttps://%s/auth/initial-setup\n", config.DashboardDomain)
|
||||||
}
|
}
|
||||||
|
|
||||||
func readString(reader *bufio.Reader, prompt string, defaultValue string) string {
|
func readString(reader *bufio.Reader, prompt string, defaultValue string) string {
|
||||||
|
@ -191,46 +304,17 @@ func collectUserInput(reader *bufio.Reader) Config {
|
||||||
config.BaseDomain = readString(reader, "Enter your base domain (no subdomain e.g. example.com)", "")
|
config.BaseDomain = readString(reader, "Enter your base domain (no subdomain e.g. example.com)", "")
|
||||||
config.DashboardDomain = readString(reader, "Enter the domain for the Pangolin dashboard", "pangolin."+config.BaseDomain)
|
config.DashboardDomain = readString(reader, "Enter the domain for the Pangolin dashboard", "pangolin."+config.BaseDomain)
|
||||||
config.LetsEncryptEmail = readString(reader, "Enter email for Let's Encrypt certificates", "")
|
config.LetsEncryptEmail = readString(reader, "Enter email for Let's Encrypt certificates", "")
|
||||||
config.InstallGerbil = readBool(reader, "Do you want to use Gerbil to allow tunned connections", true)
|
config.InstallGerbil = readBool(reader, "Do you want to use Gerbil to allow tunneled connections", true)
|
||||||
|
|
||||||
// Admin user configuration
|
|
||||||
fmt.Println("\n=== Admin User Configuration ===")
|
|
||||||
config.AdminUserEmail = readString(reader, "Enter admin user email", "admin@"+config.BaseDomain)
|
|
||||||
for {
|
|
||||||
pass1 := readPassword("Create admin user password", reader)
|
|
||||||
pass2 := readPassword("Confirm admin user password", reader)
|
|
||||||
|
|
||||||
if pass1 != pass2 {
|
|
||||||
fmt.Println("Passwords do not match")
|
|
||||||
} else {
|
|
||||||
config.AdminUserPassword = pass1
|
|
||||||
if valid, message := validatePassword(config.AdminUserPassword); valid {
|
|
||||||
break
|
|
||||||
} else {
|
|
||||||
fmt.Println("Invalid password:", message)
|
|
||||||
fmt.Println("Password requirements:")
|
|
||||||
fmt.Println("- At least one uppercase English letter")
|
|
||||||
fmt.Println("- At least one lowercase English letter")
|
|
||||||
fmt.Println("- At least one digit")
|
|
||||||
fmt.Println("- At least one special character")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Security settings
|
|
||||||
fmt.Println("\n=== Security Settings ===")
|
|
||||||
config.DisableSignupWithoutInvite = readBool(reader, "Disable signup without invite", true)
|
|
||||||
config.DisableUserCreateOrg = readBool(reader, "Disable users from creating organizations", false)
|
|
||||||
|
|
||||||
// Email configuration
|
// Email configuration
|
||||||
fmt.Println("\n=== Email Configuration ===")
|
fmt.Println("\n=== Email Configuration ===")
|
||||||
config.EnableEmail = readBool(reader, "Enable email functionality", false)
|
config.EnableEmail = readBool(reader, "Enable email functionality (SMTP)", false)
|
||||||
|
|
||||||
if config.EnableEmail {
|
if config.EnableEmail {
|
||||||
config.EmailSMTPHost = readString(reader, "Enter SMTP host", "")
|
config.EmailSMTPHost = readString(reader, "Enter SMTP host", "")
|
||||||
config.EmailSMTPPort = readInt(reader, "Enter SMTP port (default 587)", 587)
|
config.EmailSMTPPort = readInt(reader, "Enter SMTP port (default 587)", 587)
|
||||||
config.EmailSMTPUser = readString(reader, "Enter SMTP username", "")
|
config.EmailSMTPUser = readString(reader, "Enter SMTP username", "")
|
||||||
config.EmailSMTPPass = readString(reader, "Enter SMTP password", "")
|
config.EmailSMTPPass = readString(reader, "Enter SMTP password", "") // Should this be readPassword?
|
||||||
config.EmailNoReply = readString(reader, "Enter no-reply email address", "")
|
config.EmailNoReply = readString(reader, "Enter no-reply email address", "")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -247,60 +331,10 @@ func collectUserInput(reader *bufio.Reader) Config {
|
||||||
fmt.Println("Error: Let's Encrypt email is required")
|
fmt.Println("Error: Let's Encrypt email is required")
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
if config.AdminUserEmail == "" || config.AdminUserPassword == "" {
|
|
||||||
fmt.Println("Error: Admin user email and password are required")
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
return config
|
return config
|
||||||
}
|
}
|
||||||
|
|
||||||
func validatePassword(password string) (bool, string) {
|
|
||||||
if len(password) == 0 {
|
|
||||||
return false, "Password cannot be empty"
|
|
||||||
}
|
|
||||||
|
|
||||||
var (
|
|
||||||
hasUpper bool
|
|
||||||
hasLower bool
|
|
||||||
hasDigit bool
|
|
||||||
hasSpecial bool
|
|
||||||
)
|
|
||||||
|
|
||||||
for _, char := range password {
|
|
||||||
switch {
|
|
||||||
case unicode.IsUpper(char):
|
|
||||||
hasUpper = true
|
|
||||||
case unicode.IsLower(char):
|
|
||||||
hasLower = true
|
|
||||||
case unicode.IsDigit(char):
|
|
||||||
hasDigit = true
|
|
||||||
case unicode.IsPunct(char) || unicode.IsSymbol(char):
|
|
||||||
hasSpecial = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var missing []string
|
|
||||||
if !hasUpper {
|
|
||||||
missing = append(missing, "an uppercase letter")
|
|
||||||
}
|
|
||||||
if !hasLower {
|
|
||||||
missing = append(missing, "a lowercase letter")
|
|
||||||
}
|
|
||||||
if !hasDigit {
|
|
||||||
missing = append(missing, "a digit")
|
|
||||||
}
|
|
||||||
if !hasSpecial {
|
|
||||||
missing = append(missing, "a special character")
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(missing) > 0 {
|
|
||||||
return false, fmt.Sprintf("Password must contain %s", strings.Join(missing, ", "))
|
|
||||||
}
|
|
||||||
|
|
||||||
return true, ""
|
|
||||||
}
|
|
||||||
|
|
||||||
func createConfigFiles(config Config) error {
|
func createConfigFiles(config Config) error {
|
||||||
os.MkdirAll("config", 0755)
|
os.MkdirAll("config", 0755)
|
||||||
os.MkdirAll("config/letsencrypt", 0755)
|
os.MkdirAll("config/letsencrypt", 0755)
|
||||||
|
@ -370,7 +404,6 @@ func createConfigFiles(config Config) error {
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("error walking config files: %v", err)
|
return fmt.Errorf("error walking config files: %v", err)
|
||||||
}
|
}
|
||||||
|
@ -427,24 +460,44 @@ func installDocker() error {
|
||||||
apt-get install -y docker-ce docker-ce-cli containerd.io docker-compose-plugin
|
apt-get install -y docker-ce docker-ce-cli containerd.io docker-compose-plugin
|
||||||
`, dockerArch))
|
`, dockerArch))
|
||||||
case strings.Contains(osRelease, "ID=fedora"):
|
case strings.Contains(osRelease, "ID=fedora"):
|
||||||
|
// Detect Fedora version to handle DNF 5 changes
|
||||||
|
versionCmd := exec.Command("bash", "-c", "grep VERSION_ID /etc/os-release | cut -d'=' -f2 | tr -d '\"'")
|
||||||
|
versionOutput, err := versionCmd.Output()
|
||||||
|
var fedoraVersion int
|
||||||
|
if err == nil {
|
||||||
|
if v, parseErr := strconv.Atoi(strings.TrimSpace(string(versionOutput))); parseErr == nil {
|
||||||
|
fedoraVersion = v
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use appropriate DNF syntax based on version
|
||||||
|
var repoCmd string
|
||||||
|
if fedoraVersion >= 41 {
|
||||||
|
// DNF 5 syntax for Fedora 41+
|
||||||
|
repoCmd = "dnf config-manager addrepo --from-repofile=https://download.docker.com/linux/fedora/docker-ce.repo"
|
||||||
|
} else {
|
||||||
|
// DNF 4 syntax for Fedora < 41
|
||||||
|
repoCmd = "dnf config-manager --add-repo https://download.docker.com/linux/fedora/docker-ce.repo"
|
||||||
|
}
|
||||||
|
|
||||||
installCmd = exec.Command("bash", "-c", fmt.Sprintf(`
|
installCmd = exec.Command("bash", "-c", fmt.Sprintf(`
|
||||||
dnf -y install dnf-plugins-core &&
|
dnf -y install dnf-plugins-core &&
|
||||||
dnf config-manager --add-repo https://download.docker.com/linux/fedora/docker-ce.repo &&
|
%s &&
|
||||||
dnf install -y docker-ce docker-ce-cli containerd.io docker-compose-plugin
|
dnf install -y docker-ce docker-ce-cli containerd.io docker-compose-plugin
|
||||||
`))
|
`, repoCmd))
|
||||||
case strings.Contains(osRelease, "ID=opensuse") || strings.Contains(osRelease, "ID=\"opensuse-"):
|
case strings.Contains(osRelease, "ID=opensuse") || strings.Contains(osRelease, "ID=\"opensuse-"):
|
||||||
installCmd = exec.Command("bash", "-c", `
|
installCmd = exec.Command("bash", "-c", `
|
||||||
zypper install -y docker docker-compose &&
|
zypper install -y docker docker-compose &&
|
||||||
systemctl enable docker
|
systemctl enable docker
|
||||||
`)
|
`)
|
||||||
case strings.Contains(osRelease, "ID=rhel") || strings.Contains(osRelease, "ID=\"rhel"):
|
case strings.Contains(osRelease, "ID=rhel") || strings.Contains(osRelease, "ID=\"rhel"):
|
||||||
installCmd = exec.Command("bash", "-c", fmt.Sprintf(`
|
installCmd = exec.Command("bash", "-c", `
|
||||||
dnf remove -y runc &&
|
dnf remove -y runc &&
|
||||||
dnf -y install yum-utils &&
|
dnf -y install yum-utils &&
|
||||||
dnf config-manager --add-repo https://download.docker.com/linux/rhel/docker-ce.repo &&
|
dnf config-manager --add-repo https://download.docker.com/linux/rhel/docker-ce.repo &&
|
||||||
dnf install -y docker-ce docker-ce-cli containerd.io docker-compose-plugin &&
|
dnf install -y docker-ce docker-ce-cli containerd.io docker-compose-plugin &&
|
||||||
systemctl enable docker
|
systemctl enable docker
|
||||||
`))
|
`)
|
||||||
case strings.Contains(osRelease, "ID=amzn"):
|
case strings.Contains(osRelease, "ID=amzn"):
|
||||||
installCmd = exec.Command("bash", "-c", `
|
installCmd = exec.Command("bash", "-c", `
|
||||||
yum update -y &&
|
yum update -y &&
|
||||||
|
@ -455,178 +508,198 @@ func installDocker() error {
|
||||||
default:
|
default:
|
||||||
return fmt.Errorf("unsupported Linux distribution")
|
return fmt.Errorf("unsupported Linux distribution")
|
||||||
}
|
}
|
||||||
|
|
||||||
installCmd.Stdout = os.Stdout
|
installCmd.Stdout = os.Stdout
|
||||||
installCmd.Stderr = os.Stderr
|
installCmd.Stderr = os.Stderr
|
||||||
return installCmd.Run()
|
return installCmd.Run()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func startDockerService() error {
|
||||||
|
if runtime.GOOS == "linux" {
|
||||||
|
cmd := exec.Command("systemctl", "enable", "--now", "docker")
|
||||||
|
cmd.Stdout = os.Stdout
|
||||||
|
cmd.Stderr = os.Stderr
|
||||||
|
return cmd.Run()
|
||||||
|
} else if runtime.GOOS == "darwin" {
|
||||||
|
// On macOS, Docker is usually started via the Docker Desktop application
|
||||||
|
fmt.Println("Please start Docker Desktop manually on macOS.")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return fmt.Errorf("unsupported operating system for starting Docker service")
|
||||||
|
}
|
||||||
|
|
||||||
func isDockerInstalled() bool {
|
func isDockerInstalled() bool {
|
||||||
cmd := exec.Command("docker", "--version")
|
return isContainerInstalled("docker")
|
||||||
|
}
|
||||||
|
|
||||||
|
func isPodmanInstalled() bool {
|
||||||
|
return isContainerInstalled("podman") && isContainerInstalled("podman-compose")
|
||||||
|
}
|
||||||
|
|
||||||
|
func isContainerInstalled(container string) bool {
|
||||||
|
cmd := exec.Command(container, "--version")
|
||||||
if err := cmd.Run(); err != nil {
|
if err := cmd.Run(); err != nil {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
func getCommandString(useNewStyle bool) string {
|
func isUserInDockerGroup() bool {
|
||||||
if useNewStyle {
|
if runtime.GOOS == "darwin" {
|
||||||
return "'docker compose'"
|
// Docker group is not applicable on macOS
|
||||||
|
// So we assume that the user can run Docker commands
|
||||||
|
return true
|
||||||
}
|
}
|
||||||
return "'docker-compose'"
|
|
||||||
|
if os.Geteuid() == 0 {
|
||||||
|
return true // Root user can run Docker commands anyway
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if the current user is in the docker group
|
||||||
|
if dockerGroup, err := user.LookupGroup("docker"); err == nil {
|
||||||
|
if currentUser, err := user.Current(); err == nil {
|
||||||
|
if currentUserGroupIds, err := currentUser.GroupIds(); err == nil {
|
||||||
|
for _, groupId := range currentUserGroupIds {
|
||||||
|
if groupId == dockerGroup.Gid {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Eventually, if any of the checks fail, we assume the user cannot run Docker commands
|
||||||
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
func pullAndStartContainers() error {
|
// isDockerRunning checks if the Docker daemon is running by using the `docker info` command.
|
||||||
fmt.Println("Starting containers...")
|
func isDockerRunning() bool {
|
||||||
|
cmd := exec.Command("docker", "info")
|
||||||
|
if err := cmd.Run(); err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
// Check which docker compose command is available
|
// executeDockerComposeCommandWithArgs executes the appropriate docker command with arguments supplied
|
||||||
|
func executeDockerComposeCommandWithArgs(args ...string) error {
|
||||||
|
var cmd *exec.Cmd
|
||||||
var useNewStyle bool
|
var useNewStyle bool
|
||||||
|
|
||||||
|
if !isDockerInstalled() {
|
||||||
|
return fmt.Errorf("docker is not installed")
|
||||||
|
}
|
||||||
|
|
||||||
checkCmd := exec.Command("docker", "compose", "version")
|
checkCmd := exec.Command("docker", "compose", "version")
|
||||||
if err := checkCmd.Run(); err == nil {
|
if err := checkCmd.Run(); err == nil {
|
||||||
useNewStyle = true
|
useNewStyle = true
|
||||||
} else {
|
} else {
|
||||||
// Check if docker-compose (old style) is available
|
|
||||||
checkCmd = exec.Command("docker-compose", "version")
|
checkCmd = exec.Command("docker-compose", "version")
|
||||||
if err := checkCmd.Run(); err != nil {
|
if err := checkCmd.Run(); err == nil {
|
||||||
return fmt.Errorf("neither 'docker compose' nor 'docker-compose' command is available: %v", err)
|
useNewStyle = false
|
||||||
|
} else {
|
||||||
|
return fmt.Errorf("neither 'docker compose' nor 'docker-compose' command is available")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Helper function to execute docker compose commands
|
|
||||||
executeCommand := func(args ...string) error {
|
|
||||||
var cmd *exec.Cmd
|
|
||||||
if useNewStyle {
|
if useNewStyle {
|
||||||
cmd = exec.Command("docker", append([]string{"compose"}, args...)...)
|
cmd = exec.Command("docker", append([]string{"compose"}, args...)...)
|
||||||
} else {
|
} else {
|
||||||
cmd = exec.Command("docker-compose", args...)
|
cmd = exec.Command("docker-compose", args...)
|
||||||
}
|
}
|
||||||
|
|
||||||
cmd.Stdout = os.Stdout
|
cmd.Stdout = os.Stdout
|
||||||
cmd.Stderr = os.Stderr
|
cmd.Stderr = os.Stderr
|
||||||
return cmd.Run()
|
return cmd.Run()
|
||||||
|
}
|
||||||
|
|
||||||
|
// pullContainers pulls the containers using the appropriate command.
|
||||||
|
func pullContainers(containerType SupportedContainer) error {
|
||||||
|
fmt.Println("Pulling the container images...")
|
||||||
|
if containerType == Podman {
|
||||||
|
if err := run("podman-compose", "-f", "docker-compose.yml", "pull"); err != nil {
|
||||||
|
return fmt.Errorf("failed to pull the containers: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Pull containers
|
return nil
|
||||||
fmt.Printf("Using %s command to pull containers...\n", getCommandString(useNewStyle))
|
|
||||||
if err := executeCommand("-f", "docker-compose.yml", "pull"); err != nil {
|
|
||||||
return fmt.Errorf("failed to pull containers: %v", err)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Start containers
|
if containerType == Docker {
|
||||||
fmt.Printf("Using %s command to start containers...\n", getCommandString(useNewStyle))
|
if err := executeDockerComposeCommandWithArgs("-f", "docker-compose.yml", "pull", "--policy", "always"); err != nil {
|
||||||
if err := executeCommand("-f", "docker-compose.yml", "up", "-d"); err != nil {
|
return fmt.Errorf("failed to pull the containers: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.Errorf("Unsupported container type: %s", containerType)
|
||||||
|
}
|
||||||
|
|
||||||
|
// startContainers starts the containers using the appropriate command.
|
||||||
|
func startContainers(containerType SupportedContainer) error {
|
||||||
|
fmt.Println("Starting containers...")
|
||||||
|
|
||||||
|
if containerType == Podman {
|
||||||
|
if err := run("podman-compose", "-f", "docker-compose.yml", "up", "-d", "--force-recreate"); err != nil {
|
||||||
|
return fmt.Errorf("failed start containers: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if containerType == Docker {
|
||||||
|
if err := executeDockerComposeCommandWithArgs("-f", "docker-compose.yml", "up", "-d", "--force-recreate"); err != nil {
|
||||||
return fmt.Errorf("failed to start containers: %v", err)
|
return fmt.Errorf("failed to start containers: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.Errorf("Unsupported container type: %s", containerType)
|
||||||
}
|
}
|
||||||
|
|
||||||
// bring containers down
|
// stopContainers stops the containers using the appropriate command.
|
||||||
func stopContainers() error {
|
func stopContainers(containerType SupportedContainer) error {
|
||||||
fmt.Println("Stopping containers...")
|
fmt.Println("Stopping containers...")
|
||||||
|
if containerType == Podman {
|
||||||
// Check which docker compose command is available
|
if err := run("podman-compose", "-f", "docker-compose.yml", "down"); err != nil {
|
||||||
var useNewStyle bool
|
|
||||||
checkCmd := exec.Command("docker", "compose", "version")
|
|
||||||
if err := checkCmd.Run(); err == nil {
|
|
||||||
useNewStyle = true
|
|
||||||
} else {
|
|
||||||
// Check if docker-compose (old style) is available
|
|
||||||
checkCmd = exec.Command("docker-compose", "version")
|
|
||||||
if err := checkCmd.Run(); err != nil {
|
|
||||||
return fmt.Errorf("neither 'docker compose' nor 'docker-compose' command is available: %v", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Helper function to execute docker compose commands
|
|
||||||
executeCommand := func(args ...string) error {
|
|
||||||
var cmd *exec.Cmd
|
|
||||||
if useNewStyle {
|
|
||||||
cmd = exec.Command("docker", append([]string{"compose"}, args...)...)
|
|
||||||
} else {
|
|
||||||
cmd = exec.Command("docker-compose", args...)
|
|
||||||
}
|
|
||||||
cmd.Stdout = os.Stdout
|
|
||||||
cmd.Stderr = os.Stderr
|
|
||||||
return cmd.Run()
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := executeCommand("-f", "docker-compose.yml", "down"); err != nil {
|
|
||||||
return fmt.Errorf("failed to stop containers: %v", err)
|
return fmt.Errorf("failed to stop containers: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
|
||||||
|
|
||||||
// just start containers
|
|
||||||
func startContainers() error {
|
|
||||||
fmt.Println("Starting containers...")
|
|
||||||
|
|
||||||
// Check which docker compose command is available
|
|
||||||
var useNewStyle bool
|
|
||||||
checkCmd := exec.Command("docker", "compose", "version")
|
|
||||||
if err := checkCmd.Run(); err == nil {
|
|
||||||
useNewStyle = true
|
|
||||||
} else {
|
|
||||||
// Check if docker-compose (old style) is available
|
|
||||||
checkCmd = exec.Command("docker-compose", "version")
|
|
||||||
if err := checkCmd.Run(); err != nil {
|
|
||||||
return fmt.Errorf("neither 'docker compose' nor 'docker-compose' command is available: %v", err)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Helper function to execute docker compose commands
|
if containerType == Docker {
|
||||||
executeCommand := func(args ...string) error {
|
if err := executeDockerComposeCommandWithArgs("-f", "docker-compose.yml", "down"); err != nil {
|
||||||
var cmd *exec.Cmd
|
return fmt.Errorf("failed to stop containers: %v", err)
|
||||||
if useNewStyle {
|
|
||||||
cmd = exec.Command("docker", append([]string{"compose"}, args...)...)
|
|
||||||
} else {
|
|
||||||
cmd = exec.Command("docker-compose", args...)
|
|
||||||
}
|
|
||||||
cmd.Stdout = os.Stdout
|
|
||||||
cmd.Stderr = os.Stderr
|
|
||||||
return cmd.Run()
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := executeCommand("-f", "docker-compose.yml", "up", "-d"); err != nil {
|
|
||||||
return fmt.Errorf("failed to start containers: %v", err)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.Errorf("Unsupported container type: %s", containerType)
|
||||||
}
|
}
|
||||||
|
|
||||||
func restartContainer(container string) error {
|
// restartContainer restarts a specific container using the appropriate command.
|
||||||
fmt.Printf("Restarting %s container...\n", container)
|
func restartContainer(container string, containerType SupportedContainer) error {
|
||||||
|
fmt.Println("Restarting containers...")
|
||||||
// Check which docker compose command is available
|
if containerType == Podman {
|
||||||
var useNewStyle bool
|
if err := run("podman-compose", "-f", "docker-compose.yml", "restart"); err != nil {
|
||||||
checkCmd := exec.Command("docker", "compose", "version")
|
return fmt.Errorf("failed to stop the container \"%s\": %v", container, err)
|
||||||
if err := checkCmd.Run(); err == nil {
|
|
||||||
useNewStyle = true
|
|
||||||
} else {
|
|
||||||
// Check if docker-compose (old style) is available
|
|
||||||
checkCmd = exec.Command("docker-compose", "version")
|
|
||||||
if err := checkCmd.Run(); err != nil {
|
|
||||||
return fmt.Errorf("neither 'docker compose' nor 'docker-compose' command is available: %v", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Helper function to execute docker compose commands
|
|
||||||
executeCommand := func(args ...string) error {
|
|
||||||
var cmd *exec.Cmd
|
|
||||||
if useNewStyle {
|
|
||||||
cmd = exec.Command("docker", append([]string{"compose"}, args...)...)
|
|
||||||
} else {
|
|
||||||
cmd = exec.Command("docker-compose", args...)
|
|
||||||
}
|
|
||||||
cmd.Stdout = os.Stdout
|
|
||||||
cmd.Stderr = os.Stderr
|
|
||||||
return cmd.Run()
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := executeCommand("-f", "docker-compose.yml", "restart", container); err != nil {
|
|
||||||
return fmt.Errorf("failed to restart %s container: %v", container, err)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if containerType == Docker {
|
||||||
|
if err := executeDockerComposeCommandWithArgs("-f", "docker-compose.yml", "restart", container); err != nil {
|
||||||
|
return fmt.Errorf("failed to stop the container \"%s\": %v", container, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.Errorf("Unsupported container type: %s", containerType)
|
||||||
}
|
}
|
||||||
|
|
||||||
func copyFile(src, dst string) error {
|
func copyFile(src, dst string) error {
|
||||||
|
@ -654,13 +727,13 @@ func moveFile(src, dst string) error {
|
||||||
return os.Remove(src)
|
return os.Remove(src)
|
||||||
}
|
}
|
||||||
|
|
||||||
func waitForContainer(containerName string) error {
|
func waitForContainer(containerName string, containerType SupportedContainer) error {
|
||||||
maxAttempts := 30
|
maxAttempts := 30
|
||||||
retryInterval := time.Second * 2
|
retryInterval := time.Second * 2
|
||||||
|
|
||||||
for attempt := 0; attempt < maxAttempts; attempt++ {
|
for attempt := 0; attempt < maxAttempts; attempt++ {
|
||||||
// Check if container is running
|
// Check if container is running
|
||||||
cmd := exec.Command("docker", "container", "inspect", "-f", "{{.State.Running}}", containerName)
|
cmd := exec.Command(string(containerType), "container", "inspect", "-f", "{{.State.Running}}", containerName)
|
||||||
var out bytes.Buffer
|
var out bytes.Buffer
|
||||||
cmd.Stdout = &out
|
cmd.Stdout = &out
|
||||||
|
|
||||||
|
@ -681,3 +754,25 @@ func waitForContainer(containerName string) error {
|
||||||
|
|
||||||
return fmt.Errorf("container %s did not start within %v seconds", containerName, maxAttempts*int(retryInterval.Seconds()))
|
return fmt.Errorf("container %s did not start within %v seconds", containerName, maxAttempts*int(retryInterval.Seconds()))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func generateRandomSecretKey() string {
|
||||||
|
const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
|
||||||
|
const length = 32
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run external commands with stdio/stderr attached.
|
||||||
|
func run(name string, args ...string) error {
|
||||||
|
cmd := exec.Command(name, args...)
|
||||||
|
cmd.Stdout = os.Stdout
|
||||||
|
cmd.Stderr = os.Stderr
|
||||||
|
return cmd.Run()
|
||||||
|
}
|
||||||
|
|
|
@ -1,267 +0,0 @@
|
||||||
## Login site
|
|
||||||
|
|
||||||
| EN | DE | Notes |
|
|
||||||
| --------------------- | ---------------------------------- | ----------- |
|
|
||||||
| Welcome to Pangolin | Willkommen bei Pangolin | |
|
|
||||||
| Log in to get started | Melden Sie sich an, um zu beginnen | |
|
|
||||||
| Email | E-Mail | |
|
|
||||||
| Enter your email | Geben Sie Ihre E-Mail-Adresse ein | placeholder |
|
|
||||||
| Password | Passwort | |
|
|
||||||
| Enter your password | Geben Sie Ihr Passwort ein | placeholder |
|
|
||||||
| Forgot your password? | Passwort vergessen? | |
|
|
||||||
| Log in | Anmelden | |
|
|
||||||
|
|
||||||
# Ogranization site after successful login
|
|
||||||
|
|
||||||
| EN | DE | Notes |
|
|
||||||
| ----------------------------------------- | -------------------------------------------- | ----- |
|
|
||||||
| Welcome to Pangolin | Willkommen bei Pangolin | |
|
|
||||||
| You're a member of {number} organization. | Sie sind Mitglied von {number} Organisation. | |
|
|
||||||
|
|
||||||
## Shared Header, Navbar and Footer
|
|
||||||
##### Header
|
|
||||||
|
|
||||||
| EN | DE | Notes |
|
|
||||||
| ------------------- | ------------------- | ----- |
|
|
||||||
| Documentation | Dokumentation | |
|
|
||||||
| Support | Support | |
|
|
||||||
| Organization {name} | Organisation {name} | |
|
|
||||||
##### Organization selector
|
|
||||||
|
|
||||||
| EN | DE | Notes |
|
|
||||||
| ---------------- | ----------------- | ----- |
|
|
||||||
| Search… | Suchen… | |
|
|
||||||
| Create | Erstellen | |
|
|
||||||
| New Organization | Neue Organisation | |
|
|
||||||
| Organizations | Organisationen | |
|
|
||||||
|
|
||||||
##### Navbar
|
|
||||||
|
|
||||||
| EN | DE | Notes |
|
|
||||||
| --------------- | ----------------- | ----- |
|
|
||||||
| Sites | Websites | |
|
|
||||||
| Resources | Ressourcen | |
|
|
||||||
| User & Roles | Benutzer & Rollen | |
|
|
||||||
| Shareable Links | Teilbare Links | |
|
|
||||||
| General | Allgemein | |
|
|
||||||
##### Footer
|
|
||||||
| EN | DE | |
|
|
||||||
| ------------------------- | --------------------------- | ------------------- |
|
|
||||||
| Page {number} of {number} | Seite {number} von {number} | |
|
|
||||||
| Rows per page | Zeilen pro Seite | |
|
|
||||||
| Pangolin | Pangolin | unten auf der Seite |
|
|
||||||
| Built by Fossorial | Erstellt von Fossorial | unten auf der Seite |
|
|
||||||
| Open Source | Open Source | unten auf der Seite |
|
|
||||||
| Documentation | Dokumentation | unten auf der Seite |
|
|
||||||
| {version} | {version} | unten auf der Seite |
|
|
||||||
|
|
||||||
## Main “Sites”
|
|
||||||
##### “Hero” section
|
|
||||||
|
|
||||||
| EN | DE | Notes |
|
|
||||||
| ------------------------------------------------------------ | ------------------------------------------------------------ | ----- |
|
|
||||||
| Newt (Recommended) | Newt (empfohlen) | |
|
|
||||||
| For the best user experience, use Newt. It uses WireGuard under the hood and allows you to address your private resources by their LAN address on your private network from within the Pangolin dashboard. | Für das beste Benutzererlebnis verwenden Sie Newt. Es nutzt WireGuard im Hintergrund und ermöglicht es Ihnen, auf Ihre privaten Ressourcen über ihre LAN-Adresse in Ihrem privaten Netzwerk direkt aus dem Pangolin-Dashboard zuzugreifen. | |
|
|
||||||
| Runs in Docker | Läuft in Docker | |
|
|
||||||
| Runs in shell on macOS, Linux, and Windows | Läuft in der Shell auf macOS, Linux und Windows | |
|
|
||||||
| Install Newt | Newt installieren | |
|
|
||||||
| Basic WireGuard<br> | Verwenden Sie einen beliebigen WireGuard-Client, um eine Verbindung herzustellen. Sie müssen auf Ihre internen Ressourcen über die Peer-IP-Adresse zugreifen. | |
|
|
||||||
| Compatible with all WireGuard clients<br> | Kompatibel mit allen WireGuard-Clients<br> | |
|
|
||||||
| Manual configuration required | Manuelle Konfiguration erforderlich<br> | |
|
|
||||||
##### Content
|
|
||||||
|
|
||||||
| EN | DE | Notes |
|
|
||||||
| --------------------------------------------------------- | ------------------------------------------------------------ | -------------------------------- |
|
|
||||||
| Manage Sites | Seiten verwalten | |
|
|
||||||
| Allow connectivity to your network through secure tunnels | Ermöglichen Sie die Verbindung zu Ihrem Netzwerk über ein sicheren Tunnel | |
|
|
||||||
| Search sites | Seiten suchen | placeholder |
|
|
||||||
| Add Site | Seite hinzufügen | |
|
|
||||||
| Name | Name | table header |
|
|
||||||
| Online | Status | table header |
|
|
||||||
| Site | Seite | table header |
|
|
||||||
| Data In | Eingehende Daten | table header |
|
|
||||||
| Data Out | Ausgehende Daten | table header |
|
|
||||||
| Connection Type | Verbindungstyp | table header |
|
|
||||||
| Online | Online | site state |
|
|
||||||
| Offline | Offline | site state |
|
|
||||||
| Edit → | Bearbeiten → | |
|
|
||||||
| View settings | Einstellungen anzeigen | Popup after clicking “…” on site |
|
|
||||||
| Delete | Löschen | Popup after clicking “…” on site |
|
|
||||||
##### Add Site Popup
|
|
||||||
|
|
||||||
| EN | DE | Notes |
|
|
||||||
| ------------------------------------------------------ | ----------------------------------------------------------- | ----------- |
|
|
||||||
| Create Site | Seite erstellen | |
|
|
||||||
| Create a new site to start connection for this site | Erstellen Sie eine neue Seite, um die Verbindung zu starten | |
|
|
||||||
| Name | Name | |
|
|
||||||
| Site name | Seiten-Name | placeholder |
|
|
||||||
| This is the name that will be displayed for this site. | So wird Ihre Seite angezeigt | desc |
|
|
||||||
| Method | Methode | |
|
|
||||||
| Local | Lokal | |
|
|
||||||
| Newt | Newt | |
|
|
||||||
| WireGuard | WireGuard | |
|
|
||||||
| This is how you will expose connections. | So werden Verbindungen freigegeben. | |
|
|
||||||
| You will only be able to see the configuration once. | Diese Konfiguration können Sie nur einmal sehen. | |
|
|
||||||
| Learn how to install Newt on your system | Erfahren Sie, wie Sie Newt auf Ihrem System installieren | |
|
|
||||||
| I have copied the config | Ich habe die Konfiguration kopiert | |
|
|
||||||
| Create Site | Website erstellen | |
|
|
||||||
| Close | Schließen | |
|
|
||||||
|
|
||||||
## Main “Resources”
|
|
||||||
|
|
||||||
##### “Hero” section
|
|
||||||
|
|
||||||
| EN | DE | Notes |
|
|
||||||
| ------------------------------------------------------------ | ------------------------------------------------------------ | ----- |
|
|
||||||
| Resources | Ressourcen | |
|
|
||||||
| Ressourcen sind Proxy-Server für Anwendungen, die in Ihrem privaten Netzwerk laufen. Erstellen Sie eine Ressource für jede HTTP- oder HTTPS-Anwendung in Ihrem privaten Netzwerk. Jede Ressource muss mit einer Website verbunden sein, um eine private und sichere Verbindung über den verschlüsselten WireGuard-Tunnel zu ermöglichen. | Ressourcen sind Proxy-Server für Anwendungen, die in Ihrem privaten Netzwerk laufen. Erstellen Sie eine Ressource für jede HTTP- oder HTTPS-Anwendung in Ihrem privaten Netzwerk. Jede Ressource muss mit einer Website verbunden sein, um eine private und sichere Verbindung über den verschlüsselten WireGuard-Tunnel zu ermöglichen. | |
|
|
||||||
| Secure connectivity with WireGuard encryption | Sichere Verbindung mit WireGuard-Verschlüsselung | |
|
|
||||||
| Configure multiple authentication methods | Konfigurieren Sie mehrere Authentifizierungsmethoden | |
|
|
||||||
| User and role-based access control | Benutzer- und rollenbasierte Zugriffskontrolle | |
|
|
||||||
##### Content
|
|
||||||
|
|
||||||
| EN | DE | Notes |
|
|
||||||
| -------------------------------------------------- | ---------------------------------------------------------- | -------------------- |
|
|
||||||
| Manage Resources | Ressourcen verwalten | |
|
|
||||||
| Create secure proxies to your private applications | Erstellen Sie sichere Proxys für Ihre privaten Anwendungen | |
|
|
||||||
| Search resources | Ressourcen durchsuchen | placeholder |
|
|
||||||
| Name | Name | |
|
|
||||||
| Site | Website | |
|
|
||||||
| Full URL | Vollständige URL | |
|
|
||||||
| Authentication | Authentifizierung | |
|
|
||||||
| Not Protected | Nicht geschützt | authentication state |
|
|
||||||
| Protected | Geschützt | authentication state |
|
|
||||||
| Edit → | Bearbeiten → | |
|
|
||||||
| Add Resource | Ressource hinzufügen | |
|
|
||||||
##### Add Resource Popup
|
|
||||||
|
|
||||||
| EN | DE | Notes |
|
|
||||||
| ------------------------------------------------------------ | ------------------------------------------------------------ | ------------------- |
|
|
||||||
| Create Resource | Ressource erstellen | |
|
|
||||||
| Create a new resource to proxy request to your app | Erstellen Sie eine neue Ressource, um Anfragen an Ihre App zu proxen | |
|
|
||||||
| Name | Name | |
|
|
||||||
| My Resource | Neue Ressource | name placeholder |
|
|
||||||
| This is the name that will be displayed for this resource. | Dies ist der Name, der für diese Ressource angezeigt wird | |
|
|
||||||
| Subdomain | Subdomain | |
|
|
||||||
| Enter subdomain | Subdomain eingeben | |
|
|
||||||
| This is the fully qualified domain name that will be used to access the resource. | Dies ist der vollständige Domainname, der für den Zugriff auf die Ressource verwendet wird. | |
|
|
||||||
| Site | Website | |
|
|
||||||
| Search site… | Website suchen… | Site selector popup |
|
|
||||||
| This is the site that will be used in the dashboard. | Dies ist die Website, die im Dashboard verwendet wird. | |
|
|
||||||
| Create Resource | Ressource erstellen | |
|
|
||||||
| Close | Schließen | |
|
|
||||||
|
|
||||||
|
|
||||||
## Main “User & Roles”
|
|
||||||
##### Content
|
|
||||||
|
|
||||||
| EN | DE | Notes |
|
|
||||||
| ------------------------------------------------------------ | ------------------------------------------------------------ | ----------------------------- |
|
|
||||||
| Manage User & Roles | Benutzer & Rollen verwalten | |
|
|
||||||
| Invite users and add them to roles to manage access to your organization | Laden Sie Benutzer ein und weisen Sie ihnen Rollen zu, um den Zugriff auf Ihre Organisation zu verwalten | |
|
|
||||||
| Users | Benutzer | sidebar item |
|
|
||||||
| Roles | Rollen | sidebar item |
|
|
||||||
| **User tab** | | |
|
|
||||||
| Search users | Benutzer suchen | placeholder |
|
|
||||||
| Invite User | Benutzer einladen | addbutton |
|
|
||||||
| Email | E-Mail | table header |
|
|
||||||
| Status | Status | table header |
|
|
||||||
| Role | Rolle | table header |
|
|
||||||
| Confirmed | Bestätigt | account status |
|
|
||||||
| Not confirmed (?) | Nicht bestätigt (?) | unknown for me account status |
|
|
||||||
| Owner | Besitzer | role |
|
|
||||||
| Admin | Administrator | role |
|
|
||||||
| Member | Mitglied | role |
|
|
||||||
| **Roles Tab** | | |
|
|
||||||
| Search roles | Rollen suchen | placeholder |
|
|
||||||
| Add Role | Rolle hinzufügen | addbutton |
|
|
||||||
| Name | Name | table header |
|
|
||||||
| Description | Beschreibung | table header |
|
|
||||||
| Admin | Administrator | role |
|
|
||||||
| Member | Mitglied | role |
|
|
||||||
| Admin role with the most permissions | Administratorrolle mit den meisten Berechtigungen | admin role desc |
|
|
||||||
| Members can only view resources | Mitglieder können nur Ressourcen anzeigen | member role desc |
|
|
||||||
|
|
||||||
##### Invite User popup
|
|
||||||
|
|
||||||
| EN | DE | Notes |
|
|
||||||
| ----------------- | ------------------------------------------------------- | ----------- |
|
|
||||||
| Invite User | Geben Sie neuen Benutzern Zugriff auf Ihre Organisation | |
|
|
||||||
| Email | E-Mail | |
|
|
||||||
| Enter an email | E-Mail eingeben | placeholder |
|
|
||||||
| Role | Rolle | |
|
|
||||||
| Select role | Rolle auswählen | placeholder |
|
|
||||||
| Gültig für | Gültig bis | |
|
|
||||||
| 1 day | Tag | |
|
|
||||||
| 2 days | 2 Tage | |
|
|
||||||
| 3 days | 3 Tage | |
|
|
||||||
| 4 days | 4 Tage | |
|
|
||||||
| 5 days | 5 Tage | |
|
|
||||||
| 6 days | 6 Tage | |
|
|
||||||
| 7 days | 7 Tage | |
|
|
||||||
| Create Invitation | Einladung erstellen | |
|
|
||||||
| Close | Schließen | |
|
|
||||||
|
|
||||||
|
|
||||||
## Main “Shareable Links”
|
|
||||||
##### “Hero” section
|
|
||||||
|
|
||||||
| EN | DE | Notes |
|
|
||||||
| ------------------------------------------------------------ | ------------------------------------------------------------ | ----- |
|
|
||||||
| Shareable Links | Teilbare Links | |
|
|
||||||
| Create shareable links to your resources. Links provide temporary or unlimited access to your resource. You can configure the expiration duration of the link when you create one. | Erstellen Sie teilbare Links zu Ihren Ressourcen. Links bieten temporären oder unbegrenzten Zugriff auf Ihre Ressource. Sie können die Gültigkeitsdauer des Links beim Erstellen konfigurieren. | |
|
|
||||||
| Easy to create and share | Einfach zu erstellen und zu teilen | |
|
|
||||||
| Configurable expiration duration | Konfigurierbare Gültigkeitsdauer | |
|
|
||||||
| Secure and revocable | Sicher und widerrufbar | |
|
|
||||||
##### Content
|
|
||||||
|
|
||||||
| EN | DE | Notes |
|
|
||||||
| ------------------------------------------------------------ | ------------------------------------------------------------ | ----------------- |
|
|
||||||
| Manage Shareable Links | Teilbare Links verwalten | |
|
|
||||||
| Create shareable links to grant temporary or permanent access to your resources | Erstellen Sie teilbare Links, um temporären oder permanenten Zugriff auf Ihre Ressourcen zu gewähren | |
|
|
||||||
| Search links | Links suchen | placeholder |
|
|
||||||
| Create Share Link | Neuen Link erstellen | addbutton |
|
|
||||||
| Resource | Ressource | table header |
|
|
||||||
| Title | Titel | table header |
|
|
||||||
| Created | Erstellt | table header |
|
|
||||||
| Expires | Gültig bis | table header |
|
|
||||||
| No links. Create one to get started. | Keine Links. Erstellen Sie einen, um zu beginnen. | table placeholder |
|
|
||||||
|
|
||||||
##### Create Shareable Link popup
|
|
||||||
|
|
||||||
| EN | DE | Notes |
|
|
||||||
| ------------------------------------------------------------ | ------------------------------------------------------------ | ----------------------- |
|
|
||||||
| Create Shareable Link | Teilbaren Link erstellen | |
|
|
||||||
| Anyone with this link can access the resource | Jeder mit diesem Link kann auf die Ressource zugreifen | |
|
|
||||||
| Resource | Ressource | |
|
|
||||||
| Select resource | Ressource auswählen | |
|
|
||||||
| Search resources… | Ressourcen suchen… | resource selector popup |
|
|
||||||
| Title (optional) | Titel (optional) | |
|
|
||||||
| Enter title | Titel eingeben | placeholder |
|
|
||||||
| Expire in | Gültig bis | |
|
|
||||||
| Minutes | Minuten | |
|
|
||||||
| Hours | Stunden | |
|
|
||||||
| Days | Tage | |
|
|
||||||
| Months | Monate | |
|
|
||||||
| Years | Jahre | |
|
|
||||||
| Never expire | Nie ablaufen | |
|
|
||||||
| Expiration time is how long the link will be usable and provide access to the resource. After this time, the link will no longer work, and users who used this link will lose access to the resource. | Die Gültigkeitsdauer bestimmt, wie lange der Link nutzbar ist und Zugriff auf die Ressource bietet. Nach Ablauf dieser Zeit funktioniert der Link nicht mehr, und Benutzer, die diesen Link verwendet haben, verlieren den Zugriff auf die Ressource. | |
|
|
||||||
| Create Link | Link erstellen | |
|
|
||||||
| Close | Schließen | |
|
|
||||||
|
|
||||||
|
|
||||||
## Main “General”
|
|
||||||
|
|
||||||
| EN | DE | Notes |
|
|
||||||
| ------------------------------------------------------------ | ------------------------------------------------------------ | ------------ |
|
|
||||||
| General | Allgemein | |
|
|
||||||
| Configure your organization’s general settings | Konfigurieren Sie die allgemeinen Einstellungen Ihrer Organisation | |
|
|
||||||
| General | Allgemein | sidebar item |
|
|
||||||
| Organization Settings | Organisationseinstellungen | |
|
|
||||||
| Manage your organization details and configuration | Verwalten Sie die Details und Konfiguration Ihrer Organisation | |
|
|
||||||
| Name | Name | |
|
|
||||||
| This is the display name of the org | Dies ist der Anzeigename Ihrer Organisation | |
|
|
||||||
| Save Settings | Einstellungen speichern | |
|
|
||||||
| Danger Zone | Gefahrenzone | |
|
|
||||||
| Once you delete this org, there is no going back. Please be certain. | Wenn Sie diese Organisation löschen, gibt es kein Zurück. Bitte seien Sie sicher. | |
|
|
||||||
| Delete Organization Data | Organisationsdaten löschen | |
|
|
|
@ -1,291 +0,0 @@
|
||||||
## Authentication Site
|
|
||||||
|
|
||||||
|
|
||||||
| EN | ES | Notes |
|
|
||||||
| -------------------------------------------------------- | ------------------------------------------------------------ | ---------- |
|
|
||||||
| Powered by [Pangolin](https://github.com/fosrl/pangolin) | Desarrollado por [Pangolin](https://github.com/fosrl/pangolin) | |
|
|
||||||
| Authentication Required | Se requiere autenticación | |
|
|
||||||
| Choose your preferred method to access {resource} | Elije tu método requerido para acceder a {resource} | |
|
|
||||||
| PIN | PIN | |
|
|
||||||
| User | Usuario | |
|
|
||||||
| 6-digit PIN Code | Código PIN de 6 dígitos | pin login |
|
|
||||||
| Login in with PIN | Registrate con PIN | pin login |
|
|
||||||
| Email | Email | user login |
|
|
||||||
| Enter your email | Introduce tu email | user login |
|
|
||||||
| Password | Contraseña | user login |
|
|
||||||
| Enter your password | Introduce tu contraseña | user login |
|
|
||||||
| Forgot your password? | ¿Olvidaste tu contraseña? | user login |
|
|
||||||
| Log in | Iniciar sesión | user login |
|
|
||||||
|
|
||||||
|
|
||||||
## Login site
|
|
||||||
|
|
||||||
| EN | ES | Notes |
|
|
||||||
| --------------------- | ---------------------------------- | ----------- |
|
|
||||||
| Welcome to Pangolin | Binvenido a Pangolin | |
|
|
||||||
| Log in to get started | Registrate para comenzar | |
|
|
||||||
| Email | Email | |
|
|
||||||
| Enter your email | Introduce tu email | placeholder |
|
|
||||||
| Password | Contraseña | |
|
|
||||||
| Enter your password | Introduce tu contraseña | placeholder |
|
|
||||||
| Forgot your password? | ¿Olvidaste tu contraseña? | |
|
|
||||||
| Log in | Iniciar sesión | |
|
|
||||||
|
|
||||||
# Ogranization site after successful login
|
|
||||||
|
|
||||||
| EN | ES | Notes |
|
|
||||||
| ----------------------------------------- | -------------------------------------------- | ----- |
|
|
||||||
| Welcome to Pangolin | Binvenido a Pangolin | |
|
|
||||||
| You're a member of {number} organization. | Eres miembro de la organización {number}. | |
|
|
||||||
|
|
||||||
## Shared Header, Navbar and Footer
|
|
||||||
##### Header
|
|
||||||
|
|
||||||
| EN | ES | Notes |
|
|
||||||
| ------------------- | ------------------- | ----- |
|
|
||||||
| Documentation | Documentación | |
|
|
||||||
| Support | Soporte | |
|
|
||||||
| Organization {name} | Organización {name} | |
|
|
||||||
##### Organization selector
|
|
||||||
|
|
||||||
| EN | ES | Notes |
|
|
||||||
| ---------------- | ----------------- | ----- |
|
|
||||||
| Search… | Buscar… | |
|
|
||||||
| Create | Crear | |
|
|
||||||
| New Organization | Nueva Organización| |
|
|
||||||
| Organizations | Organizaciones | |
|
|
||||||
|
|
||||||
##### Navbar
|
|
||||||
|
|
||||||
| EN | ES | Notes |
|
|
||||||
| --------------- | -----------------------| ----- |
|
|
||||||
| Sites | Sitios | |
|
|
||||||
| Resources | Recursos | |
|
|
||||||
| User & Roles | Usuarios y roles | |
|
|
||||||
| Shareable Links | Enlaces para compartir | |
|
|
||||||
| General | General | |
|
|
||||||
|
|
||||||
##### Footer
|
|
||||||
| EN | ES | |
|
|
||||||
| ------------------------- | --------------------------- | -------|
|
|
||||||
| Page {number} of {number} | Página {number} de {number} | footer |
|
|
||||||
| Rows per page | Filas por página | footer |
|
|
||||||
| Pangolin | Pangolin | footer |
|
|
||||||
| Built by Fossorial | Construido por Fossorial | footer |
|
|
||||||
| Open Source | Código abierto | footer |
|
|
||||||
| Documentation | Documentación | footer |
|
|
||||||
| {version} | {version} | footer |
|
|
||||||
|
|
||||||
## Main “Sites”
|
|
||||||
##### “Hero” section
|
|
||||||
|
|
||||||
| EN | ES | Notes |
|
|
||||||
| ------------------------------------------------------------ | ------------------------------------------------------------ | ----- |
|
|
||||||
| Newt (Recommended) | Newt (Recomendado) | |
|
|
||||||
| For the best user experience, use Newt. It uses WireGuard under the hood and allows you to address your private resources by their LAN address on your private network from within the Pangolin dashboard. | Para obtener la mejor experiencia de usuario, utiliza Newt. Utiliza WireGuard internamente y te permite abordar tus recursos privados mediante tu dirección LAN en tu red privada desde el panel de Pangolin. | |
|
|
||||||
| Runs in Docker | Se ejecuta en Docker | |
|
|
||||||
| Runs in shell on macOS, Linux, and Windows | Se ejecuta en shell en macOS, Linux y Windows | |
|
|
||||||
| Install Newt | Instalar Newt | |
|
|
||||||
| Basic WireGuard<br> | WireGuard básico<br> | |
|
|
||||||
| Compatible with all WireGuard clients<br> | Compatible con todos los clientes WireGuard<br> | |
|
|
||||||
| Manual configuration required | Se requiere configuración manual | |
|
|
||||||
|
|
||||||
##### Content
|
|
||||||
|
|
||||||
| EN | ES | Notes |
|
|
||||||
| --------------------------------------------------------- | ------------------------------------------------------------ | -------------------------------- |
|
|
||||||
| Manage Sites | Administrar sitios | |
|
|
||||||
| Allow connectivity to your network through secure tunnels | Permitir la conectividad a tu red a través de túneles seguros| |
|
|
||||||
| Search sites | Buscar sitios | placeholder |
|
|
||||||
| Add Site | Agregar sitio | |
|
|
||||||
| Name | Nombre | table header |
|
|
||||||
| Online | Conectado | table header |
|
|
||||||
| Site | Sitio | table header |
|
|
||||||
| Data In | Datos en | table header |
|
|
||||||
| Data Out | Datos de salida | table header |
|
|
||||||
| Connection Type | Tipo de conexión | table header |
|
|
||||||
| Online | Conectado | site state |
|
|
||||||
| Offline | Desconectado | site state |
|
|
||||||
| Edit → | Editar → | |
|
|
||||||
| View settings | Ver configuración | Popup after clicking “…” on site |
|
|
||||||
| Delete | Borrar | Popup after clicking “…” on site |
|
|
||||||
|
|
||||||
##### Add Site Popup
|
|
||||||
|
|
||||||
| EN | ES | Notes |
|
|
||||||
| ------------------------------------------------------ | ----------------------------------------------------------- | ----------- |
|
|
||||||
| Create Site | Crear sitio | |
|
|
||||||
| Create a new site to start connection for this site | Crear un nuevo sitio para iniciar la conexión para este sitio | |
|
|
||||||
| Name | Nombre | |
|
|
||||||
| Site name | Nombre del sitio | placeholder |
|
|
||||||
| This is the name that will be displayed for this site. | Este es el nombre que se mostrará para este sitio. | desc |
|
|
||||||
| Method | Método | |
|
|
||||||
| Local | Local | |
|
|
||||||
| Newt | Newt | |
|
|
||||||
| WireGuard | WireGuard | |
|
|
||||||
| This is how you will expose connections. | Así es como expondrás las conexiones. | |
|
|
||||||
| You will only be able to see the configuration once. | Solo podrás ver la configuración una vez. | |
|
|
||||||
| Learn how to install Newt on your system | Aprende a instalar Newt en tu sistema | |
|
|
||||||
| I have copied the config | He copiado la configuración | |
|
|
||||||
| Create Site | Crear sitio | |
|
|
||||||
| Close | Cerrar | |
|
|
||||||
|
|
||||||
## Main “Resources”
|
|
||||||
|
|
||||||
##### “Hero” section
|
|
||||||
|
|
||||||
| EN | ES | Notes |
|
|
||||||
| ------------------------------------------------------------ | ------------------------------------------------------------ | ----- |
|
|
||||||
| Resources | Recursos | |
|
|
||||||
| Ressourcen sind Proxy-Server für Anwendungen, die in Ihrem privaten Netzwerk laufen. Erstellen Sie eine Ressource für jede HTTP- oder HTTPS-Anwendung in Ihrem privaten Netzwerk. Jede Ressource muss mit einer Website verbunden sein, um eine private und sichere Verbindung über den verschlüsselten WireGuard-Tunnel zu ermöglichen. |Los recursos son servidores proxy para aplicaciones que se ejecutan en su red privada. Cree un recurso para cada aplicación HTTP o HTTPS en su red privada. Cada recurso debe estar conectado a un sitio web para proporcionar una conexión privada y segura a través del túnel cifrado WireGuard. | |
|
|
||||||
| Secure connectivity with WireGuard encryption | Conectividad segura con encriptación WireGuard | |
|
|
||||||
| Configure multiple authentication methods | Configura múltiples métodos de autenticación | |
|
|
||||||
| User and role-based access control | Control de acceso basado en usuarios y roles | |
|
|
||||||
|
|
||||||
##### Content
|
|
||||||
|
|
||||||
| EN | ES | Notes |
|
|
||||||
| -------------------------------------------------- | ---------------------------------------------------------- | -------------------- |
|
|
||||||
| Manage Resources | Administrar recursos | |
|
|
||||||
| Create secure proxies to your private applications | Crea servidores proxy seguros para tus aplicaciones privadas | |
|
|
||||||
| Search resources | Buscar recursos | placeholder |
|
|
||||||
| Name | Nombre | |
|
|
||||||
| Site | Sitio | |
|
|
||||||
| Full URL | URL completa | |
|
|
||||||
| Authentication | Autenticación | |
|
|
||||||
| Not Protected | No protegido | authentication state |
|
|
||||||
| Protected | Protegido | authentication state |
|
|
||||||
| Edit → | Editar → | |
|
|
||||||
| Add Resource | Agregar recurso | |
|
|
||||||
|
|
||||||
##### Add Resource Popup
|
|
||||||
|
|
||||||
| EN | ES | Notes |
|
|
||||||
| ------------------------------------------------------------ | ------------------------------------------------------------ | ------------------- |
|
|
||||||
| Create Resource | Crear recurso | |
|
|
||||||
| Create a new resource to proxy request to your app | Crea un nuevo recurso para enviar solicitudes a tu aplicación | |
|
|
||||||
| Name | Nombre | |
|
|
||||||
| My Resource | Mi recurso | name placeholder |
|
|
||||||
| This is the name that will be displayed for this resource. | Este es el nombre que se mostrará para este recurso. | |
|
|
||||||
| Subdomain | Subdominio | |
|
|
||||||
| Enter subdomain | Ingresar subdominio | |
|
|
||||||
| This is the fully qualified domain name that will be used to access the resource. | Este es el nombre de dominio completo que se utilizará para acceder al recurso. | |
|
|
||||||
| Site | Sitio | |
|
|
||||||
| Search site… | Buscar sitio… | Site selector popup |
|
|
||||||
| This is the site that will be used in the dashboard. | Este es el sitio que se utilizará en el panel de control. | |
|
|
||||||
| Create Resource | Crear recurso | |
|
|
||||||
| Close | Cerrar | |
|
|
||||||
|
|
||||||
## Main “User & Roles”
|
|
||||||
##### Content
|
|
||||||
|
|
||||||
| EN | ES | Notes |
|
|
||||||
| ------------------------------------------------------------ | ------------------------------------------------------------ | ----------------------------- |
|
|
||||||
| Manage User & Roles | Administrar usuarios y roles | |
|
|
||||||
| Invite users and add them to roles to manage access to your organization | Invita a usuarios y agrégalos a roles para administrar el acceso a tu organización | |
|
|
||||||
| Users | Usuarios | sidebar item |
|
|
||||||
| Roles | Roles | sidebar item |
|
|
||||||
| **User tab** | **Pestaña de usuario** | |
|
|
||||||
| Search users | Buscar usuarios | placeholder |
|
|
||||||
| Invite User | Invitar usuario | addbutton |
|
|
||||||
| Email | Email | table header |
|
|
||||||
| Status | Estado | table header |
|
|
||||||
| Role | Role | table header |
|
|
||||||
| Confirmed | Confirmado | account status |
|
|
||||||
| Not confirmed (?) | No confirmado (?) | unknown for me account status |
|
|
||||||
| Owner | Dueño | role |
|
|
||||||
| Admin | Administrador | role |
|
|
||||||
| Member | Miembro | role |
|
|
||||||
| **Roles Tab** | **Pestaña Roles** | |
|
|
||||||
| Search roles | Buscar roles | placeholder |
|
|
||||||
| Add Role | Agregar rol | addbutton |
|
|
||||||
| Name | Nombre | table header |
|
|
||||||
| Description | Descripción | table header |
|
|
||||||
| Admin | Administrador | role |
|
|
||||||
| Member | Miembro | role |
|
|
||||||
| Admin role with the most permissions | Rol de administrador con más permisos | admin role desc |
|
|
||||||
| Members can only view resources | Los miembros sólo pueden ver los recursos | member role desc |
|
|
||||||
|
|
||||||
##### Invite User popup
|
|
||||||
|
|
||||||
| EN | ES | Notes |
|
|
||||||
| ----------------- | ------------------------------------------------------- | ----------- |
|
|
||||||
| Invite User | Invitar usuario | |
|
|
||||||
| Email | Email | |
|
|
||||||
| Enter an email | Introduzca un email | placeholder |
|
|
||||||
| Role | Rol | |
|
|
||||||
| Select role | Seleccionar rol | placeholder |
|
|
||||||
| Gültig für | Válido para | |
|
|
||||||
| 1 day | 1 día | |
|
|
||||||
| 2 days | 2 días | |
|
|
||||||
| 3 days | 3 días | |
|
|
||||||
| 4 days | 4 días | |
|
|
||||||
| 5 days | 5 días | |
|
|
||||||
| 6 days | 6 días | |
|
|
||||||
| 7 days | 7 días | |
|
|
||||||
| Create Invitation | Crear invitación | |
|
|
||||||
| Close | Cerrar | |
|
|
||||||
|
|
||||||
|
|
||||||
## Main “Shareable Links”
|
|
||||||
##### “Hero” section
|
|
||||||
|
|
||||||
| EN | ES | Notes |
|
|
||||||
| ------------------------------------------------------------ | ------------------------------------------------------------ | ----- |
|
|
||||||
| Shareable Links | Enlaces para compartir | |
|
|
||||||
| Create shareable links to your resources. Links provide temporary or unlimited access to your resource. You can configure the expiration duration of the link when you create one. | Crear enlaces que se puedan compartir a tus recursos. Los enlaces proporcionan acceso temporal o ilimitado a tu recurso. Puedes configurar la duración de caducidad del enlace cuando lo creas. | |
|
|
||||||
| Easy to create and share | Fácil de crear y compartir | |
|
|
||||||
| Configurable expiration duration | Duración de expiración configurable | |
|
|
||||||
| Secure and revocable | Seguro y revocable | |
|
|
||||||
##### Content
|
|
||||||
|
|
||||||
| EN | ES | Notes |
|
|
||||||
| ------------------------------------------------------------ | ------------------------------------------------------------ | ----------------- |
|
|
||||||
| Manage Shareable Links | Administrar enlaces compartibles | |
|
|
||||||
| Create shareable links to grant temporary or permanent access to your resources | Crear enlaces compartibles para otorgar acceso temporal o permanente a tus recursos | |
|
|
||||||
| Search links | Buscar enlaces | placeholder |
|
|
||||||
| Create Share Link | Crear enlace para compartir | addbutton |
|
|
||||||
| Resource | Recurso | table header |
|
|
||||||
| Title | Título | table header |
|
|
||||||
| Created | Creado | table header |
|
|
||||||
| Expires | Caduca | table header |
|
|
||||||
| No links. Create one to get started. | No hay enlaces. Crea uno para comenzar. | table placeholder |
|
|
||||||
|
|
||||||
##### Create Shareable Link popup
|
|
||||||
|
|
||||||
| EN | ES | Notes |
|
|
||||||
| ------------------------------------------------------------ | ------------------------------------------------------------ | ----------------------- |
|
|
||||||
| Create Shareable Link | Crear un enlace para compartir | |
|
|
||||||
| Anyone with this link can access the resource | Cualquier persona con este enlace puede acceder al recurso. | |
|
|
||||||
| Resource | Recurso | |
|
|
||||||
| Select resource | Seleccionar recurso | |
|
|
||||||
| Search resources… | Buscar recursos… | resource selector popup |
|
|
||||||
| Title (optional) | Título (opcional) | |
|
|
||||||
| Enter title | Introducir título | placeholder |
|
|
||||||
| Expire in | Caduca en | |
|
|
||||||
| Minutes | Minutos | |
|
|
||||||
| Hours | Horas | |
|
|
||||||
| Days | Días | |
|
|
||||||
| Months | Meses | |
|
|
||||||
| Years | Años | |
|
|
||||||
| Never expire | Nunca caduca | |
|
|
||||||
| Expiration time is how long the link will be usable and provide access to the resource. After this time, the link will no longer work, and users who used this link will lose access to the resource. | El tiempo de expiración es el tiempo durante el cual el enlace se podrá utilizar y brindará acceso al recurso. Después de este tiempo, el enlace dejará de funcionar y los usuarios que lo hayan utilizado perderán el acceso al recurso. | |
|
|
||||||
| Create Link | Crear enlace | |
|
|
||||||
| Close | Cerrar | |
|
|
||||||
|
|
||||||
|
|
||||||
## Main “General”
|
|
||||||
|
|
||||||
| EN | ES | Notes |
|
|
||||||
| ------------------------------------------------------------ | ------------------------------------------------------------ | ------------ |
|
|
||||||
| General | General | |
|
|
||||||
| Configure your organization’s general settings | Configura los ajustes generales de tu organización | |
|
|
||||||
| General | General | sidebar item |
|
|
||||||
| Organization Settings | Configuración de la organización | |
|
|
||||||
| Manage your organization details and configuration | Administra los detalles y la configuración de tu organización| |
|
|
||||||
| Name | Nombre | |
|
|
||||||
| This is the display name of the org | Este es el nombre para mostrar de la organización. | |
|
|
||||||
| Save Settings | Guardar configuración | |
|
|
||||||
| Danger Zone | Zona de peligro | |
|
|
||||||
| Once you delete this org, there is no going back. Please be certain. | Una vez que elimines esta organización, no habrá vuelta atrás. Asegúrate de hacerlo. | |
|
|
||||||
| Delete Organization Data | Eliminar datos de la organización | |
|
|
|
@ -1,287 +0,0 @@
|
||||||
## Authentication Site
|
|
||||||
|
|
||||||
|
|
||||||
| EN | PL | Notes |
|
|
||||||
| -------------------------------------------------------- | ------------------------------------------------------------ | ---------- |
|
|
||||||
| Powered by [Pangolin](https://github.com/fosrl/pangolin) | Zasilane przez [Pangolin](https://github.com/fosrl/pangolin) | |
|
|
||||||
| Authentication Required | Wymagane uwierzytelnienie | |
|
|
||||||
| Choose your preferred method to access {resource} | Wybierz preferowaną metodę dostępu do {resource} | |
|
|
||||||
| PIN | PIN | |
|
|
||||||
| User | Zaloguj | |
|
|
||||||
| 6-digit PIN Code | 6-cyfrowy kod PIN | pin login |
|
|
||||||
| Login in with PIN | Zaloguj się PIN’em | pin login |
|
|
||||||
| Email | Email | user login |
|
|
||||||
| Enter your email | Wprowadź swój email | user login |
|
|
||||||
| Password | Hasło | user login |
|
|
||||||
| Enter your password | Wprowadź swoje hasło | user login |
|
|
||||||
| Forgot your password? | Zapomniałeś hasła? | user login |
|
|
||||||
| Log in | Zaloguj | user login |
|
|
||||||
|
|
||||||
|
|
||||||
## Login site
|
|
||||||
|
|
||||||
| EN | PL | Notes |
|
|
||||||
| --------------------- | ------------------------------ | ----------- |
|
|
||||||
| Welcome to Pangolin | Witaj w Pangolin | |
|
|
||||||
| Log in to get started | Zaloguj się, aby rozpocząć<br> | |
|
|
||||||
| Email | Email | |
|
|
||||||
| Enter your email | Wprowadź swój adres e-mail<br> | placeholder |
|
|
||||||
| Password | Hasło | |
|
|
||||||
| Enter your password | Wprowadź swoje hasło | placeholder |
|
|
||||||
| Forgot your password? | Nie pamiętasz hasła? | |
|
|
||||||
| Log in | Zaloguj | |
|
|
||||||
|
|
||||||
# Ogranization site after successful login
|
|
||||||
|
|
||||||
|
|
||||||
| EN | PL | Notes |
|
|
||||||
| ----------------------------------------- | ------------------------------------------ | ----- |
|
|
||||||
| Welcome to Pangolin | Witaj w Pangolin | |
|
|
||||||
| You're a member of {number} organization. | Jesteś użytkownikiem {number} organizacji. | |
|
|
||||||
|
|
||||||
## Shared Header, Navbar and Footer
|
|
||||||
##### Header
|
|
||||||
|
|
||||||
| EN | PL | Notes |
|
|
||||||
| ------------------- | ------------------ | ----- |
|
|
||||||
| Documentation | Dokumentacja | |
|
|
||||||
| Support | Wsparcie | |
|
|
||||||
| Organization {name} | Organizacja {name} | |
|
|
||||||
##### Organization selector
|
|
||||||
|
|
||||||
| EN | PL | Notes |
|
|
||||||
| ---------------- | ---------------- | ----- |
|
|
||||||
| Search… | Szukaj… | |
|
|
||||||
| Create | Utwórz | |
|
|
||||||
| New Organization | Nowa organizacja | |
|
|
||||||
| Organizations | Organizacje | |
|
|
||||||
|
|
||||||
##### Navbar
|
|
||||||
|
|
||||||
| EN | PL | Notes |
|
|
||||||
| --------------- | ---------------------- | ----- |
|
|
||||||
| Sites | Witryny | |
|
|
||||||
| Resources | Zasoby | |
|
|
||||||
| User & Roles | Użytkownicy i Role | |
|
|
||||||
| Shareable Links | Łącza do udostępniania | |
|
|
||||||
| General | Ogólne | |
|
|
||||||
##### Footer
|
|
||||||
| EN | PL | |
|
|
||||||
| ------------------------- | -------------------------- | -------------- |
|
|
||||||
| Page {number} of {number} | Strona {number} z {number} | |
|
|
||||||
| Rows per page | Wierszy na stronę | |
|
|
||||||
| Pangolin | Pangolin | bottom of site |
|
|
||||||
| Built by Fossorial | Stworzone przez Fossorial | bottom of site |
|
|
||||||
| Open Source | Open source | bottom of site |
|
|
||||||
| Documentation | Dokumentacja | bottom of site |
|
|
||||||
| {version} | {version} | bottom of site |
|
|
||||||
## Main “Sites”
|
|
||||||
##### “Hero” section
|
|
||||||
|
|
||||||
| EN | PL | Notes |
|
|
||||||
| ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ----- |
|
|
||||||
| Newt (Recommended) | Newt (zalecane) | |
|
|
||||||
| For the best user experience, use Newt. It uses WireGuard under the hood and allows you to address your private resources by their LAN address on your private network from within the Pangolin dashboard. | Aby zapewnić najlepsze doświadczenie użytkownika, korzystaj z Newt. Wykorzystuje on technologię WireGuard w tle i pozwala na dostęp do Twoich prywatnych zasobów za pomocą ich adresu LAN w prywatnej sieci bezpośrednio z poziomu pulpitu nawigacyjnego Pangolin. | |
|
|
||||||
| Runs in Docker | Działa w Dockerze | |
|
|
||||||
| Runs in shell on macOS, Linux, and Windows | Działa w powłoce na systemach macOS, Linux i Windows | |
|
|
||||||
| Install Newt | Zainstaluj Newt | |
|
|
||||||
| Podstawowy WireGuard<br> | Użyj dowolnego klienta WireGuard, aby się połączyć. Będziesz musiał uzyskiwać dostęp do swoich wewnętrznych zasobów za pomocą adresu IP równorzędnego | |
|
|
||||||
| Compatible with all WireGuard clients<br> | Kompatybilny ze wszystkimi klientami WireGuard<br> | |
|
|
||||||
| Manual configuration required | Wymagana ręczna konfiguracja<br> | |
|
|
||||||
##### Content
|
|
||||||
|
|
||||||
| EN | PL | Notes |
|
|
||||||
| --------------------------------------------------------- | ------------------------------------------------------------------------ | -------------------------------- |
|
|
||||||
| Manage Sites | Zarządzanie witrynami | |
|
|
||||||
| Allow connectivity to your network through secure tunnels | Zezwalaj na łączność z Twoją siecią za pośrednictwem bezpiecznych tuneli | |
|
|
||||||
| Search sites | Szukaj witryny | placeholder |
|
|
||||||
| Add Site | Dodaj witrynę | |
|
|
||||||
| Name | Nazwa | table header |
|
|
||||||
| Online | Status | table header |
|
|
||||||
| Site | Witryna | table header |
|
|
||||||
| Data In | Dane wchodzące | table header |
|
|
||||||
| Data Out | Dane wychodzące | table header |
|
|
||||||
| Connection Type | Typ połączenia | table header |
|
|
||||||
| Online | Online | site state |
|
|
||||||
| Offline | Poza siecią | site state |
|
|
||||||
| Edit → | Edytuj → | |
|
|
||||||
| View settings | Pokaż ustawienia | Popup after clicking “…” on site |
|
|
||||||
| Delete | Usuń | Popup after clicking “…” on site |
|
|
||||||
##### Add Site Popup
|
|
||||||
|
|
||||||
| EN | PL | Notes |
|
|
||||||
| ------------------------------------------------------ | --------------------------------------------------- | ----------- |
|
|
||||||
| Create Site | Utwórz witrynę | |
|
|
||||||
| Create a new site to start connection for this site | Utwórz nową witrynę aby rozpocząć połączenie | |
|
|
||||||
| Name | Nazwa | |
|
|
||||||
| Site name | Nazwa witryny | placeholder |
|
|
||||||
| This is the name that will be displayed for this site. | Tak będzie wyświetlana twoja witryna | desc |
|
|
||||||
| Method | Metoda | |
|
|
||||||
| Local | Lokalna | |
|
|
||||||
| Newt | Newt | |
|
|
||||||
| WireGuard | WireGuard | |
|
|
||||||
| This is how you will expose connections. | Tak będą eksponowane połączenie. | |
|
|
||||||
| You will only be able to see the configuration once. | Tą konfigurację możesz zobaczyć tylko raz. | |
|
|
||||||
| Learn how to install Newt on your system | Dowiedz się jak zainstalować Newt na twoim systemie | |
|
|
||||||
| I have copied the config | Skopiowałem konfigurację | |
|
|
||||||
| Create Site | Utwórz witrynę | |
|
|
||||||
| Close | Zamknij | |
|
|
||||||
|
|
||||||
## Main “Resources”
|
|
||||||
|
|
||||||
##### “Hero” section
|
|
||||||
|
|
||||||
| EN | PL | Notes |
|
|
||||||
| ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ----- |
|
|
||||||
| Resources | Zasoby | |
|
|
||||||
| Zasoby to serwery proxy dla aplikacji działających w Twojej prywatnej sieci. Utwórz zasób dla dowolnej aplikacji HTTP lub HTTPS w swojej prywatnej sieci. Każdy zasób musi być połączony z witryną, aby umożliwić prywatne i bezpieczne połączenie przez szyfrowany tunel WireGuard. | Zasoby to serwery proxy dla aplikacji działających w Twojej prywatnej sieci. Utwórz zasób dla dowolnej aplikacji HTTP lub HTTPS w swojej prywatnej sieci. Każdy zasób musi być połączony z witryną, aby umożliwić prywatne i bezpieczne połączenie przez szyfrowany tunel WireGuard. | |
|
|
||||||
| Secure connectivity with WireGuard encryption | Bezpieczna łączność z szyfrowaniem WireGuard | |
|
|
||||||
| Configure multiple authentication methods | Konfigurowanie wielu metod uwierzytelniania | |
|
|
||||||
| User and role-based access control | Kontrola dostępu oparta na użytkownikach i rolach | |
|
|
||||||
##### Content
|
|
||||||
|
|
||||||
| EN | PL | Notes |
|
|
||||||
| -------------------------------------------------- | -------------------------------------------------------------- | -------------------- |
|
|
||||||
| Manage Resources | Zarządzaj zasobami | |
|
|
||||||
| Create secure proxies to your private applications | Twórz bezpieczne serwery proxy dla swoich prywatnych aplikacji | |
|
|
||||||
| Search resources | Szukaj w zasobach | placeholder |
|
|
||||||
| Name | Nazwa | |
|
|
||||||
| Site | Witryna | |
|
|
||||||
| Full URL | Pełny URL | |
|
|
||||||
| Authentication | Uwierzytelnianie | |
|
|
||||||
| Not Protected | Niezabezpieczony | authentication state |
|
|
||||||
| Protected | Zabezpieczony | authentication state |
|
|
||||||
| Edit → | Edytuj → | |
|
|
||||||
| Add Resource | Dodaj zasób | |
|
|
||||||
##### Add Resource Popup
|
|
||||||
|
|
||||||
| EN | PL | Notes |
|
|
||||||
| --------------------------------------------------------------------------------- | ---------------------------------------------------------------------- | ------------------- |
|
|
||||||
| Create Resource | Utwórz zasób | |
|
|
||||||
| Create a new resource to proxy request to your app | Utwórz nowy zasób, aby przekazywać żądania do swojej aplikacji | |
|
|
||||||
| Name | Nazwa | |
|
|
||||||
| My Resource | Nowy zasób | name placeholder |
|
|
||||||
| This is the name that will be displayed for this resource. | To jest nazwa, która będzie wyświetlana dla tego zasobu | |
|
|
||||||
| Subdomain | Subdomena | |
|
|
||||||
| Enter subdomain | Wprowadź subdomenę | |
|
|
||||||
| This is the fully qualified domain name that will be used to access the resource. | To jest pełna nazwa domeny, która będzie używana do dostępu do zasobu. | |
|
|
||||||
| Site | Witryna | |
|
|
||||||
| Search site… | Szukaj witryny… | Site selector popup |
|
|
||||||
| This is the site that will be used in the dashboard. | To jest witryna, która będzie używana w pulpicie nawigacyjnym. | |
|
|
||||||
| Create Resource | Utwórz zasób | |
|
|
||||||
| Close | Zamknij | |
|
|
||||||
|
|
||||||
|
|
||||||
## Main “User & Roles”
|
|
||||||
##### Content
|
|
||||||
|
|
||||||
| EN | PL | Notes |
|
|
||||||
| ------------------------------------------------------------------------ | ------------------------------------------------------------------------------------ | ----------------------------- |
|
|
||||||
| Manage User & Roles | Zarządzanie użytkownikami i rolami | |
|
|
||||||
| Invite users and add them to roles to manage access to your organization | Zaproś użytkowników i przypisz im role, aby zarządzać dostępem do Twojej organizacji | |
|
|
||||||
| Users | Użytkownicy | sidebar item |
|
|
||||||
| Roles | Role | sidebar item |
|
|
||||||
| **User tab** | | |
|
|
||||||
| Search users | Wyszukaj użytkownika | placeholder |
|
|
||||||
| Invite User | Zaproś użytkownika | addbutton |
|
|
||||||
| Email | Email | table header |
|
|
||||||
| Status | Status | table header |
|
|
||||||
| Role | Rola | table header |
|
|
||||||
| Confirmed | Zatwierdzony | account status |
|
|
||||||
| Not confirmed (?) | Niezatwierdzony (?) | unknown for me account status |
|
|
||||||
| Owner | Właściciel | role |
|
|
||||||
| Admin | Administrator | role |
|
|
||||||
| Member | Użytkownik | role |
|
|
||||||
| **Roles Tab** | | |
|
|
||||||
| Search roles | Wyszukaj role | placeholder |
|
|
||||||
| Add Role | Dodaj role | addbutton |
|
|
||||||
| Name | Nazwa | table header |
|
|
||||||
| Description | Opis | table header |
|
|
||||||
| Admin | Administrator | role |
|
|
||||||
| Member | Użytkownik | role |
|
|
||||||
| Admin role with the most permissions | Rola administratora z najszerszymi uprawnieniami | admin role desc |
|
|
||||||
| Members can only view resources | Członkowie mogą jedynie przeglądać zasoby | member role desc |
|
|
||||||
|
|
||||||
##### Invite User popup
|
|
||||||
|
|
||||||
| EN | PL | Notes |
|
|
||||||
| ----------------- | ------------------------------------------ | ----------- |
|
|
||||||
| Invite User | Give new users access to your organization | |
|
|
||||||
| Email | Email | |
|
|
||||||
| Enter an email | Wprowadź email | placeholder |
|
|
||||||
| Role | Rola | |
|
|
||||||
| Select role | Wybierz role | placeholder |
|
|
||||||
| Vaild for | Ważne do | |
|
|
||||||
| 1 day | Dzień | |
|
|
||||||
| 2 days | 2 dni | |
|
|
||||||
| 3 days | 3 dni | |
|
|
||||||
| 4 days | 4 dni | |
|
|
||||||
| 5 days | 5 dni | |
|
|
||||||
| 6 days | 6 dni | |
|
|
||||||
| 7 days | 7 dni | |
|
|
||||||
| Create Invitation | Utwórz zaproszenie | |
|
|
||||||
| Close | Zamknij | |
|
|
||||||
|
|
||||||
|
|
||||||
## Main “Shareable Links”
|
|
||||||
##### “Hero” section
|
|
||||||
|
|
||||||
| EN | PL | Notes |
|
|
||||||
| ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----- |
|
|
||||||
| Shareable Links | Łącza do udostępniania | |
|
|
||||||
| Create shareable links to your resources. Links provide temporary or unlimited access to your resource. You can configure the expiration duration of the link when you create one. | Twórz linki do udostępniania swoich zasobów. Linki zapewniają tymczasowy lub nieograniczony dostęp do zasobu. Możesz skonfigurować czas wygaśnięcia linku podczas jego tworzenia. | |
|
|
||||||
| Easy to create and share | Łatwe tworzenie i udostępnianie | |
|
|
||||||
| Configurable expiration duration | Konfigurowalny czas wygaśnięcia | |
|
|
||||||
| Secure and revocable | Bezpieczne i odwołalne | |
|
|
||||||
##### Content
|
|
||||||
|
|
||||||
| EN | PL | Notes |
|
|
||||||
| ------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------- | ----------------- |
|
|
||||||
| Manage Shareable Links | Zarządzaj łączami do udostępniania | |
|
|
||||||
| Create shareable links to grant temporary or permament access to your resources | Utwórz łącze do udostępniania w celu przyznania tymczasowego lub stałego dostępu do zasobów | |
|
|
||||||
| Search links | Szukaj łączy | placeholder |
|
|
||||||
| Create Share Link | Utwórz nowe łącze | addbutton |
|
|
||||||
| Resource | Zasób | table header |
|
|
||||||
| Title | Tytuł | table header |
|
|
||||||
| Created | Utworzone | table header |
|
|
||||||
| Expires | Wygasa | table header |
|
|
||||||
| No links. Create one to get started. | Brak łączy. Utwórz, aby rozpocząć. | table placeholder |
|
|
||||||
|
|
||||||
##### Create Shareable Link popup
|
|
||||||
|
|
||||||
| EN | PL | Notes |
|
|
||||||
| ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------- |
|
|
||||||
| Create Shareable Link | Utwórz łącze do udostępnienia | |
|
|
||||||
| Anyone with this link can access the resource | Każdy kto ma ten link może korzystać z zasobu | |
|
|
||||||
| Resource | Zasób | |
|
|
||||||
| Select resource | Wybierz zasób | |
|
|
||||||
| Search resources… | Szukaj zasobów… | resource selector popup |
|
|
||||||
| Title (optional) | Tytuł (opcjonalny) | |
|
|
||||||
| Enter title | Wprowadź tytuł | placeholder |
|
|
||||||
| Expire in | Wygasa za | |
|
|
||||||
| Minutes | Minut | |
|
|
||||||
| Hours | Godzin | |
|
|
||||||
| Days | Dni | |
|
|
||||||
| Months | Miesięcy | |
|
|
||||||
| Years | Lat | |
|
|
||||||
| Never expire | Nie wygasa | |
|
|
||||||
| Expiration time is how long the link will be usable and provide access to the resource. After this time, the link will no longer work, and users who used this link will lose access to the resource. | Czas wygaśnięcia to okres, przez który link będzie aktywny i zapewni dostęp do zasobu. Po upływie tego czasu link przestanie działać, a użytkownicy, którzy go użyli, stracą dostęp do zasobu. | |
|
|
||||||
| Create Link | Utwórz łącze | |
|
|
||||||
| Close | Zamknij | |
|
|
||||||
|
|
||||||
|
|
||||||
## Main “General”
|
|
||||||
|
|
||||||
| EN | PL | Notes |
|
|
||||||
| -------------------------------------------------------------------- | ------------------------------------------------------------------- | ------------ |
|
|
||||||
| General | Ogólne | |
|
|
||||||
| Configure your organization’s general settings | Zarządzaj ogólnymi ustawieniami twoich organizacji | |
|
|
||||||
| General | Ogólne | sidebar item |
|
|
||||||
| Organization Settings | Ustawienia organizacji | |
|
|
||||||
| Manage your organization details and configuration | Zarządzaj szczegółami i konfiguracją organizacji | |
|
|
||||||
| Name | Nazwa | |
|
|
||||||
| This is the display name of the org | To jest wyświetlana nazwa Twojej organizacji | |
|
|
||||||
| Save Settings | Zapisz ustawienia | |
|
|
||||||
| Danger Zone | Niebezpieczna strefa | |
|
|
||||||
| Once you delete this org, there is no going back. Please be certain. | Jeśli usuniesz swoją tą organizację, nie ma odwrotu. Bądź ostrożny! | |
|
|
||||||
| Delete Organization Data | Usuń dane organizacji | |
|
|
1322
messages/cs-CZ.json
Normal file
1322
messages/de-DE.json
Normal file
1322
messages/en-US.json
Normal file
1322
messages/es-ES.json
Normal file
1322
messages/fr-FR.json
Normal file
1322
messages/it-IT.json
Normal file
1322
messages/ko-KR.json
Normal file
1322
messages/nl-NL.json
Normal file
1322
messages/pl-PL.json
Normal file
1322
messages/pt-PT.json
Normal file
1322
messages/ru-RU.json
Normal file
1322
messages/tr-TR.json
Normal file
1322
messages/zh-CN.json
Normal file
|
@ -1,9 +1,13 @@
|
||||||
/** @type {import('next').NextConfig} */
|
import createNextIntlPlugin from "next-intl/plugin";
|
||||||
|
|
||||||
|
const withNextIntl = createNextIntlPlugin();
|
||||||
|
|
||||||
|
/** @type {import("next").NextConfig} */
|
||||||
const nextConfig = {
|
const nextConfig = {
|
||||||
eslint: {
|
eslint: {
|
||||||
ignoreDuringBuilds: true,
|
ignoreDuringBuilds: true
|
||||||
},
|
},
|
||||||
output: "standalone"
|
output: "standalone"
|
||||||
};
|
};
|
||||||
|
|
||||||
export default nextConfig;
|
export default withNextIntl(nextConfig);
|
||||||
|
|
7991
package-lock.json
generated
162
package.json
|
@ -12,105 +12,139 @@
|
||||||
"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:generate": "drizzle-kit generate",
|
"db:pg:generate": "drizzle-kit generate --config=./drizzle.pg.config.ts",
|
||||||
"db:push": "npx tsx server/db/migrate.ts",
|
"db:sqlite:generate": "drizzle-kit generate --config=./drizzle.sqlite.config.ts",
|
||||||
"db:studio": "drizzle-kit studio",
|
"db:pg:push": "npx tsx server/db/pg/migrate.ts",
|
||||||
"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",
|
"db:sqlite:push": "npx tsx server/db/sqlite/migrate.ts",
|
||||||
"start": "NODE_OPTIONS=--enable-source-maps NODE_ENV=development ENVIRONMENT=prod sh -c 'node dist/migrations.mjs && node dist/server.mjs'",
|
"db:sqlite:studio": "drizzle-kit studio --config=./drizzle.sqlite.config.ts",
|
||||||
"email": "email dev --dir server/emails/templates --port 3005"
|
"db:pg:studio": "drizzle-kit studio --config=./drizzle.pg.config.ts",
|
||||||
|
"db:clear-migrations": "rm -rf server/migrations",
|
||||||
|
"build:sqlite": "mkdir -p dist && next build && node esbuild.mjs -e server/index.ts -o dist/server.mjs && node esbuild.mjs -e server/setup/migrationsSqlite.ts -o dist/migrations.mjs",
|
||||||
|
"build:pg": "mkdir -p dist && next build && node esbuild.mjs -e server/index.ts -o dist/server.mjs && node esbuild.mjs -e server/setup/migrationsPg.ts -o dist/migrations.mjs",
|
||||||
|
"start:sqlite": "DB_TYPE=sqlite NODE_OPTIONS=--enable-source-maps NODE_ENV=development ENVIRONMENT=prod sh -c 'node dist/migrations.mjs && node dist/server.mjs'",
|
||||||
|
"start:pg": "DB_TYPE=pg NODE_OPTIONS=--enable-source-maps NODE_ENV=development ENVIRONMENT=prod sh -c 'node dist/migrations.mjs && node dist/server.mjs'",
|
||||||
|
"email": "email dev --dir server/emails/templates --port 3005",
|
||||||
|
"build:cli": "node esbuild.mjs -e cli/index.ts -o dist/cli.mjs"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@asteasolutions/zod-to-openapi": "^7.3.4",
|
||||||
"@hookform/resolvers": "3.9.1",
|
"@hookform/resolvers": "3.9.1",
|
||||||
"@node-rs/argon2": "2.0.2",
|
"@node-rs/argon2": "^2.0.2",
|
||||||
"@oslojs/crypto": "1.0.1",
|
"@oslojs/crypto": "1.0.1",
|
||||||
"@oslojs/encoding": "1.1.0",
|
"@oslojs/encoding": "1.1.0",
|
||||||
"@radix-ui/react-avatar": "1.1.2",
|
"@radix-ui/react-avatar": "1.1.10",
|
||||||
"@radix-ui/react-checkbox": "1.1.3",
|
"@radix-ui/react-checkbox": "1.3.2",
|
||||||
"@radix-ui/react-collapsible": "1.1.2",
|
"@radix-ui/react-collapsible": "1.1.11",
|
||||||
"@radix-ui/react-dialog": "1.1.4",
|
"@radix-ui/react-dialog": "1.1.14",
|
||||||
"@radix-ui/react-dropdown-menu": "2.1.4",
|
"@radix-ui/react-dropdown-menu": "2.1.15",
|
||||||
"@radix-ui/react-icons": "1.3.2",
|
"@radix-ui/react-icons": "1.3.2",
|
||||||
"@radix-ui/react-label": "2.1.1",
|
"@radix-ui/react-label": "2.1.7",
|
||||||
"@radix-ui/react-popover": "1.1.4",
|
"@radix-ui/react-popover": "1.1.14",
|
||||||
"@radix-ui/react-radio-group": "1.2.2",
|
"@radix-ui/react-progress": "^1.1.7",
|
||||||
"@radix-ui/react-select": "2.1.4",
|
"@radix-ui/react-radio-group": "1.3.7",
|
||||||
"@radix-ui/react-separator": "1.1.1",
|
"@radix-ui/react-scroll-area": "^1.2.9",
|
||||||
"@radix-ui/react-slot": "1.1.1",
|
"@radix-ui/react-select": "2.2.5",
|
||||||
"@radix-ui/react-switch": "1.1.2",
|
"@radix-ui/react-separator": "1.1.7",
|
||||||
"@radix-ui/react-tabs": "1.1.2",
|
"@radix-ui/react-slot": "1.2.3",
|
||||||
"@radix-ui/react-toast": "1.2.4",
|
"@radix-ui/react-switch": "1.2.5",
|
||||||
"@react-email/components": "0.0.31",
|
"@radix-ui/react-tabs": "1.1.12",
|
||||||
"@react-email/tailwind": "1.0.4",
|
"@radix-ui/react-toast": "1.2.14",
|
||||||
"@tanstack/react-table": "8.20.6",
|
"@radix-ui/react-tooltip": "^1.2.7",
|
||||||
"axios": "1.7.9",
|
"@react-email/components": "0.3.1",
|
||||||
|
"@react-email/render": "^1.1.2",
|
||||||
|
"@simplewebauthn/browser": "^13.1.0",
|
||||||
|
"@simplewebauthn/server": "^9.0.3",
|
||||||
|
"@react-email/tailwind": "1.2.1",
|
||||||
|
"@tailwindcss/forms": "^0.5.10",
|
||||||
|
"@tanstack/react-table": "8.21.3",
|
||||||
|
"arctic": "^3.7.0",
|
||||||
|
"axios": "1.10.0",
|
||||||
"better-sqlite3": "11.7.0",
|
"better-sqlite3": "11.7.0",
|
||||||
"class-variance-authority": "0.7.1",
|
"canvas-confetti": "1.9.3",
|
||||||
|
"class-variance-authority": "^0.7.1",
|
||||||
"clsx": "2.1.1",
|
"clsx": "2.1.1",
|
||||||
"cmdk": "1.0.4",
|
"cmdk": "1.1.1",
|
||||||
|
"cookie": "^1.0.2",
|
||||||
"cookie-parser": "1.4.7",
|
"cookie-parser": "1.4.7",
|
||||||
|
"cookies": "^0.9.1",
|
||||||
"cors": "2.8.5",
|
"cors": "2.8.5",
|
||||||
"drizzle-orm": "0.38.3",
|
"crypto-js": "^4.2.0",
|
||||||
"eslint": "9.17.0",
|
"drizzle-orm": "0.44.2",
|
||||||
"eslint-config-next": "15.1.3",
|
"eslint": "9.31.0",
|
||||||
|
"eslint-config-next": "15.3.5",
|
||||||
"express": "4.21.2",
|
"express": "4.21.2",
|
||||||
"express-rate-limit": "7.5.0",
|
"express-rate-limit": "7.5.1",
|
||||||
"glob": "11.0.0",
|
"glob": "11.0.3",
|
||||||
"helmet": "8.0.0",
|
"helmet": "8.1.0",
|
||||||
"http-errors": "2.0.0",
|
"http-errors": "2.0.0",
|
||||||
"i": "^0.3.7",
|
"i": "^0.3.7",
|
||||||
"input-otp": "1.4.1",
|
"input-otp": "1.4.2",
|
||||||
|
"jmespath": "^0.16.0",
|
||||||
"js-yaml": "4.1.0",
|
"js-yaml": "4.1.0",
|
||||||
"lucide-react": "0.469.0",
|
"jsonwebtoken": "^9.0.2",
|
||||||
|
"lucide-react": "0.525.0",
|
||||||
"moment": "2.30.1",
|
"moment": "2.30.1",
|
||||||
"next": "15.1.3",
|
"next": "15.3.5",
|
||||||
"next-themes": "0.4.4",
|
"next-intl": "^4.3.4",
|
||||||
|
"next-themes": "0.4.6",
|
||||||
"node-cache": "5.1.2",
|
"node-cache": "5.1.2",
|
||||||
"node-fetch": "3.3.2",
|
"node-fetch": "3.3.2",
|
||||||
"nodemailer": "6.9.16",
|
"nodemailer": "7.0.5",
|
||||||
"npm": "^11.2.0",
|
"npm": "^11.4.2",
|
||||||
"oslo": "1.2.1",
|
"oslo": "1.2.1",
|
||||||
|
"pg": "^8.16.2",
|
||||||
"qrcode.react": "4.2.0",
|
"qrcode.react": "4.2.0",
|
||||||
"react": "19.0.0",
|
"react": "19.1.0",
|
||||||
"react-dom": "19.0.0",
|
"react-dom": "19.1.0",
|
||||||
"react-easy-sort": "^1.6.0",
|
"react-easy-sort": "^1.6.0",
|
||||||
"react-hook-form": "7.54.2",
|
"react-hook-form": "7.60.0",
|
||||||
"react-icons": "^5.5.0",
|
"react-icons": "^5.5.0",
|
||||||
"rebuild": "0.1.2",
|
"rebuild": "0.1.2",
|
||||||
"semver": "7.6.3",
|
"semver": "^7.7.2",
|
||||||
"tailwind-merge": "2.6.0",
|
"swagger-ui-express": "^5.0.1",
|
||||||
"tailwindcss-animate": "1.0.7",
|
"tailwind-merge": "3.3.1",
|
||||||
|
"tw-animate-css": "^1.3.5",
|
||||||
|
"uuid": "^11.1.0",
|
||||||
"vaul": "1.1.2",
|
"vaul": "1.1.2",
|
||||||
"winston": "3.17.0",
|
"winston": "3.17.0",
|
||||||
"winston-daily-rotate-file": "5.0.0",
|
"winston-daily-rotate-file": "5.0.0",
|
||||||
"ws": "8.18.0",
|
"ws": "8.18.3",
|
||||||
"zod": "3.24.1",
|
"zod": "3.25.76",
|
||||||
"zod-validation-error": "3.4.0"
|
"zod-validation-error": "3.5.2",
|
||||||
|
"yargs": "18.0.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@dotenvx/dotenvx": "1.32.0",
|
"@dotenvx/dotenvx": "1.47.6",
|
||||||
"@esbuild-plugins/tsconfig-paths": "0.1.2",
|
"@esbuild-plugins/tsconfig-paths": "0.1.2",
|
||||||
|
"@tailwindcss/postcss": "^4.1.10",
|
||||||
"@types/better-sqlite3": "7.6.12",
|
"@types/better-sqlite3": "7.6.12",
|
||||||
"@types/cookie-parser": "1.4.8",
|
"@types/cookie-parser": "1.4.9",
|
||||||
"@types/cors": "2.8.17",
|
"@types/cors": "2.8.19",
|
||||||
|
"@types/crypto-js": "^4.2.2",
|
||||||
"@types/express": "5.0.0",
|
"@types/express": "5.0.0",
|
||||||
|
"@types/express-session": "^1.18.2",
|
||||||
|
"@types/jmespath": "^0.15.2",
|
||||||
"@types/js-yaml": "4.0.9",
|
"@types/js-yaml": "4.0.9",
|
||||||
"@types/node": "^22",
|
"@types/jsonwebtoken": "^9.0.10",
|
||||||
|
"@types/node": "^24",
|
||||||
"@types/nodemailer": "6.4.17",
|
"@types/nodemailer": "6.4.17",
|
||||||
"@types/react": "19.0.2",
|
"@types/pg": "8.15.4",
|
||||||
"@types/react-dom": "19.0.2",
|
"@types/react": "19.1.8",
|
||||||
"@types/semver": "7.5.8",
|
"@types/react-dom": "19.1.6",
|
||||||
"@types/ws": "8.5.13",
|
"@types/semver": "^7.7.0",
|
||||||
|
"@types/swagger-ui-express": "^4.1.8",
|
||||||
|
"@types/ws": "8.18.1",
|
||||||
"@types/yargs": "17.0.33",
|
"@types/yargs": "17.0.33",
|
||||||
"drizzle-kit": "0.30.1",
|
"drizzle-kit": "0.31.4",
|
||||||
"esbuild": "0.24.2",
|
"esbuild": "0.25.6",
|
||||||
"esbuild-node-externals": "1.16.0",
|
"esbuild-node-externals": "1.18.0",
|
||||||
"postcss": "^8",
|
"postcss": "^8",
|
||||||
"react-email": "3.0.4",
|
"react-email": "4.1.0",
|
||||||
"tailwindcss": "^3.4.17",
|
"tailwindcss": "^4.1.4",
|
||||||
"tsc-alias": "1.8.10",
|
"tsc-alias": "1.8.16",
|
||||||
"tsx": "4.19.2",
|
"tsx": "4.20.3",
|
||||||
"typescript": "^5",
|
"typescript": "^5",
|
||||||
"yargs": "17.7.2"
|
"typescript-eslint": "^8.36.0"
|
||||||
},
|
},
|
||||||
"overrides": {
|
"overrides": {
|
||||||
"emblor": {
|
"emblor": {
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
/** @type {import('postcss-load-config').Config} */
|
/** @type {import('postcss-load-config').Config} */
|
||||||
const config = {
|
const config = {
|
||||||
plugins: {
|
plugins: {
|
||||||
tailwindcss: {},
|
"@tailwindcss/postcss": {},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
BIN
public/auth-diagram1.png
Normal file
After Width: | Height: | Size: 647 KiB |
BIN
public/clip.gif
Normal file
After Width: | Height: | Size: 500 KiB |
132
public/diagram-dark.svg
Normal file
After Width: | Height: | Size: 52 KiB |
132
public/diagram.svg
Normal file
After Width: | Height: | Size: 52 KiB |
|
@ -1,22 +1,21 @@
|
||||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||||
|
|
||||||
<svg
|
<svg
|
||||||
|
width="900.82861"
|
||||||
|
height="955.20648"
|
||||||
|
viewBox="0 0 238.34422 252.7317"
|
||||||
version="1.1"
|
version="1.1"
|
||||||
x="0px"
|
id="svg420"
|
||||||
y="0px"
|
inkscape:export-filename="logo.svg"
|
||||||
viewBox="0 0 399.99999 400.00002"
|
inkscape:export-xdpi="221.14999"
|
||||||
enable-background="new 0 0 419.528 419.528"
|
inkscape:export-ydpi="221.14999"
|
||||||
xml:space="preserve"
|
|
||||||
id="svg52"
|
|
||||||
sodipodi:docname="noun-pangolin-1798092.svg"
|
|
||||||
width="400"
|
|
||||||
height="400"
|
|
||||||
inkscape:version="1.2.2 (b0a8486541, 2022-12-01)"
|
|
||||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
xmlns:svg="http://www.w3.org/2000/svg"><defs
|
xmlns:svg="http://www.w3.org/2000/svg">
|
||||||
id="defs56" /><sodipodi:namedview
|
<sodipodi:namedview
|
||||||
id="namedview54"
|
id="namedview422"
|
||||||
pagecolor="#ffffff"
|
pagecolor="#ffffff"
|
||||||
bordercolor="#666666"
|
bordercolor="#666666"
|
||||||
borderopacity="1.0"
|
borderopacity="1.0"
|
||||||
|
@ -24,15 +23,18 @@
|
||||||
inkscape:pageopacity="0.0"
|
inkscape:pageopacity="0.0"
|
||||||
inkscape:pagecheckerboard="0"
|
inkscape:pagecheckerboard="0"
|
||||||
inkscape:deskcolor="#d1d1d1"
|
inkscape:deskcolor="#d1d1d1"
|
||||||
showgrid="false"
|
inkscape:document-units="mm"
|
||||||
inkscape:zoom="1.9583914"
|
showgrid="false" />
|
||||||
inkscape:cx="209.86611"
|
<defs
|
||||||
inkscape:cy="262.20499"
|
id="defs417" />
|
||||||
inkscape:window-width="3840"
|
<g
|
||||||
inkscape:window-height="2136"
|
inkscape:label="Layer 1"
|
||||||
inkscape:window-x="0"
|
inkscape:groupmode="layer"
|
||||||
inkscape:window-y="0"
|
id="layer1"
|
||||||
inkscape:window-maximized="1"
|
transform="translate(-13.119542,-5.9258171)">
|
||||||
inkscape:current-layer="svg52" /><path
|
<path
|
||||||
d="m 62.232921,184.91974 c 0,2.431 -1.97,4.402 -4.399,4.402 -2.429,0 -4.399,-1.972 -4.399,-4.402 0,-2.429 1.97,-4.399 4.399,-4.399 2.429,-10e-4 4.399,1.97 4.399,4.399 z m 58.993999,-4.821 c -25.943999,-2.826 -38.978999,7.453 -71.181999,31.357 -27.572,20.467 -32.767,4.381 -31.748,-2.614 1.499,-10.282 25.222,-58.573 48.079,-88.461 28.273,7.34 49.869999,30.727 54.850999,59.718 z m -55.915999,4.821 c 0,-4.131 -3.349,-7.478 -7.478,-7.478 -4.129,0 -7.478,3.347 -7.478,7.478 0,4.131 3.349,7.481 7.478,7.481 4.13,0 7.478,-3.35 7.478,-7.481 z m -15.032,48.424 -0.234,14.041 20.413,22.687 -9.818,7.353 33.306,27.492 -11.759,8.124 42.631999,19.939 -10.825,9.747 48.291,8.078 -7.526,10.307 48.758,-4.531 -3.997,11.725 53.916,-18.153 -2.76,13.357 48.077,-34.345 1.479,13.562 34.087,-48.576 7.478,14.206 15.187,-58.89 10.391,8.533 -2.14,-57.884 13.814,5.13 -21.082,-51.204 13.404,0.048 -33.696,-42.131 15.312,-1.366 -47.026,-32.831002 14.255,-8.399 -54.817,-14.682 9.257,-11.695 -49.625,0.352 0.6,-13.337 -38.537,14.084 -1.597,-12.689 -29.984,21.429 -6.446,-10.852 -22.59,26.504 -7.021,-9.572 -18.923,30.294 -9.595999,-8.744 -16.754,30.138002 c 31.509999,10.197 54.979999,37.951 59.126999,71.547 0.404,0.087 -22.37,31.257 10.955,57.85 -0.576,-2.985 -6.113,-53.902 47.496,-57.61 26.668,-1.844 48.4,21.666 48.4,48.399 0,8.184 -2.05,15.883 -5.636,22.64 -15.927,29.611 -64.858,30.755 -80.429,30.596 -45.154,-0.459 -104.051999,-51.521 -104.051999,-51.521 z"
|
d="m 213.66176,90.072122 c 4.95655,0 8.97383,4.018046 8.97383,8.973827 0,4.956581 -4.01728,8.974621 -8.97383,8.974621 -4.95657,0 -8.97462,-4.01804 -8.97462,-8.974621 0,-4.955781 4.01805,-8.973827 8.97462,-8.973827 z m 35.2316,37.450998 c -0.90048,29.80928 -23.66033,69.21262 -54.51292,79.34466 -36.04206,11.836 -63.40991,-5.92226 -72.08409,-26.74061 -6.75754,-16.21966 -1.65117,-35.62363 10.96266,-43.83669 10.6506,-6.93533 30.48543,-8.76736 47.15454,2.19144 -5.85627,-15.34246 -21.62491,-25.4256 -35.59101,-28.49424 -13.96613,-3.06867 -28.38324,0.43858 -38.74504,5.69946 13.29071,-14.68572 44.40801,-28.946049 78.24077,-10.95958 22.67676,12.05491 32.43775,28.93208 42.0489,51.72763 C 251.59637,117.87858 234.026,71.411066 203.39074,43.794029 172.15544,15.636686 129.95516,4.340214 97.668803,6.103155 108.32483,12.678273 120.84625,22.06586 132.41209,33.053363 81.298533,26.697169 39.174705,38.314245 13.119542,73.749217 27.67508,70.878527 46.868833,69.073666 65.974711,70.016861 28.737658,96.252107 7.1124298,140.38147 18.105298,186.43137 c 6.718497,-11.74129 16.767711,-25.84558 28.726275,-38.62863 -3.677175,34.36994 1.42836,80.83745 45.62293,110.85478 -2.25587,-9.42394 -4.08014,-20.88443 -4.91466,-33.0154 20.673197,16.1282 50.685067,29.42205 87.917917,20.24096 65.77679,-16.21975 83.34719,-79.78335 73.4356,-118.35996"
|
||||||
id="path46" /></svg>
|
style="fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.0776283"
|
||||||
|
id="path32" />
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
|
Before Width: | Height: | Size: 2.6 KiB After Width: | Height: | Size: 2.5 KiB |
|
@ -1,39 +1,22 @@
|
||||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||||
|
|
||||||
<svg
|
<svg
|
||||||
|
width="900.82861"
|
||||||
|
height="955.20648"
|
||||||
|
viewBox="0 0 238.34422 252.7317"
|
||||||
version="1.1"
|
version="1.1"
|
||||||
x="0px"
|
id="svg420"
|
||||||
y="0px"
|
|
||||||
viewBox="0 0 399.99999 400.00002"
|
|
||||||
enable-background="new 0 0 419.528 419.528"
|
|
||||||
xml:space="preserve"
|
|
||||||
id="svg52"
|
|
||||||
sodipodi:docname="pangolin_orange.svg"
|
|
||||||
width="400"
|
|
||||||
height="400"
|
|
||||||
inkscape:version="1.2.2 (b0a8486541, 2022-12-01)"
|
|
||||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
|
||||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
xmlns:svg="http://www.w3.org/2000/svg"><defs
|
xmlns:svg="http://www.w3.org/2000/svg">
|
||||||
id="defs56" /><sodipodi:namedview
|
<defs
|
||||||
id="namedview54"
|
id="defs417" />
|
||||||
pagecolor="#ffffff"
|
<g
|
||||||
bordercolor="#666666"
|
id="layer1"
|
||||||
borderopacity="1.0"
|
transform="translate(-13.119542,-5.9258171)">
|
||||||
inkscape:showpageshadow="2"
|
<path
|
||||||
inkscape:pageopacity="0.0"
|
d="m 213.66176,90.072122 c 4.95655,0 8.97383,4.018046 8.97383,8.973827 0,4.956581 -4.01728,8.974621 -8.97383,8.974621 -4.95657,0 -8.97462,-4.01804 -8.97462,-8.974621 0,-4.955781 4.01805,-8.973827 8.97462,-8.973827 z m 35.2316,37.450998 c -0.90048,29.80928 -23.66033,69.21262 -54.51292,79.34466 -36.04206,11.836 -63.40991,-5.92226 -72.08409,-26.74061 -6.75754,-16.21966 -1.65117,-35.62363 10.96266,-43.83669 10.6506,-6.93533 30.48543,-8.76736 47.15454,2.19144 -5.85627,-15.34246 -21.62491,-25.4256 -35.59101,-28.49424 -13.96613,-3.06867 -28.38324,0.43858 -38.74504,5.69946 13.29071,-14.68572 44.40801,-28.946049 78.24077,-10.95958 22.67676,12.05491 32.43775,28.93208 42.0489,51.72763 C 251.59637,117.87858 234.026,71.411066 203.39074,43.794029 172.15544,15.636686 129.95516,4.340214 97.668803,6.103155 108.32483,12.678273 120.84625,22.06586 132.41209,33.053363 81.298533,26.697169 39.174705,38.314245 13.119542,73.749217 27.67508,70.878527 46.868833,69.073666 65.974711,70.016861 28.737658,96.252107 7.1124298,140.38147 18.105298,186.43137 c 6.718497,-11.74129 16.767711,-25.84558 28.726275,-38.62863 -3.677175,34.36994 1.42836,80.83745 45.62293,110.85478 -2.25587,-9.42394 -4.08014,-20.88443 -4.91466,-33.0154 20.673197,16.1282 50.685067,29.42205 87.917917,20.24096 65.77679,-16.21975 83.34719,-79.78335 73.4356,-118.35996"
|
||||||
inkscape:pagecheckerboard="0"
|
style="fill:#f36118;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.0776283"
|
||||||
inkscape:deskcolor="#d1d1d1"
|
id="path32" />
|
||||||
showgrid="false"
|
</g>
|
||||||
inkscape:zoom="1.9583914"
|
</svg>
|
||||||
inkscape:cx="127.40048"
|
|
||||||
inkscape:cy="262.71561"
|
|
||||||
inkscape:window-width="1436"
|
|
||||||
inkscape:window-height="1236"
|
|
||||||
inkscape:window-x="2208"
|
|
||||||
inkscape:window-y="511"
|
|
||||||
inkscape:window-maximized="0"
|
|
||||||
inkscape:current-layer="svg52" /><path
|
|
||||||
d="m 62.232921,184.91974 c 0,2.431 -1.97,4.402 -4.399,4.402 -2.429,0 -4.399,-1.972 -4.399,-4.402 0,-2.429 1.97,-4.399 4.399,-4.399 2.429,-10e-4 4.399,1.97 4.399,4.399 z m 58.993999,-4.821 c -25.943999,-2.826 -38.978999,7.453 -71.181999,31.357 -27.572,20.467 -32.767,4.381 -31.748,-2.614 1.499,-10.282 25.222,-58.573 48.079,-88.461 28.273,7.34 49.869999,30.727 54.850999,59.718 z m -55.915999,4.821 c 0,-4.131 -3.349,-7.478 -7.478,-7.478 -4.129,0 -7.478,3.347 -7.478,7.478 0,4.131 3.349,7.481 7.478,7.481 4.13,0 7.478,-3.35 7.478,-7.481 z m -15.032,48.424 -0.234,14.041 20.413,22.687 -9.818,7.353 33.306,27.492 -11.759,8.124 42.631999,19.939 -10.825,9.747 48.291,8.078 -7.526,10.307 48.758,-4.531 -3.997,11.725 53.916,-18.153 -2.76,13.357 48.077,-34.345 1.479,13.562 34.087,-48.576 7.478,14.206 15.187,-58.89 10.391,8.533 -2.14,-57.884 13.814,5.13 -21.082,-51.204 13.404,0.048 -33.696,-42.131 15.312,-1.366 -47.026,-32.831002 14.255,-8.399 -54.817,-14.682 9.257,-11.695 -49.625,0.352 0.6,-13.337 -38.537,14.084 -1.597,-12.689 -29.984,21.429 -6.446,-10.852 -22.59,26.504 -7.021,-9.572 -18.923,30.294 -9.595999,-8.744 -16.754,30.138002 c 31.509999,10.197 54.979999,37.951 59.126999,71.547 0.404,0.087 -22.37,31.257 10.955,57.85 -0.576,-2.985 -6.113,-53.902 47.496,-57.61 26.668,-1.844 48.4,21.666 48.4,48.399 0,8.184 -2.05,15.883 -5.636,22.64 -15.927,29.611 -64.858,30.755 -80.429,30.596 -45.154,-0.459 -104.051999,-51.521 -104.051999,-51.521 z"
|
|
||||||
id="path46"
|
|
||||||
style="fill:#f97315;fill-opacity:1" /></svg>
|
|
||||||
|
|
Before Width: | Height: | Size: 2.6 KiB After Width: | Height: | Size: 1.8 KiB |
Before Width: | Height: | Size: 7.8 KiB After Width: | Height: | Size: 11 KiB |
Before Width: | Height: | Size: 27 KiB After Width: | Height: | Size: 24 KiB |
Before Width: | Height: | Size: 3.7 KiB After Width: | Height: | Size: 7.4 KiB |
BIN
public/logo/pangolin_profile_picture.png
Normal file
After Width: | Height: | Size: 24 KiB |
Before Width: | Height: | Size: 62 KiB After Width: | Height: | Size: 36 KiB |
BIN
public/logo/word_mark_black.png
Normal file
After Width: | Height: | Size: 34 KiB |
BIN
public/logo/word_mark_white.png
Normal file
After Width: | Height: | Size: 33 KiB |
Before Width: | Height: | Size: 1.1 MiB |
BIN
public/screenshots/create-api-key.png
Normal file
After Width: | Height: | Size: 748 KiB |
BIN
public/screenshots/create-idp.png
Normal file
After Width: | Height: | Size: 688 KiB |
BIN
public/screenshots/create-resource.png
Normal file
After Width: | Height: | Size: 687 KiB |
BIN
public/screenshots/create-share-link.png
Normal file
After Width: | Height: | Size: 669 KiB |
BIN
public/screenshots/create-site.png
Normal file
After Width: | Height: | Size: 713 KiB |
BIN
public/screenshots/edit-resource.png
Normal file
After Width: | Height: | Size: 636 KiB |
BIN
public/screenshots/hero.png
Normal file
After Width: | Height: | Size: 713 KiB |
BIN
public/screenshots/resource-auth.png
Normal file
After Width: | Height: | Size: 356 KiB |
BIN
public/screenshots/resource-authentication.png
Normal file
After Width: | Height: | Size: 707 KiB |
Before Width: | Height: | Size: 706 KiB After Width: | Height: | Size: 713 KiB |
BIN
public/screenshots/roles.png
Normal file
After Width: | Height: | Size: 556 KiB |
BIN
public/screenshots/site-online.png
Normal file
After Width: | Height: | Size: 585 KiB |
BIN
public/screenshots/sites-fade.png
Normal file
After Width: | Height: | Size: 456 KiB |
Before Width: | Height: | Size: 729 KiB After Width: | Height: | Size: 674 KiB |
BIN
public/screenshots/users.png
Normal file
After Width: | Height: | Size: 597 KiB |
|
@ -5,23 +5,29 @@ import config from "@server/lib/config";
|
||||||
import logger from "@server/logger";
|
import logger from "@server/logger";
|
||||||
import {
|
import {
|
||||||
errorHandlerMiddleware,
|
errorHandlerMiddleware,
|
||||||
notFoundMiddleware,
|
notFoundMiddleware
|
||||||
rateLimitMiddleware
|
|
||||||
} from "@server/middlewares";
|
} from "@server/middlewares";
|
||||||
import { authenticated, unauthenticated } from "@server/routers/external";
|
import { authenticated, unauthenticated } from "@server/routers/external";
|
||||||
import { router as wsRouter, handleWSUpgrade } from "@server/routers/ws";
|
import { router as wsRouter, handleWSUpgrade } from "@server/routers/ws";
|
||||||
import { logIncomingMiddleware } from "./middlewares/logIncoming";
|
import { logIncomingMiddleware } from "./middlewares/logIncoming";
|
||||||
import { csrfProtectionMiddleware } from "./middlewares/csrfProtection";
|
import { csrfProtectionMiddleware } from "./middlewares/csrfProtection";
|
||||||
import helmet from "helmet";
|
import helmet from "helmet";
|
||||||
|
import rateLimit from "express-rate-limit";
|
||||||
|
import createHttpError from "http-errors";
|
||||||
|
import HttpCode from "./types/HttpCode";
|
||||||
|
import requestTimeoutMiddleware from "./middlewares/requestTimeout";
|
||||||
|
import { createStore } from "./lib/rateLimitStore";
|
||||||
|
|
||||||
const dev = config.isDev;
|
const dev = config.isDev;
|
||||||
const externalPort = config.getRawConfig().server.external_port;
|
const externalPort = config.getRawConfig().server.external_port;
|
||||||
|
|
||||||
export function createApiServer() {
|
export function createApiServer() {
|
||||||
const apiServer = express();
|
const apiServer = express();
|
||||||
|
const prefix = `/api/v1`;
|
||||||
|
|
||||||
if (config.getRawConfig().server.trust_proxy) {
|
const trustProxy = config.getRawConfig().server.trust_proxy;
|
||||||
apiServer.set("trust proxy", 1);
|
if (trustProxy) {
|
||||||
|
apiServer.set("trust proxy", trustProxy);
|
||||||
}
|
}
|
||||||
|
|
||||||
const corsConfig = config.getRawConfig().server.cors;
|
const corsConfig = config.getRawConfig().server.cors;
|
||||||
|
@ -53,19 +59,30 @@ export function createApiServer() {
|
||||||
apiServer.use(cookieParser());
|
apiServer.use(cookieParser());
|
||||||
apiServer.use(express.json());
|
apiServer.use(express.json());
|
||||||
|
|
||||||
|
// Add request timeout middleware
|
||||||
|
apiServer.use(requestTimeoutMiddleware(60000)); // 60 second timeout
|
||||||
|
|
||||||
if (!dev) {
|
if (!dev) {
|
||||||
apiServer.use(
|
apiServer.use(
|
||||||
rateLimitMiddleware({
|
rateLimit({
|
||||||
windowMin:
|
windowMs:
|
||||||
config.getRawConfig().rate_limits.global.window_minutes,
|
config.getRawConfig().rate_limits.global.window_minutes *
|
||||||
|
60 *
|
||||||
|
1000,
|
||||||
max: config.getRawConfig().rate_limits.global.max_requests,
|
max: config.getRawConfig().rate_limits.global.max_requests,
|
||||||
type: "IP_AND_PATH"
|
keyGenerator: (req) => `apiServerGlobal:${req.ip}:${req.path}`,
|
||||||
|
handler: (req, res, next) => {
|
||||||
|
const message = `Rate limit exceeded. You can make ${config.getRawConfig().rate_limits.global.max_requests} requests every ${config.getRawConfig().rate_limits.global.window_minutes} minute(s).`;
|
||||||
|
return next(
|
||||||
|
createHttpError(HttpCode.TOO_MANY_REQUESTS, message)
|
||||||
|
);
|
||||||
|
},
|
||||||
|
store: createStore()
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// API routes
|
// API routes
|
||||||
const prefix = `/api/v1`;
|
|
||||||
apiServer.use(logIncomingMiddleware);
|
apiServer.use(logIncomingMiddleware);
|
||||||
apiServer.use(prefix, unauthenticated);
|
apiServer.use(prefix, unauthenticated);
|
||||||
apiServer.use(prefix, authenticated);
|
apiServer.use(prefix, authenticated);
|
||||||
|
|
|
@ -1,11 +1,14 @@
|
||||||
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/schemas";
|
import { userActions, roleActions, userOrgs } from "@server/db";
|
||||||
import { and, eq } from "drizzle-orm";
|
import { and, eq } from "drizzle-orm";
|
||||||
import createHttpError from "http-errors";
|
import createHttpError from "http-errors";
|
||||||
import HttpCode from "@server/types/HttpCode";
|
import HttpCode from "@server/types/HttpCode";
|
||||||
|
|
||||||
export enum ActionsEnum {
|
export enum ActionsEnum {
|
||||||
|
createOrgUser = "createOrgUser",
|
||||||
|
listOrgs = "listOrgs",
|
||||||
|
listUserOrgs = "listUserOrgs",
|
||||||
createOrg = "createOrg",
|
createOrg = "createOrg",
|
||||||
// deleteOrg = "deleteOrg",
|
// deleteOrg = "deleteOrg",
|
||||||
getOrg = "getOrg",
|
getOrg = "getOrg",
|
||||||
|
@ -32,6 +35,8 @@ export enum ActionsEnum {
|
||||||
listRoles = "listRoles",
|
listRoles = "listRoles",
|
||||||
updateRole = "updateRole",
|
updateRole = "updateRole",
|
||||||
inviteUser = "inviteUser",
|
inviteUser = "inviteUser",
|
||||||
|
listInvitations = "listInvitations",
|
||||||
|
removeInvitation = "removeInvitation",
|
||||||
removeUser = "removeUser",
|
removeUser = "removeUser",
|
||||||
listUsers = "listUsers",
|
listUsers = "listUsers",
|
||||||
listSiteRoles = "listSiteRoles",
|
listSiteRoles = "listSiteRoles",
|
||||||
|
@ -51,6 +56,8 @@ export enum ActionsEnum {
|
||||||
// removeUserAction = "removeUserAction",
|
// removeUserAction = "removeUserAction",
|
||||||
// removeUserSite = "removeUserSite",
|
// removeUserSite = "removeUserSite",
|
||||||
getOrgUser = "getOrgUser",
|
getOrgUser = "getOrgUser",
|
||||||
|
updateUser = "updateUser",
|
||||||
|
getUser = "getUser",
|
||||||
setResourcePassword = "setResourcePassword",
|
setResourcePassword = "setResourcePassword",
|
||||||
setResourcePincode = "setResourcePincode",
|
setResourcePincode = "setResourcePincode",
|
||||||
setResourceWhitelist = "setResourceWhitelist",
|
setResourceWhitelist = "setResourceWhitelist",
|
||||||
|
@ -62,8 +69,33 @@ export enum ActionsEnum {
|
||||||
deleteResourceRule = "deleteResourceRule",
|
deleteResourceRule = "deleteResourceRule",
|
||||||
listResourceRules = "listResourceRules",
|
listResourceRules = "listResourceRules",
|
||||||
updateResourceRule = "updateResourceRule",
|
updateResourceRule = "updateResourceRule",
|
||||||
|
createClient = "createClient",
|
||||||
|
deleteClient = "deleteClient",
|
||||||
|
updateClient = "updateClient",
|
||||||
|
listClients = "listClients",
|
||||||
|
getClient = "getClient",
|
||||||
listOrgDomains = "listOrgDomains",
|
listOrgDomains = "listOrgDomains",
|
||||||
createNewt = "createNewt",
|
createNewt = "createNewt",
|
||||||
|
createIdp = "createIdp",
|
||||||
|
updateIdp = "updateIdp",
|
||||||
|
deleteIdp = "deleteIdp",
|
||||||
|
listIdps = "listIdps",
|
||||||
|
getIdp = "getIdp",
|
||||||
|
createIdpOrg = "createIdpOrg",
|
||||||
|
deleteIdpOrg = "deleteIdpOrg",
|
||||||
|
listIdpOrgs = "listIdpOrgs",
|
||||||
|
updateIdpOrg = "updateIdpOrg",
|
||||||
|
checkOrgId = "checkOrgId",
|
||||||
|
createApiKey = "createApiKey",
|
||||||
|
deleteApiKey = "deleteApiKey",
|
||||||
|
setApiKeyActions = "setApiKeyActions",
|
||||||
|
setApiKeyOrgs = "setApiKeyOrgs",
|
||||||
|
listApiKeyActions = "listApiKeyActions",
|
||||||
|
listApiKeys = "listApiKeys",
|
||||||
|
getApiKey = "getApiKey",
|
||||||
|
createOrgDomain = "createOrgDomain",
|
||||||
|
deleteOrgDomain = "deleteOrgDomain",
|
||||||
|
restartOrgDomain = "restartOrgDomain"
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function checkUserActionPermission(
|
export async function checkUserActionPermission(
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import db from "@server/db";
|
import { db } from "@server/db";
|
||||||
import { and, eq } from "drizzle-orm";
|
import { and, eq } from "drizzle-orm";
|
||||||
import { roleResources, userResources } from "@server/db/schemas";
|
import { roleResources, userResources } from "@server/db";
|
||||||
|
|
||||||
export async function canUserAccessResource({
|
export async function canUserAccessResource({
|
||||||
userId,
|
userId,
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import db from "@server/db";
|
import { db } from "@server/db";
|
||||||
import { UserInvite, userInvites } from "@server/db/schemas";
|
import { UserInvite, userInvites } from "@server/db";
|
||||||
import { isWithinExpirationDate } from "oslo";
|
import { isWithinExpirationDate } from "oslo";
|
||||||
import { verifyPassword } from "./password";
|
import { verifyPassword } from "./password";
|
||||||
import { eq } from "drizzle-orm";
|
import { eq } from "drizzle-orm";
|
||||||
|
|
|
@ -1,40 +0,0 @@
|
||||||
import { db } from '@server/db';
|
|
||||||
import { limitsTable } from '@server/db/schemas';
|
|
||||||
import { and, eq } from 'drizzle-orm';
|
|
||||||
import createHttpError from 'http-errors';
|
|
||||||
import HttpCode from '@server/types/HttpCode';
|
|
||||||
|
|
||||||
interface CheckLimitOptions {
|
|
||||||
orgId: string;
|
|
||||||
limitName: string;
|
|
||||||
currentValue: number;
|
|
||||||
increment?: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function checkOrgLimit({ orgId, limitName, currentValue, increment = 0 }: CheckLimitOptions): Promise<boolean> {
|
|
||||||
try {
|
|
||||||
const limit = await db.select()
|
|
||||||
.from(limitsTable)
|
|
||||||
.where(
|
|
||||||
and(
|
|
||||||
eq(limitsTable.orgId, orgId),
|
|
||||||
eq(limitsTable.name, limitName)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
.limit(1);
|
|
||||||
|
|
||||||
if (limit.length === 0) {
|
|
||||||
throw createHttpError(HttpCode.NOT_FOUND, `Limit "${limitName}" not found for organization`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const limitValue = limit[0].value;
|
|
||||||
|
|
||||||
// Check if the current value plus the increment is within the limit
|
|
||||||
return (currentValue + increment) <= limitValue;
|
|
||||||
} catch (error) {
|
|
||||||
if (error instanceof Error) {
|
|
||||||
throw createHttpError(HttpCode.INTERNAL_SERVER_ERROR, `Error checking limit: ${error.message}`);
|
|
||||||
}
|
|
||||||
throw createHttpError(HttpCode.INTERNAL_SERVER_ERROR, 'Unknown error occurred while checking limit');
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,5 +1,5 @@
|
||||||
import db from "@server/db";
|
import { db } from "@server/db";
|
||||||
import { resourceOtp } from "@server/db/schemas";
|
import { resourceOtp } from "@server/db";
|
||||||
import { and, eq } from "drizzle-orm";
|
import { and, eq } from "drizzle-orm";
|
||||||
import { createDate, isWithinExpirationDate, TimeSpan } from "oslo";
|
import { createDate, isWithinExpirationDate, TimeSpan } from "oslo";
|
||||||
import { alphabet, generateRandomString, sha256 } from "oslo/crypto";
|
import { alphabet, generateRandomString, sha256 } from "oslo/crypto";
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import { TimeSpan, createDate } from "oslo";
|
import { TimeSpan, createDate } from "oslo";
|
||||||
import { generateRandomString, alphabet } from "oslo/crypto";
|
import { generateRandomString, alphabet } from "oslo/crypto";
|
||||||
import db from "@server/db";
|
import { db } from "@server/db";
|
||||||
import { users, emailVerificationCodes } from "@server/db/schemas";
|
import { users, emailVerificationCodes } from "@server/db";
|
||||||
import { eq } from "drizzle-orm";
|
import { eq } from "drizzle-orm";
|
||||||
import { sendEmail } from "@server/emails";
|
import { sendEmail } from "@server/emails";
|
||||||
import config from "@server/lib/config";
|
import config from "@server/lib/config";
|
||||||
|
|
|
@ -9,8 +9,8 @@ import {
|
||||||
sessions,
|
sessions,
|
||||||
User,
|
User,
|
||||||
users
|
users
|
||||||
} from "@server/db/schemas";
|
} 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";
|
||||||
import type { RandomReader } from "@oslojs/crypto/random";
|
import type { RandomReader } from "@oslojs/crypto/random";
|
||||||
|
|