Compare commits
588 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 | ||
|
|
590296e64d | ||
|
|
17b39d16a3 | ||
|
|
ca0e950cdf | ||
|
|
cec9e4101d | ||
|
|
862fabf6ad | ||
|
|
128a9aae4e | ||
|
|
1983d0067d | ||
|
|
bdaf48da20 | ||
|
|
09024a93e9 | ||
|
|
064e7071b2 | ||
|
|
2b2fedb380 | ||
|
|
5ff7c216b9 | ||
|
|
c476ac7bc5 | ||
|
|
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 | ||
|
|
e885676ad8 | ||
|
|
b75d0a921e | ||
|
|
34fa5fe438 | ||
|
|
c2449ce795 | ||
|
|
dd7b91f770 | ||
|
|
fbc1aa25a3 | ||
|
|
e8870cf174 | ||
|
|
d768bb163a | ||
|
|
dc6fafba41 | ||
|
|
9f979c5019 | ||
|
|
1c18b2bffb | ||
|
|
d523ebe0e0 | ||
|
|
dba5a73e0e | ||
|
|
f07e8d08c3 | ||
|
|
ea24759bb3 | ||
|
|
af3694da34 | ||
|
|
ae4ef4eb99 | ||
|
|
547e777eb0 | ||
|
|
d9ee40c898 | ||
|
|
eff812eaa8 | ||
|
|
731ec1da69 | ||
|
|
b8ed5ac1c5 | ||
|
|
d2d84be99a | ||
|
|
96bfc3cf36 | ||
|
|
6f54e3da9e | ||
|
|
d3b9363392 | ||
|
|
285b99f1b7 | ||
|
|
8ad8f98f48 | ||
|
|
160a7ff3db | ||
|
|
1f95d7161a | ||
|
|
3a1f4d7545 | ||
|
|
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 | ||
|
|
a76dd9c9d1 | ||
|
|
752c474983 | ||
|
|
02ccb029ae | ||
|
|
b01fa82627 | ||
|
|
0c370e4299 | ||
|
|
d737fda8bc | ||
|
|
475a431859 | ||
|
|
490ddfcd88 | ||
|
|
fa6fc9e80d | ||
|
|
f960fb7d67 | ||
|
|
f6a19631dc | ||
|
|
e2efd0e65a | ||
|
|
361f487384 | ||
|
|
dc49027b30 | ||
|
|
581fdd67b1 | ||
|
|
d664aa204f | ||
|
|
db0328fa71 | ||
|
|
569635f3ed | ||
|
|
b7ae712b63 | ||
|
|
8398f7b7c0 | ||
|
|
e57574ba9c | ||
|
|
68ebdda1ff | ||
|
|
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/
|
||||
LICENSE
|
||||
CONTRIBUTING.md
|
||||
dist
|
||||
.git
|
||||
|
||||
16
.github/dependabot.yml
vendored
@@ -13,10 +13,6 @@ updates:
|
||||
dependency-type: "development"
|
||||
update-types:
|
||||
- "minor"
|
||||
dev-major-updates:
|
||||
dependency-type: "development"
|
||||
update-types:
|
||||
- "major"
|
||||
prod-patch-updates:
|
||||
dependency-type: "production"
|
||||
update-types:
|
||||
@@ -25,10 +21,6 @@ updates:
|
||||
dependency-type: "production"
|
||||
update-types:
|
||||
- "minor"
|
||||
prod-major-updates:
|
||||
dependency-type: "production"
|
||||
update-types:
|
||||
- "major"
|
||||
|
||||
- package-ecosystem: "docker"
|
||||
directory: "/"
|
||||
@@ -41,6 +33,8 @@ updates:
|
||||
minor-updates:
|
||||
update-types:
|
||||
- "minor"
|
||||
major-updates:
|
||||
update-types:
|
||||
- "major"
|
||||
|
||||
- package-ecosystem: "github-actions"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
|
||||
8
.github/workflows/cicd.yml
vendored
@@ -12,13 +12,13 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v2
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Log in to Docker Hub
|
||||
uses: docker/login-action@v2
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_HUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_HUB_ACCESS_TOKEN }}
|
||||
@@ -28,7 +28,7 @@ jobs:
|
||||
run: echo "TAG=${GITHUB_REF#refs/tags/}" >> $GITHUB_ENV
|
||||
|
||||
- name: Install Go
|
||||
uses: actions/setup-go@v4
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
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
|
||||
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
|
||||
|
||||
- name: Build Docker image pg
|
||||
run: make build-pg
|
||||
3
.gitignore
vendored
@@ -18,6 +18,7 @@ yarn-error.log*
|
||||
next-env.d.ts
|
||||
*.db
|
||||
*.sqlite
|
||||
!Dockerfile.sqlite
|
||||
*.sqlite3
|
||||
*.log
|
||||
.machinelogs*.json
|
||||
@@ -33,4 +34,4 @@ bin
|
||||
.secrets
|
||||
test_event.json
|
||||
.idea/
|
||||
package-lock.json
|
||||
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
|
||||
|
||||
For ideas about what features to work on and our future plans, please see the roadmap:
|
||||
|
||||
https://docs.fossorial.io/roadmap
|
||||
|
||||
### 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.
|
||||
@@ -21,4 +17,4 @@ By creating this pull request, I grant the project maintainers an unlimited,
|
||||
perpetual license to use, modify, and redistribute these contributions under any terms they
|
||||
choose, including both the AGPLv3 and the Fossorial Commercial license terms. I
|
||||
represent that I have the right to grant this license for all contributed content.
|
||||
```
|
||||
```
|
||||
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"]
|
||||
@@ -3,8 +3,8 @@ FROM node:20-alpine AS builder
|
||||
WORKDIR /app
|
||||
|
||||
# COPY package.json package-lock.json ./
|
||||
COPY package.json ./
|
||||
RUN npm install
|
||||
COPY package*.json ./
|
||||
RUN npm ci
|
||||
|
||||
COPY . .
|
||||
|
||||
@@ -13,6 +13,7 @@ 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
|
||||
|
||||
@@ -22,14 +23,17 @@ WORKDIR /app
|
||||
RUN apk add --no-cache curl
|
||||
|
||||
# COPY package.json package-lock.json ./
|
||||
COPY package.json ./
|
||||
RUN npm install --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/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
|
||||
|
||||
@@ -3,8 +3,8 @@ FROM node:20-alpine AS builder
|
||||
WORKDIR /app
|
||||
|
||||
# COPY package.json package-lock.json ./
|
||||
COPY package.json ./
|
||||
RUN npm install
|
||||
COPY package*.json ./
|
||||
RUN npm ci
|
||||
|
||||
COPY . .
|
||||
|
||||
@@ -13,6 +13,7 @@ RUN echo 'export * from "./sqlite";' > server/db/index.ts
|
||||
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
|
||||
|
||||
@@ -22,14 +23,17 @@ WORKDIR /app
|
||||
RUN apk add --no-cache curl
|
||||
|
||||
# COPY package.json package-lock.json ./
|
||||
COPY package.json ./
|
||||
RUN npm install --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/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
|
||||
15
Makefile
@@ -1,10 +1,12 @@
|
||||
.PHONY: build build-pg build-release build-arm build-x86 test clean
|
||||
|
||||
build-release:
|
||||
@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; \
|
||||
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:$(tag) -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.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 .
|
||||
|
||||
@@ -14,8 +16,11 @@ build-arm:
|
||||
build-x86:
|
||||
docker buildx build --platform linux/amd64 -t fosrl/pangolin:latest .
|
||||
|
||||
build:
|
||||
docker build -t fosrl/pangolin:latest .
|
||||
build-sqlite:
|
||||
docker build -t fosrl/pangolin:latest -f Dockerfile.sqlite .
|
||||
|
||||
build-pg:
|
||||
docker build -t fosrl/pangolin:postgresql-latest -f Dockerfile.pg .
|
||||
|
||||
test:
|
||||
docker run -it -p 3000:3000 -p 3001:3001 -p 3002:3002 -v ./config:/app/config fosrl/pangolin:latest
|
||||
|
||||
104
README.md
@@ -7,10 +7,10 @@
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
<h4 align="center">Tunneled Reverse Proxy Server with Access Control</h4>
|
||||
<h4 align="center">Secure gateway to your private networks</h4>
|
||||
<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>
|
||||
|
||||
@@ -36,22 +36,32 @@ _Your own self-hosted zero trust tunnel._
|
||||
|
||||
</div>
|
||||
|
||||
<p align="center">
|
||||
<strong>
|
||||
Start testing Pangolin at <a href="https://pangolin.fossorial.io/auth/signup">pangolin.fossorial.io</a>
|
||||
<br/>
|
||||
</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.
|
||||
|
||||
<img src="public/screenshots/hero.png" alt="Preview"/>
|
||||
|
||||
_Resources page of Pangolin dashboard (dark mode) showing multiple resources available to connect._
|
||||

|
||||
|
||||
## Key Features
|
||||
|
||||
### Reverse Proxy Through WireGuard Tunnel
|
||||
|
||||
- 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.
|
||||
- Automated **SSL certificates** (https) via [LetsEncrypt](https://letsencrypt.org/).
|
||||
- Support for HTTP/HTTPS and **raw TCP/UDP services**.
|
||||
- Load balancing.
|
||||
- Extend functionality with existing [Traefik](https://github.com/traefik/traefik) plugins, such as [CrowdSec](https://plugins.traefik.io/plugins/6335346ca4caa9ddeffda116/crowdsec-bouncer-traefik-plugin) and [Geoblock](https://github.com/PascalMinder/geoblock).
|
||||
- **Automatically install and configure Crowdsec via Pangolin's installer script.**
|
||||
- Attach as many sites to the central server as you wish.
|
||||
|
||||
### Identity & Access Management
|
||||
|
||||
@@ -65,89 +75,73 @@ _Resources page of Pangolin dashboard (dark mode) showing multiple resources ava
|
||||
- **Temporary, self-destructing share links.**
|
||||
- Resource specific pin codes.
|
||||
- Resource specific passwords.
|
||||
- Passkeys
|
||||
- External identity provider (IdP) support with OAuth2/OIDC, such as Authentik, Keycloak, Okta, and others.
|
||||
- Auto-provision users and roles from your IdP.
|
||||
|
||||
### Simple Dashboard UI
|
||||
<img src="public/auth-diagram1.png" alt="Auth and diagram"/>
|
||||
|
||||
- Manage sites, users, and roles with a clean and intuitive UI.
|
||||
- Monitor site usage and connectivity.
|
||||
- Light and dark mode options.
|
||||
- Mobile friendly.
|
||||
## Use Cases
|
||||
|
||||
### Easy Deployment
|
||||
### Manage Access to Internal Apps
|
||||
|
||||
- Run on any cloud provider or on-premises.
|
||||
- **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.
|
||||
- Use the API to create custom integrations and scripts.
|
||||
- Fine-grained access control to the API via scoped API keys.
|
||||
- Comprehensive Swagger documentation for the API.
|
||||
- Grant users access to your apps from anywhere using just a web browser. No client software required.
|
||||
|
||||
### 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](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.
|
||||
- Expose and test internal tools and dashboards like **Grafana**. Bring localhost or private IPs online for easy access.
|
||||
|
||||
<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.
|
||||
|
||||
<img src="public/screenshots/sites.png" alt="Sites"/>
|
||||
|
||||
## Deployment Options
|
||||
|
||||
### Fully Self Hosted
|
||||
|
||||
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.
|
||||
|
||||
> [!TIP]
|
||||
> 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!
|
||||
> 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.
|
||||
|
||||
1. **Domain Configuration**:
|
||||
### Pangolin Cloud
|
||||
|
||||
- Point your domain name to the VPS and configure Pangolin with your preferred settings.
|
||||
Easy to use with simple pay as you go pricing. [Check it out here](https://pangolin.fossorial.io/auth/signup).
|
||||
|
||||
2. **Connect Private Sites**:
|
||||
- Everything you get with self hosted Pangolin, but fully managed for you.
|
||||
|
||||
- Install Newt or use another WireGuard client on private sites.
|
||||
- Automatically establish a connection from these sites to the central server.
|
||||
### Hybrid & High Availability
|
||||
|
||||
3. **Expose Resources**:
|
||||
Managed control plane, your infrastructure
|
||||
|
||||
- Add resources to the central server and configure access control rules.
|
||||
- Access these resources securely from anywhere.
|
||||
- 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.
|
||||
|
||||
**Use Case Example - Bypassing Port Restrictions in Home Lab**:
|
||||
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.
|
||||
If interested, [contact us](mailto:numbat@fossorial.io).
|
||||
|
||||
**Use Case Example - Deploying Services For Your Business**:
|
||||
You can use Pangolin as an easy way to expose your business applications to your users behind a safe authentication portal you can integrate into your IdP solution. Expose resources on prem and on the cloud.
|
||||
### Full Enterprise On-Premises
|
||||
|
||||
**Use Case Example - IoT Networks**:
|
||||
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.
|
||||
|
||||
## Similar Projects and Inspirations
|
||||
|
||||
**Cloudflare Tunnels**:
|
||||
A similar approach to proxying private resources securely, but Pangolin is a self-hosted alternative, giving you full control over your infrastructure.
|
||||
|
||||
**Authelia**:
|
||||
This inspired Pangolin’s centralized authentication system for proxies, enabling robust user and role management.
|
||||
[Contact us](mailto:numbat@fossorial.io) for a full distributed and enterprise deployments on your infrastructure controlled by your team.
|
||||
|
||||
## Project Development / Roadmap
|
||||
|
||||
> [!NOTE]
|
||||
> 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.
|
||||
We want to hear your feature requests! Add them to the [discussion board](https://github.com/orgs/fosrl/discussions/categories/feature-requests).
|
||||
|
||||
## Licensing
|
||||
|
||||
Pangolin is dual licensed under the AGPL-3 and the Fossorial Commercial license. Please see the [LICENSE](./LICENSE) file in the repository 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
|
||||
|
||||
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 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 "$@"
|
||||
@@ -41,14 +41,8 @@ rate_limits:
|
||||
window_minutes: 1
|
||||
max_requests: 500
|
||||
|
||||
users:
|
||||
server_admin:
|
||||
email: "admin@example.com"
|
||||
password: "Password123!"
|
||||
|
||||
flags:
|
||||
require_email_verification: false
|
||||
disable_signup_without_invite: true
|
||||
disable_user_create_org: true
|
||||
allow_raw_resources: true
|
||||
allow_base_domain_resources: true
|
||||
|
||||
3
crowdin.yml
Normal file
@@ -0,0 +1,3 @@
|
||||
files:
|
||||
- source: /messages/en-US.json
|
||||
translation: /messages/%locale%.json
|
||||
12
docker-compose.pgr.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
|
||||
@@ -3,7 +3,7 @@ import path from "path";
|
||||
|
||||
export default defineConfig({
|
||||
dialect: "postgresql",
|
||||
schema: path.join("server", "db", "pg", "schema.ts"),
|
||||
schema: [path.join("server", "db", "pg", "schema.ts")],
|
||||
out: path.join("server", "migrations"),
|
||||
verbose: true,
|
||||
dbCredentials: {
|
||||
|
||||
@@ -1,9 +1,19 @@
|
||||
// eslint.config.js
|
||||
export default [
|
||||
{
|
||||
rules: {
|
||||
semi: "error",
|
||||
"prefer-const": "error"
|
||||
}
|
||||
import tseslint from 'typescript-eslint';
|
||||
|
||||
export default tseslint.config({
|
||||
files: ["**/*.{ts,tsx,js,jsx}"],
|
||||
languageOptions: {
|
||||
parser: tseslint.parser,
|
||||
parserOptions: {
|
||||
ecmaVersion: "latest",
|
||||
sourceType: "module",
|
||||
ecmaFeatures: {
|
||||
jsx: true
|
||||
}
|
||||
}
|
||||
];
|
||||
},
|
||||
rules: {
|
||||
"semi": "error",
|
||||
"prefer-const": "warn"
|
||||
}
|
||||
});
|
||||
@@ -21,4 +21,4 @@ update-versions:
|
||||
echo "Updated main.go with latest versions"
|
||||
|
||||
put-back:
|
||||
mv main.go.bak main.go
|
||||
mv main.go.bak main.go
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
app:
|
||||
dashboard_url: "https://{{.DashboardDomain}}"
|
||||
log_level: "info"
|
||||
save_logs: false
|
||||
|
||||
domains:
|
||||
domain1:
|
||||
@@ -12,56 +11,32 @@ domains:
|
||||
cert_resolver: "letsencrypt"
|
||||
|
||||
server:
|
||||
external_port: 3000
|
||||
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"
|
||||
secret: {{.Secret}}
|
||||
secret: "{{.Secret}}"
|
||||
cors:
|
||||
origins: ["https://{{.DashboardDomain}}"]
|
||||
methods: ["GET", "POST", "PUT", "DELETE", "PATCH"]
|
||||
allowed_headers: ["X-CSRF-Token", "Content-Type"]
|
||||
credentials: false
|
||||
|
||||
traefik:
|
||||
cert_resolver: "letsencrypt"
|
||||
http_entrypoint: "web"
|
||||
https_entrypoint: "websecure"
|
||||
|
||||
gerbil:
|
||||
start_port: 51820
|
||||
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
|
||||
orgs:
|
||||
block_size: 24
|
||||
subnet_group: 100.89.138.0/20
|
||||
|
||||
{{if .EnableEmail}}
|
||||
email:
|
||||
smtp_host: "{{.EmailSMTPHost}}"
|
||||
smtp_port: {{.EmailSMTPPort}}
|
||||
smtp_user: "{{.EmailSMTPUser}}"
|
||||
smtp_user: "{{.EmailSMTPUser}allow_base_domain_resources}"
|
||||
smtp_pass: "{{.EmailSMTPPass}}"
|
||||
no_reply: "{{.EmailNoReply}}"
|
||||
{{end}}
|
||||
users:
|
||||
server_admin:
|
||||
email: "{{.AdminUserEmail}}"
|
||||
password: "{{.AdminUserPassword}}"
|
||||
|
||||
flags:
|
||||
require_email_verification: {{.EnableEmail}}
|
||||
disable_signup_without_invite: {{.DisableSignupWithoutInvite}}
|
||||
disable_user_create_org: {{.DisableUserCreateOrg}}
|
||||
disable_signup_without_invite: true
|
||||
disable_user_create_org: false
|
||||
allow_raw_resources: true
|
||||
allow_base_domain_resources: true
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
name: captcha_remediation
|
||||
iame: captcha_remediation
|
||||
filters:
|
||||
- Alert.Remediation == true && Alert.GetScope() == "Ip" && Alert.GetScenario() contains "http"
|
||||
decisions:
|
||||
|
||||
@@ -35,7 +35,7 @@ services:
|
||||
- 80:80 # Port for traefik because of the network_mode
|
||||
{{end}}
|
||||
traefik:
|
||||
image: traefik:v3.4.0
|
||||
image: traefik:v3.4.1
|
||||
container_name: traefik
|
||||
restart: unless-stopped
|
||||
{{if .InstallGerbil}}
|
||||
@@ -58,4 +58,4 @@ services:
|
||||
networks:
|
||||
default:
|
||||
driver: bridge
|
||||
name: pangolin
|
||||
name: pangolin
|
||||
|
||||
109
install/main.go
@@ -16,7 +16,6 @@ import (
|
||||
"syscall"
|
||||
"text/template"
|
||||
"time"
|
||||
"unicode"
|
||||
"math/rand"
|
||||
"strconv"
|
||||
|
||||
@@ -40,10 +39,6 @@ type Config struct {
|
||||
BaseDomain string
|
||||
DashboardDomain string
|
||||
LetsEncryptEmail string
|
||||
AdminUserEmail string
|
||||
AdminUserPassword string
|
||||
DisableSignupWithoutInvite bool
|
||||
DisableUserCreateOrg bool
|
||||
EnableEmail bool
|
||||
EmailSMTPHost string
|
||||
EmailSMTPPort int
|
||||
@@ -75,15 +70,15 @@ func main() {
|
||||
}
|
||||
|
||||
var config Config
|
||||
|
||||
|
||||
// check if there is already a config file
|
||||
if _, err := os.Stat("config/config.yml"); err != nil {
|
||||
config = collectUserInput(reader)
|
||||
|
||||
|
||||
loadVersions(&config)
|
||||
config.DoCrowdsecInstall = false
|
||||
config.Secret = generateRandomSecretKey()
|
||||
|
||||
|
||||
if err := createConfigFiles(config); err != nil {
|
||||
fmt.Printf("Error creating config files: %v\n", err)
|
||||
os.Exit(1)
|
||||
@@ -171,6 +166,7 @@ func main() {
|
||||
}
|
||||
|
||||
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 {
|
||||
@@ -236,38 +232,9 @@ func collectUserInput(reader *bufio.Reader) Config {
|
||||
config.LetsEncryptEmail = readString(reader, "Enter email for Let's Encrypt certificates", "")
|
||||
config.InstallGerbil = readBool(reader, "Do you want to use Gerbil to allow tunneled connections", true)
|
||||
|
||||
// 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
|
||||
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 {
|
||||
config.EmailSMTPHost = readString(reader, "Enter SMTP host", "")
|
||||
@@ -290,60 +257,10 @@ func collectUserInput(reader *bufio.Reader) Config {
|
||||
fmt.Println("Error: Let's Encrypt email is required")
|
||||
os.Exit(1)
|
||||
}
|
||||
if config.AdminUserEmail == "" || config.AdminUserPassword == "" {
|
||||
fmt.Println("Error: Admin user email and password are required")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
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 {
|
||||
os.MkdirAll("config", 0755)
|
||||
os.MkdirAll("config/letsencrypt", 0755)
|
||||
@@ -429,7 +346,7 @@ func installDocker() error {
|
||||
return fmt.Errorf("failed to detect Linux distribution: %v", err)
|
||||
}
|
||||
osRelease := string(output)
|
||||
|
||||
|
||||
// Detect system architecture
|
||||
archCmd := exec.Command("uname", "-m")
|
||||
archOutput, err := archCmd.Output()
|
||||
@@ -437,7 +354,7 @@ func installDocker() error {
|
||||
return fmt.Errorf("failed to detect system architecture: %v", err)
|
||||
}
|
||||
arch := strings.TrimSpace(string(archOutput))
|
||||
|
||||
|
||||
// Map architecture to Docker's architecture naming
|
||||
var dockerArch string
|
||||
switch arch {
|
||||
@@ -479,7 +396,7 @@ func installDocker() error {
|
||||
fedoraVersion = v
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Use appropriate DNF syntax based on version
|
||||
var repoCmd string
|
||||
if fedoraVersion >= 41 {
|
||||
@@ -489,7 +406,7 @@ func installDocker() error {
|
||||
// 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(`
|
||||
dnf -y install dnf-plugins-core &&
|
||||
%s &&
|
||||
@@ -518,7 +435,7 @@ func installDocker() error {
|
||||
default:
|
||||
return fmt.Errorf("unsupported Linux distribution")
|
||||
}
|
||||
|
||||
|
||||
installCmd.Stdout = os.Stdout
|
||||
installCmd.Stderr = os.Stderr
|
||||
return installCmd.Run()
|
||||
@@ -603,7 +520,7 @@ func executeDockerComposeCommandWithArgs(args ...string) error {
|
||||
return fmt.Errorf("neither 'docker compose' nor 'docker-compose' command is available")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if useNewStyle {
|
||||
cmd = exec.Command("docker", append([]string{"compose"}, args...)...)
|
||||
} else {
|
||||
@@ -639,7 +556,7 @@ func startContainers() error {
|
||||
// stopContainers stops the containers using the appropriate command.
|
||||
func stopContainers() error {
|
||||
fmt.Println("Stopping containers...")
|
||||
|
||||
|
||||
if err := executeDockerComposeCommandWithArgs("-f", "docker-compose.yml", "down"); err != nil {
|
||||
return fmt.Errorf("failed to stop containers: %v", err)
|
||||
}
|
||||
@@ -650,7 +567,7 @@ func stopContainers() error {
|
||||
// restartContainer restarts a specific container using the appropriate command.
|
||||
func restartContainer(container string) error {
|
||||
fmt.Println("Restarting containers...")
|
||||
|
||||
|
||||
if err := executeDockerComposeCommandWithArgs("-f", "docker-compose.yml", "restart", container); err != nil {
|
||||
return fmt.Errorf("failed to stop the container \"%s\": %v", container, err)
|
||||
}
|
||||
|
||||
@@ -1,287 +0,0 @@
|
||||
## Authentication Site
|
||||
|
||||
| EN | DE | Notes |
|
||||
| -------------------------------------------------------- | ---------------------------------------------------------------------------------- | ---------- |
|
||||
| Powered by [Pangolin](https://github.com/fosrl/pangolin) | Bereitgestellt von [Pangolin](https://github.com/fosrl/pangolin) | |
|
||||
| Authentication Required | Authentifizierung erforderlich | |
|
||||
| Choose your preferred method to access {resource} | Wählen Sie Ihre bevorzugte Methode, um auf {resource} zuzugreifen | |
|
||||
| PIN | PIN | |
|
||||
| User | Benutzer | |
|
||||
| 6-digit PIN Code | 6-stelliger PIN-Code | pin login |
|
||||
| Login in with PIN | Mit PIN anmelden | pin login |
|
||||
| Email | E-Mail | user login |
|
||||
| Enter your email | Geben Sie Ihre E-Mail-Adresse ein | user login |
|
||||
| Password | Passwort | user login |
|
||||
| Enter your password | Geben Sie Ihr Passwort ein | user login |
|
||||
| Forgot your password? | Passwort vergessen? | user login |
|
||||
| Log in | Anmelden | user login |
|
||||
|
||||
---
|
||||
|
||||
## 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 | |
|
||||
@@ -1,310 +0,0 @@
|
||||
## Authentication Site
|
||||
|
||||
| EN | TR | Notes |
|
||||
| -------------------------------------------------------- | ---------------------------------------------------------------------------------- | ---------- |
|
||||
| Powered by [Pangolin](https://github.com/fosrl/pangolin) | Pangolin Tarafından Destekleniyor | |
|
||||
| Authentication Required | Kimlik Doğrulaması Gerekli | |
|
||||
| Choose your preferred method to access {resource} | {resource}'a erişmek için tercih ettiğiniz yöntemi seçin | |
|
||||
| PIN | PIN | |
|
||||
| User | Kullanıcı | |
|
||||
| 6-digit PIN Code | 6 haneli PIN Kodu | pin login |
|
||||
| Login in with PIN | PIN ile Giriş Yap | pin login |
|
||||
| Email | E-posta | user login |
|
||||
| Enter your email | E-postanızı girin | user login |
|
||||
| Password | Şifre | user login |
|
||||
| Enter your password | Şifrenizi girin | user login |
|
||||
| Forgot your password? | Şifrenizi mi unuttunuz? | user login |
|
||||
| Log in | Giriş Yap | user login |
|
||||
|
||||
---
|
||||
|
||||
## Login site
|
||||
|
||||
| EN | TR | Notes |
|
||||
| --------------------- | ------------------------------------------------------ | ----------- |
|
||||
| Welcome to Pangolin | Pangolin'e Hoşgeldiniz | |
|
||||
| Log in to get started | Başlamak için giriş yapın | |
|
||||
| Email | E-posta | |
|
||||
| Enter your email | E-posta adresinizi girin | placeholder |
|
||||
| Password | Şifre | |
|
||||
| Enter your password | Şifrenizi girin | placeholder |
|
||||
| Forgot your password? | Şifrenizi mi unuttunuz? | |
|
||||
| Log in | Giriş Yap | |
|
||||
|
||||
---
|
||||
|
||||
# Organization site after successful login
|
||||
|
||||
| EN | TR | Notes |
|
||||
| ----------------------------------------- | ------------------------------------------------------------------- | ----- |
|
||||
| Welcome to Pangolin | Pangolin'e Hoşgeldiniz | |
|
||||
| You're a member of {number} organization. | {number} organizasyonunun üyesiniz. | |
|
||||
|
||||
---
|
||||
|
||||
## Shared Header, Navbar and Footer
|
||||
|
||||
##### Header
|
||||
|
||||
| EN | TR | Notes |
|
||||
| ------------------- | -------------------------- | ----- |
|
||||
| Documentation | Dokümantasyon | |
|
||||
| Support | Destek | |
|
||||
| Organization {name} | Organizasyon {name} | |
|
||||
|
||||
##### Organization selector
|
||||
|
||||
| EN | TR | Notes |
|
||||
| ---------------- | ---------------------- | ----- |
|
||||
| Search… | Ara… | |
|
||||
| Create | Oluştur | |
|
||||
| New Organization | Yeni Organizasyon | |
|
||||
| Organizations | Organizasyonlar | |
|
||||
|
||||
##### Navbar
|
||||
|
||||
| EN | TR | Notes |
|
||||
| --------------- | ------------------------------- | ----- |
|
||||
| Sites | Siteler | |
|
||||
| Resources | Kaynaklar | |
|
||||
| User & Roles | Kullanıcılar ve Roller | |
|
||||
| Shareable Links | Paylaşılabilir Linkler | |
|
||||
| General | Genel | |
|
||||
|
||||
##### Footer
|
||||
|
||||
| EN | TR | Notes |
|
||||
| ------------------------- | ------------------------------------------------ | -------------------- |
|
||||
| Page {number} of {number} | Sayfa {number} / {number} | |
|
||||
| Rows per page | Sayfa başına satırlar | |
|
||||
| Pangolin | Pangolin | Footer'da yer alır |
|
||||
| Built by Fossorial | Fossorial tarafından oluşturuldu | Footer'da yer alır |
|
||||
| Open Source | Açık Kaynak | Footer'da yer alır |
|
||||
| Documentation | Dokümantasyon | Footer'da yer alır |
|
||||
| {version} | {version} | Footer'da yer alır |
|
||||
|
||||
---
|
||||
|
||||
## Main “Sites”
|
||||
|
||||
##### “Hero” section
|
||||
|
||||
| EN | TR | Notes |
|
||||
| ------------------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------- | ----- |
|
||||
| Newt (Recommended) | Newt (Tavsiye Edilen) | |
|
||||
| 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. | En iyi kullanıcı deneyimi için Newt'i kullanın. Newt, arka planda WireGuard kullanır ve Pangolin kontrol paneli üzerinden özel ağınızdaki kaynaklarınıza LAN adresleriyle erişmenizi sağlar. | |
|
||||
| Runs in Docker | Docker üzerinde çalışır | |
|
||||
| Runs in shell on macOS, Linux, and Windows | macOS, Linux ve Windows’ta komut satırında çalışır | |
|
||||
| Install Newt | Newt'i Yükle | |
|
||||
| Basic WireGuard<br> | Temel WireGuard<br> | |
|
||||
| Compatible with all WireGuard clients<br> | Tüm WireGuard istemcileriyle uyumlu<br> | |
|
||||
| Manual configuration required | Manuel yapılandırma gereklidir | |
|
||||
|
||||
##### Content
|
||||
|
||||
| EN | TR | Notes |
|
||||
| --------------------------------------------------------- | --------------------------------------------------------------------------- | ------------ |
|
||||
| Manage Sites | Siteleri Yönet | |
|
||||
| Allow connectivity to your network through secure tunnels | Güvenli tüneller aracılığıyla ağınıza bağlantı sağlayın | |
|
||||
| Search sites | Siteleri ara | placeholder |
|
||||
| Add Site | Site Ekle | |
|
||||
| Name | Ad | Table Header |
|
||||
| Online | Çevrimiçi | Table Header |
|
||||
| Site | Site | Table Header |
|
||||
| Data In | Gelen Veri | Table Header |
|
||||
| Data Out | Giden Veri | Table Header |
|
||||
| Connection Type | Bağlantı Türü | Table Header |
|
||||
| Online | Çevrimiçi | Site state |
|
||||
| Offline | Çevrimdışı | Site state |
|
||||
| Edit → | Düzenle → | |
|
||||
| View settings | Ayarları Görüntüle | Popup |
|
||||
| Delete | Sil | Popup |
|
||||
|
||||
##### Add Site Popup
|
||||
|
||||
| EN | TR | Notes |
|
||||
| ------------------------------------------------------ | ------------------------------------------------------------------------------------------- | ----------- |
|
||||
| Create Site | Site Oluştur | |
|
||||
| Create a new site to start connection for this site | Bu site için bağlantıyı başlatmak amacıyla yeni bir site oluşturun | |
|
||||
| Name | Ad | |
|
||||
| Site name | Site adı | placeholder |
|
||||
| This is the name that will be displayed for this site. | Bu, site için görüntülenecek addır. | desc |
|
||||
| Method | Yöntem | |
|
||||
| Local | Yerel | |
|
||||
| Newt | Newt | |
|
||||
| WireGuard | WireGuard | |
|
||||
| This is how you will expose connections. | Bağlantılarınızı bu şekilde açığa çıkaracaksınız. | |
|
||||
| You will only be able to see the configuration once. | Yapılandırmayı yalnızca bir kez görüntüleyebilirsiniz. | |
|
||||
| Learn how to install Newt on your system | Sisteminizde Newt'in nasıl kurulacağını öğrenin | |
|
||||
| I have copied the config | Yapılandırmayı kopyaladım | |
|
||||
| Create Site | Site Oluştur | |
|
||||
| Close | Kapat | |
|
||||
|
||||
---
|
||||
|
||||
## Main “Resources”
|
||||
|
||||
##### “Hero” section
|
||||
|
||||
| EN | TR | Notes |
|
||||
| ------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------ | ----- |
|
||||
| Resources | Kaynaklar | |
|
||||
| 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. | Kaynaklar, özel ağınızda çalışan uygulamalar için proxy sunucularıdır. Özel ağınızdaki her HTTP veya HTTPS uygulaması için bir kaynak oluşturun. Her kaynağın, şifrelenmiş WireGuard tüneli üzerinden özel ve güvenli bağlantı sağlamak üzere bir siteyle ilişkili olması gerekir. | |
|
||||
| Secure connectivity with WireGuard encryption | WireGuard şifrelemesiyle güvenli bağlantı | |
|
||||
| Configure multiple authentication methods | Birden çok kimlik doğrulama yöntemini yapılandırın | |
|
||||
| User and role-based access control | Kullanıcı ve role dayalı erişim kontrolü | |
|
||||
|
||||
##### Content
|
||||
|
||||
| EN | TR | Notes |
|
||||
| -------------------------------------------------- | ------------------------------------------------------------- | -------------------- |
|
||||
| Manage Resources | Kaynakları Yönet | |
|
||||
| Create secure proxies to your private applications | Özel uygulamalarınız için güvenli proxy’ler oluşturun | |
|
||||
| Search resources | Kaynakları ara | placeholder |
|
||||
| Name | Ad | |
|
||||
| Site | Site | |
|
||||
| Full URL | Tam URL | |
|
||||
| Authentication | Kimlik Doğrulama | |
|
||||
| Not Protected | Korunmayan | authentication state |
|
||||
| Protected | Korunan | authentication state |
|
||||
| Edit → | Düzenle → | |
|
||||
| Add Resource | Kaynak Ekle | |
|
||||
|
||||
##### Add Resource Popup
|
||||
|
||||
| EN | TR | Notes |
|
||||
| ------------------------------------------------------------ | ----------------------------------------------------------------------------------------------- | ------------- |
|
||||
| Create Resource | Kaynak Oluştur | |
|
||||
| Create a new resource to proxy request to your app | Uygulamanıza gelen istekleri yönlendirmek için yeni bir kaynak oluşturun | |
|
||||
| Name | Ad | |
|
||||
| My Resource | Kaynağım | name placeholder |
|
||||
| This is the name that will be displayed for this resource. | Bu, kaynağın görüntülenecek adıdır. | |
|
||||
| Subdomain | Alt alan adı | |
|
||||
| Enter subdomain | Alt alan adını girin | |
|
||||
| This is the fully qualified domain name that will be used to access the resource. | Kaynağa erişmek için kullanılacak tam nitelikli alan adıdır. | |
|
||||
| Site | Site | |
|
||||
| Search site… | Site ara… | Site selector popup |
|
||||
| This is the site that will be used in the dashboard. | Kontrol panelinde kullanılacak sitedir. | |
|
||||
| Create Resource | Kaynak Oluştur | |
|
||||
| Close | Kapat | |
|
||||
|
||||
---
|
||||
|
||||
## Main “User & Roles”
|
||||
|
||||
##### Content
|
||||
|
||||
| EN | TR | Notes |
|
||||
| ------------------------------------------------------------ | ------------------------------------------------------------------------------------------ | ----------------------------- |
|
||||
| Manage User & Roles | Kullanıcılar ve Rolleri Yönet | |
|
||||
| Invite users and add them to roles to manage access to your organization | Organizasyonunuza erişimi yönetmek için kullanıcıları davet edin ve rollere atayın | |
|
||||
| Users | Kullanıcılar | sidebar item |
|
||||
| Roles | Roller | sidebar item |
|
||||
| **User tab** | **Kullanıcı Sekmesi** | |
|
||||
| Search users | Kullanıcıları ara | placeholder |
|
||||
| Invite User | Kullanıcı Davet Et | addbutton |
|
||||
| Email | E-posta | table header |
|
||||
| Status | Durum | table header |
|
||||
| Role | Rol | table header |
|
||||
| Confirmed | Onaylandı | account status |
|
||||
| Not confirmed (?) | Onaylanmadı (?) | account status |
|
||||
| Owner | Sahip | role |
|
||||
| Admin | Yönetici | role |
|
||||
| Member | Üye | role |
|
||||
| **Roles Tab** | **Roller Sekmesi** | |
|
||||
| Search roles | Rolleri ara | placeholder |
|
||||
| Add Role | Rol Ekle | addbutton |
|
||||
| Name | Ad | table header |
|
||||
| Description | Açıklama | table header |
|
||||
| Admin | Yönetici | role |
|
||||
| Member | Üye | role |
|
||||
| Admin role with the most permissions | En fazla yetkiye sahip yönetici rolü | admin role desc |
|
||||
| Members can only view resources | Üyeler yalnızca kaynakları görüntüleyebilir | member role desc |
|
||||
|
||||
##### Invite User popup
|
||||
|
||||
| EN | TR | Notes |
|
||||
| ----------------- | ----------------------------------------------------------------------- | ----------- |
|
||||
| Invite User | Kullanıcı Davet Et | |
|
||||
| Email | E-posta | |
|
||||
| Enter an email | Bir e-posta adresi girin | placeholder |
|
||||
| Role | Rol | |
|
||||
| Select role | Rol seçin | placeholder |
|
||||
| Gültig für | Geçerlilik Süresi | |
|
||||
| 1 day | 1 gün | |
|
||||
| 2 days | 2 gün | |
|
||||
| 3 days | 3 gün | |
|
||||
| 4 days | 4 gün | |
|
||||
| 5 days | 5 gün | |
|
||||
| 6 days | 6 gün | |
|
||||
| 7 days | 7 gün | |
|
||||
| Create Invitation | Davetiye Oluştur | |
|
||||
| Close | Kapat | |
|
||||
|
||||
---
|
||||
|
||||
## Main “Shareable Links”
|
||||
|
||||
##### “Hero” section
|
||||
|
||||
| EN | TR | Notes |
|
||||
| ------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------- | ----- |
|
||||
| Shareable Links | Paylaşılabilir Bağlantılar | |
|
||||
| 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. | Kaynaklarınıza paylaşılabilir bağlantılar oluşturun. Bağlantılar, kaynağınıza geçici veya sınırsız erişim sağlar. Oluştururken bağlantının geçerlilik süresini ayarlayabilirsiniz. | |
|
||||
| Easy to create and share | Oluşturması ve paylaşması kolay | |
|
||||
| Configurable expiration duration | Yapılandırılabilir geçerlilik süresi | |
|
||||
| Secure and revocable | Güvenli ve iptal edilebilir | |
|
||||
|
||||
##### Content
|
||||
|
||||
| EN | TR | Notes |
|
||||
| ------------------------------------------------------------ | ---------------------------------------------------------------------------------------- | -------------- |
|
||||
| Manage Shareable Links | Paylaşılabilir Bağlantıları Yönet | |
|
||||
| Create shareable links to grant temporary or permanent access to your resources | Kaynaklarınıza geçici veya kalıcı erişim sağlamak için paylaşılabilir bağlantılar oluşturun | |
|
||||
| Search links | Bağlantıları ara | placeholder |
|
||||
| Create Share Link | Bağlantı Oluştur | addbutton |
|
||||
| Resource | Kaynak | table header |
|
||||
| Title | Başlık | table header |
|
||||
| Created | Oluşturulma Tarihi | table header |
|
||||
| Expires | Son Kullanma Tarihi | table header |
|
||||
| No links. Create one to get started. | Bağlantı yok. Başlamak için bir tane oluşturun. | table placeholder |
|
||||
|
||||
##### Create Shareable Link popup
|
||||
|
||||
| EN | TR | Notes |
|
||||
| ------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------- | ----------------------- |
|
||||
| Create Shareable Link | Paylaşılabilir Bağlantı Oluştur | |
|
||||
| Anyone with this link can access the resource | Bu bağlantıya sahip olan herkes kaynağa erişebilir | |
|
||||
| Resource | Kaynak | |
|
||||
| Select resource | Kaynak seçin | |
|
||||
| Search resources… | Kaynak ara… | resource selector popup |
|
||||
| Title (optional) | Başlık (isteğe bağlı) | |
|
||||
| Enter title | Başlık girin | placeholder |
|
||||
| Expire in | Sona Erme Süresi | |
|
||||
| Minutes | Dakika | |
|
||||
| Hours | Saat | |
|
||||
| Days | Gün | |
|
||||
| Months | Ay | |
|
||||
| Years | Yıl | |
|
||||
| Never expire | Asla sona erme | |
|
||||
| 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. | Bağlantının geçerlilik süresi, bağlantının ne kadar süreyle kullanılabilir olacağını ve kaynağa erişim sağlayacağını belirler. Bu sürenin sonunda bağlantı çalışmaz hale gelir ve bağlantıyı kullananlar kaynağa erişimini kaybeder. | |
|
||||
| Create Link | Bağlantı Oluştur | |
|
||||
| Close | Kapat | |
|
||||
|
||||
---
|
||||
|
||||
## Main “General”
|
||||
|
||||
| EN | TR | Notes |
|
||||
| ------------------------------------------------------------ | ------------------------------------------------------------------------------------------- | ------------ |
|
||||
| General | Genel | |
|
||||
| Configure your organization’s general settings | Organizasyonunuzun genel ayarlarını yapılandırın | |
|
||||
| General | Genel | sidebar item |
|
||||
| Organization Settings | Organizasyon Ayarları | |
|
||||
| Manage your organization details and configuration | Organizasyonunuzun detaylarını ve yapılandırmasını yönetin | |
|
||||
| Name | Ad | |
|
||||
| This is the display name of the org | Bu, organizasyonunuzun görüntülenecek adıdır. | |
|
||||
| Save Settings | Ayarları Kaydet | |
|
||||
| Danger Zone | Tehlikeli Bölge | |
|
||||
| Once you delete this org, there is no going back. Please be certain. | Bu organizasyonu sildikten sonra geri dönüş yoktur. Lütfen emin olun. | |
|
||||
| Delete Organization Data | Organizasyon Verilerini Sil | |
|
||||
1277
messages/cs-CZ.json
Normal file
1277
messages/de-DE.json
Normal file
1277
messages/en-US.json
Normal file
1277
messages/es-ES.json
Normal file
1277
messages/fr-FR.json
Normal file
1277
messages/it-IT.json
Normal file
1277
messages/ko-KR.json
Normal file
1277
messages/nl-NL.json
Normal file
1277
messages/pl-PL.json
Normal file
1277
messages/pt-PT.json
Normal file
1277
messages/ru-RU.json
Normal file
1277
messages/tr-TR.json
Normal file
1277
messages/zh-CN.json
Normal file
@@ -1,4 +1,8 @@
|
||||
/** @type {import('next').NextConfig} */
|
||||
import createNextIntlPlugin from "next-intl/plugin";
|
||||
|
||||
const withNextIntl = createNextIntlPlugin();
|
||||
|
||||
/** @type {import("next").NextConfig} */
|
||||
const nextConfig = {
|
||||
eslint: {
|
||||
ignoreDuringBuilds: true
|
||||
@@ -6,4 +10,4 @@ const nextConfig = {
|
||||
output: "standalone"
|
||||
};
|
||||
|
||||
export default nextConfig;
|
||||
export default withNextIntl(nextConfig);
|
||||
|
||||
30869
package-lock.json
generated
82
package.json
@@ -23,10 +23,11 @@
|
||||
"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"
|
||||
"email": "email dev --dir server/emails/templates --port 3005",
|
||||
"build:cli": "node esbuild.mjs -e cli/index.ts -o dist/cli.mjs"
|
||||
},
|
||||
"dependencies": {
|
||||
"@asteasolutions/zod-to-openapi": "^7.3.2",
|
||||
"@asteasolutions/zod-to-openapi": "^7.3.4",
|
||||
"@hookform/resolvers": "3.9.1",
|
||||
"@node-rs/argon2": "^2.0.2",
|
||||
"@oslojs/crypto": "1.0.1",
|
||||
@@ -48,16 +49,19 @@
|
||||
"@radix-ui/react-switch": "1.2.5",
|
||||
"@radix-ui/react-tabs": "1.1.12",
|
||||
"@radix-ui/react-toast": "1.2.14",
|
||||
"@react-email/components": "0.0.41",
|
||||
"@radix-ui/react-tooltip": "^1.2.7",
|
||||
"@react-email/components": "0.3.1",
|
||||
"@react-email/render": "^1.1.2",
|
||||
"@react-email/tailwind": "1.0.5",
|
||||
"@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.9.0",
|
||||
"axios": "1.10.0",
|
||||
"better-sqlite3": "11.7.0",
|
||||
"canvas-confetti": "1.9.3",
|
||||
"class-variance-authority": "0.7.1",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "2.1.1",
|
||||
"cmdk": "1.1.1",
|
||||
"cookie": "^1.0.2",
|
||||
@@ -65,12 +69,12 @@
|
||||
"cookies": "^0.9.1",
|
||||
"cors": "2.8.5",
|
||||
"crypto-js": "^4.2.0",
|
||||
"drizzle-orm": "0.38.3",
|
||||
"eslint": "9.28.0",
|
||||
"eslint-config-next": "15.3.3",
|
||||
"drizzle-orm": "0.44.2",
|
||||
"eslint": "9.31.0",
|
||||
"eslint-config-next": "15.3.5",
|
||||
"express": "4.21.2",
|
||||
"express-rate-limit": "7.5.0",
|
||||
"glob": "11.0.2",
|
||||
"express-rate-limit": "7.5.1",
|
||||
"glob": "11.0.3",
|
||||
"helmet": "8.1.0",
|
||||
"http-errors": "2.0.0",
|
||||
"i": "^0.3.7",
|
||||
@@ -78,65 +82,69 @@
|
||||
"jmespath": "^0.16.0",
|
||||
"js-yaml": "4.1.0",
|
||||
"jsonwebtoken": "^9.0.2",
|
||||
"lucide-react": "0.511.0",
|
||||
"lucide-react": "0.525.0",
|
||||
"moment": "2.30.1",
|
||||
"next": "15.3.3",
|
||||
"next": "15.3.5",
|
||||
"next-intl": "^4.3.4",
|
||||
"next-themes": "0.4.6",
|
||||
"node-cache": "5.1.2",
|
||||
"node-fetch": "3.3.2",
|
||||
"nodemailer": "6.9.16",
|
||||
"npm": "^11.4.1",
|
||||
"nodemailer": "7.0.5",
|
||||
"npm": "^11.4.2",
|
||||
"oslo": "1.2.1",
|
||||
"pg": "^8.16.0",
|
||||
"pg": "^8.16.2",
|
||||
"qrcode.react": "4.2.0",
|
||||
"react": "19.1.0",
|
||||
"react-dom": "19.1.0",
|
||||
"react-easy-sort": "^1.6.0",
|
||||
"react-hook-form": "7.56.4",
|
||||
"react-hook-form": "7.60.0",
|
||||
"react-icons": "^5.5.0",
|
||||
"rebuild": "0.1.2",
|
||||
"semver": "7.7.2",
|
||||
"semver": "^7.7.2",
|
||||
"swagger-ui-express": "^5.0.1",
|
||||
"tailwind-merge": "2.6.0",
|
||||
"tw-animate-css": "^1.3.3",
|
||||
"tailwind-merge": "3.3.1",
|
||||
"tw-animate-css": "^1.3.5",
|
||||
"uuid": "^11.1.0",
|
||||
"vaul": "1.1.2",
|
||||
"winston": "3.17.0",
|
||||
"winston-daily-rotate-file": "5.0.0",
|
||||
"ws": "8.18.2",
|
||||
"zod": "3.25.46",
|
||||
"zod-validation-error": "3.4.1"
|
||||
"ws": "8.18.3",
|
||||
"zod": "3.25.76",
|
||||
"zod-validation-error": "3.5.2",
|
||||
"yargs": "18.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@dotenvx/dotenvx": "1.44.1",
|
||||
"@dotenvx/dotenvx": "1.47.6",
|
||||
"@esbuild-plugins/tsconfig-paths": "0.1.2",
|
||||
"@tailwindcss/postcss": "^4.1.8",
|
||||
"@tailwindcss/postcss": "^4.1.10",
|
||||
"@types/better-sqlite3": "7.6.12",
|
||||
"@types/cookie-parser": "1.4.8",
|
||||
"@types/cors": "2.8.18",
|
||||
"@types/cookie-parser": "1.4.9",
|
||||
"@types/cors": "2.8.19",
|
||||
"@types/crypto-js": "^4.2.2",
|
||||
"@types/express": "5.0.0",
|
||||
"@types/express-session": "^1.18.2",
|
||||
"@types/jmespath": "^0.15.2",
|
||||
"@types/js-yaml": "4.0.9",
|
||||
"@types/jsonwebtoken": "^9.0.9",
|
||||
"@types/node": "^22",
|
||||
"@types/jsonwebtoken": "^9.0.10",
|
||||
"@types/node": "^24",
|
||||
"@types/nodemailer": "6.4.17",
|
||||
"@types/react": "19.1.6",
|
||||
"@types/react-dom": "19.1.5",
|
||||
"@types/semver": "7.7.0",
|
||||
"@types/pg": "8.15.4",
|
||||
"@types/react": "19.1.8",
|
||||
"@types/react-dom": "19.1.6",
|
||||
"@types/semver": "^7.7.0",
|
||||
"@types/swagger-ui-express": "^4.1.8",
|
||||
"@types/ws": "8.18.1",
|
||||
"@types/yargs": "17.0.33",
|
||||
"drizzle-kit": "0.31.1",
|
||||
"esbuild": "0.25.5",
|
||||
"drizzle-kit": "0.31.4",
|
||||
"esbuild": "0.25.6",
|
||||
"esbuild-node-externals": "1.18.0",
|
||||
"postcss": "^8",
|
||||
"react-email": "4.0.15",
|
||||
"react-email": "4.1.0",
|
||||
"tailwindcss": "^4.1.4",
|
||||
"tsc-alias": "1.8.16",
|
||||
"tsx": "4.19.4",
|
||||
"tsx": "4.20.3",
|
||||
"typescript": "^5",
|
||||
"yargs": "18.0.0"
|
||||
"typescript-eslint": "^8.36.0"
|
||||
},
|
||||
"overrides": {
|
||||
"emblor": {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/** @type {import('postcss-load-config').Config} */
|
||||
const config = {
|
||||
plugins: {
|
||||
'@tailwindcss/postcss': {},
|
||||
"@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 |
|
Before Width: | Height: | Size: 574 KiB |
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 |
|
Before Width: | Height: | Size: 434 KiB 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 |
BIN
public/screenshots/resources.png
Normal file
|
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 |
BIN
public/screenshots/sites.png
Normal file
|
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 {
|
||||
errorHandlerMiddleware,
|
||||
notFoundMiddleware,
|
||||
rateLimitMiddleware
|
||||
notFoundMiddleware
|
||||
} from "@server/middlewares";
|
||||
import { authenticated, unauthenticated } from "@server/routers/external";
|
||||
import { router as wsRouter, handleWSUpgrade } from "@server/routers/ws";
|
||||
import { logIncomingMiddleware } from "./middlewares/logIncoming";
|
||||
import { csrfProtectionMiddleware } from "./middlewares/csrfProtection";
|
||||
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 externalPort = config.getRawConfig().server.external_port;
|
||||
|
||||
export function createApiServer() {
|
||||
const apiServer = express();
|
||||
const prefix = `/api/v1`;
|
||||
|
||||
if (config.getRawConfig().server.trust_proxy) {
|
||||
apiServer.set("trust proxy", 1);
|
||||
const trustProxy = config.getRawConfig().server.trust_proxy;
|
||||
if (trustProxy) {
|
||||
apiServer.set("trust proxy", trustProxy);
|
||||
}
|
||||
|
||||
const corsConfig = config.getRawConfig().server.cors;
|
||||
@@ -53,19 +59,30 @@ export function createApiServer() {
|
||||
apiServer.use(cookieParser());
|
||||
apiServer.use(express.json());
|
||||
|
||||
// Add request timeout middleware
|
||||
apiServer.use(requestTimeoutMiddleware(60000)); // 60 second timeout
|
||||
|
||||
if (!dev) {
|
||||
apiServer.use(
|
||||
rateLimitMiddleware({
|
||||
windowMin:
|
||||
config.getRawConfig().rate_limits.global.window_minutes,
|
||||
rateLimit({
|
||||
windowMs:
|
||||
config.getRawConfig().rate_limits.global.window_minutes *
|
||||
60 *
|
||||
1000,
|
||||
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
|
||||
const prefix = `/api/v1`;
|
||||
apiServer.use(logIncomingMiddleware);
|
||||
apiServer.use(prefix, unauthenticated);
|
||||
apiServer.use(prefix, authenticated);
|
||||
|
||||
@@ -56,6 +56,8 @@ export enum ActionsEnum {
|
||||
// removeUserAction = "removeUserAction",
|
||||
// removeUserSite = "removeUserSite",
|
||||
getOrgUser = "getOrgUser",
|
||||
updateUser = "updateUser",
|
||||
getUser = "getUser",
|
||||
setResourcePassword = "setResourcePassword",
|
||||
setResourcePincode = "setResourcePincode",
|
||||
setResourceWhitelist = "setResourceWhitelist",
|
||||
@@ -67,6 +69,11 @@ export enum ActionsEnum {
|
||||
deleteResourceRule = "deleteResourceRule",
|
||||
listResourceRules = "listResourceRules",
|
||||
updateResourceRule = "updateResourceRule",
|
||||
createClient = "createClient",
|
||||
deleteClient = "deleteClient",
|
||||
updateClient = "updateClient",
|
||||
listClients = "listClients",
|
||||
getClient = "getClient",
|
||||
listOrgDomains = "listOrgDomains",
|
||||
createNewt = "createNewt",
|
||||
createIdp = "createIdp",
|
||||
@@ -85,7 +92,10 @@ export enum ActionsEnum {
|
||||
setApiKeyOrgs = "setApiKeyOrgs",
|
||||
listApiKeyActions = "listApiKeyActions",
|
||||
listApiKeys = "listApiKeys",
|
||||
getApiKey = "getApiKey"
|
||||
getApiKey = "getApiKey",
|
||||
createOrgDomain = "createOrgDomain",
|
||||
deleteOrgDomain = "deleteOrgDomain",
|
||||
restartOrgDomain = "restartOrgDomain"
|
||||
}
|
||||
|
||||
export async function checkUserActionPermission(
|
||||
|
||||
@@ -1,40 +0,0 @@
|
||||
import { db } from '@server/db';
|
||||
import { limitsTable } from '@server/db';
|
||||
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');
|
||||
}
|
||||
}
|
||||
72
server/auth/sessions/olm.ts
Normal file
@@ -0,0 +1,72 @@
|
||||
import {
|
||||
encodeHexLowerCase,
|
||||
} from "@oslojs/encoding";
|
||||
import { sha256 } from "@oslojs/crypto/sha2";
|
||||
import { Olm, olms, olmSessions, OlmSession } from "@server/db";
|
||||
import { db } from "@server/db";
|
||||
import { eq } from "drizzle-orm";
|
||||
|
||||
export const EXPIRES = 1000 * 60 * 60 * 24 * 30;
|
||||
|
||||
export async function createOlmSession(
|
||||
token: string,
|
||||
olmId: string,
|
||||
): Promise<OlmSession> {
|
||||
const sessionId = encodeHexLowerCase(
|
||||
sha256(new TextEncoder().encode(token)),
|
||||
);
|
||||
const session: OlmSession = {
|
||||
sessionId: sessionId,
|
||||
olmId,
|
||||
expiresAt: new Date(Date.now() + EXPIRES).getTime(),
|
||||
};
|
||||
await db.insert(olmSessions).values(session);
|
||||
return session;
|
||||
}
|
||||
|
||||
export async function validateOlmSessionToken(
|
||||
token: string,
|
||||
): Promise<SessionValidationResult> {
|
||||
const sessionId = encodeHexLowerCase(
|
||||
sha256(new TextEncoder().encode(token)),
|
||||
);
|
||||
const result = await db
|
||||
.select({ olm: olms, session: olmSessions })
|
||||
.from(olmSessions)
|
||||
.innerJoin(olms, eq(olmSessions.olmId, olms.olmId))
|
||||
.where(eq(olmSessions.sessionId, sessionId));
|
||||
if (result.length < 1) {
|
||||
return { session: null, olm: null };
|
||||
}
|
||||
const { olm, session } = result[0];
|
||||
if (Date.now() >= session.expiresAt) {
|
||||
await db
|
||||
.delete(olmSessions)
|
||||
.where(eq(olmSessions.sessionId, session.sessionId));
|
||||
return { session: null, olm: null };
|
||||
}
|
||||
if (Date.now() >= session.expiresAt - (EXPIRES / 2)) {
|
||||
session.expiresAt = new Date(
|
||||
Date.now() + EXPIRES,
|
||||
).getTime();
|
||||
await db
|
||||
.update(olmSessions)
|
||||
.set({
|
||||
expiresAt: session.expiresAt,
|
||||
})
|
||||
.where(eq(olmSessions.sessionId, session.sessionId));
|
||||
}
|
||||
return { session, olm };
|
||||
}
|
||||
|
||||
export async function invalidateOlmSession(sessionId: string): Promise<void> {
|
||||
await db.delete(olmSessions).where(eq(olmSessions.sessionId, sessionId));
|
||||
}
|
||||
|
||||
export async function invalidateAllOlmSessions(olmId: string): Promise<void> {
|
||||
await db.delete(olmSessions).where(eq(olmSessions.olmId, olmId));
|
||||
}
|
||||
|
||||
export type SessionValidationResult =
|
||||
| { session: OlmSession; olm: Olm }
|
||||
| { session: null; olm: null };
|
||||
1
server/build.ts
Normal file
@@ -0,0 +1 @@
|
||||
export const build = "oss" as any;
|
||||
@@ -1,2 +0,0 @@
|
||||
export * from "./sqlite";
|
||||
// export * from "./pg";
|
||||
@@ -59,7 +59,7 @@ export async function getUniqueExitNodeEndpointName(): Promise<string> {
|
||||
|
||||
|
||||
export function generateName(): string {
|
||||
return (
|
||||
const name = (
|
||||
names.descriptors[
|
||||
Math.floor(Math.random() * names.descriptors.length)
|
||||
] +
|
||||
@@ -68,4 +68,7 @@ export function generateName(): string {
|
||||
)
|
||||
.toLowerCase()
|
||||
.replace(/\s/g, "-");
|
||||
|
||||
// clean out any non-alphanumeric characters except for dashes
|
||||
return name.replace(/[^a-z0-9-]/g, "");
|
||||
}
|
||||
|
||||
@@ -1,16 +1,51 @@
|
||||
import { drizzle as DrizzlePostgres } from "drizzle-orm/node-postgres";
|
||||
import { Pool } from "pg";
|
||||
import { readConfigFile } from "@server/lib/readConfigFile";
|
||||
import { withReplicas } from "drizzle-orm/pg-core";
|
||||
|
||||
function createDb() {
|
||||
const config = readConfigFile();
|
||||
|
||||
const connectionString = config.postgres?.connection_string;
|
||||
|
||||
if (!connectionString) {
|
||||
throw new Error("Postgres connection string is not defined in the configuration file.");
|
||||
if (!config.postgres) {
|
||||
throw new Error(
|
||||
"Postgres configuration is missing in the configuration file."
|
||||
);
|
||||
}
|
||||
|
||||
return DrizzlePostgres(connectionString);
|
||||
const connectionString = config.postgres?.connection_string;
|
||||
const replicaConnections = config.postgres?.replicas || [];
|
||||
|
||||
if (!connectionString) {
|
||||
throw new Error(
|
||||
"A primary db connection string is required in the configuration file."
|
||||
);
|
||||
}
|
||||
|
||||
// Create connection pools instead of individual connections
|
||||
const primaryPool = new Pool({
|
||||
connectionString,
|
||||
max: 20,
|
||||
idleTimeoutMillis: 30000,
|
||||
connectionTimeoutMillis: 2000,
|
||||
});
|
||||
|
||||
const replicas = [];
|
||||
|
||||
if (!replicaConnections.length) {
|
||||
replicas.push(DrizzlePostgres(primaryPool));
|
||||
} else {
|
||||
for (const conn of replicaConnections) {
|
||||
const replicaPool = new Pool({
|
||||
connectionString: conn.connection_string,
|
||||
max: 10,
|
||||
idleTimeoutMillis: 30000,
|
||||
connectionTimeoutMillis: 2000,
|
||||
});
|
||||
replicas.push(DrizzlePostgres(replicaPool));
|
||||
}
|
||||
}
|
||||
|
||||
return withReplicas(DrizzlePostgres(primaryPool), replicas as any);
|
||||
}
|
||||
|
||||
export const db = createDb();
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
export * from "./driver";
|
||||
export * from "./schema";
|
||||
export * from "./schema";
|
||||
@@ -1,5 +1,5 @@
|
||||
import { migrate } from "drizzle-orm/node-postgres/migrator";
|
||||
import db from "./driver";
|
||||
import { db } from "./driver";
|
||||
import path from "path";
|
||||
|
||||
const migrationsFolder = path.join("server/migrations");
|
||||
|
||||
@@ -12,12 +12,17 @@ import { InferSelectModel } from "drizzle-orm";
|
||||
export const domains = pgTable("domains", {
|
||||
domainId: varchar("domainId").primaryKey(),
|
||||
baseDomain: varchar("baseDomain").notNull(),
|
||||
configManaged: boolean("configManaged").notNull().default(false)
|
||||
configManaged: boolean("configManaged").notNull().default(false),
|
||||
type: varchar("type"), // "ns", "cname", "wildcard"
|
||||
verified: boolean("verified").notNull().default(false),
|
||||
failed: boolean("failed").notNull().default(false),
|
||||
tries: integer("tries").notNull().default(0)
|
||||
});
|
||||
|
||||
export const orgs = pgTable("orgs", {
|
||||
orgId: varchar("orgId").primaryKey(),
|
||||
name: varchar("name").notNull()
|
||||
name: varchar("name").notNull(),
|
||||
subnet: varchar("subnet")
|
||||
});
|
||||
|
||||
export const orgDomains = pgTable("orgDomains", {
|
||||
@@ -42,12 +47,17 @@ export const sites = pgTable("sites", {
|
||||
}),
|
||||
name: varchar("name").notNull(),
|
||||
pubKey: varchar("pubKey"),
|
||||
subnet: varchar("subnet").notNull(),
|
||||
megabytesIn: real("bytesIn"),
|
||||
megabytesOut: real("bytesOut"),
|
||||
subnet: varchar("subnet"),
|
||||
megabytesIn: real("bytesIn").default(0),
|
||||
megabytesOut: real("bytesOut").default(0),
|
||||
lastBandwidthUpdate: varchar("lastBandwidthUpdate"),
|
||||
type: varchar("type").notNull(), // "newt" or "wireguard"
|
||||
online: boolean("online").notNull().default(false),
|
||||
address: varchar("address"),
|
||||
endpoint: varchar("endpoint"),
|
||||
publicKey: varchar("publicKey"),
|
||||
lastHolePunch: bigint("lastHolePunch", { mode: "number" }),
|
||||
listenPort: integer("listenPort"),
|
||||
dockerSocketEnabled: boolean("dockerSocketEnabled").notNull().default(true)
|
||||
});
|
||||
|
||||
@@ -78,7 +88,6 @@ export const resources = pgTable("resources", {
|
||||
emailWhitelistEnabled: boolean("emailWhitelistEnabled")
|
||||
.notNull()
|
||||
.default(false),
|
||||
isBaseDomain: boolean("isBaseDomain"),
|
||||
applyRules: boolean("applyRules").notNull().default(false),
|
||||
enabled: boolean("enabled").notNull().default(true),
|
||||
stickySession: boolean("stickySession").notNull().default(false),
|
||||
@@ -107,7 +116,8 @@ export const exitNodes = pgTable("exitNodes", {
|
||||
endpoint: varchar("endpoint").notNull(),
|
||||
publicKey: varchar("publicKey").notNull(),
|
||||
listenPort: integer("listenPort").notNull(),
|
||||
reachableAt: varchar("reachableAt")
|
||||
reachableAt: varchar("reachableAt"),
|
||||
maxConnections: integer("maxConnections")
|
||||
});
|
||||
|
||||
export const users = pgTable("user", {
|
||||
@@ -121,6 +131,7 @@ export const users = pgTable("user", {
|
||||
}),
|
||||
passwordHash: varchar("passwordHash"),
|
||||
twoFactorEnabled: boolean("twoFactorEnabled").notNull().default(false),
|
||||
twoFactorSetupRequested: boolean("twoFactorSetupRequested").default(false),
|
||||
twoFactorSecret: varchar("twoFactorSecret"),
|
||||
emailVerified: boolean("emailVerified").notNull().default(false),
|
||||
dateCreated: varchar("dateCreated").notNull(),
|
||||
@@ -131,6 +142,7 @@ export const newts = pgTable("newt", {
|
||||
newtId: varchar("id").primaryKey(),
|
||||
secretHash: varchar("secretHash").notNull(),
|
||||
dateCreated: varchar("dateCreated").notNull(),
|
||||
version: varchar("version"),
|
||||
siteId: integer("siteId").references(() => sites.siteId, {
|
||||
onDelete: "cascade"
|
||||
})
|
||||
@@ -273,18 +285,6 @@ export const userResources = pgTable("userResources", {
|
||||
.references(() => resources.resourceId, { onDelete: "cascade" })
|
||||
});
|
||||
|
||||
export const limitsTable = pgTable("limits", {
|
||||
limitId: serial("limitId").primaryKey(),
|
||||
orgId: varchar("orgId")
|
||||
.references(() => orgs.orgId, {
|
||||
onDelete: "cascade"
|
||||
})
|
||||
.notNull(),
|
||||
name: varchar("name").notNull(),
|
||||
value: bigint("value", { mode: "number" }).notNull(),
|
||||
description: varchar("description")
|
||||
});
|
||||
|
||||
export const userInvites = pgTable("userInvites", {
|
||||
inviteId: varchar("inviteId").primaryKey(),
|
||||
orgId: varchar("orgId")
|
||||
@@ -491,6 +491,99 @@ export const idpOrg = pgTable("idpOrg", {
|
||||
orgMapping: varchar("orgMapping")
|
||||
});
|
||||
|
||||
export const clients = pgTable("clients", {
|
||||
clientId: serial("id").primaryKey(),
|
||||
orgId: varchar("orgId")
|
||||
.references(() => orgs.orgId, {
|
||||
onDelete: "cascade"
|
||||
})
|
||||
.notNull(),
|
||||
exitNodeId: integer("exitNode").references(() => exitNodes.exitNodeId, {
|
||||
onDelete: "set null"
|
||||
}),
|
||||
name: varchar("name").notNull(),
|
||||
pubKey: varchar("pubKey"),
|
||||
subnet: varchar("subnet").notNull(),
|
||||
megabytesIn: integer("bytesIn"),
|
||||
megabytesOut: integer("bytesOut"),
|
||||
lastBandwidthUpdate: varchar("lastBandwidthUpdate"),
|
||||
lastPing: varchar("lastPing"),
|
||||
type: varchar("type").notNull(), // "olm"
|
||||
online: boolean("online").notNull().default(false),
|
||||
endpoint: varchar("endpoint"),
|
||||
lastHolePunch: integer("lastHolePunch"),
|
||||
maxConnections: integer("maxConnections")
|
||||
});
|
||||
|
||||
export const clientSites = pgTable("clientSites", {
|
||||
clientId: integer("clientId")
|
||||
.notNull()
|
||||
.references(() => clients.clientId, { onDelete: "cascade" }),
|
||||
siteId: integer("siteId")
|
||||
.notNull()
|
||||
.references(() => sites.siteId, { onDelete: "cascade" }),
|
||||
isRelayed: boolean("isRelayed").notNull().default(false)
|
||||
});
|
||||
|
||||
export const olms = pgTable("olms", {
|
||||
olmId: varchar("id").primaryKey(),
|
||||
secretHash: varchar("secretHash").notNull(),
|
||||
dateCreated: varchar("dateCreated").notNull(),
|
||||
clientId: integer("clientId").references(() => clients.clientId, {
|
||||
onDelete: "cascade"
|
||||
})
|
||||
});
|
||||
|
||||
export const olmSessions = pgTable("clientSession", {
|
||||
sessionId: varchar("id").primaryKey(),
|
||||
olmId: varchar("olmId")
|
||||
.notNull()
|
||||
.references(() => olms.olmId, { onDelete: "cascade" }),
|
||||
expiresAt: integer("expiresAt").notNull()
|
||||
});
|
||||
|
||||
export const userClients = pgTable("userClients", {
|
||||
userId: varchar("userId")
|
||||
.notNull()
|
||||
.references(() => users.userId, { onDelete: "cascade" }),
|
||||
clientId: integer("clientId")
|
||||
.notNull()
|
||||
.references(() => clients.clientId, { onDelete: "cascade" })
|
||||
});
|
||||
|
||||
export const roleClients = pgTable("roleClients", {
|
||||
roleId: integer("roleId")
|
||||
.notNull()
|
||||
.references(() => roles.roleId, { onDelete: "cascade" }),
|
||||
clientId: integer("clientId")
|
||||
.notNull()
|
||||
.references(() => clients.clientId, { onDelete: "cascade" })
|
||||
});
|
||||
|
||||
export const securityKeys = pgTable("webauthnCredentials", {
|
||||
credentialId: varchar("credentialId").primaryKey(),
|
||||
userId: varchar("userId").notNull().references(() => users.userId, {
|
||||
onDelete: "cascade"
|
||||
}),
|
||||
publicKey: varchar("publicKey").notNull(),
|
||||
signCount: integer("signCount").notNull(),
|
||||
transports: varchar("transports"),
|
||||
name: varchar("name"),
|
||||
lastUsed: varchar("lastUsed").notNull(),
|
||||
dateCreated: varchar("dateCreated").notNull(),
|
||||
securityKeyName: varchar("securityKeyName")
|
||||
});
|
||||
|
||||
export const webauthnChallenge = pgTable("webauthnChallenge", {
|
||||
sessionId: varchar("sessionId").primaryKey(),
|
||||
challenge: varchar("challenge").notNull(),
|
||||
securityKeyName: varchar("securityKeyName"),
|
||||
userId: varchar("userId").references(() => users.userId, {
|
||||
onDelete: "cascade"
|
||||
}),
|
||||
expiresAt: bigint("expiresAt", { mode: "number" }).notNull() // Unix timestamp
|
||||
});
|
||||
|
||||
export type Org = InferSelectModel<typeof orgs>;
|
||||
export type User = InferSelectModel<typeof users>;
|
||||
export type Site = InferSelectModel<typeof sites>;
|
||||
@@ -513,7 +606,6 @@ export type RoleSite = InferSelectModel<typeof roleSites>;
|
||||
export type UserSite = InferSelectModel<typeof userSites>;
|
||||
export type RoleResource = InferSelectModel<typeof roleResources>;
|
||||
export type UserResource = InferSelectModel<typeof userResources>;
|
||||
export type Limit = InferSelectModel<typeof limitsTable>;
|
||||
export type UserInvite = InferSelectModel<typeof userInvites>;
|
||||
export type UserOrg = InferSelectModel<typeof userOrgs>;
|
||||
export type ResourceSession = InferSelectModel<typeof resourceSessions>;
|
||||
@@ -530,3 +622,10 @@ export type Idp = InferSelectModel<typeof idp>;
|
||||
export type ApiKey = InferSelectModel<typeof apiKeys>;
|
||||
export type ApiKeyAction = InferSelectModel<typeof apiKeyActions>;
|
||||
export type ApiKeyOrg = InferSelectModel<typeof apiKeyOrg>;
|
||||
export type Client = InferSelectModel<typeof clients>;
|
||||
export type ClientSite = InferSelectModel<typeof clientSites>;
|
||||
export type Olm = InferSelectModel<typeof olms>;
|
||||
export type OlmSession = InferSelectModel<typeof olmSessions>;
|
||||
export type UserClient = InferSelectModel<typeof userClients>;
|
||||
export type RoleClient = InferSelectModel<typeof roleClients>;
|
||||
export type OrgDomains = InferSelectModel<typeof orgDomains>;
|
||||
|
||||
@@ -2,19 +2,16 @@ import { drizzle as DrizzleSqlite } from "drizzle-orm/better-sqlite3";
|
||||
import Database from "better-sqlite3";
|
||||
import * as schema from "./schema";
|
||||
import path from "path";
|
||||
import fs from "fs/promises";
|
||||
import fs from "fs";
|
||||
import { APP_PATH } from "@server/lib/consts";
|
||||
import { existsSync, mkdirSync } from "fs";
|
||||
import { readConfigFile } from "@server/lib/readConfigFile";
|
||||
|
||||
export const location = path.join(APP_PATH, "db", "db.sqlite");
|
||||
export const exists = await checkFileExists(location);
|
||||
export const exists = checkFileExists(location);
|
||||
|
||||
bootstrapVolume();
|
||||
|
||||
function createDb() {
|
||||
const config = readConfigFile();
|
||||
|
||||
const sqlite = new Database(location);
|
||||
return DrizzleSqlite(sqlite, { schema });
|
||||
}
|
||||
@@ -22,9 +19,9 @@ function createDb() {
|
||||
export const db = createDb();
|
||||
export default db;
|
||||
|
||||
async function checkFileExists(filePath: string): Promise<boolean> {
|
||||
function checkFileExists(filePath: string): boolean {
|
||||
try {
|
||||
await fs.access(filePath);
|
||||
fs.accessSync(filePath);
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { migrate } from "drizzle-orm/better-sqlite3/migrator";
|
||||
import db from "./driver";
|
||||
import { db } from "./driver";
|
||||
import path from "path";
|
||||
|
||||
const migrationsFolder = path.join("server/migrations");
|
||||
|
||||
@@ -6,12 +6,26 @@ export const domains = sqliteTable("domains", {
|
||||
baseDomain: text("baseDomain").notNull(),
|
||||
configManaged: integer("configManaged", { mode: "boolean" })
|
||||
.notNull()
|
||||
.default(false)
|
||||
.default(false),
|
||||
type: text("type"), // "ns", "cname", "wildcard"
|
||||
verified: integer("verified", { mode: "boolean" }).notNull().default(false),
|
||||
failed: integer("failed", { mode: "boolean" }).notNull().default(false),
|
||||
tries: integer("tries").notNull().default(0)
|
||||
});
|
||||
|
||||
export const orgs = sqliteTable("orgs", {
|
||||
orgId: text("orgId").primaryKey(),
|
||||
name: text("name").notNull()
|
||||
name: text("name").notNull(),
|
||||
subnet: text("subnet")
|
||||
});
|
||||
|
||||
export const userDomains = sqliteTable("userDomains", {
|
||||
userId: text("userId")
|
||||
.notNull()
|
||||
.references(() => users.userId, { onDelete: "cascade" }),
|
||||
domainId: text("domainId")
|
||||
.notNull()
|
||||
.references(() => domains.domainId, { onDelete: "cascade" })
|
||||
});
|
||||
|
||||
export const orgDomains = sqliteTable("orgDomains", {
|
||||
@@ -36,12 +50,19 @@ export const sites = sqliteTable("sites", {
|
||||
}),
|
||||
name: text("name").notNull(),
|
||||
pubKey: text("pubKey"),
|
||||
subnet: text("subnet").notNull(),
|
||||
megabytesIn: integer("bytesIn"),
|
||||
megabytesOut: integer("bytesOut"),
|
||||
subnet: text("subnet"),
|
||||
megabytesIn: integer("bytesIn").default(0),
|
||||
megabytesOut: integer("bytesOut").default(0),
|
||||
lastBandwidthUpdate: text("lastBandwidthUpdate"),
|
||||
type: text("type").notNull(), // "newt" or "wireguard"
|
||||
online: integer("online", { mode: "boolean" }).notNull().default(false),
|
||||
|
||||
// exit node stuff that is how to connect to the site when it has a wg server
|
||||
address: text("address"), // this is the address of the wireguard interface in newt
|
||||
endpoint: text("endpoint"), // this is how to reach gerbil externally - gets put into the wireguard config
|
||||
publicKey: text("publicKey"), // TODO: Fix typo in publicKey
|
||||
lastHolePunch: integer("lastHolePunch"),
|
||||
listenPort: integer("listenPort"),
|
||||
dockerSocketEnabled: integer("dockerSocketEnabled", { mode: "boolean" })
|
||||
.notNull()
|
||||
.default(true)
|
||||
@@ -76,7 +97,6 @@ export const resources = sqliteTable("resources", {
|
||||
emailWhitelistEnabled: integer("emailWhitelistEnabled", { mode: "boolean" })
|
||||
.notNull()
|
||||
.default(false),
|
||||
isBaseDomain: integer("isBaseDomain", { mode: "boolean" }),
|
||||
applyRules: integer("applyRules", { mode: "boolean" })
|
||||
.notNull()
|
||||
.default(false),
|
||||
@@ -109,7 +129,8 @@ export const exitNodes = sqliteTable("exitNodes", {
|
||||
endpoint: text("endpoint").notNull(), // this is how to reach gerbil externally - gets put into the wireguard config
|
||||
publicKey: text("publicKey").notNull(),
|
||||
listenPort: integer("listenPort").notNull(),
|
||||
reachableAt: text("reachableAt") // this is the internal address of the gerbil http server for command control
|
||||
reachableAt: text("reachableAt"), // this is the internal address of the gerbil http server for command control
|
||||
maxConnections: integer("maxConnections")
|
||||
});
|
||||
|
||||
export const users = sqliteTable("user", {
|
||||
@@ -125,6 +146,9 @@ export const users = sqliteTable("user", {
|
||||
twoFactorEnabled: integer("twoFactorEnabled", { mode: "boolean" })
|
||||
.notNull()
|
||||
.default(false),
|
||||
twoFactorSetupRequested: integer("twoFactorSetupRequested", {
|
||||
mode: "boolean"
|
||||
}).default(false),
|
||||
twoFactorSecret: text("twoFactorSecret"),
|
||||
emailVerified: integer("emailVerified", { mode: "boolean" })
|
||||
.notNull()
|
||||
@@ -135,15 +159,81 @@ export const users = sqliteTable("user", {
|
||||
.default(false)
|
||||
});
|
||||
|
||||
export const securityKeys = sqliteTable("webauthnCredentials", {
|
||||
credentialId: text("credentialId").primaryKey(),
|
||||
userId: text("userId").notNull().references(() => users.userId, {
|
||||
onDelete: "cascade"
|
||||
}),
|
||||
publicKey: text("publicKey").notNull(),
|
||||
signCount: integer("signCount").notNull(),
|
||||
transports: text("transports"),
|
||||
name: text("name"),
|
||||
lastUsed: text("lastUsed").notNull(),
|
||||
dateCreated: text("dateCreated").notNull()
|
||||
});
|
||||
|
||||
export const webauthnChallenge = sqliteTable("webauthnChallenge", {
|
||||
sessionId: text("sessionId").primaryKey(),
|
||||
challenge: text("challenge").notNull(),
|
||||
securityKeyName: text("securityKeyName"),
|
||||
userId: text("userId").references(() => users.userId, {
|
||||
onDelete: "cascade"
|
||||
}),
|
||||
expiresAt: integer("expiresAt").notNull() // Unix timestamp
|
||||
});
|
||||
|
||||
export const newts = sqliteTable("newt", {
|
||||
newtId: text("id").primaryKey(),
|
||||
secretHash: text("secretHash").notNull(),
|
||||
dateCreated: text("dateCreated").notNull(),
|
||||
version: text("version"),
|
||||
siteId: integer("siteId").references(() => sites.siteId, {
|
||||
onDelete: "cascade"
|
||||
})
|
||||
});
|
||||
|
||||
export const clients = sqliteTable("clients", {
|
||||
clientId: integer("id").primaryKey({ autoIncrement: true }),
|
||||
orgId: text("orgId")
|
||||
.references(() => orgs.orgId, {
|
||||
onDelete: "cascade"
|
||||
})
|
||||
.notNull(),
|
||||
exitNodeId: integer("exitNode").references(() => exitNodes.exitNodeId, {
|
||||
onDelete: "set null"
|
||||
}),
|
||||
name: text("name").notNull(),
|
||||
pubKey: text("pubKey"),
|
||||
subnet: text("subnet").notNull(),
|
||||
megabytesIn: integer("bytesIn"),
|
||||
megabytesOut: integer("bytesOut"),
|
||||
lastBandwidthUpdate: text("lastBandwidthUpdate"),
|
||||
lastPing: text("lastPing"),
|
||||
type: text("type").notNull(), // "olm"
|
||||
online: integer("online", { mode: "boolean" }).notNull().default(false),
|
||||
endpoint: text("endpoint"),
|
||||
lastHolePunch: integer("lastHolePunch")
|
||||
});
|
||||
|
||||
export const clientSites = sqliteTable("clientSites", {
|
||||
clientId: integer("clientId")
|
||||
.notNull()
|
||||
.references(() => clients.clientId, { onDelete: "cascade" }),
|
||||
siteId: integer("siteId")
|
||||
.notNull()
|
||||
.references(() => sites.siteId, { onDelete: "cascade" }),
|
||||
isRelayed: integer("isRelayed", { mode: "boolean" }).notNull().default(false)
|
||||
});
|
||||
|
||||
export const olms = sqliteTable("olms", {
|
||||
olmId: text("id").primaryKey(),
|
||||
secretHash: text("secretHash").notNull(),
|
||||
dateCreated: text("dateCreated").notNull(),
|
||||
clientId: integer("clientId").references(() => clients.clientId, {
|
||||
onDelete: "cascade"
|
||||
})
|
||||
});
|
||||
|
||||
export const twoFactorBackupCodes = sqliteTable("twoFactorBackupCodes", {
|
||||
codeId: integer("id").primaryKey({ autoIncrement: true }),
|
||||
userId: text("userId")
|
||||
@@ -168,6 +258,14 @@ export const newtSessions = sqliteTable("newtSession", {
|
||||
expiresAt: integer("expiresAt").notNull()
|
||||
});
|
||||
|
||||
export const olmSessions = sqliteTable("clientSession", {
|
||||
sessionId: text("id").primaryKey(),
|
||||
olmId: text("olmId")
|
||||
.notNull()
|
||||
.references(() => olms.olmId, { onDelete: "cascade" }),
|
||||
expiresAt: integer("expiresAt").notNull()
|
||||
});
|
||||
|
||||
export const userOrgs = sqliteTable("userOrgs", {
|
||||
userId: text("userId")
|
||||
.notNull()
|
||||
@@ -263,6 +361,24 @@ export const userSites = sqliteTable("userSites", {
|
||||
.references(() => sites.siteId, { onDelete: "cascade" })
|
||||
});
|
||||
|
||||
export const userClients = sqliteTable("userClients", {
|
||||
userId: text("userId")
|
||||
.notNull()
|
||||
.references(() => users.userId, { onDelete: "cascade" }),
|
||||
clientId: integer("clientId")
|
||||
.notNull()
|
||||
.references(() => clients.clientId, { onDelete: "cascade" })
|
||||
});
|
||||
|
||||
export const roleClients = sqliteTable("roleClients", {
|
||||
roleId: integer("roleId")
|
||||
.notNull()
|
||||
.references(() => roles.roleId, { onDelete: "cascade" }),
|
||||
clientId: integer("clientId")
|
||||
.notNull()
|
||||
.references(() => clients.clientId, { onDelete: "cascade" })
|
||||
});
|
||||
|
||||
export const roleResources = sqliteTable("roleResources", {
|
||||
roleId: integer("roleId")
|
||||
.notNull()
|
||||
@@ -521,6 +637,8 @@ export type Target = InferSelectModel<typeof targets>;
|
||||
export type Session = InferSelectModel<typeof sessions>;
|
||||
export type Newt = InferSelectModel<typeof newts>;
|
||||
export type NewtSession = InferSelectModel<typeof newtSessions>;
|
||||
export type Olm = InferSelectModel<typeof olms>;
|
||||
export type OlmSession = InferSelectModel<typeof olmSessions>;
|
||||
export type EmailVerificationCode = InferSelectModel<
|
||||
typeof emailVerificationCodes
|
||||
>;
|
||||
@@ -546,8 +664,13 @@ export type ResourceWhitelist = InferSelectModel<typeof resourceWhitelist>;
|
||||
export type VersionMigration = InferSelectModel<typeof versionMigrations>;
|
||||
export type ResourceRule = InferSelectModel<typeof resourceRules>;
|
||||
export type Domain = InferSelectModel<typeof domains>;
|
||||
export type Client = InferSelectModel<typeof clients>;
|
||||
export type ClientSite = InferSelectModel<typeof clientSites>;
|
||||
export type RoleClient = InferSelectModel<typeof roleClients>;
|
||||
export type UserClient = InferSelectModel<typeof userClients>;
|
||||
export type SupporterKey = InferSelectModel<typeof supporterKey>;
|
||||
export type Idp = InferSelectModel<typeof idp>;
|
||||
export type ApiKey = InferSelectModel<typeof apiKeys>;
|
||||
export type ApiKeyAction = InferSelectModel<typeof apiKeyActions>;
|
||||
export type ApiKeyOrg = InferSelectModel<typeof apiKeyOrg>;
|
||||
export type OrgDomains = InferSelectModel<typeof orgDomains>;
|
||||
|
||||
@@ -2,6 +2,7 @@ import { render } from "@react-email/render";
|
||||
import { ReactElement } from "react";
|
||||
import emailClient from "@server/emails";
|
||||
import logger from "@server/logger";
|
||||
import config from "@server/lib/config";
|
||||
|
||||
export async function sendEmail(
|
||||
template: ReactElement,
|
||||
@@ -24,9 +25,11 @@ export async function sendEmail(
|
||||
|
||||
const emailHtml = await render(template);
|
||||
|
||||
const appName = "Pangolin";
|
||||
|
||||
await emailClient.sendMail({
|
||||
from: {
|
||||
name: opts.name || "Pangolin",
|
||||
name: opts.name || appName,
|
||||
address: opts.from,
|
||||
},
|
||||
to: opts.to,
|
||||
|
||||
@@ -1,11 +1,5 @@
|
||||
import {
|
||||
Body,
|
||||
Head,
|
||||
Html,
|
||||
Preview,
|
||||
Tailwind
|
||||
} from "@react-email/components";
|
||||
import * as React from "react";
|
||||
import React from "react";
|
||||
import { Body, Head, Html, Preview, Tailwind } from "@react-email/components";
|
||||
import { themeColors } from "./lib/theme";
|
||||
import {
|
||||
EmailContainer,
|
||||
@@ -22,29 +16,29 @@ interface Props {
|
||||
}
|
||||
|
||||
export const ConfirmPasswordReset = ({ email }: Props) => {
|
||||
const previewText = `Your password has been reset`;
|
||||
const previewText = `Your password has been successfully reset.`;
|
||||
|
||||
return (
|
||||
<Html>
|
||||
<Head />
|
||||
<Preview>{previewText}</Preview>
|
||||
<Tailwind config={themeColors}>
|
||||
<Body className="font-sans relative">
|
||||
<Body className="font-sans bg-gray-50">
|
||||
<EmailContainer>
|
||||
<EmailLetterHead />
|
||||
|
||||
<EmailHeading>Password Reset Confirmation</EmailHeading>
|
||||
{/* <EmailHeading>Password Successfully Reset</EmailHeading> */}
|
||||
|
||||
<EmailGreeting>Hi {email || "there"},</EmailGreeting>
|
||||
<EmailGreeting>Hi there,</EmailGreeting>
|
||||
|
||||
<EmailText>
|
||||
This email confirms that your password has just been
|
||||
reset. If you made this change, no further action is
|
||||
required.
|
||||
Your password has been successfully reset. You can
|
||||
now sign in to your account using your new password.
|
||||
</EmailText>
|
||||
|
||||
<EmailText>
|
||||
Thank you for keeping your account secure.
|
||||
If you didn't make this change, please contact our
|
||||
support team immediately to secure your account.
|
||||
</EmailText>
|
||||
|
||||
<EmailFooter>
|
||||
|
||||
@@ -1,11 +1,5 @@
|
||||
import {
|
||||
Body,
|
||||
Head,
|
||||
Html,
|
||||
Preview,
|
||||
Tailwind
|
||||
} from "@react-email/components";
|
||||
import * as React from "react";
|
||||
import React from "react";
|
||||
import { Body, Head, Html, Preview, Tailwind } from "@react-email/components";
|
||||
import { themeColors } from "./lib/theme";
|
||||
import {
|
||||
EmailContainer,
|
||||
@@ -18,6 +12,7 @@ import {
|
||||
EmailText
|
||||
} from "./components/Email";
|
||||
import CopyCodeBox from "./components/CopyCodeBox";
|
||||
import ButtonLink from "./components/ButtonLink";
|
||||
|
||||
interface Props {
|
||||
email: string;
|
||||
@@ -26,37 +21,39 @@ interface Props {
|
||||
}
|
||||
|
||||
export const ResetPasswordCode = ({ email, code, link }: Props) => {
|
||||
const previewText = `Your password reset code is ${code}`;
|
||||
const previewText = `Reset your password with code: ${code}`;
|
||||
|
||||
return (
|
||||
<Html>
|
||||
<Head />
|
||||
<Preview>{previewText}</Preview>
|
||||
<Tailwind config={themeColors}>
|
||||
<Body className="font-sans">
|
||||
<Body className="font-sans bg-gray-50">
|
||||
<EmailContainer>
|
||||
<EmailLetterHead />
|
||||
|
||||
<EmailHeading>Password Reset Request</EmailHeading>
|
||||
{/* <EmailHeading>Reset Your Password</EmailHeading> */}
|
||||
|
||||
<EmailGreeting>Hi {email || "there"},</EmailGreeting>
|
||||
<EmailGreeting>Hi there,</EmailGreeting>
|
||||
|
||||
<EmailText>
|
||||
You’ve requested to reset your password. Please{" "}
|
||||
<a href={link} className="text-primary">
|
||||
click here
|
||||
</a>{" "}
|
||||
and follow the instructions to reset your password,
|
||||
or manually enter the following code:
|
||||
You've requested to reset your password. Click the
|
||||
button below to reset your password, or use the
|
||||
verification code provided if prompted.
|
||||
</EmailText>
|
||||
|
||||
<EmailSection>
|
||||
<ButtonLink href={link}>Reset Password</ButtonLink>
|
||||
</EmailSection>
|
||||
|
||||
<EmailSection>
|
||||
<CopyCodeBox text={code} />
|
||||
</EmailSection>
|
||||
|
||||
<EmailText>
|
||||
If you didn’t request this, you can safely ignore
|
||||
this email.
|
||||
This reset code will expire in 2 hours. If you
|
||||
didn't request a password reset, you can safely
|
||||
ignore this email.
|
||||
</EmailText>
|
||||
|
||||
<EmailFooter>
|
||||
|
||||
@@ -1,11 +1,5 @@
|
||||
import {
|
||||
Body,
|
||||
Head,
|
||||
Html,
|
||||
Preview,
|
||||
Tailwind
|
||||
} from "@react-email/components";
|
||||
import * as React from "react";
|
||||
import React from "react";
|
||||
import { Body, Head, Html, Preview, Tailwind } from "@react-email/components";
|
||||
import {
|
||||
EmailContainer,
|
||||
EmailLetterHead,
|
||||
@@ -32,34 +26,40 @@ export const ResourceOTPCode = ({
|
||||
orgName: organizationName,
|
||||
otp
|
||||
}: ResourceOTPCodeProps) => {
|
||||
const previewText = `Your one-time password for ${resourceName} is ${otp}`;
|
||||
const previewText = `Your access code for ${resourceName}: ${otp}`;
|
||||
|
||||
return (
|
||||
<Html>
|
||||
<Head />
|
||||
<Preview>{previewText}</Preview>
|
||||
<Tailwind config={themeColors}>
|
||||
<Body className="font-sans">
|
||||
<Body className="font-sans bg-gray-50">
|
||||
<EmailContainer>
|
||||
<EmailLetterHead />
|
||||
|
||||
<EmailHeading>
|
||||
Your One-Time Code for {resourceName}
|
||||
</EmailHeading>
|
||||
{/* <EmailHeading> */}
|
||||
{/* Access Code for {resourceName} */}
|
||||
{/* </EmailHeading> */}
|
||||
|
||||
<EmailGreeting>Hi {email || "there"},</EmailGreeting>
|
||||
<EmailGreeting>Hi there,</EmailGreeting>
|
||||
|
||||
<EmailText>
|
||||
You’ve requested a one-time password to access{" "}
|
||||
You've requested access to{" "}
|
||||
<strong>{resourceName}</strong> in{" "}
|
||||
<strong>{organizationName}</strong>. Use the code
|
||||
below to complete your authentication:
|
||||
<strong>{organizationName}</strong>. Use the
|
||||
verification code below to complete your
|
||||
authentication.
|
||||
</EmailText>
|
||||
|
||||
<EmailSection>
|
||||
<CopyCodeBox text={otp} />
|
||||
</EmailSection>
|
||||
|
||||
<EmailText>
|
||||
This code will expire in 15 minutes. If you didn't
|
||||
request this code, please ignore this email.
|
||||
</EmailText>
|
||||
|
||||
<EmailFooter>
|
||||
<EmailSignature />
|
||||
</EmailFooter>
|
||||
|
||||
@@ -1,11 +1,5 @@
|
||||
import {
|
||||
Body,
|
||||
Head,
|
||||
Html,
|
||||
Preview,
|
||||
Tailwind,
|
||||
} from "@react-email/components";
|
||||
import * as React from "react";
|
||||
import React from "react";
|
||||
import { Body, Head, Html, Preview, Tailwind } from "@react-email/components";
|
||||
import { themeColors } from "./lib/theme";
|
||||
import {
|
||||
EmailContainer,
|
||||
@@ -41,35 +35,44 @@ export const SendInviteLink = ({
|
||||
<Head />
|
||||
<Preview>{previewText}</Preview>
|
||||
<Tailwind config={themeColors}>
|
||||
<Body className="font-sans">
|
||||
<Body className="font-sans bg-gray-50">
|
||||
<EmailContainer>
|
||||
<EmailLetterHead />
|
||||
|
||||
<EmailHeading>Invited to Join {orgName}</EmailHeading>
|
||||
{/* <EmailHeading> */}
|
||||
{/* You're Invited to Join {orgName} */}
|
||||
{/* </EmailHeading> */}
|
||||
|
||||
<EmailGreeting>Hi {email || "there"},</EmailGreeting>
|
||||
<EmailGreeting>Hi there,</EmailGreeting>
|
||||
|
||||
<EmailText>
|
||||
You’ve been invited to join the organization{" "}
|
||||
You've been invited to join{" "}
|
||||
<strong>{orgName}</strong>
|
||||
{inviterName ? ` by ${inviterName}.` : "."} Please
|
||||
access the link below to accept the invite.
|
||||
</EmailText>
|
||||
|
||||
<EmailText>
|
||||
This invite will expire in{" "}
|
||||
<strong>
|
||||
{expiresInDays}{" "}
|
||||
{expiresInDays === "1" ? "day" : "days"}.
|
||||
</strong>
|
||||
{inviterName ? ` by ${inviterName}` : ""}. Click the
|
||||
button below to accept your invitation and get
|
||||
started.
|
||||
</EmailText>
|
||||
|
||||
<EmailSection>
|
||||
<ButtonLink href={inviteLink}>
|
||||
Accept Invite to {orgName}
|
||||
Accept Invitation
|
||||
</ButtonLink>
|
||||
</EmailSection>
|
||||
|
||||
{/* <EmailText> */}
|
||||
{/* If you're having trouble clicking the button, copy */}
|
||||
{/* and paste the URL below into your web browser: */}
|
||||
{/* <br /> */}
|
||||
{/* <span className="break-all">{inviteLink}</span> */}
|
||||
{/* </EmailText> */}
|
||||
|
||||
<EmailText>
|
||||
This invite expires in {expiresInDays}{" "}
|
||||
{expiresInDays === "1" ? "day" : "days"}. If the
|
||||
link has expired, please contact the owner of the
|
||||
organization to request a new invitation.
|
||||
</EmailText>
|
||||
|
||||
<EmailFooter>
|
||||
<EmailSignature />
|
||||
</EmailFooter>
|
||||
|
||||
@@ -1,11 +1,5 @@
|
||||
import {
|
||||
Body,
|
||||
Head,
|
||||
Html,
|
||||
Preview,
|
||||
Tailwind
|
||||
} from "@react-email/components";
|
||||
import * as React from "react";
|
||||
import React from "react";
|
||||
import { Body, Head, Html, Preview, Tailwind } from "@react-email/components";
|
||||
import { themeColors } from "./lib/theme";
|
||||
import {
|
||||
EmailContainer,
|
||||
@@ -23,44 +17,52 @@ interface Props {
|
||||
}
|
||||
|
||||
export const TwoFactorAuthNotification = ({ email, enabled }: Props) => {
|
||||
const previewText = `Two-Factor Authentication has been ${enabled ? "enabled" : "disabled"}`;
|
||||
const previewText = `Two-Factor Authentication ${enabled ? "enabled" : "disabled"} for your account`;
|
||||
|
||||
return (
|
||||
<Html>
|
||||
<Head />
|
||||
<Preview>{previewText}</Preview>
|
||||
<Tailwind config={themeColors}>
|
||||
<Body className="font-sans">
|
||||
<Body className="font-sans bg-gray-50">
|
||||
<EmailContainer>
|
||||
<EmailLetterHead />
|
||||
|
||||
<EmailHeading>
|
||||
Two-Factor Authentication{" "}
|
||||
{enabled ? "Enabled" : "Disabled"}
|
||||
</EmailHeading>
|
||||
{/* <EmailHeading> */}
|
||||
{/* Security Update: 2FA{" "} */}
|
||||
{/* {enabled ? "Enabled" : "Disabled"} */}
|
||||
{/* </EmailHeading> */}
|
||||
|
||||
<EmailGreeting>Hi {email || "there"},</EmailGreeting>
|
||||
<EmailGreeting>Hi there,</EmailGreeting>
|
||||
|
||||
<EmailText>
|
||||
This email confirms that Two-Factor Authentication
|
||||
has been successfully{" "}
|
||||
{enabled ? "enabled" : "disabled"} on your account.
|
||||
Two-factor authentication has been successfully{" "}
|
||||
<strong>{enabled ? "enabled" : "disabled"}</strong>{" "}
|
||||
on your account.
|
||||
</EmailText>
|
||||
|
||||
{enabled ? (
|
||||
<EmailText>
|
||||
With Two-Factor Authentication enabled, your
|
||||
account is now more secure. Please ensure you
|
||||
keep your authentication method safe.
|
||||
</EmailText>
|
||||
<>
|
||||
<EmailText>
|
||||
Your account is now protected with an
|
||||
additional layer of security. Keep your
|
||||
authentication method safe and accessible.
|
||||
</EmailText>
|
||||
</>
|
||||
) : (
|
||||
<EmailText>
|
||||
With Two-Factor Authentication disabled, your
|
||||
account may be less secure. We recommend
|
||||
enabling it to protect your account.
|
||||
</EmailText>
|
||||
<>
|
||||
<EmailText>
|
||||
We recommend re-enabling two-factor
|
||||
authentication to keep your account secure.
|
||||
</EmailText>
|
||||
</>
|
||||
)}
|
||||
|
||||
<EmailText>
|
||||
If you didn't make this change, please contact our
|
||||
support team immediately.
|
||||
</EmailText>
|
||||
|
||||
<EmailFooter>
|
||||
<EmailSignature />
|
||||
</EmailFooter>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React from "react";
|
||||
import { Body, Head, Html, Preview, Tailwind } from "@react-email/components";
|
||||
import * as React from "react";
|
||||
import { themeColors } from "./lib/theme";
|
||||
import {
|
||||
EmailContainer,
|
||||
@@ -24,25 +24,24 @@ export const VerifyEmail = ({
|
||||
verificationCode,
|
||||
verifyLink
|
||||
}: VerifyEmailProps) => {
|
||||
const previewText = `Your verification code is ${verificationCode}`;
|
||||
const previewText = `Verify your email with code: ${verificationCode}`;
|
||||
|
||||
return (
|
||||
<Html>
|
||||
<Head />
|
||||
<Preview>{previewText}</Preview>
|
||||
<Tailwind config={themeColors}>
|
||||
<Body className="font-sans">
|
||||
<Body className="font-sans bg-gray-50">
|
||||
<EmailContainer>
|
||||
<EmailLetterHead />
|
||||
|
||||
<EmailHeading>Please Verify Your Email</EmailHeading>
|
||||
{/* <EmailHeading>Verify Your Email Address</EmailHeading> */}
|
||||
|
||||
<EmailGreeting>Hi {username || "there"},</EmailGreeting>
|
||||
<EmailGreeting>Hi there,</EmailGreeting>
|
||||
|
||||
<EmailText>
|
||||
You’ve requested to verify your email. Please use
|
||||
the code below to complete the verification process
|
||||
upon logging in.
|
||||
Welcome! To complete your account setup, please
|
||||
verify your email address using the code below.
|
||||
</EmailText>
|
||||
|
||||
<EmailSection>
|
||||
@@ -50,7 +49,8 @@ export const VerifyEmail = ({
|
||||
</EmailSection>
|
||||
|
||||
<EmailText>
|
||||
If you didn’t request this, you can safely ignore
|
||||
This verification code will expire in 15 minutes. If
|
||||
you didn't create an account, you can safely ignore
|
||||
this email.
|
||||
</EmailText>
|
||||
|
||||
|
||||
131
server/emails/templates/WelcomeQuickStart.tsx
Normal file
@@ -0,0 +1,131 @@
|
||||
import React from "react";
|
||||
import { Body, Head, Html, Preview, Tailwind } from "@react-email/components";
|
||||
import { themeColors } from "./lib/theme";
|
||||
import {
|
||||
EmailContainer,
|
||||
EmailFooter,
|
||||
EmailGreeting,
|
||||
EmailHeading,
|
||||
EmailLetterHead,
|
||||
EmailSection,
|
||||
EmailSignature,
|
||||
EmailText,
|
||||
EmailInfoSection
|
||||
} from "./components/Email";
|
||||
import ButtonLink from "./components/ButtonLink";
|
||||
import CopyCodeBox from "./components/CopyCodeBox";
|
||||
|
||||
interface WelcomeQuickStartProps {
|
||||
username?: string;
|
||||
link: string;
|
||||
fallbackLink: string;
|
||||
resourceMethod: string;
|
||||
resourceHostname: string;
|
||||
resourcePort: string | number;
|
||||
resourceUrl: string;
|
||||
cliCommand: string;
|
||||
}
|
||||
|
||||
export const WelcomeQuickStart = ({
|
||||
username,
|
||||
link,
|
||||
fallbackLink,
|
||||
resourceMethod,
|
||||
resourceHostname,
|
||||
resourcePort,
|
||||
resourceUrl,
|
||||
cliCommand
|
||||
}: WelcomeQuickStartProps) => {
|
||||
const previewText = "Welcome! Here's what to do next";
|
||||
|
||||
return (
|
||||
<Html>
|
||||
<Head />
|
||||
<Preview>{previewText}</Preview>
|
||||
<Tailwind config={themeColors}>
|
||||
<Body className="font-sans bg-gray-50">
|
||||
<EmailContainer>
|
||||
<EmailLetterHead />
|
||||
|
||||
<EmailGreeting>Hi there,</EmailGreeting>
|
||||
|
||||
<EmailText>
|
||||
Thank you for trying out Pangolin! We're excited to
|
||||
have you on board.
|
||||
</EmailText>
|
||||
|
||||
<EmailText>
|
||||
To continue to configure your site, resources, and
|
||||
other features, complete your account setup to
|
||||
access the full dashboard.
|
||||
</EmailText>
|
||||
|
||||
<EmailSection>
|
||||
<ButtonLink href={link}>
|
||||
View Your Dashboard
|
||||
</ButtonLink>
|
||||
{/* <p className="text-sm text-gray-300 mt-2"> */}
|
||||
{/* If the button above doesn't work, you can also */}
|
||||
{/* use this{" "} */}
|
||||
{/* <a href={fallbackLink} className="underline"> */}
|
||||
{/* link */}
|
||||
{/* </a> */}
|
||||
{/* . */}
|
||||
{/* </p> */}
|
||||
</EmailSection>
|
||||
|
||||
<EmailSection>
|
||||
<div className="mb-2 font-semibold text-gray-900 text-base text-left">
|
||||
Connect your site using Newt
|
||||
</div>
|
||||
<div className="inline-block w-full">
|
||||
<div className="bg-gray-50 border border-gray-200 rounded-lg px-6 py-4 mx-auto text-left">
|
||||
<span className="text-sm font-mono text-gray-900 tracking-wider">
|
||||
{cliCommand}
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-xs text-gray-500 mt-2">
|
||||
To learn how to use Newt, including more
|
||||
installation methods, visit the{" "}
|
||||
<a
|
||||
href="https://docs.fossorial.io"
|
||||
className="underline"
|
||||
>
|
||||
docs
|
||||
</a>
|
||||
.
|
||||
</p>
|
||||
</div>
|
||||
</EmailSection>
|
||||
|
||||
<EmailInfoSection
|
||||
title="Your Demo Resource"
|
||||
items={[
|
||||
{ label: "Method", value: resourceMethod },
|
||||
{ label: "Hostname", value: resourceHostname },
|
||||
{ label: "Port", value: resourcePort },
|
||||
{
|
||||
label: "Resource URL",
|
||||
value: (
|
||||
<a
|
||||
href={resourceUrl}
|
||||
className="underline text-blue-600"
|
||||
>
|
||||
{resourceUrl}
|
||||
</a>
|
||||
)
|
||||
}
|
||||
]}
|
||||
/>
|
||||
|
||||
<EmailFooter>
|
||||
<EmailSignature />
|
||||
</EmailFooter>
|
||||
</EmailContainer>
|
||||
</Body>
|
||||
</Tailwind>
|
||||
</Html>
|
||||
);
|
||||
};
|
||||
|
||||
export default WelcomeQuickStart;
|
||||
@@ -12,7 +12,11 @@ export default function ButtonLink({
|
||||
return (
|
||||
<a
|
||||
href={href}
|
||||
className={`rounded-full bg-primary px-4 py-2 text-center font-semibold text-white text-xl no-underline inline-block ${className}`}
|
||||
className={`inline-block bg-primary text-white font-semibold px-8 py-3 rounded-lg text-center no-underline ${className}`}
|
||||
style={{
|
||||
backgroundColor: "#F97316",
|
||||
textDecoration: "none"
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</a>
|
||||
|
||||
@@ -2,10 +2,15 @@ import React from "react";
|
||||
|
||||
export default function CopyCodeBox({ text }: { text: string }) {
|
||||
return (
|
||||
<div className="text-center rounded-lg bg-neutral-100 p-2">
|
||||
<span className="text-2xl font-mono text-neutral-600 tracking-wide">
|
||||
{text}
|
||||
</span>
|
||||
<div className="inline-block">
|
||||
<div className="bg-gray-50 border border-gray-200 rounded-lg px-6 py-4 mx-auto">
|
||||
<span className="text-2xl font-mono text-gray-900 tracking-wider font-semibold">
|
||||
{text}
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-xs text-gray-500 mt-2">
|
||||
Copy and paste this code when prompted
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,47 +1,27 @@
|
||||
import { Container } from "@react-email/components";
|
||||
import React from "react";
|
||||
import { Container, Img } from "@react-email/components";
|
||||
import { build } from "@server/build";
|
||||
|
||||
// EmailContainer: Wraps the entire email layout
|
||||
export function EmailContainer({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
<Container className="bg-white border border-solid border-gray-200 p-6 max-w-lg mx-auto my-8 rounded-lg">
|
||||
<Container className="bg-white border border-solid border-gray-200 max-w-lg mx-auto my-8 rounded-lg overflow-hidden shadow-sm">
|
||||
{children}
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
||||
// EmailLetterHead: For branding or logo at the top
|
||||
// EmailLetterHead: For branding with logo on dark background
|
||||
export function EmailLetterHead() {
|
||||
return (
|
||||
<div className="mb-4">
|
||||
<table
|
||||
role="presentation"
|
||||
width="100%"
|
||||
style={{
|
||||
marginBottom: "24px"
|
||||
}}
|
||||
>
|
||||
<tr>
|
||||
<td
|
||||
style={{
|
||||
fontSize: "14px",
|
||||
fontWeight: "bold",
|
||||
color: "#F97317"
|
||||
}}
|
||||
>
|
||||
Pangolin
|
||||
</td>
|
||||
<td
|
||||
style={{
|
||||
fontSize: "14px",
|
||||
textAlign: "right",
|
||||
color: "#6B7280"
|
||||
}}
|
||||
>
|
||||
{new Date().getFullYear()}
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<div className="px-6 pt-8 pb-2 text-center">
|
||||
<Img
|
||||
src="https://fossorial-public-assets.s3.us-east-1.amazonaws.com/word_mark_black.png"
|
||||
alt="Fossorial"
|
||||
width="120"
|
||||
height="auto"
|
||||
className="mx-auto"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -49,14 +29,22 @@ export function EmailLetterHead() {
|
||||
// EmailHeading: For the primary message or headline
|
||||
export function EmailHeading({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
<h1 className="text-2xl font-semibold text-gray-800 text-center">
|
||||
{children}
|
||||
</h1>
|
||||
<div className="px-6 pt-4 pb-1">
|
||||
<h1 className="text-2xl font-semibold text-gray-900 text-center leading-tight">
|
||||
{children}
|
||||
</h1>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function EmailGreeting({ children }: { children: React.ReactNode }) {
|
||||
return <p className="text-base text-gray-700 my-4">{children}</p>;
|
||||
return (
|
||||
<div className="px-6">
|
||||
<p className="text-base text-gray-700 leading-relaxed">
|
||||
{children}
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// EmailText: For general text content
|
||||
@@ -68,9 +56,13 @@ export function EmailText({
|
||||
className?: string;
|
||||
}) {
|
||||
return (
|
||||
<p className={`my-2 text-base text-gray-700 ${className}`}>
|
||||
{children}
|
||||
</p>
|
||||
<div className="px-6">
|
||||
<p
|
||||
className={`text-base text-gray-700 leading-relaxed ${className}`}
|
||||
>
|
||||
{children}
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -82,20 +74,74 @@ export function EmailSection({
|
||||
children: React.ReactNode;
|
||||
className?: string;
|
||||
}) {
|
||||
return <div className={`text-center my-6 ${className}`}>{children}</div>;
|
||||
return (
|
||||
<div className={`px-6 py-6 text-center ${className}`}>{children}</div>
|
||||
);
|
||||
}
|
||||
|
||||
// EmailFooter: For closing or signature
|
||||
export function EmailFooter({ children }: { children: React.ReactNode }) {
|
||||
return <div className="text-sm text-gray-500 mt-6">{children}</div>;
|
||||
return (
|
||||
<>
|
||||
{build === "saas" && (
|
||||
<div className="px-6 py-6 border-t border-gray-100 bg-gray-50">
|
||||
{children}
|
||||
<p className="text-xs text-gray-400 mt-4">
|
||||
For any questions or support, please contact us at:
|
||||
<br />
|
||||
support@fossorial.io
|
||||
</p>
|
||||
<p className="text-xs text-gray-300 text-center mt-4">
|
||||
© {new Date().getFullYear()} Fossorial, Inc. All
|
||||
rights reserved.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export function EmailSignature() {
|
||||
return (
|
||||
<p>
|
||||
Best regards,
|
||||
<br />
|
||||
Fossorial
|
||||
</p>
|
||||
<div className="text-sm text-gray-600">
|
||||
<p className="mb-2">
|
||||
Best regards,
|
||||
<br />
|
||||
<strong>The Fossorial Team</strong>
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// EmailInfoSection: For structured key-value info (like resource details)
|
||||
export function EmailInfoSection({
|
||||
title,
|
||||
items
|
||||
}: {
|
||||
title?: string;
|
||||
items: { label: string; value: React.ReactNode }[];
|
||||
}) {
|
||||
return (
|
||||
<div className="px-6 py-4">
|
||||
{title && (
|
||||
<div className="mb-2 font-semibold text-gray-900 text-base">
|
||||
{title}
|
||||
</div>
|
||||
)}
|
||||
<table className="w-full text-sm text-left">
|
||||
<tbody>
|
||||
{items.map((item, idx) => (
|
||||
<tr key={idx}>
|
||||
<td className="pr-4 py-1 text-gray-600 align-top whitespace-nowrap">
|
||||
{item.label}
|
||||
</td>
|
||||
<td className="py-1 text-gray-900 break-all">
|
||||
{item.value}
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import React from "react";
|
||||
|
||||
export const themeColors = {
|
||||
theme: {
|
||||
extend: {
|
||||
|
||||
@@ -9,6 +9,7 @@ import { createIntegrationApiServer } from "./integrationApiServer";
|
||||
import config from "@server/lib/config";
|
||||
|
||||
async function startServers() {
|
||||
await config.initServer();
|
||||
await runSetupFunctions();
|
||||
|
||||
// Start all servers
|
||||
@@ -35,7 +36,7 @@ declare global {
|
||||
interface Request {
|
||||
apiKey?: ApiKey;
|
||||
user?: User;
|
||||
session?: Session;
|
||||
session: Session;
|
||||
userOrg?: UserOrg;
|
||||
apiKeyOrg?: ApiKeyOrg;
|
||||
userOrgRoleId?: number;
|
||||
|
||||
@@ -20,8 +20,9 @@ const externalPort = config.getRawConfig().server.integration_port;
|
||||
export function createIntegrationApiServer() {
|
||||
const apiServer = express();
|
||||
|
||||
if (config.getRawConfig().server.trust_proxy) {
|
||||
apiServer.set("trust proxy", 1);
|
||||
const trustProxy = config.getRawConfig().server.trust_proxy;
|
||||
if (trustProxy) {
|
||||
apiServer.set("trust proxy", trustProxy);
|
||||
}
|
||||
|
||||
apiServer.use(cors());
|
||||
|
||||
@@ -5,6 +5,7 @@ import { SupporterKey, supporterKey } from "@server/db";
|
||||
import { eq } from "drizzle-orm";
|
||||
import { license } from "@server/license/license";
|
||||
import { configSchema, readConfigFile } from "./readConfigFile";
|
||||
import { fromError } from "zod-validation-error";
|
||||
|
||||
export class Config {
|
||||
private rawConfig!: z.infer<typeof configSchema>;
|
||||
@@ -16,11 +17,35 @@ export class Config {
|
||||
isDev: boolean = process.env.ENVIRONMENT !== "prod";
|
||||
|
||||
constructor() {
|
||||
this.load();
|
||||
}
|
||||
const environment = readConfigFile();
|
||||
|
||||
public load() {
|
||||
const parsedConfig = readConfigFile();
|
||||
const {
|
||||
data: parsedConfig,
|
||||
success,
|
||||
error
|
||||
} = configSchema.safeParse(environment);
|
||||
|
||||
if (!success) {
|
||||
const errors = fromError(error);
|
||||
throw new Error(`Invalid configuration file: ${errors}`);
|
||||
}
|
||||
|
||||
if (process.env.APP_BASE_DOMAIN) {
|
||||
console.log(
|
||||
"WARNING: You're using deprecated environment variables. Transition to the configuration file. https://docs.fossorial.io/"
|
||||
);
|
||||
}
|
||||
|
||||
if (
|
||||
// @ts-ignore
|
||||
parsedConfig.users ||
|
||||
process.env.USERS_SERVERADMIN_EMAIL ||
|
||||
process.env.USERS_SERVERADMIN_PASSWORD
|
||||
) {
|
||||
console.log(
|
||||
"WARNING: Your admin credentials are still in the config file or environment variables. This method of setting admin credentials is no longer supported. It is recommended to remove them."
|
||||
);
|
||||
}
|
||||
|
||||
process.env.APP_VERSION = APP_VERSION;
|
||||
|
||||
@@ -56,22 +81,37 @@ export class Config {
|
||||
parsedConfig.server.resource_access_token_headers.token;
|
||||
process.env.RESOURCE_SESSION_REQUEST_PARAM =
|
||||
parsedConfig.server.resource_session_request_param;
|
||||
process.env.FLAGS_ALLOW_BASE_DOMAIN_RESOURCES = parsedConfig.flags
|
||||
?.allow_base_domain_resources
|
||||
process.env.DASHBOARD_URL = parsedConfig.app.dashboard_url;
|
||||
process.env.FLAGS_DISABLE_LOCAL_SITES = parsedConfig.flags
|
||||
?.disable_local_sites
|
||||
? "true"
|
||||
: "false";
|
||||
process.env.FLAGS_DISABLE_BASIC_WIREGUARD_SITES = parsedConfig.flags
|
||||
?.disable_basic_wireguard_sites
|
||||
? "true"
|
||||
: "false";
|
||||
process.env.DASHBOARD_URL = parsedConfig.app.dashboard_url;
|
||||
|
||||
license.setServerSecret(parsedConfig.server.secret);
|
||||
|
||||
this.checkKeyStatus();
|
||||
process.env.FLAGS_ENABLE_CLIENTS = parsedConfig.flags?.enable_clients
|
||||
? "true"
|
||||
: "false";
|
||||
|
||||
this.rawConfig = parsedConfig;
|
||||
}
|
||||
|
||||
public async initServer() {
|
||||
if (!this.rawConfig) {
|
||||
throw new Error("Config not loaded. Call load() first.");
|
||||
}
|
||||
license.setServerSecret(this.rawConfig.server.secret);
|
||||
|
||||
await this.checkKeyStatus();
|
||||
}
|
||||
|
||||
private async checkKeyStatus() {
|
||||
const licenseStatus = await license.check();
|
||||
if (!licenseStatus.isHostLicensed) {
|
||||
if (
|
||||
!licenseStatus.isHostLicensed
|
||||
) {
|
||||
this.checkSupporterKey();
|
||||
}
|
||||
}
|
||||
@@ -87,6 +127,9 @@ export class Config {
|
||||
}
|
||||
|
||||
public getDomain(domainId: string) {
|
||||
if (!this.rawConfig.domains || !this.rawConfig.domains[domainId]) {
|
||||
return null;
|
||||
}
|
||||
return this.rawConfig.domains[domainId];
|
||||
}
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ import path from "path";
|
||||
import { fileURLToPath } from "url";
|
||||
|
||||
// This is a placeholder value replaced by the build process
|
||||
export const APP_VERSION = "1.5.0";
|
||||
export const APP_VERSION = "1.7.0";
|
||||
|
||||
export const __FILENAME = fileURLToPath(import.meta.url);
|
||||
export const __DIRNAME = path.dirname(__FILENAME);
|
||||
|
||||
@@ -4,7 +4,14 @@ import { assertEquals } from "@test/assert";
|
||||
// Test cases
|
||||
function testFindNextAvailableCidr() {
|
||||
console.log("Running findNextAvailableCidr tests...");
|
||||
|
||||
|
||||
// Test 0: Basic IPv4 allocation with a subnet in the wrong range
|
||||
{
|
||||
const existing = ["100.90.130.1/30", "100.90.128.4/30"];
|
||||
const result = findNextAvailableCidr(existing, 30, "100.90.130.1/24");
|
||||
assertEquals(result, "100.90.130.4/30", "Basic IPv4 allocation failed");
|
||||
}
|
||||
|
||||
// Test 1: Basic IPv4 allocation
|
||||
{
|
||||
const existing = ["10.0.0.0/16", "10.1.0.0/16"];
|
||||
@@ -26,6 +33,12 @@ function testFindNextAvailableCidr() {
|
||||
assertEquals(result, null, "No available space test failed");
|
||||
}
|
||||
|
||||
// Test 4: Empty existing
|
||||
{
|
||||
const existing: string[] = [];
|
||||
const result = findNextAvailableCidr(existing, 30, "10.0.0.0/8");
|
||||
assertEquals(result, "10.0.0.0/30", "Empty existing test failed");
|
||||
}
|
||||
// // Test 4: IPv6 allocation
|
||||
// {
|
||||
// const existing = ["2001:db8::/32", "2001:db8:1::/32"];
|
||||
|
||||