mirror of
https://github.com/fosrl/pangolin.git
synced 2026-02-19 03:16:40 +00:00
Compare commits
1511 Commits
1.9.1
...
clients-us
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a3b852ef45 | ||
|
|
53bb4efbb2 | ||
|
|
96dbec9352 | ||
|
|
2d3fbb9704 | ||
|
|
d3be1fbf4c | ||
|
|
89ee57cdf9 | ||
|
|
bdfc7fbcdb | ||
|
|
8726a7f931 | ||
|
|
1cae815be5 | ||
|
|
c5befee134 | ||
|
|
9cf2dbc2cc | ||
|
|
35f9c67cfe | ||
|
|
6707b3c7fe | ||
|
|
dfb85f2c89 | ||
|
|
17dec6cf0b | ||
|
|
8ee4ee7baf | ||
|
|
b1b0702886 | ||
|
|
92aed108cd | ||
|
|
2dcc94cd14 | ||
|
|
a7185ff913 | ||
|
|
04e73515b8 | ||
|
|
2bad9daaea | ||
|
|
54670e150d | ||
|
|
761ed1de9a | ||
|
|
078692c818 | ||
|
|
53ab51691a | ||
|
|
54e2d95b55 | ||
|
|
6e6fa77625 | ||
|
|
5c0c12cabe | ||
|
|
b3ed7c0129 | ||
|
|
10a00ff225 | ||
|
|
ba09479827 | ||
|
|
1c5c36fc12 | ||
|
|
d37ff6e15b | ||
|
|
9288575341 | ||
|
|
0ceed4c812 | ||
|
|
4b61a38501 | ||
|
|
ca9273c9ea | ||
|
|
810704e190 | ||
|
|
f33be1434b | ||
|
|
82a9f2b24f | ||
|
|
7204b5f0de | ||
|
|
9b372780bd | ||
|
|
9065385b87 | ||
|
|
77306e8c97 | ||
|
|
a746ef36a8 | ||
|
|
6e565f1331 | ||
|
|
84c608c2cf | ||
|
|
6da7f58ced | ||
|
|
351097b04d | ||
|
|
bd3d339905 | ||
|
|
c6ad36d78e | ||
|
|
eaeb65e9b4 | ||
|
|
4176bdbc81 | ||
|
|
a2cdd8484c | ||
|
|
23ab76ae08 | ||
|
|
8eec122114 | ||
|
|
79ccbc8e92 | ||
|
|
d70da2aa70 | ||
|
|
c695f50122 | ||
|
|
1b09e5b9f9 | ||
|
|
7efc947e26 | ||
|
|
4b580105cd | ||
|
|
a61c82570a | ||
|
|
6734003d85 | ||
|
|
e49d796b06 | ||
|
|
4ab4029625 | ||
|
|
5afff3c662 | ||
|
|
9be5a01173 | ||
|
|
357f297a3e | ||
|
|
e1edbe6067 | ||
|
|
5a859aad29 | ||
|
|
a28b15a81d | ||
|
|
e62186f395 | ||
|
|
11c1efc19c | ||
|
|
8b0491eb52 | ||
|
|
0032634004 | ||
|
|
4af10c8108 | ||
|
|
56cb685813 | ||
|
|
ccfe1f7d0a | ||
|
|
bf987d867c | ||
|
|
3870ced635 | ||
|
|
cb3861a5c8 | ||
|
|
f5bfddd262 | ||
|
|
f060063f53 | ||
|
|
6eb6b44f41 | ||
|
|
c93ab34021 | ||
|
|
06a31bb716 | ||
|
|
152fb47ca4 | ||
|
|
3d400b2321 | ||
|
|
45a82f3ecc | ||
|
|
342bedc012 | ||
|
|
18db4a11c8 | ||
|
|
a7e32d4013 | ||
|
|
beea28daf3 | ||
|
|
b5e94d44ae | ||
|
|
a623604e96 | ||
|
|
8c62dfa706 | ||
|
|
610e46f2d5 | ||
|
|
92125611e9 | ||
|
|
096da391e5 | ||
|
|
dd6b1d88d3 | ||
|
|
79f0d60533 | ||
|
|
67665864c2 | ||
|
|
c4de617751 | ||
|
|
19e3c5045e | ||
|
|
9f63d8bb5b | ||
|
|
49348c6ab7 | ||
|
|
0961ac1da1 | ||
|
|
6a79436516 | ||
|
|
85b46392e1 | ||
|
|
f721c983aa | ||
|
|
ff0b30fc2e | ||
|
|
18070a37a8 | ||
|
|
5bd31f87f0 | ||
|
|
de83cf9d8c | ||
|
|
ceae787cf5 | ||
|
|
ce6afd0019 | ||
|
|
d977d57b2a | ||
|
|
7bcd6adf01 | ||
|
|
ac68dbd545 | ||
|
|
d450e2c3ab | ||
|
|
9440a4f879 | ||
|
|
73b0411e1c | ||
|
|
6368b9d837 | ||
|
|
1b643fb4b6 | ||
|
|
d118c6b666 | ||
|
|
380e062d25 | ||
|
|
261f0333b8 | ||
|
|
24adca6108 | ||
|
|
3f440f0f7a | ||
|
|
ba6defa87c | ||
|
|
887a0ef574 | ||
|
|
200743747d | ||
|
|
2082c5eed2 | ||
|
|
a42d012788 | ||
|
|
82cc51424b | ||
|
|
7924f195aa | ||
|
|
d41bd3023f | ||
|
|
87a0dd2d12 | ||
|
|
5fd64596eb | ||
|
|
d23f61d995 | ||
|
|
7ac27b3883 | ||
|
|
9420b41e39 | ||
|
|
2cfb0e05cf | ||
|
|
5b9386b18a | ||
|
|
f5c3dff43c | ||
|
|
eeb82c8cfe | ||
|
|
3750c36aa7 | ||
|
|
3801354ae6 | ||
|
|
266fbb1762 | ||
|
|
5d1f81a92c | ||
|
|
d6e8eb5307 | ||
|
|
2bc82f49ed | ||
|
|
487985558d | ||
|
|
dc237b8052 | ||
|
|
4ed4515262 | ||
|
|
cd76fa0139 | ||
|
|
af4b9e83f7 | ||
|
|
fa5facdf33 | ||
|
|
937b36e756 | ||
|
|
e90bdf8f97 | ||
|
|
56491cc17b | ||
|
|
6da531e99b | ||
|
|
01b5158b73 | ||
|
|
8f9b665bef | ||
|
|
806949879a | ||
|
|
e72e2b53aa | ||
|
|
10f42fe2e6 | ||
|
|
51b438117a | ||
|
|
d73825dd24 | ||
|
|
b5c6191c67 | ||
|
|
97c707248e | ||
|
|
02fbc279b5 | ||
|
|
80a68507cd | ||
|
|
dbb1e37033 | ||
|
|
364b84359e | ||
|
|
93d4a40977 | ||
|
|
97312343e4 | ||
|
|
1736ad486a | ||
|
|
a07ad843a2 | ||
|
|
fef9101058 | ||
|
|
2890ff2605 | ||
|
|
026ad2ccb9 | ||
|
|
a82969b778 | ||
|
|
612b04c26f | ||
|
|
2162f5f76f | ||
|
|
710f16ce68 | ||
|
|
61a4f468ba | ||
|
|
b00fea5656 | ||
|
|
269ff630aa | ||
|
|
986f7121bd | ||
|
|
21f0501bc6 | ||
|
|
2b31dd955c | ||
|
|
e7aeb4ff89 | ||
|
|
9dd1192033 | ||
|
|
e61da0958f | ||
|
|
fce588057e | ||
|
|
33331fd3c8 | ||
|
|
1261ad3a00 | ||
|
|
7dcf4d5192 | ||
|
|
dc87df5d38 | ||
|
|
5d2f65daa9 | ||
|
|
58cf471bc4 | ||
|
|
7db99a7dd5 | ||
|
|
000904eb31 | ||
|
|
6d1713b6b9 | ||
|
|
de8262d7b9 | ||
|
|
4f026acad8 | ||
|
|
5b31bbce8d | ||
|
|
e6e80f6fc7 | ||
|
|
bde4492d49 | ||
|
|
7c728c144c | ||
|
|
8ad7bcc0d6 | ||
|
|
e62806d6fb | ||
|
|
4e0a2e441b | ||
|
|
aabe39137b | ||
|
|
d9564ed6fe | ||
|
|
0798a0c6c2 | ||
|
|
c9786946b7 | ||
|
|
9344ab3546 | ||
|
|
1a4078b8a1 | ||
|
|
ca66637270 | ||
|
|
8674ca931b | ||
|
|
08c82e072e | ||
|
|
23c9827e4c | ||
|
|
864b587b89 | ||
|
|
ca89aa7ce8 | ||
|
|
63a1ecfb86 | ||
|
|
fbce392137 | ||
|
|
c004e969cb | ||
|
|
c6611471b1 | ||
|
|
bdf1625976 | ||
|
|
0a5dc17800 | ||
|
|
fa7aa508ea | ||
|
|
2973b61676 | ||
|
|
2428413442 | ||
|
|
5602d8ee64 | ||
|
|
a70799c8c0 | ||
|
|
d38b321f85 | ||
|
|
b0ff50a76f | ||
|
|
37acdc2796 | ||
|
|
f3d31cb6de | ||
|
|
a336955066 | ||
|
|
a229fc1c61 | ||
|
|
7995fd364e | ||
|
|
5e0d822d45 | ||
|
|
4fddaa8f11 | ||
|
|
4a87cecf89 | ||
|
|
ac5ee5c7ca | ||
|
|
8a8c357563 | ||
|
|
263fd80c18 | ||
|
|
7bdf05bdf5 | ||
|
|
d00f12967d | ||
|
|
d9991a18e2 | ||
|
|
a51c21cdd2 | ||
|
|
265cab5b64 | ||
|
|
da15e5e77b | ||
|
|
a717ca2675 | ||
|
|
693c9fbe0f | ||
|
|
564b290244 | ||
|
|
84d78df67e | ||
|
|
107053a98f | ||
|
|
6422a78e6f | ||
|
|
10f8298161 | ||
|
|
5f11630e27 | ||
|
|
a776b2ea94 | ||
|
|
b83ec1b503 | ||
|
|
83bd5957cd | ||
|
|
f98b4baa73 | ||
|
|
0af51cebbe | ||
|
|
abc5f8ec68 | ||
|
|
ddc14d164e | ||
|
|
aeda85fcfb | ||
|
|
66124f09c4 | ||
|
|
ac5fe1486a | ||
|
|
50ac52d316 | ||
|
|
f85d9f8b6e | ||
|
|
feb0bd58c8 | ||
|
|
32949127d2 | ||
|
|
84d24d9bf5 | ||
|
|
8e1bb6a6fd | ||
|
|
66c14c2d09 | ||
|
|
cad4d97fb3 | ||
|
|
de53cfb912 | ||
|
|
55fd276773 | ||
|
|
7125b49024 | ||
|
|
fb9ed8f592 | ||
|
|
020cb2d794 | ||
|
|
9b2c0d0b67 | ||
|
|
3993e5b705 | ||
|
|
47bcadb329 | ||
|
|
00df2c876f | ||
|
|
b4535f3dc4 | ||
|
|
e51fca1f61 | ||
|
|
0e7f5b1aef | ||
|
|
579a4e1021 | ||
|
|
c813202f92 | ||
|
|
94e1c534ca | ||
|
|
41e21acf42 | ||
|
|
b6e98632b5 | ||
|
|
51db267a4a | ||
|
|
8a5f59cb9f | ||
|
|
669817818a | ||
|
|
b84453bfbe | ||
|
|
15d561f59f | ||
|
|
0745734273 | ||
|
|
aa3f07f1ba | ||
|
|
2b8204fdc8 | ||
|
|
90e72c6aca | ||
|
|
62e2b7ca9e | ||
|
|
f7e7993fd4 | ||
|
|
18cdf070c7 | ||
|
|
563a5b3e7e | ||
|
|
3756aaecda | ||
|
|
58a13de0ff | ||
|
|
d32505a833 | ||
|
|
42091e88cb | ||
|
|
c2f607bb9a | ||
|
|
3f38080b46 | ||
|
|
9f9aa07c2d | ||
|
|
76d54b2d0f | ||
|
|
bdb564823d | ||
|
|
b3a616c9f3 | ||
|
|
ec1f94791a | ||
|
|
bea1c65076 | ||
|
|
2274a3525b | ||
|
|
749cea5a4d | ||
|
|
999fb2fff1 | ||
|
|
2a7529c39e | ||
|
|
f27ae210ed | ||
|
|
ea744f8d28 | ||
|
|
0b70cbb1a3 | ||
|
|
fce887436d | ||
|
|
f928708156 | ||
|
|
fae899a8f1 | ||
|
|
3489107a49 | ||
|
|
45fb0a4156 | ||
|
|
a62299c387 | ||
|
|
18757d7eb3 | ||
|
|
296b220bf3 | ||
|
|
0a9f37c44d | ||
|
|
776c33d79d | ||
|
|
9fd6af3a31 | ||
|
|
4ade878320 | ||
|
|
9e2477587c | ||
|
|
c7787352c8 | ||
|
|
85892c30b2 | ||
|
|
7a2dd31019 | ||
|
|
096ca379ce | ||
|
|
41601010f4 | ||
|
|
64b87e203a | ||
|
|
c64b102aaa | ||
|
|
f371c7df81 | ||
|
|
030f90db2e | ||
|
|
e51b6b545e | ||
|
|
ef5d72663f | ||
|
|
6ddfc9b8fe | ||
|
|
301654b63e | ||
|
|
c73f8c88f7 | ||
|
|
2274404324 | ||
|
|
6d349693a7 | ||
|
|
b9ce316574 | ||
|
|
a247ef7564 | ||
|
|
18566c09dc | ||
|
|
1090dca634 | ||
|
|
44f419d4f7 | ||
|
|
162c6d567c | ||
|
|
2f1abfbef8 | ||
|
|
a26a441d56 | ||
|
|
f628a76223 | ||
|
|
8088e30e06 | ||
|
|
801cdec7f3 | ||
|
|
3fd3f9871d | ||
|
|
959a562e7c | ||
|
|
3b12a77cf0 | ||
|
|
03e0e8d9c2 | ||
|
|
7cd31313d8 | ||
|
|
52a311bf36 | ||
|
|
9822deb4bf | ||
|
|
83e0282212 | ||
|
|
8942cb7aa7 | ||
|
|
f0f219f293 | ||
|
|
dc75d72522 | ||
|
|
6da81b3817 | ||
|
|
847479b639 | ||
|
|
0790f37f5e | ||
|
|
9dd472c59b | ||
|
|
5746d69f98 | ||
|
|
8356c5933f | ||
|
|
2c488baa80 | ||
|
|
d30743a428 | ||
|
|
009d84a3c6 | ||
|
|
e888b76747 | ||
|
|
6174599754 | ||
|
|
8ba04aeb74 | ||
|
|
43590896e9 | ||
|
|
3547c4832b | ||
|
|
1cd098252e | ||
|
|
4adbc31dae | ||
|
|
99031feb35 | ||
|
|
d363b06d0e | ||
|
|
2af100cc86 | ||
|
|
3e90211108 | ||
|
|
6dd161fe17 | ||
|
|
558bd040c6 | ||
|
|
f2c48975f6 | ||
|
|
fc43a56bb3 | ||
|
|
ca7f557a3c | ||
|
|
7477713eef | ||
|
|
c16e762fa4 | ||
|
|
41592133a6 | ||
|
|
54f7525f1b | ||
|
|
ad6bb3da9f | ||
|
|
49bc2dc5da | ||
|
|
cdf77087cd | ||
|
|
8e5dde887c | ||
|
|
f21188000e | ||
|
|
1b3eb32bf4 | ||
|
|
eec3f183e6 | ||
|
|
31b66cd911 | ||
|
|
ad425e8d9e | ||
|
|
da0196a308 | ||
|
|
e585972b7b | ||
|
|
cc62cd4add | ||
|
|
25225a452c | ||
|
|
678644c7fb | ||
|
|
32f20ed984 | ||
|
|
4eb5bf08d5 | ||
|
|
35c93f38e0 | ||
|
|
f60c2f4fb9 | ||
|
|
b2cf152b9e | ||
|
|
444928dffd | ||
|
|
4d7e2d5840 | ||
|
|
318046ce1d | ||
|
|
808ad1e272 | ||
|
|
05a1195661 | ||
|
|
c46322c6a6 | ||
|
|
80d5efc41f | ||
|
|
0409ab7dc1 | ||
|
|
63f079ec76 | ||
|
|
5988f1e8da | ||
|
|
ed0c0edeba | ||
|
|
34b4841f4d | ||
|
|
ff47c5a8ad | ||
|
|
9430a53c0c | ||
|
|
03334e3f0f | ||
|
|
6f2ecf9d0d | ||
|
|
6f803c3b4b | ||
|
|
15d400c842 | ||
|
|
3ddf150661 | ||
|
|
5b519afee4 | ||
|
|
15ea9f3dcc | ||
|
|
d5e2536f8d | ||
|
|
d7e9083e06 | ||
|
|
e0cc338c3a | ||
|
|
624c5741e2 | ||
|
|
558507dd71 | ||
|
|
565340bd53 | ||
|
|
756745487a | ||
|
|
d2ece4d370 | ||
|
|
d5f5d1da1e | ||
|
|
dfaf1a72cc | ||
|
|
ff8e5b871c | ||
|
|
927dda4e53 | ||
|
|
0e51bac307 | ||
|
|
7a50af14f3 | ||
|
|
396477c2e2 | ||
|
|
8765874d9a | ||
|
|
49dffe086d | ||
|
|
77ddadcded | ||
|
|
05b297ddec | ||
|
|
feb0de9a08 | ||
|
|
f4f2361d22 | ||
|
|
cae6a9f51c | ||
|
|
2872f5c018 | ||
|
|
0512c21ad7 | ||
|
|
922a69feed | ||
|
|
24192c79d4 | ||
|
|
17c22a635f | ||
|
|
bcbcf417b5 | ||
|
|
acf7596368 | ||
|
|
34c7d925ca | ||
|
|
c10730ebb9 | ||
|
|
e50743b922 | ||
|
|
75b0745e42 | ||
|
|
ebd99f95a3 | ||
|
|
0e649883cb | ||
|
|
3d376c8d14 | ||
|
|
adedb0e391 | ||
|
|
521935786c | ||
|
|
885b9d186b | ||
|
|
356f023539 | ||
|
|
de8d3f45da | ||
|
|
72c9956190 | ||
|
|
6dc4cbe448 | ||
|
|
77364488c2 | ||
|
|
5a61040027 | ||
|
|
c6f7be40df | ||
|
|
c36fb63f8c | ||
|
|
48aebea6cf | ||
|
|
55082d2ef8 | ||
|
|
cc03b97234 | ||
|
|
5542873368 | ||
|
|
1db5d76ef1 | ||
|
|
ca6c45087b | ||
|
|
3333eb95f9 | ||
|
|
d681725fc3 | ||
|
|
f5eadc9e1e | ||
|
|
219e213c1e | ||
|
|
af654e663b | ||
|
|
39b3b4ef9d | ||
|
|
6c62a0900f | ||
|
|
ddd772eb43 | ||
|
|
69458ab649 | ||
|
|
c7df70143e | ||
|
|
a81ea7cc8f | ||
|
|
02330a0756 | ||
|
|
db49b599b5 | ||
|
|
bb0bfd440a | ||
|
|
10ce732b8d | ||
|
|
4c567cf2d7 | ||
|
|
2783d2989d | ||
|
|
c3d6510231 | ||
|
|
3bb948991f | ||
|
|
4b9ce22f06 | ||
|
|
772bda69f9 | ||
|
|
8b4722b1c9 | ||
|
|
9e5c9d9c34 | ||
|
|
ee533df38f | ||
|
|
52dc8e011c | ||
|
|
bd5cc790d6 | ||
|
|
7d6d5a7787 | ||
|
|
ba6e7dd06a | ||
|
|
6270fb3237 | ||
|
|
16ec50a6ee | ||
|
|
3d2021c8a1 | ||
|
|
15d63ddffa | ||
|
|
7ce6fadb3d | ||
|
|
6b18a24f9b | ||
|
|
a38cb961c7 | ||
|
|
3c5fe21078 | ||
|
|
b44305694f | ||
|
|
be217e2b6f | ||
|
|
6ce04c2aa1 | ||
|
|
85e4b649db | ||
|
|
73a3335148 | ||
|
|
32845c5a3d | ||
|
|
05a878ac34 | ||
|
|
847d015243 | ||
|
|
51cde2681c | ||
|
|
9c0606942c | ||
|
|
646d476bdb | ||
|
|
31261681a0 | ||
|
|
f6fae820c4 | ||
|
|
b3cbf925aa | ||
|
|
aa1ae3ee42 | ||
|
|
80f6c8b74e | ||
|
|
79d8e8d59d | ||
|
|
9193375586 | ||
|
|
240bcb8759 | ||
|
|
a5dcafb84c | ||
|
|
192207a857 | ||
|
|
d18fafb0ef | ||
|
|
380c86898c | ||
|
|
b59a6b82ef | ||
|
|
77ba568c36 | ||
|
|
a0f05cc77b | ||
|
|
80f43a9774 | ||
|
|
c04d9eda6b | ||
|
|
cabf3e9695 | ||
|
|
ff7b4386d6 | ||
|
|
4dbbe159ee | ||
|
|
eeab92719a | ||
|
|
43e6b7de07 | ||
|
|
4cfd1b1ff5 | ||
|
|
09ba018493 | ||
|
|
7acf7dd0eb | ||
|
|
592d085de6 | ||
|
|
2cf2c64651 | ||
|
|
560974f7d2 | ||
|
|
85270f497a | ||
|
|
9fbea4a380 | ||
|
|
cbf9c5361e | ||
|
|
44316731c0 | ||
|
|
60513af8ed | ||
|
|
24cfe02979 | ||
|
|
8f3324560a | ||
|
|
2041edcf30 | ||
|
|
1227b3c11a | ||
|
|
8973726f63 | ||
|
|
5559fef1bc | ||
|
|
9cb3c3821a | ||
|
|
c85e367ded | ||
|
|
5e20487216 | ||
|
|
bc6b9eb905 | ||
|
|
5940bbd498 | ||
|
|
f4a0f6a2e6 | ||
|
|
0df7d45678 | ||
|
|
a05ee2483b | ||
|
|
f5dbc18c05 | ||
|
|
dd052fa1af | ||
|
|
2cc4ad9c30 | ||
|
|
4dd741cc3f | ||
|
|
9ce81b34c9 | ||
|
|
460df46abc | ||
|
|
1e70e4289b | ||
|
|
5fa0ac5927 | ||
|
|
4b40e7b8d6 | ||
|
|
29cd035a05 | ||
|
|
39d6b93d42 | ||
|
|
629f17294a | ||
|
|
10a5af67aa | ||
|
|
b542d82553 | ||
|
|
2a644c3f88 | ||
|
|
f6de61968d | ||
|
|
68f0c4df3a | ||
|
|
0743daf56a | ||
|
|
58b6ab2601 | ||
|
|
038f8829c2 | ||
|
|
ddcf77a62d | ||
|
|
adefbdbeb3 | ||
|
|
921285e5b1 | ||
|
|
264bf46798 | ||
|
|
5a7b5d65a4 | ||
|
|
23b13f0a0e | ||
|
|
90ddffce0e | ||
|
|
e30fde5237 | ||
|
|
ac683c3ff7 | ||
|
|
b5a931c96e | ||
|
|
5b61742075 | ||
|
|
4e4a38f7e9 | ||
|
|
c1bb029a1c | ||
|
|
eae2c37388 | ||
|
|
7193fea068 | ||
|
|
9b85deebf8 | ||
|
|
0211f75cb6 | ||
|
|
fa6b7ca3ed | ||
|
|
007d03e7f6 | ||
|
|
a534301eb7 | ||
|
|
1baa987016 | ||
|
|
a5b48ab392 | ||
|
|
7f981f05fb | ||
|
|
259cea1c42 | ||
|
|
9024b2a974 | ||
|
|
f2c31d3ca6 | ||
|
|
6f8b5dd909 | ||
|
|
6521b66b7c | ||
|
|
202d2075a6 | ||
|
|
e575fae73b | ||
|
|
d84ee3d03d | ||
|
|
ba745588e9 | ||
|
|
84731bdc19 | ||
|
|
f748c5dbe4 | ||
|
|
fdd4d5244f | ||
|
|
9301477262 | ||
|
|
9a787e6ef8 | ||
|
|
5b8cdf7884 | ||
|
|
5fd104bb30 | ||
|
|
9ba42a8fa3 | ||
|
|
fe8fd2e3a8 | ||
|
|
9ebce35d2b | ||
|
|
654145be84 | ||
|
|
3662d42374 | ||
|
|
d392fb371e | ||
|
|
1142d6ac48 | ||
|
|
bdc3b2425b | ||
|
|
9a64f45815 | ||
|
|
3633e02ff7 | ||
|
|
2c502ec764 | ||
|
|
b17d7f0e27 | ||
|
|
65364d6b0f | ||
|
|
6fd6c77ce6 | ||
|
|
e447549de1 | ||
|
|
6b0dd00aa5 | ||
|
|
461866836e | ||
|
|
3ae42f054f | ||
|
|
5a571f19e1 | ||
|
|
70aeaf7b5d | ||
|
|
7a6838f5a5 | ||
|
|
07f5e8f215 | ||
|
|
2b05bc1f5f | ||
|
|
edf64ae7b5 | ||
|
|
7370448be9 | ||
|
|
51af293d66 | ||
|
|
d37e28215e | ||
|
|
2c01849f2e | ||
|
|
c29ba9bb5f | ||
|
|
8fdf120ec2 | ||
|
|
a9b9161c40 | ||
|
|
43f907ebec | ||
|
|
ae670e1eb5 | ||
|
|
f102718901 | ||
|
|
9d452efc7d | ||
|
|
156fe529b5 | ||
|
|
df24525105 | ||
|
|
d938345deb | ||
|
|
d6681733dd | ||
|
|
2f1aec02f0 | ||
|
|
d30e0a3c51 | ||
|
|
3f3e9cf1bb | ||
|
|
e77909d498 | ||
|
|
d10830f892 | ||
|
|
18d8f72da2 | ||
|
|
4a59823e58 | ||
|
|
f3149e46cd | ||
|
|
60379a7b4e | ||
|
|
605b3cccee | ||
|
|
843799f4f6 | ||
|
|
a69cda5c13 | ||
|
|
dbaa3dbd52 | ||
|
|
58197c6fb2 | ||
|
|
7813093452 | ||
|
|
3f2c3dc987 | ||
|
|
08ddba25d0 | ||
|
|
d47fa7e64f | ||
|
|
c87aa2e537 | ||
|
|
bc430546bc | ||
|
|
9428e065eb | ||
|
|
10408c5717 | ||
|
|
ae902da913 | ||
|
|
0be5a91eff | ||
|
|
7dcf46ce98 | ||
|
|
33e6e4b411 | ||
|
|
bab6e4eb0d | ||
|
|
6a7c7521d8 | ||
|
|
d070244ea7 | ||
|
|
9219bb7d6e | ||
|
|
54e83f35e5 | ||
|
|
eb138d6526 | ||
|
|
edd0c3099b | ||
|
|
04455d40cf | ||
|
|
221af94d15 | ||
|
|
48ac3bb7af | ||
|
|
07273b8b7f | ||
|
|
bfb5b2864d | ||
|
|
07330e84fb | ||
|
|
0e39704b3a | ||
|
|
f25e794e7c | ||
|
|
df46ce8bdc | ||
|
|
4d83f537dc | ||
|
|
58443ef53f | ||
|
|
1ee52ad86b | ||
|
|
bc941239ec | ||
|
|
9a52d5387d | ||
|
|
1f50bc3752 | ||
|
|
0819df0910 | ||
|
|
663787c15b | ||
|
|
2c39d07261 | ||
|
|
dce84b9b09 | ||
|
|
a5bab6bb80 | ||
|
|
7536c03f63 | ||
|
|
ada5d2ef0e | ||
|
|
b8bead0590 | ||
|
|
68f852d6d1 | ||
|
|
d9fe5a8819 | ||
|
|
346183a23f | ||
|
|
dcfd7f5443 | ||
|
|
e59cd6672b | ||
|
|
7c8c440f67 | ||
|
|
f258c41f15 | ||
|
|
ae4a24f4aa | ||
|
|
476cdcfe86 | ||
|
|
f869df2f65 | ||
|
|
03cfabacd9 | ||
|
|
47ac5875f3 | ||
|
|
f67327358e | ||
|
|
4901823f15 | ||
|
|
5407e3c821 | ||
|
|
1d5cdad8b7 | ||
|
|
cd2424cb77 | ||
|
|
c17efde6bf | ||
|
|
40cd8cdec7 | ||
|
|
6768672a44 | ||
|
|
240c5b005b | ||
|
|
8dde170a35 | ||
|
|
c07abf8ff9 | ||
|
|
e5a436593f | ||
|
|
bb6e093ac6 | ||
|
|
59a334ce24 | ||
|
|
d241dcfb27 | ||
|
|
af263e7913 | ||
|
|
6610e7d405 | ||
|
|
c476e65cf2 | ||
|
|
b69b2eeeb3 | ||
|
|
89dab0917b | ||
|
|
73efdb95ae | ||
|
|
1bcca88614 | ||
|
|
3af1e0ef56 | ||
|
|
8387571c1d | ||
|
|
1d017f60b4 | ||
|
|
81effda9e8 | ||
|
|
9343906ab1 | ||
|
|
08b7d6735c | ||
|
|
a91ebd1e91 | ||
|
|
312e03b4eb | ||
|
|
e8a57e432c | ||
|
|
bca2eef2e8 | ||
|
|
ec7211a15d | ||
|
|
46807c6477 | ||
|
|
b578786e62 | ||
|
|
2e0ad8d262 | ||
|
|
003f0cfa6d | ||
|
|
ee3df081ef | ||
|
|
08eeb12519 | ||
|
|
e66c6b2505 | ||
|
|
d2a880d9c8 | ||
|
|
edc0b86470 | ||
|
|
aebe6b80b7 | ||
|
|
4d87333b43 | ||
|
|
ef32f3ed5a | ||
|
|
216ded3034 | ||
|
|
cb59fe2cee | ||
|
|
7776f6d09c | ||
|
|
c50392c947 | ||
|
|
ceee978fcd | ||
|
|
c5a73dc87e | ||
|
|
7198ef2774 | ||
|
|
7e9a066797 | ||
|
|
ba96332313 | ||
|
|
e2d0338b0b | ||
|
|
59ecab5738 | ||
|
|
721bf3403d | ||
|
|
3b8ba47377 | ||
|
|
e752929f69 | ||
|
|
e41c3e6f54 | ||
|
|
9dedd1a8de | ||
|
|
c4a5fae28f | ||
|
|
5f95a3233f | ||
|
|
d3174d0196 | ||
|
|
3710d71974 | ||
|
|
f62e88eb67 | ||
|
|
904b302fb6 | ||
|
|
5fc096f2d5 | ||
|
|
87668c492f | ||
|
|
6d7a8b97ad | ||
|
|
282d444933 | ||
|
|
f3d7d97fb9 | ||
|
|
de857a7c4e | ||
|
|
20a0ebfc9d | ||
|
|
ba8166bdeb | ||
|
|
2b634fc6c5 | ||
|
|
5429bc03ab | ||
|
|
a558b34608 | ||
|
|
1850d56977 | ||
|
|
61b4c62824 | ||
|
|
10e5ccfe86 | ||
|
|
9f5d475e80 | ||
|
|
9bb9a3acbe | ||
|
|
0923b7e3c5 | ||
|
|
ccd81f6fe2 | ||
|
|
0f74107e86 | ||
|
|
8377434c08 | ||
|
|
1fbf2bfb8d | ||
|
|
42facf8e12 | ||
|
|
4bb3d85c25 | ||
|
|
c0039190bd | ||
|
|
a8d00a47cd | ||
|
|
57bcbf6c48 | ||
|
|
c57db1479e | ||
|
|
cd8062ada3 | ||
|
|
244d05adb1 | ||
|
|
812bd64325 | ||
|
|
276d1361ac | ||
|
|
881eac4722 | ||
|
|
2a2a550a6a | ||
|
|
e75001080a | ||
|
|
6fbba38a76 | ||
|
|
902b413881 | ||
|
|
8b2f8ad3ef | ||
|
|
377cb77307 | ||
|
|
733bf0b169 | ||
|
|
8faff3e075 | ||
|
|
48af91c976 | ||
|
|
6664efaa13 | ||
|
|
e5ee96cf52 | ||
|
|
38faf1f905 | ||
|
|
2cff142266 | ||
|
|
2c99cfacc0 | ||
|
|
0c63ea1f50 | ||
|
|
f50df66e3a | ||
|
|
4b93491160 | ||
|
|
19210cbf7d | ||
|
|
9af206b69a | ||
|
|
b6b9c71c5e | ||
|
|
c000c4502f | ||
|
|
b6c1d9a592 | ||
|
|
7a75fe0cad | ||
|
|
a83e660902 | ||
|
|
65eb3e4b95 | ||
|
|
093fb419f3 | ||
|
|
026e56aead | ||
|
|
fa9bc59f62 | ||
|
|
06ec80db42 | ||
|
|
24d564b79b | ||
|
|
2f5e6248cd | ||
|
|
c0cc81ed96 | ||
|
|
b33a54a449 | ||
|
|
94137e587c | ||
|
|
a6086d3724 | ||
|
|
0a377150e3 | ||
|
|
d20e0a228a | ||
|
|
ca146a1b57 | ||
|
|
c7c3e3ee73 | ||
|
|
cd27f6459c | ||
|
|
b1e212721e | ||
|
|
ccd2773331 | ||
|
|
cfa82b51fb | ||
|
|
9c91a8db46 | ||
|
|
b160eee8d2 | ||
|
|
37ceabdf5d | ||
|
|
e7828a43fa | ||
|
|
ccb1f04ad8 | ||
|
|
4c14ccbb63 | ||
|
|
25c24ca9cf | ||
|
|
787869fe21 | ||
|
|
b51c27a823 | ||
|
|
5917881b47 | ||
|
|
c7a40d59b7 | ||
|
|
a50c0d84e9 | ||
|
|
f17a957058 | ||
|
|
2c63851130 | ||
|
|
6b125bba7c | ||
|
|
d92b87b7c8 | ||
|
|
f64a477c3d | ||
|
|
b6f8ed1e4a | ||
|
|
bad88e4741 | ||
|
|
01db519691 | ||
|
|
e601038c0f | ||
|
|
e0996a17ef | ||
|
|
526307e192 | ||
|
|
1b01c4f053 | ||
|
|
a184e23f16 | ||
|
|
06156e0ca6 | ||
|
|
02b1de3266 | ||
|
|
c5b3d92466 | ||
|
|
186a78b064 | ||
|
|
9a808dc139 | ||
|
|
977404b8c3 | ||
|
|
b00143ce9b | ||
|
|
4435d9a248 | ||
|
|
7d0303e2be | ||
|
|
a0da9c1129 | ||
|
|
5e73690570 | ||
|
|
b0409b7d52 | ||
|
|
fe474b3989 | ||
|
|
5154d5d3ee | ||
|
|
62df92f63a | ||
|
|
e2534af40e | ||
|
|
b627e391ac | ||
|
|
40a3eac704 | ||
|
|
2ee3f10e02 | ||
|
|
5a3bf2f758 | ||
|
|
e121dd0d1d | ||
|
|
2c46a37a53 | ||
|
|
23f05d7f4e | ||
|
|
6105eea7a9 | ||
|
|
850e9a734a | ||
|
|
2d30b155f2 | ||
|
|
1333e21553 | ||
|
|
4c412528f5 | ||
|
|
a8fce47ba0 | ||
|
|
cb7c57fd03 | ||
|
|
494d0f7c14 | ||
|
|
a4e480e02b | ||
|
|
cd285cc019 | ||
|
|
9e8e00d4bb | ||
|
|
389834f735 | ||
|
|
b14ddc07fb | ||
|
|
4447fb8202 | ||
|
|
1c9c4b1802 | ||
|
|
19e15f4ef5 | ||
|
|
c2c29e2cd2 | ||
|
|
7b33dc591d | ||
|
|
a95f2e76f4 | ||
|
|
979860a951 | ||
|
|
9e9a81d9e8 | ||
|
|
8f09561114 | ||
|
|
b167d94ead | ||
|
|
e4c0a157e3 | ||
|
|
956869ab58 | ||
|
|
e5f4da9a99 | ||
|
|
9649d9a46b | ||
|
|
22477b7e81 | ||
|
|
b6c76a2164 | ||
|
|
043834274d | ||
|
|
1e4ca69c89 | ||
|
|
ff2bcfb0e7 | ||
|
|
b47fc9f901 | ||
|
|
165f4023d0 | ||
|
|
d51053ce86 | ||
|
|
229872589c | ||
|
|
e4c47c46a6 | ||
|
|
84fe2fb92e | ||
|
|
076912c648 | ||
|
|
033653e234 | ||
|
|
0624087373 | ||
|
|
346d886f8a | ||
|
|
67ac01b31a | ||
|
|
87f1cf6730 | ||
|
|
0f46651500 | ||
|
|
65bf055e0f | ||
|
|
4c995f786b | ||
|
|
4853c8c872 | ||
|
|
170da08001 | ||
|
|
a39a133ee5 | ||
|
|
1251b1e870 | ||
|
|
418120196f | ||
|
|
759661420e | ||
|
|
bb28f856da | ||
|
|
f90e6bef9e | ||
|
|
cabaa2e6d6 | ||
|
|
c8bddd4289 | ||
|
|
71ba980757 | ||
|
|
38c3c49778 | ||
|
|
ab7ac9cb60 | ||
|
|
e4787924e7 | ||
|
|
3385a92b0f | ||
|
|
e73e6956a5 | ||
|
|
024eb2b157 | ||
|
|
ccff0592ca | ||
|
|
942f7c2bc9 | ||
|
|
b3a6cd0660 | ||
|
|
c5569fccf1 | ||
|
|
cc7c443145 | ||
|
|
8d7e5baf9d | ||
|
|
ed64d4b5ae | ||
|
|
8fe42bc6aa | ||
|
|
a67aa3852d | ||
|
|
c2c907852d | ||
|
|
3123f858bb | ||
|
|
6a18369891 | ||
|
|
0f4ef40600 | ||
|
|
42a7fb949a | ||
|
|
bbfa6e9c82 | ||
|
|
0d8ae0d615 | ||
|
|
7bbbc88c34 | ||
|
|
e2ad197d7e | ||
|
|
ca8f52d304 | ||
|
|
7395a64b26 | ||
|
|
4dd672a590 | ||
|
|
cff3f739db | ||
|
|
7fb35cfebb | ||
|
|
ddfda31924 | ||
|
|
353e085b0e | ||
|
|
989b548ef9 | ||
|
|
8f60e7e200 | ||
|
|
ec74525fde | ||
|
|
a317c50737 | ||
|
|
c62b46268a | ||
|
|
42ef075d4f | ||
|
|
f52605289b | ||
|
|
68e0911866 | ||
|
|
756fcbb590 | ||
|
|
a49d900951 | ||
|
|
38f212d632 | ||
|
|
204fdfd233 | ||
|
|
b5e04e8111 | ||
|
|
21fc829766 | ||
|
|
39851c3412 | ||
|
|
21811465b6 | ||
|
|
7574726815 | ||
|
|
236e0f9ab6 | ||
|
|
8e95f0b73f | ||
|
|
bed45a5fbd | ||
|
|
c50c2e2b01 | ||
|
|
adf982fcd6 | ||
|
|
9b4103be75 | ||
|
|
672eec0c33 | ||
|
|
0d8c06595e | ||
|
|
a5a7ca5fcc | ||
|
|
8767d20c47 | ||
|
|
4cbf3fffb1 | ||
|
|
51fad19d0d | ||
|
|
664aa6ed2a | ||
|
|
574cd2a754 | ||
|
|
1b34ee7369 | ||
|
|
7b2f1dd4c6 | ||
|
|
a97b6efe9c | ||
|
|
3722b67724 | ||
|
|
218a5ec9e4 | ||
|
|
90d3ac07a9 | ||
|
|
149a4b916b | ||
|
|
70914e836f | ||
|
|
a2dae8aa13 | ||
|
|
b6ea0808e4 | ||
|
|
089e43e1ce | ||
|
|
42936ab8dc | ||
|
|
411fa9345f | ||
|
|
336e118096 | ||
|
|
d1707801bf | ||
|
|
71bcf25718 | ||
|
|
288da0ef05 | ||
|
|
fec29eb349 | ||
|
|
032d48e394 | ||
|
|
a433d97573 | ||
|
|
6bd571f1b3 | ||
|
|
1dd89601ad | ||
|
|
a7cf359672 | ||
|
|
baa98952fa | ||
|
|
55afbf4db5 | ||
|
|
dca0fb327b | ||
|
|
e34a31941d | ||
|
|
dbba5002d9 | ||
|
|
4dd9e34a11 | ||
|
|
a30222a13e | ||
|
|
5797144083 | ||
|
|
db513b43e7 | ||
|
|
d387fa3bfb | ||
|
|
1bff9f550e | ||
|
|
0167b30bf1 | ||
|
|
bf993d04f1 | ||
|
|
be2b2c6c77 | ||
|
|
8851156f23 | ||
|
|
1a13694843 | ||
|
|
3872831bd7 | ||
|
|
ef4ce115ff | ||
|
|
516b300731 | ||
|
|
88d97dd49b | ||
|
|
be9494dd54 | ||
|
|
e43fc59634 | ||
|
|
4523a8df0f | ||
|
|
2c8082451f | ||
|
|
7ab498702c | ||
|
|
a06e8c8f83 | ||
|
|
1a01e8d53a | ||
|
|
5ce60cf1cd | ||
|
|
de1a6025d0 | ||
|
|
ca6ae53fe6 | ||
|
|
4eff52ab62 | ||
|
|
e5c5780547 | ||
|
|
f348c9daa7 | ||
|
|
e1dd29dd0b | ||
|
|
558f302342 | ||
|
|
5fee1c3ebd | ||
|
|
248debb7c4 | ||
|
|
8504fd8d9d | ||
|
|
e360a5323d | ||
|
|
1ad5eb010a | ||
|
|
ca7f1e5db8 | ||
|
|
2981e35c75 | ||
|
|
f3e8677ae4 | ||
|
|
d209c8af9d | ||
|
|
26b2233168 | ||
|
|
b2669aaa34 | ||
|
|
1438eef62b | ||
|
|
a92f7dbb7c | ||
|
|
4710bab697 | ||
|
|
52aa27025d | ||
|
|
8e544c056f | ||
|
|
3c2a8b9031 | ||
|
|
fff4883bca | ||
|
|
dc234beab1 | ||
|
|
66d310fcca | ||
|
|
702b5eb3dd | ||
|
|
06477b6e7f | ||
|
|
fc76899384 | ||
|
|
97102b9be9 | ||
|
|
1c0dfa830e | ||
|
|
53bfaac0c0 | ||
|
|
30790fdcb6 | ||
|
|
b8b256da2e | ||
|
|
0472dc1b25 | ||
|
|
1ec3e53e11 | ||
|
|
9f66e09e44 | ||
|
|
a71b0a8924 | ||
|
|
e555d3c496 | ||
|
|
df92e41384 | ||
|
|
d10fdac670 | ||
|
|
b63bffa524 | ||
|
|
957cfdd5d7 | ||
|
|
1352316492 | ||
|
|
73cd82081a | ||
|
|
812820472f | ||
|
|
21f0cd6e3f | ||
|
|
b2ee8ef7de | ||
|
|
1e066cbabd | ||
|
|
4cc38d44e0 | ||
|
|
dcf7393259 | ||
|
|
bab070b09c | ||
|
|
2bd4ad5770 | ||
|
|
61ecebf911 | ||
|
|
33c8663a5b | ||
|
|
76da2ee324 | ||
|
|
31896c9be9 | ||
|
|
f61d722aee | ||
|
|
1f9f3fdede | ||
|
|
a778109214 | ||
|
|
cb7fa9375b | ||
|
|
515ecb09e7 | ||
|
|
a12a620697 | ||
|
|
0c3b2bc2f5 | ||
|
|
78ba27dc63 | ||
|
|
dc20b863ed | ||
|
|
c9a211d5cf | ||
|
|
95f94cffd2 | ||
|
|
0da95cbdb8 | ||
|
|
dadd1e3101 | ||
|
|
d523ae3ffa | ||
|
|
9a41cac6e1 | ||
|
|
5d3c5ab7cc | ||
|
|
08c930e6cf | ||
|
|
e94ded920b | ||
|
|
c882fbd59a | ||
|
|
46b50a042e | ||
|
|
fda9e95786 | ||
|
|
ea1ad23bff | ||
|
|
7ffc5e0212 | ||
|
|
acba9444f4 | ||
|
|
f7e3671801 | ||
|
|
a1b2e36a5d | ||
|
|
44e96942b3 | ||
|
|
f2efa760ff | ||
|
|
256df9042b | ||
|
|
6d7091fb5c | ||
|
|
0d1f88a368 | ||
|
|
2ae601717d | ||
|
|
c7c8b463b4 | ||
|
|
282f839211 | ||
|
|
b2eb846b69 | ||
|
|
62cf925dcf | ||
|
|
e699f84c4d | ||
|
|
c1189dadc5 | ||
|
|
76bc080a6d | ||
|
|
7f989f77ac | ||
|
|
b916f768fe | ||
|
|
e4509c5714 | ||
|
|
ddb6893a64 | ||
|
|
248751ba1d | ||
|
|
b4b74ed53a | ||
|
|
76903cd67f | ||
|
|
e4f2eac703 | ||
|
|
3aa45007a7 | ||
|
|
f452892c88 | ||
|
|
a0fece8a0e | ||
|
|
e3d493209b | ||
|
|
2e8b63553d | ||
|
|
fb8f4b95b7 | ||
|
|
83e107c713 | ||
|
|
e97642a790 | ||
|
|
426d8684bf | ||
|
|
5e7409a4f0 | ||
|
|
c225a4cd48 | ||
|
|
24df9e1ce6 | ||
|
|
eab1fd3722 | ||
|
|
93bd041693 | ||
|
|
665ebe993c | ||
|
|
4086130371 | ||
|
|
29aacf5238 | ||
|
|
497e6a8422 | ||
|
|
af8572add9 | ||
|
|
d6aea96400 | ||
|
|
17e26ff1a6 | ||
|
|
f5f223348d | ||
|
|
e4f90fd7ea | ||
|
|
96dff20760 | ||
|
|
d639f7f6de | ||
|
|
5b35ec2ea2 | ||
|
|
bc78b95265 | ||
|
|
97f22eccbb | ||
|
|
a4fe86e38a | ||
|
|
4bc1e10ecb | ||
|
|
5b840d73bb | ||
|
|
afa9acfb1e | ||
|
|
7b7f65da39 | ||
|
|
806da59f47 | ||
|
|
9a009a4ea3 | ||
|
|
083d890053 | ||
|
|
e693a8aeb8 | ||
|
|
831b46d7b5 | ||
|
|
8dd3022b94 | ||
|
|
b278eb7110 | ||
|
|
7a66163216 | ||
|
|
dda2043401 | ||
|
|
08d6183c9b | ||
|
|
eea0b86d6d | ||
|
|
58c04fd196 | ||
|
|
09de6f6b5f | ||
|
|
d0bbd2b539 | ||
|
|
ee8952de10 | ||
|
|
134595a6b7 | ||
|
|
4ff46f1650 | ||
|
|
4779201d4c | ||
|
|
3a8643d83c | ||
|
|
806a49b822 | ||
|
|
95d74825ee | ||
|
|
e4960909ed | ||
|
|
6cb36aaf13 | ||
|
|
cb06e93650 | ||
|
|
e3a2f7a514 | ||
|
|
01b1e817d8 | ||
|
|
c3a5195575 | ||
|
|
99765c7bd5 | ||
|
|
8929f389f4 | ||
|
|
4141d91f1b | ||
|
|
90272c84d2 | ||
|
|
3006a8e58c | ||
|
|
52a9dbd45d | ||
|
|
b2fb55d2c1 | ||
|
|
a1c16d22d8 | ||
|
|
0e9504ee4d | ||
|
|
e8cad6fc20 | ||
|
|
bc261f7739 | ||
|
|
0b8983a86b | ||
|
|
5a61da3c53 | ||
|
|
800fe6244c | ||
|
|
9e1fec812c | ||
|
|
61632f9c97 | ||
|
|
f5e44129d8 | ||
|
|
3eaca924da | ||
|
|
da1c706334 | ||
|
|
3b726dfb1e | ||
|
|
d51e7f7e40 | ||
|
|
2551e0c291 | ||
|
|
2efd5c31ab | ||
|
|
1eacb8ff36 | ||
|
|
612446c3c9 | ||
|
|
e121e16ad9 | ||
|
|
23616b41be | ||
|
|
1778ba49b2 | ||
|
|
b6f2bd4703 | ||
|
|
5fd67224f6 | ||
|
|
c9d21dde0c | ||
|
|
de2c5aa068 | ||
|
|
ad01cecae6 | ||
|
|
75ef14c75b | ||
|
|
03a5a0eddb | ||
|
|
66befd35eb | ||
|
|
3cbad16c30 | ||
|
|
3bba7c5956 | ||
|
|
0daa84c583 | ||
|
|
92358a52c0 | ||
|
|
faf17e9e86 | ||
|
|
ef6efe94b4 | ||
|
|
819d7ea23e | ||
|
|
61ff192cfd | ||
|
|
ceb1b07ce2 | ||
|
|
90188d4358 | ||
|
|
35aa0ab4e7 | ||
|
|
14dd76db8b | ||
|
|
fb26dfad65 | ||
|
|
bedc5adb75 | ||
|
|
800b1f1520 | ||
|
|
a4571a80ae | ||
|
|
a0a612618e | ||
|
|
db94728a5b | ||
|
|
04352a670a | ||
|
|
fe6e3b013e | ||
|
|
a947a74194 | ||
|
|
06055ff62b | ||
|
|
45cb1562e5 | ||
|
|
2f89a16852 | ||
|
|
86956b8cac | ||
|
|
84fb3add33 | ||
|
|
56ee68d9f3 | ||
|
|
e81fd3bb31 | ||
|
|
938ca29777 | ||
|
|
122902968f | ||
|
|
b55c30065f | ||
|
|
92ac2dbac2 | ||
|
|
d3e6decef9 | ||
|
|
579cd9d338 | ||
|
|
9e2a58dd46 | ||
|
|
64722617c1 | ||
|
|
5845ddbdda | ||
|
|
bf9ce0df9b | ||
|
|
8aee2ec3a1 | ||
|
|
3292eafe4a | ||
|
|
9ad31b2c81 | ||
|
|
374ed79a18 | ||
|
|
3d5f73e344 | ||
|
|
6761428a96 | ||
|
|
0a9b463eaa | ||
|
|
c219256fff | ||
|
|
7e48803dc5 | ||
|
|
d496b8a414 | ||
|
|
4825129560 | ||
|
|
adc54b2582 | ||
|
|
863567c9b6 | ||
|
|
102555023b | ||
|
|
f1a9eef531 | ||
|
|
5f007a5b0f | ||
|
|
9455141262 | ||
|
|
37e1379c88 | ||
|
|
55d597e519 | ||
|
|
da5ee5c951 | ||
|
|
b0bd9279fc | ||
|
|
90456339ca | ||
|
|
a653c8bad7 | ||
|
|
c4fa6cf458 | ||
|
|
268fc7b923 | ||
|
|
02604f5290 | ||
|
|
1dad7e86a0 | ||
|
|
838e3efbca | ||
|
|
3e353717f5 | ||
|
|
0a4b74b91a | ||
|
|
e69fbf3ccf | ||
|
|
0b2349d6bf | ||
|
|
3a8f04cf14 | ||
|
|
e941cf956f | ||
|
|
29f7bcf6f5 | ||
|
|
1cf1e0dc57 | ||
|
|
175283805e | ||
|
|
063c0405e8 | ||
|
|
947cb77753 | ||
|
|
28b3b305ea | ||
|
|
df85f13aea | ||
|
|
042e2c1390 | ||
|
|
e6314bee35 | ||
|
|
4292d3262e | ||
|
|
35d070ad29 | ||
|
|
cd79e77576 | ||
|
|
1f1c20d637 | ||
|
|
e87b3b1b54 | ||
|
|
a6f7b65625 | ||
|
|
722fa47132 | ||
|
|
f83e290b4c | ||
|
|
11b4047283 | ||
|
|
69b2032a86 | ||
|
|
636298569f | ||
|
|
ed8a282d35 | ||
|
|
3bd5e850e0 | ||
|
|
070f1f9159 | ||
|
|
195644cca5 | ||
|
|
8092c86ecd | ||
|
|
28f33702da | ||
|
|
570632b8be | ||
|
|
f2881e1b31 | ||
|
|
dad35e37ef | ||
|
|
39afabd60e | ||
|
|
dc7e14a34b | ||
|
|
1dca71a779 | ||
|
|
e9494efa8e | ||
|
|
8159a0f13d | ||
|
|
ee9101e738 | ||
|
|
b670e6e3dc | ||
|
|
5e5754fa62 | ||
|
|
5fcf76066f | ||
|
|
601645fa72 | ||
|
|
12765ad675 | ||
|
|
ad3383d23d | ||
|
|
7d5961cf50 | ||
|
|
864aa052f1 | ||
|
|
be16196058 | ||
|
|
8a62f12e8b | ||
|
|
78f464f6ca | ||
|
|
f37eda4739 | ||
|
|
4e106e9e5a | ||
|
|
ccf8e5e6f4 | ||
|
|
9455adf61f | ||
|
|
970ab9818a | ||
|
|
7848cf7141 | ||
|
|
8e5aa9c195 | ||
|
|
a03e9ba7dd | ||
|
|
9e646ba385 | ||
|
|
d9a4f20fe6 | ||
|
|
e659f0e75d | ||
|
|
8891d6239f | ||
|
|
e3bd3fb985 | ||
|
|
54764dfacd | ||
|
|
b156b5ff2d | ||
|
|
d8e547c9a0 | ||
|
|
a0b93377a4 | ||
|
|
e8a6efd079 | ||
|
|
18bb6caf8f | ||
|
|
bc335d15c0 | ||
|
|
2a0d440a34 | ||
|
|
b0500fac29 | ||
|
|
af16e6423a | ||
|
|
ff90471f0f | ||
|
|
bcc501c524 | ||
|
|
c4ef211a3e | ||
|
|
592c0eb7ab | ||
|
|
880a000865 | ||
|
|
a9571f6adf | ||
|
|
ca91f313bc | ||
|
|
76b9753916 | ||
|
|
cd8bbe28bf | ||
|
|
9550c11594 | ||
|
|
7ac21cad25 | ||
|
|
2008a3955a | ||
|
|
f1641c9f3e | ||
|
|
bec5bbd033 | ||
|
|
ae11f72e28 | ||
|
|
6b88cb3920 | ||
|
|
38772111e8 | ||
|
|
14f50c3e66 | ||
|
|
b89a2c9e49 | ||
|
|
2d2eda988c | ||
|
|
432033969b | ||
|
|
1fbf74e1f7 | ||
|
|
93648ff00b | ||
|
|
9513136610 | ||
|
|
43a2a39f8d | ||
|
|
218351de9a | ||
|
|
b92b922eee | ||
|
|
91be4937ee | ||
|
|
19b36a5fae | ||
|
|
bb9ee7dfd2 | ||
|
|
ac0351b525 | ||
|
|
405f5ad7cc | ||
|
|
8cc2712da3 | ||
|
|
c02ac8d1bf | ||
|
|
a1802add19 | ||
|
|
218a6642a2 | ||
|
|
21a83a5755 | ||
|
|
bec75e51f6 | ||
|
|
78d3861382 | ||
|
|
72f19274cd | ||
|
|
fb1481c69c | ||
|
|
9557f755a5 |
@@ -28,4 +28,7 @@ LICENSE
|
||||
CONTRIBUTING.md
|
||||
dist
|
||||
.git
|
||||
config/
|
||||
migrations/
|
||||
config/
|
||||
build.ts
|
||||
tsconfig.json
|
||||
47
.github/DISCUSSION_TEMPLATE/feature-requests.yml
vendored
Normal file
47
.github/DISCUSSION_TEMPLATE/feature-requests.yml
vendored
Normal file
@@ -0,0 +1,47 @@
|
||||
body:
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Summary
|
||||
description: A clear and concise summary of the requested feature.
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Motivation
|
||||
description: |
|
||||
Why is this feature important?
|
||||
Explain the problem this feature would solve or what use case it would enable.
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Proposed Solution
|
||||
description: |
|
||||
How would you like to see this feature implemented?
|
||||
Provide as much detail as possible about the desired behavior, configuration, or changes.
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Alternatives Considered
|
||||
description: Describe any alternative solutions or workarounds you've thought about.
|
||||
validations:
|
||||
required: false
|
||||
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Additional Context
|
||||
description: Add any other context, mockups, or screenshots about the feature request here.
|
||||
validations:
|
||||
required: false
|
||||
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
Before submitting, please:
|
||||
- Check if there is an existing issue for this feature.
|
||||
- Clearly explain the benefit and use case.
|
||||
- Be as specific as possible to help contributors evaluate and implement.
|
||||
51
.github/ISSUE_TEMPLATE/1.bug_report.yml
vendored
Normal file
51
.github/ISSUE_TEMPLATE/1.bug_report.yml
vendored
Normal file
@@ -0,0 +1,51 @@
|
||||
name: Bug Report
|
||||
description: Create a bug report
|
||||
labels: []
|
||||
body:
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Describe the Bug
|
||||
description: A clear and concise description of what the bug is.
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Environment
|
||||
description: Please fill out the relevant details below for your environment.
|
||||
value: |
|
||||
- OS Type & Version: (e.g., Ubuntu 22.04)
|
||||
- Pangolin Version:
|
||||
- Gerbil Version:
|
||||
- Traefik Version:
|
||||
- Newt Version:
|
||||
- Olm Version: (if applicable)
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: To Reproduce
|
||||
description: |
|
||||
Steps to reproduce the behavior, please provide a clear description of how to reproduce the issue, based on the linked minimal reproduction. Screenshots can be provided in the issue body below.
|
||||
|
||||
If using code blocks, make sure syntax highlighting is correct and double-check that the rendered preview is not broken.
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Expected Behavior
|
||||
description: A clear and concise description of what you expected to happen.
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
Before posting the issue go through the steps you've written down to make sure the steps provided are detailed and clear.
|
||||
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
Contributors should be able to follow the steps provided in order to reproduce the bug.
|
||||
8
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
8
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
@@ -0,0 +1,8 @@
|
||||
blank_issues_enabled: false
|
||||
contact_links:
|
||||
- name: Need help or have questions?
|
||||
url: https://github.com/orgs/fosrl/discussions
|
||||
about: Ask questions, get help, and discuss with other community members
|
||||
- name: Request a Feature
|
||||
url: https://github.com/orgs/fosrl/discussions/new?category=feature-requests
|
||||
about: Feature requests should be opened as discussions so others can upvote and comment
|
||||
120
.github/workflows/cicd.yml
vendored
120
.github/workflows/cicd.yml
vendored
@@ -1,34 +1,62 @@
|
||||
name: CI/CD Pipeline
|
||||
|
||||
# CI/CD workflow for building, publishing, mirroring, signing container images and building release binaries.
|
||||
# Actions are pinned to specific SHAs to reduce supply-chain risk. This workflow triggers on tag push events.
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write # for GHCR push
|
||||
id-token: write # for Cosign Keyless (OIDC) Signing
|
||||
|
||||
# Required secrets:
|
||||
# - DOCKER_HUB_USERNAME / DOCKER_HUB_ACCESS_TOKEN: push to Docker Hub
|
||||
# - GITHUB_TOKEN: used for GHCR login and OIDC keyless signing
|
||||
# - COSIGN_PRIVATE_KEY / COSIGN_PASSWORD / COSIGN_PUBLIC_KEY: for key-based signing
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- "*"
|
||||
- "[0-9]+.[0-9]+.[0-9]+"
|
||||
- "[0-9]+.[0-9]+.[0-9]+.rc.[0-9]+"
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
release:
|
||||
name: Build and Release
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: [self-hosted, linux, x64]
|
||||
# Job-level timeout to avoid runaway or stuck runs
|
||||
timeout-minutes: 120
|
||||
env:
|
||||
# Target images
|
||||
DOCKERHUB_IMAGE: docker.io/fosrl/${{ github.event.repository.name }}
|
||||
GHCR_IMAGE: ghcr.io/${{ github.repository_owner }}/${{ github.event.repository.name }}
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v5
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@c7c53464625b32c7a7e944ae62b3e17d2b600130 # v3.7.0
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # v3.11.1
|
||||
|
||||
- name: Log in to Docker Hub
|
||||
uses: docker/login-action@v3
|
||||
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
|
||||
with:
|
||||
registry: docker.io
|
||||
username: ${{ secrets.DOCKER_HUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_HUB_ACCESS_TOKEN }}
|
||||
|
||||
- name: Extract tag name
|
||||
id: get-tag
|
||||
run: echo "TAG=${GITHUB_REF#refs/tags/}" >> $GITHUB_ENV
|
||||
shell: bash
|
||||
|
||||
- name: Install Go
|
||||
uses: actions/setup-go@v5
|
||||
uses: actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00 # v6.0.0
|
||||
with:
|
||||
go-version: 1.24
|
||||
|
||||
@@ -37,18 +65,21 @@ jobs:
|
||||
TAG=${{ env.TAG }}
|
||||
sed -i "s/export const APP_VERSION = \".*\";/export const APP_VERSION = \"$TAG\";/" server/lib/consts.ts
|
||||
cat server/lib/consts.ts
|
||||
shell: bash
|
||||
|
||||
- name: Pull latest Gerbil version
|
||||
id: get-gerbil-tag
|
||||
run: |
|
||||
LATEST_TAG=$(curl -s https://api.github.com/repos/fosrl/gerbil/tags | jq -r '.[0].name')
|
||||
echo "LATEST_GERBIL_TAG=$LATEST_TAG" >> $GITHUB_ENV
|
||||
shell: bash
|
||||
|
||||
- name: Pull latest Badger version
|
||||
id: get-badger-tag
|
||||
run: |
|
||||
LATEST_TAG=$(curl -s https://api.github.com/repos/fosrl/badger/tags | jq -r '.[0].name')
|
||||
echo "LATEST_BADGER_TAG=$LATEST_TAG" >> $GITHUB_ENV
|
||||
shell: bash
|
||||
|
||||
- name: Update install/main.go
|
||||
run: |
|
||||
@@ -60,6 +91,7 @@ jobs:
|
||||
sed -i "s/config.BadgerVersion = \".*\"/config.BadgerVersion = \"$BADGER_VERSION\"/" install/main.go
|
||||
echo "Updated install/main.go with Pangolin version $PANGOLIN_VERSION, Gerbil version $GERBIL_VERSION, and Badger version $BADGER_VERSION"
|
||||
cat install/main.go
|
||||
shell: bash
|
||||
|
||||
- name: Build installer
|
||||
working-directory: install
|
||||
@@ -67,12 +99,82 @@ jobs:
|
||||
make go-build-release
|
||||
|
||||
- name: Upload artifacts from /install/bin
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
|
||||
with:
|
||||
name: install-bin
|
||||
path: install/bin/
|
||||
|
||||
- name: Build and push Docker images
|
||||
- name: Build and push Docker images (Docker Hub)
|
||||
run: |
|
||||
TAG=${{ env.TAG }}
|
||||
make build-release tag=$TAG
|
||||
echo "Built & pushed to: ${{ env.DOCKERHUB_IMAGE }}:${TAG}"
|
||||
shell: bash
|
||||
|
||||
- name: Install skopeo + jq
|
||||
# skopeo: copy/inspect images between registries
|
||||
# jq: JSON parsing tool used to extract digest values
|
||||
run: |
|
||||
sudo apt-get update -y
|
||||
sudo apt-get install -y skopeo jq
|
||||
skopeo --version
|
||||
shell: bash
|
||||
|
||||
- name: Login to GHCR
|
||||
run: |
|
||||
skopeo login ghcr.io -u "${{ github.actor }}" -p "${{ secrets.GITHUB_TOKEN }}"
|
||||
shell: bash
|
||||
|
||||
- name: Copy tag from Docker Hub to GHCR
|
||||
# Mirror the already-built image (all architectures) to GHCR so we can sign it
|
||||
run: |
|
||||
set -euo pipefail
|
||||
TAG=${{ env.TAG }}
|
||||
echo "Copying ${{ env.DOCKERHUB_IMAGE }}:${TAG} -> ${{ env.GHCR_IMAGE }}:${TAG}"
|
||||
skopeo copy --all --retry-times 3 \
|
||||
docker://$DOCKERHUB_IMAGE:$TAG \
|
||||
docker://$GHCR_IMAGE:$TAG
|
||||
shell: bash
|
||||
|
||||
- name: Install cosign
|
||||
# cosign is used to sign and verify container images (key and keyless)
|
||||
uses: sigstore/cosign-installer@faadad0cce49287aee09b3a48701e75088a2c6ad # v4.0.0
|
||||
|
||||
- name: Dual-sign and verify (GHCR & Docker Hub)
|
||||
# Sign each image by digest using keyless (OIDC) and key-based signing,
|
||||
# then verify both the public key signature and the keyless OIDC signature.
|
||||
env:
|
||||
TAG: ${{ env.TAG }}
|
||||
COSIGN_PRIVATE_KEY: ${{ secrets.COSIGN_PRIVATE_KEY }}
|
||||
COSIGN_PASSWORD: ${{ secrets.COSIGN_PASSWORD }}
|
||||
COSIGN_PUBLIC_KEY: ${{ secrets.COSIGN_PUBLIC_KEY }}
|
||||
COSIGN_YES: "true"
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
issuer="https://token.actions.githubusercontent.com"
|
||||
id_regex="^https://github.com/${{ github.repository }}/.+" # accept this repo (all workflows/refs)
|
||||
|
||||
for IMAGE in "${GHCR_IMAGE}" "${DOCKERHUB_IMAGE}"; do
|
||||
echo "Processing ${IMAGE}:${TAG}"
|
||||
|
||||
DIGEST="$(skopeo inspect --retry-times 3 docker://${IMAGE}:${TAG} | jq -r '.Digest')"
|
||||
REF="${IMAGE}@${DIGEST}"
|
||||
echo "Resolved digest: ${REF}"
|
||||
|
||||
echo "==> cosign sign (keyless) --recursive ${REF}"
|
||||
cosign sign --recursive "${REF}"
|
||||
|
||||
echo "==> cosign sign (key) --recursive ${REF}"
|
||||
cosign sign --key env://COSIGN_PRIVATE_KEY --recursive "${REF}"
|
||||
|
||||
echo "==> cosign verify (public key) ${REF}"
|
||||
cosign verify --key env://COSIGN_PUBLIC_KEY "${REF}" -o text
|
||||
|
||||
echo "==> cosign verify (keyless policy) ${REF}"
|
||||
cosign verify \
|
||||
--certificate-oidc-issuer "${issuer}" \
|
||||
--certificate-identity-regexp "${id_regex}" \
|
||||
"${REF}" -o text
|
||||
done
|
||||
shell: bash
|
||||
|
||||
16
.github/workflows/linting.yml
vendored
16
.github/workflows/linting.yml
vendored
@@ -1,5 +1,8 @@
|
||||
name: ESLint
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
paths:
|
||||
@@ -18,17 +21,18 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v5
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
|
||||
- name: Set up Node.js
|
||||
uses: actions/setup-node@v4
|
||||
uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
|
||||
with:
|
||||
node-version: '22'
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
npm ci
|
||||
run: npm ci
|
||||
|
||||
- name: Create build file
|
||||
run: npm run set:oss
|
||||
|
||||
- name: Run ESLint
|
||||
run: |
|
||||
npx eslint . --ext .js,.jsx,.ts,.tsx
|
||||
run: npx eslint . --ext .js,.jsx,.ts,.tsx
|
||||
|
||||
132
.github/workflows/mirror.yaml
vendored
Normal file
132
.github/workflows/mirror.yaml
vendored
Normal file
@@ -0,0 +1,132 @@
|
||||
name: Mirror & Sign (Docker Hub to GHCR)
|
||||
|
||||
on:
|
||||
workflow_dispatch: {}
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
id-token: write # for keyless OIDC
|
||||
|
||||
env:
|
||||
SOURCE_IMAGE: docker.io/fosrl/pangolin
|
||||
DEST_IMAGE: ghcr.io/${{ github.repository_owner }}/${{ github.event.repository.name }}
|
||||
|
||||
jobs:
|
||||
mirror-and-dual-sign:
|
||||
runs-on: amd64-runner
|
||||
steps:
|
||||
- name: Install skopeo + jq
|
||||
run: |
|
||||
sudo apt-get update -y
|
||||
sudo apt-get install -y skopeo jq
|
||||
skopeo --version
|
||||
|
||||
- name: Install cosign
|
||||
uses: sigstore/cosign-installer@faadad0cce49287aee09b3a48701e75088a2c6ad # v4.0.0
|
||||
|
||||
- name: Input check
|
||||
run: |
|
||||
test -n "${SOURCE_IMAGE}" || (echo "SOURCE_IMAGE is empty" && exit 1)
|
||||
echo "Source : ${SOURCE_IMAGE}"
|
||||
echo "Target : ${DEST_IMAGE}"
|
||||
|
||||
# Auth for skopeo (containers-auth)
|
||||
- name: Skopeo login to GHCR
|
||||
run: |
|
||||
skopeo login ghcr.io -u "${{ github.actor }}" -p "${{ secrets.GITHUB_TOKEN }}"
|
||||
|
||||
# Auth for cosign (docker-config)
|
||||
- name: Docker login to GHCR (for cosign)
|
||||
run: |
|
||||
echo "${{ secrets.GITHUB_TOKEN }}" | docker login ghcr.io -u "${{ github.actor }}" --password-stdin
|
||||
|
||||
- name: List source tags
|
||||
run: |
|
||||
set -euo pipefail
|
||||
skopeo list-tags --retry-times 3 docker://"${SOURCE_IMAGE}" \
|
||||
| jq -r '.Tags[]' | sort -u > src-tags.txt
|
||||
echo "Found source tags: $(wc -l < src-tags.txt)"
|
||||
head -n 20 src-tags.txt || true
|
||||
|
||||
- name: List destination tags (skip existing)
|
||||
run: |
|
||||
set -euo pipefail
|
||||
if skopeo list-tags --retry-times 3 docker://"${DEST_IMAGE}" >/tmp/dst.json 2>/dev/null; then
|
||||
jq -r '.Tags[]' /tmp/dst.json | sort -u > dst-tags.txt
|
||||
else
|
||||
: > dst-tags.txt
|
||||
fi
|
||||
echo "Existing destination tags: $(wc -l < dst-tags.txt)"
|
||||
|
||||
- name: Mirror, dual-sign, and verify
|
||||
env:
|
||||
# keyless
|
||||
COSIGN_YES: "true"
|
||||
# key-based
|
||||
COSIGN_PRIVATE_KEY: ${{ secrets.COSIGN_PRIVATE_KEY }}
|
||||
COSIGN_PASSWORD: ${{ secrets.COSIGN_PASSWORD }}
|
||||
# verify
|
||||
COSIGN_PUBLIC_KEY: ${{ secrets.COSIGN_PUBLIC_KEY }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
copied=0; skipped=0; v_ok=0; errs=0
|
||||
|
||||
issuer="https://token.actions.githubusercontent.com"
|
||||
id_regex="^https://github.com/${{ github.repository }}/.+"
|
||||
|
||||
while read -r tag; do
|
||||
[ -z "$tag" ] && continue
|
||||
|
||||
if grep -Fxq "$tag" dst-tags.txt; then
|
||||
echo "::notice ::Skip (exists) ${DEST_IMAGE}:${tag}"
|
||||
skipped=$((skipped+1))
|
||||
continue
|
||||
fi
|
||||
|
||||
echo "==> Copy ${SOURCE_IMAGE}:${tag} → ${DEST_IMAGE}:${tag}"
|
||||
if ! skopeo copy --all --retry-times 3 \
|
||||
docker://"${SOURCE_IMAGE}:${tag}" docker://"${DEST_IMAGE}:${tag}"; then
|
||||
echo "::warning title=Copy failed::${SOURCE_IMAGE}:${tag}"
|
||||
errs=$((errs+1)); continue
|
||||
fi
|
||||
copied=$((copied+1))
|
||||
|
||||
digest="$(skopeo inspect --retry-times 3 docker://"${DEST_IMAGE}:${tag}" | jq -r '.Digest')"
|
||||
ref="${DEST_IMAGE}@${digest}"
|
||||
|
||||
echo "==> cosign sign (keyless) --recursive ${ref}"
|
||||
if ! cosign sign --recursive "${ref}"; then
|
||||
echo "::warning title=Keyless sign failed::${ref}"
|
||||
errs=$((errs+1))
|
||||
fi
|
||||
|
||||
echo "==> cosign sign (key) --recursive ${ref}"
|
||||
if ! cosign sign --key env://COSIGN_PRIVATE_KEY --recursive "${ref}"; then
|
||||
echo "::warning title=Key sign failed::${ref}"
|
||||
errs=$((errs+1))
|
||||
fi
|
||||
|
||||
echo "==> cosign verify (public key) ${ref}"
|
||||
if ! cosign verify --key env://COSIGN_PUBLIC_KEY "${ref}" -o text; then
|
||||
echo "::warning title=Verify(pubkey) failed::${ref}"
|
||||
errs=$((errs+1))
|
||||
fi
|
||||
|
||||
echo "==> cosign verify (keyless policy) ${ref}"
|
||||
if ! cosign verify \
|
||||
--certificate-oidc-issuer "${issuer}" \
|
||||
--certificate-identity-regexp "${id_regex}" \
|
||||
"${ref}" -o text; then
|
||||
echo "::warning title=Verify(keyless) failed::${ref}"
|
||||
errs=$((errs+1))
|
||||
else
|
||||
v_ok=$((v_ok+1))
|
||||
fi
|
||||
done < src-tags.txt
|
||||
|
||||
echo "---- Summary ----"
|
||||
echo "Copied : $copied"
|
||||
echo "Skipped : $skipped"
|
||||
echo "Verified OK : $v_ok"
|
||||
echo "Errors : $errs"
|
||||
4
.github/workflows/stale-bot.yml
vendored
4
.github/workflows/stale-bot.yml
vendored
@@ -14,7 +14,7 @@ jobs:
|
||||
stale:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/stale@v9
|
||||
- uses: actions/stale@5f858e3efba33a5ca4407a664cc011ad407f2008 # v10.1.0
|
||||
with:
|
||||
days-before-stale: 14
|
||||
days-before-close: 14
|
||||
@@ -34,4 +34,4 @@ jobs:
|
||||
operations-per-run: 100
|
||||
remove-stale-when-updated: true
|
||||
delete-branch: false
|
||||
enable-statistics: true
|
||||
enable-statistics: true
|
||||
|
||||
15
.github/workflows/test.yml
vendored
15
.github/workflows/test.yml
vendored
@@ -1,5 +1,8 @@
|
||||
name: Run Tests
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches:
|
||||
@@ -11,9 +14,9 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
|
||||
- uses: actions/setup-node@v4
|
||||
- uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
|
||||
with:
|
||||
node-version: '22'
|
||||
|
||||
@@ -24,7 +27,10 @@ jobs:
|
||||
run: npm ci
|
||||
|
||||
- name: Create database index.ts
|
||||
run: echo 'export * from "./sqlite";' > server/db/index.ts
|
||||
run: npm run set:sqlite
|
||||
|
||||
- name: Create build file
|
||||
run: npm run set:oss
|
||||
|
||||
- name: Generate database migrations
|
||||
run: npm run db:sqlite:generate
|
||||
@@ -32,6 +38,9 @@ jobs:
|
||||
- name: Apply database migrations
|
||||
run: npm run db:sqlite:push
|
||||
|
||||
- name: Test with tsc
|
||||
run: npx tsc --noEmit
|
||||
|
||||
- name: Start app in background
|
||||
run: nohup npm run dev &
|
||||
|
||||
|
||||
9
.gitignore
vendored
9
.gitignore
vendored
@@ -26,6 +26,10 @@ next-env.d.ts
|
||||
migrations
|
||||
tsconfig.tsbuildinfo
|
||||
config/config.yml
|
||||
config/config.saas.yml
|
||||
config/config.oss.yml
|
||||
config/config.enterprise.yml
|
||||
config/privateConfig.yml
|
||||
config/postgres
|
||||
config/postgres*
|
||||
config/openapi.yaml
|
||||
@@ -43,4 +47,7 @@ server/db/index.ts
|
||||
server/build.ts
|
||||
postgres/
|
||||
dynamic/
|
||||
certificates/
|
||||
*.mmdb
|
||||
scratch/
|
||||
tsconfig.json
|
||||
hydrateSaas.ts
|
||||
@@ -4,7 +4,7 @@ Contributions are welcome!
|
||||
|
||||
Please see the contribution and local development guide on the docs page before getting started:
|
||||
|
||||
https://docs.digpangolin.com/development/contributing
|
||||
https://docs.pangolin.net/development/contributing
|
||||
|
||||
### Licensing Considerations
|
||||
|
||||
|
||||
38
Dockerfile
38
Dockerfile
@@ -1,10 +1,12 @@
|
||||
FROM node:22-alpine AS builder
|
||||
FROM node:25-alpine AS builder
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
ARG BUILD=oss
|
||||
ARG DATABASE=sqlite
|
||||
|
||||
RUN apk add --no-cache curl tzdata python3 make g++
|
||||
|
||||
# COPY package.json package-lock.json ./
|
||||
COPY package*.json ./
|
||||
RUN npm ci
|
||||
@@ -12,23 +14,46 @@ RUN npm ci
|
||||
COPY . .
|
||||
|
||||
RUN echo "export * from \"./$DATABASE\";" > server/db/index.ts
|
||||
RUN echo "export const driver: \"pg\" | \"sqlite\" = \"$DATABASE\";" >> server/db/index.ts
|
||||
|
||||
RUN echo "export const build = \"$BUILD\" as any;" > server/build.ts
|
||||
RUN echo "export const build = \"$BUILD\" as \"saas\" | \"enterprise\" | \"oss\";" > server/build.ts
|
||||
|
||||
RUN if [ "$DATABASE" = "pg" ]; then npx drizzle-kit generate --dialect postgresql --schema ./server/db/pg/schema.ts --out init; else npx drizzle-kit generate --dialect $DATABASE --schema ./server/db/$DATABASE/schema.ts --out init; fi
|
||||
# Copy the appropriate TypeScript configuration based on build type
|
||||
RUN if [ "$BUILD" = "oss" ]; then cp tsconfig.oss.json tsconfig.json; \
|
||||
elif [ "$BUILD" = "saas" ]; then cp tsconfig.saas.json tsconfig.json; \
|
||||
elif [ "$BUILD" = "enterprise" ]; then cp tsconfig.enterprise.json tsconfig.json; \
|
||||
fi
|
||||
|
||||
# if the build is oss then remove the server/private directory
|
||||
RUN if [ "$BUILD" = "oss" ]; then rm -rf server/private; fi
|
||||
|
||||
RUN if [ "$DATABASE" = "pg" ]; then npx drizzle-kit generate --dialect postgresql --schema ./server/db/pg/schema --out init; else npx drizzle-kit generate --dialect $DATABASE --schema ./server/db/$DATABASE/schema --out init; fi
|
||||
|
||||
RUN mkdir -p dist
|
||||
RUN npm run next:build
|
||||
RUN node esbuild.mjs -e server/index.ts -o dist/server.mjs -b $BUILD
|
||||
RUN if [ "$DATABASE" = "pg" ]; then \
|
||||
node esbuild.mjs -e server/setup/migrationsPg.ts -o dist/migrations.mjs; \
|
||||
else \
|
||||
node esbuild.mjs -e server/setup/migrationsSqlite.ts -o dist/migrations.mjs; \
|
||||
fi
|
||||
|
||||
# test to make sure the build output is there and error if not
|
||||
RUN test -f dist/server.mjs
|
||||
|
||||
RUN npm run build:$DATABASE
|
||||
RUN npm run build:cli
|
||||
|
||||
FROM node:22-alpine AS runner
|
||||
FROM node:25-alpine AS runner
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# Curl used for the health checks
|
||||
RUN apk add --no-cache curl
|
||||
# Python and build tools needed for better-sqlite3 native compilation
|
||||
RUN apk add --no-cache curl tzdata python3 make g++
|
||||
|
||||
# COPY package.json package-lock.json ./
|
||||
COPY package*.json ./
|
||||
|
||||
RUN npm ci --omit=dev && npm cache clean --force
|
||||
|
||||
COPY --from=builder /app/.next/standalone ./
|
||||
@@ -40,7 +65,6 @@ COPY ./cli/wrapper.sh /usr/local/bin/pangctl
|
||||
RUN chmod +x /usr/local/bin/pangctl ./dist/cli.mjs
|
||||
|
||||
COPY server/db/names.json ./dist/names.json
|
||||
|
||||
COPY public ./public
|
||||
|
||||
CMD ["npm", "run", "start"]
|
||||
|
||||
31
LICENSE
31
LICENSE
@@ -1,3 +1,34 @@
|
||||
Copyright (c) 2025 Fossorial, Inc.
|
||||
|
||||
Portions of this software are licensed as follows:
|
||||
|
||||
* All files that include a header specifying they are licensed under the
|
||||
"Fossorial Commercial License" are governed by the Fossorial Commercial
|
||||
License terms. The specific terms applicable to each customer depend on the
|
||||
commercial license tier agreed upon in writing with Fossorial, Inc.
|
||||
Unauthorized use, copying, modification, or distribution is strictly
|
||||
prohibited.
|
||||
|
||||
* All files that include a header specifying they are licensed under the GNU
|
||||
Affero General Public License, Version 3 ("AGPL-3"), are governed by the
|
||||
AGPL-3 terms. A full copy of the AGPL-3 license is provided below. However,
|
||||
these files are also available under the Fossorial Commercial License if a
|
||||
separate commercial license agreement has been executed between the customer
|
||||
and Fossorial, Inc.
|
||||
|
||||
* All files without a license header are, by default, licensed under the GNU
|
||||
Affero General Public License, Version 3 (AGPL-3). These files may also be
|
||||
made available under the Fossorial Commercial License upon agreement with
|
||||
Fossorial, Inc.
|
||||
|
||||
* All third-party components included in this repository are licensed under
|
||||
their respective original licenses, as provided by their authors.
|
||||
|
||||
Please consult the header of each individual file to determine the applicable
|
||||
license. For AGPL-3 licensed files, dual-licensing under the Fossorial
|
||||
Commercial License is available subject to written agreement with Fossorial,
|
||||
Inc.
|
||||
|
||||
GNU AFFERO GENERAL PUBLIC LICENSE
|
||||
Version 3, 19 November 2007
|
||||
|
||||
|
||||
72
Makefile
72
Makefile
@@ -1,14 +1,78 @@
|
||||
.PHONY: build build-pg build-release build-arm build-x86 test clean
|
||||
|
||||
major_tag := $(shell echo $(tag) | cut -d. -f1)
|
||||
minor_tag := $(shell echo $(tag) | cut -d. -f1,2)
|
||||
build-release:
|
||||
@if [ -z "$(tag)" ]; then \
|
||||
echo "Error: tag is required. Usage: make build-release tag=<tag>"; \
|
||||
exit 1; \
|
||||
fi
|
||||
docker buildx build --build-arg DATABASE=sqlite --platform linux/arm64,linux/amd64 -t fosrl/pangolin:latest --push .
|
||||
docker buildx build --build-arg DATABASE=sqlite --platform linux/arm64,linux/amd64 -t fosrl/pangolin:$(tag) --push .
|
||||
docker buildx build --build-arg DATABASE=pg --platform linux/arm64,linux/amd64 -t fosrl/pangolin:postgresql-latest --push .
|
||||
docker buildx build --build-arg DATABASE=pg --platform linux/arm64,linux/amd64 -t fosrl/pangolin:postgresql-$(tag) --push .
|
||||
docker buildx build \
|
||||
--build-arg BUILD=oss \
|
||||
--build-arg DATABASE=sqlite \
|
||||
--platform linux/arm64,linux/amd64 \
|
||||
--tag fosrl/pangolin:latest \
|
||||
--tag fosrl/pangolin:$(major_tag) \
|
||||
--tag fosrl/pangolin:$(minor_tag) \
|
||||
--tag fosrl/pangolin:$(tag) \
|
||||
--push .
|
||||
docker buildx build \
|
||||
--build-arg BUILD=oss \
|
||||
--build-arg DATABASE=pg \
|
||||
--platform linux/arm64,linux/amd64 \
|
||||
--tag fosrl/pangolin:postgresql-latest \
|
||||
--tag fosrl/pangolin:postgresql-$(major_tag) \
|
||||
--tag fosrl/pangolin:postgresql-$(minor_tag) \
|
||||
--tag fosrl/pangolin:postgresql-$(tag) \
|
||||
--push .
|
||||
docker buildx build \
|
||||
--build-arg BUILD=enterprise \
|
||||
--build-arg DATABASE=sqlite \
|
||||
--platform linux/arm64,linux/amd64 \
|
||||
--tag fosrl/pangolin:ee-latest \
|
||||
--tag fosrl/pangolin:ee-$(major_tag) \
|
||||
--tag fosrl/pangolin:ee-$(minor_tag) \
|
||||
--tag fosrl/pangolin:ee-$(tag) \
|
||||
--push .
|
||||
docker buildx build \
|
||||
--build-arg BUILD=enterprise \
|
||||
--build-arg DATABASE=pg \
|
||||
--platform linux/arm64,linux/amd64 \
|
||||
--tag fosrl/pangolin:ee-postgresql-latest \
|
||||
--tag fosrl/pangolin:ee-postgresql-$(major_tag) \
|
||||
--tag fosrl/pangolin:ee-postgresql-$(minor_tag) \
|
||||
--tag fosrl/pangolin:ee-postgresql-$(tag) \
|
||||
--push .
|
||||
|
||||
build-rc:
|
||||
@if [ -z "$(tag)" ]; then \
|
||||
echo "Error: tag is required. Usage: make build-release tag=<tag>"; \
|
||||
exit 1; \
|
||||
fi
|
||||
docker buildx build \
|
||||
--build-arg BUILD=oss \
|
||||
--build-arg DATABASE=sqlite \
|
||||
--platform linux/arm64,linux/amd64 \
|
||||
--tag fosrl/pangolin:$(tag) \
|
||||
--push .
|
||||
docker buildx build \
|
||||
--build-arg BUILD=oss \
|
||||
--build-arg DATABASE=pg \
|
||||
--platform linux/arm64,linux/amd64 \
|
||||
--tag fosrl/pangolin:postgresql-$(tag) \
|
||||
--push .
|
||||
docker buildx build \
|
||||
--build-arg BUILD=enterprise \
|
||||
--build-arg DATABASE=sqlite \
|
||||
--platform linux/arm64,linux/amd64 \
|
||||
--tag fosrl/pangolin:ee-$(tag) \
|
||||
--push .
|
||||
docker buildx build \
|
||||
--build-arg BUILD=enterprise \
|
||||
--build-arg DATABASE=pg \
|
||||
--platform linux/arm64,linux/amd64 \
|
||||
--tag fosrl/pangolin:ee-postgresql-$(tag) \
|
||||
--push .
|
||||
|
||||
build-arm:
|
||||
docker buildx build --platform linux/arm64 -t fosrl/pangolin:latest .
|
||||
|
||||
143
README.md
143
README.md
@@ -1,146 +1,95 @@
|
||||
<div align="center">
|
||||
<h2>
|
||||
<picture>
|
||||
<source media="(prefers-color-scheme: dark)" srcset="public/logo/word_mark_white.png">
|
||||
<img alt="Pangolin Logo" src="public/logo/word_mark_black.png" width="250">
|
||||
<a href="https://pangolin.net/">
|
||||
<picture>
|
||||
<source media="(prefers-color-scheme: dark)" srcset="public/logo/word_mark_white.png">
|
||||
<img alt="Pangolin Logo" src="public/logo/word_mark_black.png" width="350">
|
||||
</picture>
|
||||
</a>
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
<h4 align="center">Secure gateway to your private networks</h4>
|
||||
<div align="center">
|
||||
|
||||
_Pangolin tunnels your services to the internet so you can access anything from anywhere._
|
||||
|
||||
</div>
|
||||
|
||||
<div align="center">
|
||||
<h5>
|
||||
<a href="https://digpangolin.com">
|
||||
<a href="https://pangolin.net/">
|
||||
Website
|
||||
</a>
|
||||
<span> | </span>
|
||||
<a href="https://docs.digpangolin.com/self-host/quick-install">
|
||||
Install Guide
|
||||
<a href="https://docs.pangolin.net/">
|
||||
Documentation
|
||||
</a>
|
||||
<span> | </span>
|
||||
<a href="mailto:numbat@fossorial.io">
|
||||
<a href="mailto:contact@pangolin.net">
|
||||
Contact Us
|
||||
</a>
|
||||
</h5>
|
||||
</div>
|
||||
|
||||
<div align="center">
|
||||
|
||||
[](https://discord.gg/HCJR8Xhme4)
|
||||
[](https://pangolin.net/slack)
|
||||
[](https://hub.docker.com/r/fosrl/pangolin)
|
||||

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

|
||||
- Check out the [quick install guide](https://docs.pangolin.net/self-host/quick-install) for how to install and set up Pangolin.
|
||||
- Install from the [DigitalOcean marketplace](https://marketplace.digitalocean.com/apps/pangolin-ce-1?refcode=edf0480eeb81) for a one-click pre-configured installer.
|
||||
|
||||
## Key Features
|
||||
|
||||
### Reverse Proxy Through WireGuard Tunnel
|
||||
|
||||
- Expose private resources on your network **without opening ports** (firewall punching).
|
||||
- Secure and easy to configure private connectivity via a custom **user space WireGuard client**, [Newt](https://github.com/fosrl/newt).
|
||||
- Built-in support for any WireGuard client.
|
||||
- Automated **SSL certificates** (https) via [LetsEncrypt](https://letsencrypt.org/).
|
||||
- Support for HTTP/HTTPS and **raw TCP/UDP services**.
|
||||
- Load balancing.
|
||||
- Extend functionality with existing [Traefik](https://github.com/traefik/traefik) plugins, such as [CrowdSec](https://plugins.traefik.io/plugins/6335346ca4caa9ddeffda116/crowdsec-bouncer-traefik-plugin) and [Geoblock](https://github.com/PascalMinder/geoblock).
|
||||
- **Automatically install and configure Crowdsec via Pangolin's installer script.**
|
||||
- Attach as many sites to the central server as you wish.
|
||||
|
||||
### Identity & Access Management
|
||||
|
||||
- Centralized authentication system using platform SSO. **Users will only have to manage one login.**
|
||||
- **Define access control rules for IPs, IP ranges, and URL paths per resource.**
|
||||
- TOTP with backup codes for two-factor authentication.
|
||||
- Create organizations, each with multiple sites, users, and roles.
|
||||
- **Role-based access control** to manage resource access permissions.
|
||||
- Additional authentication options include:
|
||||
- Email whitelisting with **one-time passcodes.**
|
||||
- **Temporary, self-destructing share links.**
|
||||
- Resource specific pin codes.
|
||||
- Resource specific passwords.
|
||||
- Passkeys
|
||||
- External identity provider (IdP) support with OAuth2/OIDC, such as Authentik, Keycloak, Okta, and others.
|
||||
- Auto-provision users and roles from your IdP.
|
||||
|
||||
<img src="public/auth-diagram1.png" alt="Auth and diagram"/>
|
||||
|
||||
## Use Cases
|
||||
|
||||
### Manage Access to Internal Apps
|
||||
|
||||
- Grant users access to your apps from anywhere using just a web browser. No client software required.
|
||||
|
||||
### Developers and DevOps
|
||||
|
||||
- Expose and test internal tools and dashboards like **Grafana**. Bring localhost or private IPs online for easy access.
|
||||
|
||||
### Secure API Gateway
|
||||
|
||||
- One application load balancer across multiple clouds and on-premises.
|
||||
|
||||
### IoT and Edge Devices
|
||||
|
||||
- Easily expose **IoT devices**, **edge servers**, or **Raspberry Pi** to the internet for field equipment monitoring.
|
||||
|
||||
<img src="public/screenshots/sites.png" alt="Sites"/>
|
||||
<img src="public/screenshots/hero.png" />
|
||||
|
||||
## Deployment Options
|
||||
|
||||
### Fully Self Hosted
|
||||
| <img width=500 /> | Description |
|
||||
|-----------------|--------------|
|
||||
| **Self-Host: Community Edition** | Free, open source, and licensed under AGPL-3. |
|
||||
| **Self-Host: Enterprise Edition** | Licensed under Fossorial Commercial License. Free for personal and hobbyist use, and for businesses earning under \$100K USD annually. |
|
||||
| **Pangolin Cloud** | Fully managed service with instant setup and pay-as-you-go pricing — no infrastructure required. Or, self-host your own [remote node](https://docs.pangolin.net/manage/remote-node/nodes) and connect to our control plane. |
|
||||
|
||||
Host the full application on your own server or on the cloud with a VPS. Take a look at the [documentation](https://docs.digpangolin.com/self-host/quick-install) to get started.
|
||||
## Key Features
|
||||
|
||||
> Many of our users have had a great experience with [RackNerd](https://my.racknerd.com/aff.php?aff=13788). Depending on promotions, you can get a [**VPS with 1 vCPU, 1GB RAM, and ~20GB SSD for just around $12/year**](https://my.racknerd.com/aff.php?aff=13788&pid=912). That's a great deal!
|
||||
Pangolin packages everything you need for seamless application access and exposure into one cohesive platform.
|
||||
|
||||
### Pangolin Cloud
|
||||
| <img width=500 /> | <img width=500 /> |
|
||||
|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|--------------------------------------------------------------------|
|
||||
| **Manage applications in one place**<br /><br /> Pangolin provides a unified dashboard where you can monitor, configure, and secure all of your services regardless of where they are hosted. | <img src="public/screenshots/hero.png" width=500 /><tr></tr> |
|
||||
| **Reverse proxy across networks anywhere**<br /><br />Route traffic via tunnels to any private network. Pangolin works like a reverse proxy that spans multiple networks and handles routing, load balancing, health checking, and more to the right services on the other end. | <img src="public/screenshots/sites.png" width=500 /><tr></tr> |
|
||||
| **Enforce identity and context aware rules**<br /><br />Protect your applications with identity and context aware rules such as SSO, OIDC, PIN, password, temporary share links, geolocation, IP, and more. | <img src="public/auth-diagram1.png" width=500 /><tr></tr> |
|
||||
| **Quickly connect Pangolin sites**<br /><br />Pangolin's lightweight [Newt](https://github.com/fosrl/newt) client runs in userspace and can run anywhere. Use it as a site connector to route traffic to backends across all of your environments. | <img src="public/clip.gif" width=500 /><tr></tr> |
|
||||
|
||||
Easy to use with simple [pay as you go pricing](https://digpangolin.com/pricing). [Check it out here](https://pangolin.fossorial.io/auth/signup).
|
||||
## Get Started
|
||||
|
||||
- Everything you get with self hosted Pangolin, but fully managed for you.
|
||||
### Check out the docs
|
||||
|
||||
### Managed & High Availability
|
||||
We encourage everyone to read the full documentation first, which is
|
||||
available at [docs.pangolin.net](https://docs.pangolin.net). This README provides only a very brief subset of
|
||||
the docs to illustrate some basic ideas.
|
||||
|
||||
Managed control plane, your infrastructure
|
||||
### Sign up and try now
|
||||
|
||||
- We manage database and control plane.
|
||||
- You self-host lightweight exit-node.
|
||||
- Traffic flows through your infra.
|
||||
- We coordinate failover between your nodes or to Cloud when things go bad.
|
||||
|
||||
Try it out using [Pangolin Cloud](https://pangolin.fossorial.io)
|
||||
|
||||
### Full Enterprise On-Premises
|
||||
|
||||
[Contact us](mailto:numbat@fossorial.io) for a full distributed and enterprise deployments on your infrastructure controlled by your team.
|
||||
|
||||
## Project Development / Roadmap
|
||||
|
||||
We want to hear your feature requests! Add them to the [discussion board](https://github.com/orgs/fosrl/discussions/categories/feature-requests).
|
||||
For Pangolin's managed service, you will first need to create an account at
|
||||
[app.pangolin.net](https://app.pangolin.net). We have a generous free tier to get started.
|
||||
|
||||
## Licensing
|
||||
|
||||
Pangolin is dual licensed under the AGPL-3 and the Fossorial Commercial license. For inquiries about commercial licensing, please contact us at [numbat@fossorial.io](mailto:numbat@fossorial.io).
|
||||
Pangolin is dual licensed under the AGPL-3 and the [Fossorial Commercial License](https://pangolin.net/fcl.html). For inquiries about commercial licensing, please contact us at [contact@pangolin.net](mailto:contact@pangolin.net).
|
||||
|
||||
## Contributions
|
||||
|
||||
Looking for something to contribute? Take a look at issues marked with [help wanted](https://github.com/fosrl/pangolin/issues?q=is%3Aissue%20state%3Aopen%20label%3A%22help%20wanted%22). Also take a look through the freature requests in Discussions - any are available and some are marked as a good first issue.
|
||||
|
||||
Please see [CONTRIBUTING](./CONTRIBUTING.md) in the repository for guidelines and best practices.
|
||||
|
||||
Please post bug reports and other functional issues in the [Issues](https://github.com/fosrl/pangolin/issues) section of the repository.
|
||||
---
|
||||
|
||||
WireGuard® is a registered trademark of Jason A. Donenfeld.
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
If you discover a security vulnerability, please follow the steps below to responsibly disclose it to us:
|
||||
|
||||
1. **Do not create a public GitHub issue or discussion post.** This could put the security of other users at risk.
|
||||
2. Send a detailed report to [security@fossorial.io](mailto:security@fossorial.io) or send a **private** message to a maintainer on [Discord](https://discord.gg/HCJR8Xhme4). Include:
|
||||
2. Send a detailed report to [security@pangolin.net](mailto:security@pangolin.net) or send a **private** message to a maintainer on [Discord](https://discord.gg/HCJR8Xhme4). Include:
|
||||
|
||||
- Description and location of the vulnerability.
|
||||
- Potential impact of the vulnerability.
|
||||
|
||||
72
blueprint.py
Normal file
72
blueprint.py
Normal file
@@ -0,0 +1,72 @@
|
||||
import requests
|
||||
import yaml
|
||||
import json
|
||||
import base64
|
||||
|
||||
# The file path for the YAML file to be read
|
||||
# You can change this to the path of your YAML file
|
||||
YAML_FILE_PATH = 'blueprint.yaml'
|
||||
|
||||
# The API endpoint and headers from the curl request
|
||||
API_URL = 'http://api.pangolin.net/v1/org/test/blueprint'
|
||||
HEADERS = {
|
||||
'accept': '*/*',
|
||||
'Authorization': 'Bearer <your_token_here>',
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
|
||||
def convert_and_send(file_path, url, headers):
|
||||
"""
|
||||
Reads a YAML file, converts its content to a JSON payload,
|
||||
and sends it via a PUT request to a specified URL.
|
||||
"""
|
||||
try:
|
||||
# Read the YAML file content
|
||||
with open(file_path, 'r') as file:
|
||||
yaml_content = file.read()
|
||||
|
||||
# Parse the YAML string to a Python dictionary
|
||||
# This will be used to ensure the YAML is valid before sending
|
||||
parsed_yaml = yaml.safe_load(yaml_content)
|
||||
|
||||
# convert the parsed YAML to a JSON string
|
||||
json_payload = json.dumps(parsed_yaml)
|
||||
print("Converted JSON payload:")
|
||||
print(json_payload)
|
||||
|
||||
# Encode the JSON string to Base64
|
||||
encoded_json = base64.b64encode(json_payload.encode('utf-8')).decode('utf-8')
|
||||
|
||||
# Create the final payload with the base64 encoded data
|
||||
final_payload = {
|
||||
"blueprint": encoded_json
|
||||
}
|
||||
|
||||
print("Sending the following Base64 encoded JSON payload:")
|
||||
print(final_payload)
|
||||
print("-" * 20)
|
||||
|
||||
# Make the PUT request with the base64 encoded payload
|
||||
response = requests.put(url, headers=headers, json=final_payload)
|
||||
|
||||
# Print the API response for debugging
|
||||
print(f"API Response Status Code: {response.status_code}")
|
||||
print("API Response Content:")
|
||||
print(response.text)
|
||||
|
||||
# Raise an exception for bad status codes (4xx or 5xx)
|
||||
response.raise_for_status()
|
||||
|
||||
except FileNotFoundError:
|
||||
print(f"Error: The file '{file_path}' was not found.")
|
||||
except yaml.YAMLError as e:
|
||||
print(f"Error parsing YAML file: {e}")
|
||||
except requests.exceptions.RequestException as e:
|
||||
print(f"An error occurred during the API request: {e}")
|
||||
except Exception as e:
|
||||
print(f"An unexpected error occurred: {e}")
|
||||
|
||||
# Run the function
|
||||
if __name__ == "__main__":
|
||||
convert_and_send(YAML_FILE_PATH, API_URL, HEADERS)
|
||||
|
||||
70
blueprint.yaml
Normal file
70
blueprint.yaml
Normal file
@@ -0,0 +1,70 @@
|
||||
client-resources:
|
||||
client-resource-nice-id-uno:
|
||||
name: this is my resource
|
||||
protocol: tcp
|
||||
proxy-port: 3001
|
||||
hostname: localhost
|
||||
internal-port: 3000
|
||||
site: lively-yosemite-toad
|
||||
client-resource-nice-id-duce:
|
||||
name: this is my resource
|
||||
protocol: udp
|
||||
proxy-port: 3000
|
||||
hostname: localhost
|
||||
internal-port: 3000
|
||||
site: lively-yosemite-toad
|
||||
|
||||
proxy-resources:
|
||||
resource-nice-id-uno:
|
||||
name: this is my resource
|
||||
protocol: http
|
||||
full-domain: duce.test.example.com
|
||||
host-header: example.com
|
||||
tls-server-name: example.com
|
||||
# auth:
|
||||
# pincode: 123456
|
||||
# password: sadfasdfadsf
|
||||
# sso-enabled: true
|
||||
# sso-roles:
|
||||
# - Member
|
||||
# sso-users:
|
||||
# - owen@pangolin.net
|
||||
# whitelist-users:
|
||||
# - owen@pangolin.net
|
||||
# auto-login-idp: 1
|
||||
headers:
|
||||
- name: X-Example-Header
|
||||
value: example-value
|
||||
- name: X-Another-Header
|
||||
value: another-value
|
||||
rules:
|
||||
- action: allow
|
||||
match: ip
|
||||
value: 1.1.1.1
|
||||
- action: deny
|
||||
match: cidr
|
||||
value: 2.2.2.2/32
|
||||
- action: pass
|
||||
match: path
|
||||
value: /admin
|
||||
targets:
|
||||
- site: lively-yosemite-toad
|
||||
path: /path
|
||||
pathMatchType: prefix
|
||||
hostname: localhost
|
||||
method: http
|
||||
port: 8000
|
||||
- site: slim-alpine-chipmunk
|
||||
hostname: localhost
|
||||
path: /yoman
|
||||
pathMatchType: exact
|
||||
method: http
|
||||
port: 8001
|
||||
resource-nice-id-duce:
|
||||
name: this is other resource
|
||||
protocol: tcp
|
||||
proxy-port: 3000
|
||||
targets:
|
||||
- site: lively-yosemite-toad
|
||||
hostname: localhost
|
||||
port: 3000
|
||||
17
bruno/API Keys/Create API Key.bru
Normal file
17
bruno/API Keys/Create API Key.bru
Normal file
@@ -0,0 +1,17 @@
|
||||
meta {
|
||||
name: Create API Key
|
||||
type: http
|
||||
seq: 1
|
||||
}
|
||||
|
||||
put {
|
||||
url: http://localhost:3000/api/v1/api-key
|
||||
body: json
|
||||
auth: inherit
|
||||
}
|
||||
|
||||
body:json {
|
||||
{
|
||||
"isRoot": true
|
||||
}
|
||||
}
|
||||
11
bruno/API Keys/Delete API Key.bru
Normal file
11
bruno/API Keys/Delete API Key.bru
Normal file
@@ -0,0 +1,11 @@
|
||||
meta {
|
||||
name: Delete API Key
|
||||
type: http
|
||||
seq: 2
|
||||
}
|
||||
|
||||
delete {
|
||||
url: http://localhost:3000/api/v1/api-key/dm47aacqxxn3ubj
|
||||
body: none
|
||||
auth: inherit
|
||||
}
|
||||
11
bruno/API Keys/List API Key Actions.bru
Normal file
11
bruno/API Keys/List API Key Actions.bru
Normal file
@@ -0,0 +1,11 @@
|
||||
meta {
|
||||
name: List API Key Actions
|
||||
type: http
|
||||
seq: 6
|
||||
}
|
||||
|
||||
get {
|
||||
url: http://localhost:3000/api/v1/api-key/ex0izu2c37fjz9x/actions
|
||||
body: none
|
||||
auth: inherit
|
||||
}
|
||||
11
bruno/API Keys/List Org API Keys.bru
Normal file
11
bruno/API Keys/List Org API Keys.bru
Normal file
@@ -0,0 +1,11 @@
|
||||
meta {
|
||||
name: List Org API Keys
|
||||
type: http
|
||||
seq: 4
|
||||
}
|
||||
|
||||
get {
|
||||
url: http://localhost:3000/api/v1/org/home-lab/api-keys
|
||||
body: none
|
||||
auth: inherit
|
||||
}
|
||||
11
bruno/API Keys/List Root API Keys.bru
Normal file
11
bruno/API Keys/List Root API Keys.bru
Normal file
@@ -0,0 +1,11 @@
|
||||
meta {
|
||||
name: List Root API Keys
|
||||
type: http
|
||||
seq: 3
|
||||
}
|
||||
|
||||
get {
|
||||
url: http://localhost:3000/api/v1/root/api-keys
|
||||
body: none
|
||||
auth: inherit
|
||||
}
|
||||
17
bruno/API Keys/Set API Key Actions.bru
Normal file
17
bruno/API Keys/Set API Key Actions.bru
Normal file
@@ -0,0 +1,17 @@
|
||||
meta {
|
||||
name: Set API Key Actions
|
||||
type: http
|
||||
seq: 5
|
||||
}
|
||||
|
||||
post {
|
||||
url: http://localhost:3000/api/v1/api-key/ex0izu2c37fjz9x/actions
|
||||
body: json
|
||||
auth: inherit
|
||||
}
|
||||
|
||||
body:json {
|
||||
{
|
||||
"actionIds": ["listSites"]
|
||||
}
|
||||
}
|
||||
17
bruno/API Keys/Set API Key Orgs.bru
Normal file
17
bruno/API Keys/Set API Key Orgs.bru
Normal file
@@ -0,0 +1,17 @@
|
||||
meta {
|
||||
name: Set API Key Orgs
|
||||
type: http
|
||||
seq: 7
|
||||
}
|
||||
|
||||
post {
|
||||
url: http://localhost:3000/api/v1/api-key/ex0izu2c37fjz9x/orgs
|
||||
body: json
|
||||
auth: inherit
|
||||
}
|
||||
|
||||
body:json {
|
||||
{
|
||||
"orgIds": ["home-lab"]
|
||||
}
|
||||
}
|
||||
3
bruno/API Keys/folder.bru
Normal file
3
bruno/API Keys/folder.bru
Normal file
@@ -0,0 +1,3 @@
|
||||
meta {
|
||||
name: API Keys
|
||||
}
|
||||
@@ -5,7 +5,7 @@ meta {
|
||||
}
|
||||
|
||||
post {
|
||||
url: http://localhost:3000/api/v1/auth/logout
|
||||
url: http://localhost:4000/api/v1/auth/logout
|
||||
body: none
|
||||
auth: none
|
||||
}
|
||||
|
||||
@@ -12,6 +12,6 @@ post {
|
||||
|
||||
body:json {
|
||||
{
|
||||
"email": "milo@fossorial.io"
|
||||
"email": "milo@pangolin.net"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@ put {
|
||||
|
||||
body:json {
|
||||
{
|
||||
"email": "numbat@fossorial.io",
|
||||
"email": "numbat@pangolin.net",
|
||||
"password": "Password123!"
|
||||
}
|
||||
}
|
||||
|
||||
22
bruno/IDP/Create OIDC Provider.bru
Normal file
22
bruno/IDP/Create OIDC Provider.bru
Normal file
@@ -0,0 +1,22 @@
|
||||
meta {
|
||||
name: Create OIDC Provider
|
||||
type: http
|
||||
seq: 1
|
||||
}
|
||||
|
||||
put {
|
||||
url: http://localhost:3000/api/v1/org/home-lab/idp/oidc
|
||||
body: json
|
||||
auth: inherit
|
||||
}
|
||||
|
||||
body:json {
|
||||
{
|
||||
"clientId": "JJoSvHCZcxnXT2sn6CObj6a21MuKNRXs3kN5wbys",
|
||||
"clientSecret": "2SlGL2wOGgMEWLI9yUuMAeFxre7qSNJVnXMzyepdNzH1qlxYnC4lKhhQ6a157YQEkYH3vm40KK4RCqbYiF8QIweuPGagPX3oGxEj2exwutoXFfOhtq4hHybQKoFq01Z3",
|
||||
"authUrl": "http://localhost:9000/application/o/authorize/",
|
||||
"tokenUrl": "http://localhost:9000/application/o/token/",
|
||||
"scopes": ["email", "openid", "profile"],
|
||||
"userIdentifier": "email"
|
||||
}
|
||||
}
|
||||
11
bruno/IDP/Generate OIDC URL.bru
Normal file
11
bruno/IDP/Generate OIDC URL.bru
Normal file
@@ -0,0 +1,11 @@
|
||||
meta {
|
||||
name: Generate OIDC URL
|
||||
type: http
|
||||
seq: 2
|
||||
}
|
||||
|
||||
get {
|
||||
url: http://localhost:3000/api/v1
|
||||
body: none
|
||||
auth: inherit
|
||||
}
|
||||
3
bruno/IDP/folder.bru
Normal file
3
bruno/IDP/folder.bru
Normal file
@@ -0,0 +1,3 @@
|
||||
meta {
|
||||
name: IDP
|
||||
}
|
||||
11
bruno/Internal/Traefik Config.bru
Normal file
11
bruno/Internal/Traefik Config.bru
Normal file
@@ -0,0 +1,11 @@
|
||||
meta {
|
||||
name: Traefik Config
|
||||
type: http
|
||||
seq: 1
|
||||
}
|
||||
|
||||
get {
|
||||
url: http://localhost:3001/api/v1/traefik-config
|
||||
body: none
|
||||
auth: inherit
|
||||
}
|
||||
3
bruno/Internal/folder.bru
Normal file
3
bruno/Internal/folder.bru
Normal file
@@ -0,0 +1,3 @@
|
||||
meta {
|
||||
name: Internal
|
||||
}
|
||||
15
bruno/Olm/createOlm.bru
Normal file
15
bruno/Olm/createOlm.bru
Normal file
@@ -0,0 +1,15 @@
|
||||
meta {
|
||||
name: createOlm
|
||||
type: http
|
||||
seq: 1
|
||||
}
|
||||
|
||||
put {
|
||||
url: http://localhost:3000/api/v1/olm
|
||||
body: none
|
||||
auth: inherit
|
||||
}
|
||||
|
||||
settings {
|
||||
encodeUrl: true
|
||||
}
|
||||
8
bruno/Olm/folder.bru
Normal file
8
bruno/Olm/folder.bru
Normal file
@@ -0,0 +1,8 @@
|
||||
meta {
|
||||
name: Olm
|
||||
seq: 15
|
||||
}
|
||||
|
||||
auth {
|
||||
mode: inherit
|
||||
}
|
||||
11
bruno/Remote Exit Node/createRemoteExitNode.bru
Normal file
11
bruno/Remote Exit Node/createRemoteExitNode.bru
Normal file
@@ -0,0 +1,11 @@
|
||||
meta {
|
||||
name: createRemoteExitNode
|
||||
type: http
|
||||
seq: 1
|
||||
}
|
||||
|
||||
put {
|
||||
url: http://localhost:4000/api/v1/org/org_i21aifypnlyxur2/remote-exit-node
|
||||
body: none
|
||||
auth: none
|
||||
}
|
||||
11
bruno/Test.bru
Normal file
11
bruno/Test.bru
Normal file
@@ -0,0 +1,11 @@
|
||||
meta {
|
||||
name: Test
|
||||
type: http
|
||||
seq: 2
|
||||
}
|
||||
|
||||
get {
|
||||
url: http://localhost:3000/api/v1
|
||||
body: none
|
||||
auth: inherit
|
||||
}
|
||||
@@ -90,7 +90,8 @@ export const setAdminCredentials: CommandModule<{}, SetAdminCredentialsArgs> = {
|
||||
passwordHash,
|
||||
dateCreated: moment().toISOString(),
|
||||
serverAdmin: true,
|
||||
emailVerified: true
|
||||
emailVerified: true,
|
||||
lastPasswordChange: new Date().getTime()
|
||||
});
|
||||
|
||||
console.log("Server admin created");
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# To see all available options, please visit the docs:
|
||||
# https://docs.digpangolin.com/self-host/advanced/config-file
|
||||
# https://docs.pangolin.net/self-host/advanced/config-file
|
||||
|
||||
app:
|
||||
dashboard_url: http://localhost:3002
|
||||
@@ -25,4 +25,3 @@ flags:
|
||||
disable_user_create_org: true
|
||||
allow_raw_resources: true
|
||||
enable_integration_api: true
|
||||
enable_clients: true
|
||||
|
||||
@@ -16,8 +16,9 @@ http:
|
||||
|
||||
# Next.js router (handles everything except API and WebSocket paths)
|
||||
next-router:
|
||||
rule: "Host(`{{.DashboardDomain}}`) && !PathPrefix(`/api/v1`)"
|
||||
rule: "Host(`{{.DashboardDomain}}`)"
|
||||
service: next-service
|
||||
priority: 10
|
||||
entryPoints:
|
||||
- websecure
|
||||
tls:
|
||||
@@ -27,15 +28,7 @@ http:
|
||||
api-router:
|
||||
rule: "Host(`{{.DashboardDomain}}`) && PathPrefix(`/api/v1`)"
|
||||
service: api-service
|
||||
entryPoints:
|
||||
- websecure
|
||||
tls:
|
||||
certResolver: letsencrypt
|
||||
|
||||
# WebSocket router
|
||||
ws-router:
|
||||
rule: "Host(`{{.DashboardDomain}}`)"
|
||||
service: api-service
|
||||
priority: 100
|
||||
entryPoints:
|
||||
- websecure
|
||||
tls:
|
||||
|
||||
15
docker-compose.drizzle.yml
Normal file
15
docker-compose.drizzle.yml
Normal file
@@ -0,0 +1,15 @@
|
||||
services:
|
||||
drizzle-gateway:
|
||||
image: ghcr.io/drizzle-team/gateway:latest
|
||||
ports:
|
||||
- "4984:4983"
|
||||
depends_on:
|
||||
- db
|
||||
environment:
|
||||
- STORE_PATH=/app
|
||||
- DATABASE_URL=postgresql://postgres:password@db:5432/postgres
|
||||
volumes:
|
||||
- drizzle-gateway-data:/app
|
||||
|
||||
volumes:
|
||||
drizzle-gateway-data:
|
||||
@@ -20,7 +20,7 @@ services:
|
||||
pangolin:
|
||||
condition: service_healthy
|
||||
command:
|
||||
- --reachableAt=http://gerbil:3003
|
||||
- --reachableAt=http://gerbil:3004
|
||||
- --generateAndSaveKeyTo=/var/config/key
|
||||
- --remoteConfig=http://pangolin:3001/api/v1/
|
||||
volumes:
|
||||
@@ -35,7 +35,7 @@ services:
|
||||
- 80:80 # Port for traefik because of the network_mode
|
||||
|
||||
traefik:
|
||||
image: traefik:v3.5
|
||||
image: traefik:v3.6
|
||||
container_name: traefik
|
||||
restart: unless-stopped
|
||||
network_mode: service:gerbil # Ports appear on the gerbil service
|
||||
@@ -52,4 +52,4 @@ networks:
|
||||
default:
|
||||
driver: bridge
|
||||
name: pangolin
|
||||
enable_ipv6: true
|
||||
enable_ipv6: true
|
||||
|
||||
@@ -12,3 +12,10 @@ services:
|
||||
ports:
|
||||
- "5432:5432" # Map host port 5432 to container port 5432
|
||||
restart: no
|
||||
|
||||
redis:
|
||||
image: redis:latest # Use the latest Redis image
|
||||
container_name: dev_redis # Name your Redis container
|
||||
ports:
|
||||
- "6379:6379" # Map host port 6379 to container port 6379
|
||||
restart: no
|
||||
@@ -1,32 +0,0 @@
|
||||
name: pangolin
|
||||
services:
|
||||
gerbil:
|
||||
image: gerbil
|
||||
container_name: gerbil
|
||||
network_mode: host
|
||||
restart: unless-stopped
|
||||
command:
|
||||
- --reachableAt=http://localhost:3003
|
||||
- --generateAndSaveKeyTo=/var/config/key
|
||||
- --remoteConfig=http://localhost:3001/api/v1/
|
||||
- --sni-port=443
|
||||
volumes:
|
||||
- ./config/:/var/config
|
||||
cap_add:
|
||||
- NET_ADMIN
|
||||
- SYS_MODULE
|
||||
|
||||
traefik:
|
||||
image: docker.io/traefik:v3.4.1
|
||||
container_name: traefik
|
||||
restart: unless-stopped
|
||||
network_mode: host
|
||||
command:
|
||||
- --configFile=/etc/traefik/traefik_config.yml
|
||||
volumes:
|
||||
- ./config/traefik:/etc/traefik:ro # Volume to store the Traefik configuration
|
||||
- ./config/letsencrypt:/letsencrypt # Volume to store the Let's Encrypt certificates
|
||||
- ./config/traefik/logs:/var/log/traefik # Volume to store Traefik logs
|
||||
- ./certificates:/var/certificates:ro
|
||||
- ./dynamic:/var/dynamic:ro
|
||||
|
||||
@@ -13,7 +13,6 @@ services:
|
||||
environment:
|
||||
- NODE_ENV=development
|
||||
- ENVIRONMENT=dev
|
||||
- DB_TYPE=pg
|
||||
volumes:
|
||||
# Mount source code for hot reload
|
||||
- ./src:/app/src
|
||||
|
||||
@@ -1,9 +1,13 @@
|
||||
import { defineConfig } from "drizzle-kit";
|
||||
import path from "path";
|
||||
|
||||
const schema = [
|
||||
path.join("server", "db", "pg", "schema"),
|
||||
];
|
||||
|
||||
export default defineConfig({
|
||||
dialect: "postgresql",
|
||||
schema: [path.join("server", "db", "pg", "schema.ts")],
|
||||
schema: schema,
|
||||
out: path.join("server", "migrations"),
|
||||
verbose: true,
|
||||
dbCredentials: {
|
||||
|
||||
@@ -2,9 +2,13 @@ import { APP_PATH } from "@server/lib/consts";
|
||||
import { defineConfig } from "drizzle-kit";
|
||||
import path from "path";
|
||||
|
||||
const schema = [
|
||||
path.join("server", "db", "sqlite", "schema"),
|
||||
];
|
||||
|
||||
export default defineConfig({
|
||||
dialect: "sqlite",
|
||||
schema: path.join("server", "db", "sqlite", "schema.ts"),
|
||||
schema: schema,
|
||||
out: path.join("server", "migrations"),
|
||||
verbose: true,
|
||||
dbCredentials: {
|
||||
|
||||
214
esbuild.mjs
214
esbuild.mjs
@@ -2,8 +2,9 @@ import esbuild from "esbuild";
|
||||
import yargs from "yargs";
|
||||
import { hideBin } from "yargs/helpers";
|
||||
import { nodeExternalsPlugin } from "esbuild-node-externals";
|
||||
import path from "path";
|
||||
import fs from "fs";
|
||||
// import { glob } from "glob";
|
||||
// import path from "path";
|
||||
|
||||
const banner = `
|
||||
// patch __dirname
|
||||
@@ -18,7 +19,7 @@ const require = topLevelCreateRequire(import.meta.url);
|
||||
`;
|
||||
|
||||
const argv = yargs(hideBin(process.argv))
|
||||
.usage("Usage: $0 -entry [string] -out [string]")
|
||||
.usage("Usage: $0 -entry [string] -out [string] -build [string]")
|
||||
.option("entry", {
|
||||
alias: "e",
|
||||
describe: "Entry point file",
|
||||
@@ -31,6 +32,13 @@ const argv = yargs(hideBin(process.argv))
|
||||
type: "string",
|
||||
demandOption: true,
|
||||
})
|
||||
.option("build", {
|
||||
alias: "b",
|
||||
describe: "Build type (oss, saas, enterprise)",
|
||||
type: "string",
|
||||
choices: ["oss", "saas", "enterprise"],
|
||||
default: "oss",
|
||||
})
|
||||
.help()
|
||||
.alias("help", "h").argv;
|
||||
|
||||
@@ -46,27 +54,223 @@ function getPackagePaths() {
|
||||
return ["package.json"];
|
||||
}
|
||||
|
||||
// Plugin to guard against bad imports from #private
|
||||
function privateImportGuardPlugin() {
|
||||
return {
|
||||
name: "private-import-guard",
|
||||
setup(build) {
|
||||
const violations = [];
|
||||
|
||||
build.onResolve({ filter: /^#private\// }, (args) => {
|
||||
const importingFile = args.importer;
|
||||
|
||||
// Check if the importing file is NOT in server/private
|
||||
const normalizedImporter = path.normalize(importingFile);
|
||||
const isInServerPrivate = normalizedImporter.includes(path.normalize("server/private"));
|
||||
|
||||
if (!isInServerPrivate) {
|
||||
const violation = {
|
||||
file: importingFile,
|
||||
importPath: args.path,
|
||||
resolveDir: args.resolveDir
|
||||
};
|
||||
violations.push(violation);
|
||||
|
||||
console.log(`PRIVATE IMPORT VIOLATION:`);
|
||||
console.log(` File: ${importingFile}`);
|
||||
console.log(` Import: ${args.path}`);
|
||||
console.log(` Resolve dir: ${args.resolveDir || 'N/A'}`);
|
||||
console.log('');
|
||||
}
|
||||
|
||||
// Return null to let the default resolver handle it
|
||||
return null;
|
||||
});
|
||||
|
||||
build.onEnd((result) => {
|
||||
if (violations.length > 0) {
|
||||
console.log(`\nSUMMARY: Found ${violations.length} private import violation(s):`);
|
||||
violations.forEach((v, i) => {
|
||||
console.log(` ${i + 1}. ${path.relative(process.cwd(), v.file)} imports ${v.importPath}`);
|
||||
});
|
||||
console.log('');
|
||||
|
||||
result.errors.push({
|
||||
text: `Private import violations detected: ${violations.length} violation(s) found`,
|
||||
location: null,
|
||||
notes: violations.map(v => ({
|
||||
text: `${path.relative(process.cwd(), v.file)} imports ${v.importPath}`,
|
||||
location: null
|
||||
}))
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// Plugin to guard against bad imports from #private
|
||||
function dynamicImportGuardPlugin() {
|
||||
return {
|
||||
name: "dynamic-import-guard",
|
||||
setup(build) {
|
||||
const violations = [];
|
||||
|
||||
build.onResolve({ filter: /^#dynamic\// }, (args) => {
|
||||
const importingFile = args.importer;
|
||||
|
||||
// Check if the importing file is NOT in server/private
|
||||
const normalizedImporter = path.normalize(importingFile);
|
||||
const isInServerPrivate = normalizedImporter.includes(path.normalize("server/private"));
|
||||
|
||||
if (isInServerPrivate) {
|
||||
const violation = {
|
||||
file: importingFile,
|
||||
importPath: args.path,
|
||||
resolveDir: args.resolveDir
|
||||
};
|
||||
violations.push(violation);
|
||||
|
||||
console.log(`DYNAMIC IMPORT VIOLATION:`);
|
||||
console.log(` File: ${importingFile}`);
|
||||
console.log(` Import: ${args.path}`);
|
||||
console.log(` Resolve dir: ${args.resolveDir || 'N/A'}`);
|
||||
console.log('');
|
||||
}
|
||||
|
||||
// Return null to let the default resolver handle it
|
||||
return null;
|
||||
});
|
||||
|
||||
build.onEnd((result) => {
|
||||
if (violations.length > 0) {
|
||||
console.log(`\nSUMMARY: Found ${violations.length} dynamic import violation(s):`);
|
||||
violations.forEach((v, i) => {
|
||||
console.log(` ${i + 1}. ${path.relative(process.cwd(), v.file)} imports ${v.importPath}`);
|
||||
});
|
||||
console.log('');
|
||||
|
||||
result.errors.push({
|
||||
text: `Dynamic import violations detected: ${violations.length} violation(s) found`,
|
||||
location: null,
|
||||
notes: violations.map(v => ({
|
||||
text: `${path.relative(process.cwd(), v.file)} imports ${v.importPath}`,
|
||||
location: null
|
||||
}))
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// Plugin to dynamically switch imports based on build type
|
||||
function dynamicImportSwitcherPlugin(buildValue) {
|
||||
return {
|
||||
name: "dynamic-import-switcher",
|
||||
setup(build) {
|
||||
const switches = [];
|
||||
|
||||
build.onStart(() => {
|
||||
console.log(`Dynamic import switcher using build type: ${buildValue}`);
|
||||
});
|
||||
|
||||
build.onResolve({ filter: /^#dynamic\// }, (args) => {
|
||||
// Extract the path after #dynamic/
|
||||
const dynamicPath = args.path.replace(/^#dynamic\//, '');
|
||||
|
||||
// Determine the replacement based on build type
|
||||
let replacement;
|
||||
if (buildValue === "oss") {
|
||||
replacement = `#open/${dynamicPath}`;
|
||||
} else if (buildValue === "saas" || buildValue === "enterprise") {
|
||||
replacement = `#closed/${dynamicPath}`; // We use #closed here so that the route guards dont complain after its been changed but this is the same as #private
|
||||
} else {
|
||||
console.warn(`Unknown build type '${buildValue}', defaulting to #open/`);
|
||||
replacement = `#open/${dynamicPath}`;
|
||||
}
|
||||
|
||||
const switchInfo = {
|
||||
file: args.importer,
|
||||
originalPath: args.path,
|
||||
replacementPath: replacement,
|
||||
buildType: buildValue
|
||||
};
|
||||
switches.push(switchInfo);
|
||||
|
||||
console.log(`DYNAMIC IMPORT SWITCH:`);
|
||||
console.log(` File: ${args.importer}`);
|
||||
console.log(` Original: ${args.path}`);
|
||||
console.log(` Switched to: ${replacement} (build: ${buildValue})`);
|
||||
console.log('');
|
||||
|
||||
// Rewrite the import path and let the normal resolution continue
|
||||
return build.resolve(replacement, {
|
||||
importer: args.importer,
|
||||
namespace: args.namespace,
|
||||
resolveDir: args.resolveDir,
|
||||
kind: args.kind
|
||||
});
|
||||
});
|
||||
|
||||
build.onEnd((result) => {
|
||||
if (switches.length > 0) {
|
||||
console.log(`\nDYNAMIC IMPORT SUMMARY: Switched ${switches.length} import(s) for build type '${buildValue}':`);
|
||||
switches.forEach((s, i) => {
|
||||
console.log(` ${i + 1}. ${path.relative(process.cwd(), s.file)}`);
|
||||
console.log(` ${s.originalPath} → ${s.replacementPath}`);
|
||||
});
|
||||
console.log('');
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
esbuild
|
||||
.build({
|
||||
entryPoints: [argv.entry],
|
||||
bundle: true,
|
||||
outfile: argv.out,
|
||||
format: "esm",
|
||||
minify: true,
|
||||
minify: false,
|
||||
banner: {
|
||||
js: banner,
|
||||
},
|
||||
platform: "node",
|
||||
external: ["body-parser"],
|
||||
plugins: [
|
||||
privateImportGuardPlugin(),
|
||||
dynamicImportGuardPlugin(),
|
||||
dynamicImportSwitcherPlugin(argv.build),
|
||||
nodeExternalsPlugin({
|
||||
packagePath: getPackagePaths(),
|
||||
}),
|
||||
],
|
||||
sourcemap: true,
|
||||
sourcemap: "inline",
|
||||
target: "node22",
|
||||
})
|
||||
.then(() => {
|
||||
.then((result) => {
|
||||
// Check if there were any errors in the build result
|
||||
if (result.errors && result.errors.length > 0) {
|
||||
console.error(`Build failed with ${result.errors.length} error(s):`);
|
||||
result.errors.forEach((error, i) => {
|
||||
console.error(`${i + 1}. ${error.text}`);
|
||||
if (error.notes) {
|
||||
error.notes.forEach(note => {
|
||||
console.error(` - ${note.text}`);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// remove the output file if it was created
|
||||
if (fs.existsSync(argv.out)) {
|
||||
fs.unlinkSync(argv.out);
|
||||
}
|
||||
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
console.log("Build completed successfully");
|
||||
})
|
||||
.catch((error) => {
|
||||
|
||||
@@ -18,7 +18,11 @@ put-back:
|
||||
mv main.go.bak main.go
|
||||
|
||||
dev-update-versions:
|
||||
PANGOLIN_VERSION=$$(curl -s https://api.github.com/repos/fosrl/pangolin/tags | jq -r '.[0].name') && \
|
||||
if [ -z "$(tag)" ]; then \
|
||||
PANGOLIN_VERSION=$$(curl -s https://api.github.com/repos/fosrl/pangolin/tags | jq -r '.[0].name'); \
|
||||
else \
|
||||
PANGOLIN_VERSION=$(tag); \
|
||||
fi && \
|
||||
GERBIL_VERSION=$$(curl -s https://api.github.com/repos/fosrl/gerbil/tags | jq -r '.[0].name') && \
|
||||
BADGER_VERSION=$$(curl -s https://api.github.com/repos/fosrl/badger/tags | jq -r '.[0].name') && \
|
||||
echo "Latest versions - Pangolin: $$PANGOLIN_VERSION, Gerbil: $$GERBIL_VERSION, Badger: $$BADGER_VERSION" && \
|
||||
|
||||
@@ -1,23 +1,19 @@
|
||||
# To see all available options, please visit the docs:
|
||||
# https://docs.digpangolin.com/self-host/advanced/config-file
|
||||
# https://docs.pangolin.net/
|
||||
|
||||
gerbil:
|
||||
start_port: 51820
|
||||
base_endpoint: "{{.DashboardDomain}}"
|
||||
{{if .HybridMode}}
|
||||
managed:
|
||||
id: "{{.HybridId}}"
|
||||
secret: "{{.HybridSecret}}"
|
||||
|
||||
{{else}}
|
||||
|
||||
app:
|
||||
dashboard_url: "https://{{.DashboardDomain}}"
|
||||
log_level: "info"
|
||||
telemetry:
|
||||
anonymous_usage: true
|
||||
|
||||
domains:
|
||||
domain1:
|
||||
base_domain: "{{.BaseDomain}}"
|
||||
cert_resolver: "letsencrypt"
|
||||
|
||||
server:
|
||||
secret: "{{.Secret}}"
|
||||
@@ -26,6 +22,7 @@ server:
|
||||
methods: ["GET", "POST", "PUT", "DELETE", "PATCH"]
|
||||
allowed_headers: ["X-CSRF-Token", "Content-Type"]
|
||||
credentials: false
|
||||
{{if .EnableGeoblocking}}maxmind_db_path: "./config/GeoLite2-Country.mmdb"{{end}}
|
||||
{{if .EnableEmail}}
|
||||
email:
|
||||
smtp_host: "{{.EmailSMTPHost}}"
|
||||
@@ -39,4 +36,3 @@ flags:
|
||||
disable_signup_without_invite: true
|
||||
disable_user_create_org: false
|
||||
allow_raw_resources: true
|
||||
{{end}}
|
||||
@@ -6,8 +6,6 @@ services:
|
||||
restart: unless-stopped
|
||||
volumes:
|
||||
- ./config:/app/config
|
||||
- pangolin-data:/var/certificates
|
||||
- pangolin-data:/var/dynamic
|
||||
healthcheck:
|
||||
test: ["CMD", "curl", "-f", "http://localhost:3001/api/v1/"]
|
||||
interval: "10s"
|
||||
@@ -22,7 +20,7 @@ services:
|
||||
pangolin:
|
||||
condition: service_healthy
|
||||
command:
|
||||
- --reachableAt=http://gerbil:3003
|
||||
- --reachableAt=http://gerbil:3004
|
||||
- --generateAndSaveKeyTo=/var/config/key
|
||||
- --remoteConfig=http://pangolin:3001/api/v1/
|
||||
volumes:
|
||||
@@ -33,11 +31,11 @@ services:
|
||||
ports:
|
||||
- 51820:51820/udp
|
||||
- 21820:21820/udp
|
||||
- 443:{{if .HybridMode}}8443{{else}}443{{end}}
|
||||
- 443:443
|
||||
- 80:80
|
||||
{{end}}
|
||||
traefik:
|
||||
image: docker.io/traefik:v3.5
|
||||
image: docker.io/traefik:v3.6
|
||||
container_name: traefik
|
||||
restart: unless-stopped
|
||||
{{if .InstallGerbil}}
|
||||
@@ -56,15 +54,9 @@ services:
|
||||
- ./config/traefik:/etc/traefik:ro # Volume to store the Traefik configuration
|
||||
- ./config/letsencrypt:/letsencrypt # Volume to store the Let's Encrypt certificates
|
||||
- ./config/traefik/logs:/var/log/traefik # Volume to store Traefik logs
|
||||
# Shared volume for certificates and dynamic config in file mode
|
||||
- pangolin-data:/var/certificates:ro
|
||||
- pangolin-data:/var/dynamic:ro
|
||||
|
||||
networks:
|
||||
default:
|
||||
driver: bridge
|
||||
name: pangolin
|
||||
{{if .EnableIPv6}} enable_ipv6: true{{end}}
|
||||
|
||||
volumes:
|
||||
pangolin-data:
|
||||
|
||||
@@ -51,3 +51,12 @@ http:
|
||||
loadBalancer:
|
||||
servers:
|
||||
- url: "http://pangolin:3000" # API/WebSocket server
|
||||
|
||||
tcp:
|
||||
serversTransports:
|
||||
pp-transport-v1:
|
||||
proxyProtocol:
|
||||
version: 1
|
||||
pp-transport-v2:
|
||||
proxyProtocol:
|
||||
version: 2
|
||||
@@ -3,17 +3,12 @@ api:
|
||||
dashboard: true
|
||||
|
||||
providers:
|
||||
{{if not .HybridMode}}
|
||||
http:
|
||||
endpoint: "http://pangolin:3001/api/v1/traefik-config"
|
||||
pollInterval: "5s"
|
||||
file:
|
||||
filename: "/etc/traefik/dynamic_config.yml"
|
||||
{{else}}
|
||||
file:
|
||||
directory: "/var/dynamic"
|
||||
watch: true
|
||||
{{end}}
|
||||
|
||||
experimental:
|
||||
plugins:
|
||||
badger:
|
||||
@@ -27,7 +22,7 @@ log:
|
||||
maxBackups: 3
|
||||
maxAge: 3
|
||||
compress: true
|
||||
{{if not .HybridMode}}
|
||||
|
||||
certificatesResolvers:
|
||||
letsencrypt:
|
||||
acme:
|
||||
@@ -36,7 +31,7 @@ certificatesResolvers:
|
||||
email: "{{.LetsEncryptEmail}}"
|
||||
storage: "/letsencrypt/acme.json"
|
||||
caServer: "https://acme-v02.api.letsencrypt.org/directory"
|
||||
{{end}}
|
||||
|
||||
entryPoints:
|
||||
web:
|
||||
address: ":80"
|
||||
@@ -45,9 +40,9 @@ entryPoints:
|
||||
transport:
|
||||
respondingTimeouts:
|
||||
readTimeout: "30m"
|
||||
{{if not .HybridMode}} http:
|
||||
http:
|
||||
tls:
|
||||
certResolver: "letsencrypt"{{end}}
|
||||
certResolver: "letsencrypt"
|
||||
|
||||
serversTransport:
|
||||
insecureSkipVerify: true
|
||||
|
||||
@@ -73,7 +73,7 @@ func installDocker() error {
|
||||
case strings.Contains(osRelease, "ID=ubuntu"):
|
||||
installCmd = exec.Command("bash", "-c", fmt.Sprintf(`
|
||||
apt-get update &&
|
||||
apt-get install -y apt-transport-https ca-certificates curl software-properties-common &&
|
||||
apt-get install -y apt-transport-https ca-certificates curl &&
|
||||
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | gpg --dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg &&
|
||||
echo "deb [arch=%s signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable" > /etc/apt/sources.list.d/docker.list &&
|
||||
apt-get update &&
|
||||
@@ -82,7 +82,7 @@ func installDocker() error {
|
||||
case strings.Contains(osRelease, "ID=debian"):
|
||||
installCmd = exec.Command("bash", "-c", fmt.Sprintf(`
|
||||
apt-get update &&
|
||||
apt-get install -y apt-transport-https ca-certificates curl software-properties-common &&
|
||||
apt-get install -y apt-transport-https ca-certificates curl &&
|
||||
curl -fsSL https://download.docker.com/linux/debian/gpg | gpg --dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg &&
|
||||
echo "deb [arch=%s signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] https://download.docker.com/linux/debian $(lsb_release -cs) stable" > /etc/apt/sources.list.d/docker.list &&
|
||||
apt-get update &&
|
||||
|
||||
180
install/get-installer.sh
Normal file
180
install/get-installer.sh
Normal file
@@ -0,0 +1,180 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Get installer - Cross-platform installation script
|
||||
# Usage: curl -fsSL https://raw.githubusercontent.com/fosrl/installer/refs/heads/main/get-installer.sh | bash
|
||||
|
||||
set -e
|
||||
|
||||
# Colors for output
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
NC='\033[0m' # No Color
|
||||
|
||||
# GitHub repository info
|
||||
REPO="fosrl/pangolin"
|
||||
GITHUB_API_URL="https://api.github.com/repos/${REPO}/releases/latest"
|
||||
|
||||
# Function to print colored output
|
||||
print_status() {
|
||||
echo -e "${GREEN}[INFO]${NC} $1"
|
||||
}
|
||||
|
||||
print_warning() {
|
||||
echo -e "${YELLOW}[WARN]${NC} $1"
|
||||
}
|
||||
|
||||
print_error() {
|
||||
echo -e "${RED}[ERROR]${NC} $1"
|
||||
}
|
||||
|
||||
# Function to get latest version from GitHub API
|
||||
get_latest_version() {
|
||||
local latest_info
|
||||
|
||||
if command -v curl >/dev/null 2>&1; then
|
||||
latest_info=$(curl -fsSL "$GITHUB_API_URL" 2>/dev/null)
|
||||
elif command -v wget >/dev/null 2>&1; then
|
||||
latest_info=$(wget -qO- "$GITHUB_API_URL" 2>/dev/null)
|
||||
else
|
||||
print_error "Neither curl nor wget is available. Please install one of them." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ -z "$latest_info" ]; then
|
||||
print_error "Failed to fetch latest version information" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Extract version from JSON response (works without jq)
|
||||
local version=$(echo "$latest_info" | grep '"tag_name"' | head -1 | sed 's/.*"tag_name": *"\([^"]*\)".*/\1/')
|
||||
|
||||
if [ -z "$version" ]; then
|
||||
print_error "Could not parse version from GitHub API response" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Remove 'v' prefix if present
|
||||
version=$(echo "$version" | sed 's/^v//')
|
||||
|
||||
echo "$version"
|
||||
}
|
||||
|
||||
# Detect OS and architecture
|
||||
detect_platform() {
|
||||
local os arch
|
||||
|
||||
# Detect OS - only support Linux
|
||||
case "$(uname -s)" in
|
||||
Linux*) os="linux" ;;
|
||||
*)
|
||||
print_error "Unsupported operating system: $(uname -s). Only Linux is supported."
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
# Detect architecture - only support amd64 and arm64
|
||||
case "$(uname -m)" in
|
||||
x86_64|amd64) arch="amd64" ;;
|
||||
arm64|aarch64) arch="arm64" ;;
|
||||
*)
|
||||
print_error "Unsupported architecture: $(uname -m). Only amd64 and arm64 are supported on Linux."
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
echo "${os}_${arch}"
|
||||
}
|
||||
|
||||
# Get installation directory
|
||||
get_install_dir() {
|
||||
# Install to the current directory
|
||||
local install_dir="$(pwd)"
|
||||
if [ ! -d "$install_dir" ]; then
|
||||
print_error "Installation directory does not exist: $install_dir"
|
||||
exit 1
|
||||
fi
|
||||
echo "$install_dir"
|
||||
}
|
||||
|
||||
# Download and install installer
|
||||
install_installer() {
|
||||
local platform="$1"
|
||||
local install_dir="$2"
|
||||
local binary_name="installer_${platform}"
|
||||
|
||||
local download_url="${BASE_URL}/${binary_name}"
|
||||
local temp_file="/tmp/installer"
|
||||
local final_path="${install_dir}/installer"
|
||||
|
||||
print_status "Downloading installer from ${download_url}"
|
||||
|
||||
# Download the binary
|
||||
if command -v curl >/dev/null 2>&1; then
|
||||
curl -fsSL "$download_url" -o "$temp_file"
|
||||
elif command -v wget >/dev/null 2>&1; then
|
||||
wget -q "$download_url" -O "$temp_file"
|
||||
else
|
||||
print_error "Neither curl nor wget is available. Please install one of them."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Create install directory if it doesn't exist
|
||||
mkdir -p "$install_dir"
|
||||
|
||||
# Move binary to install directory
|
||||
mv "$temp_file" "$final_path"
|
||||
|
||||
# Make executable
|
||||
chmod +x "$final_path"
|
||||
|
||||
print_status "Installer downloaded to ${final_path}"
|
||||
}
|
||||
|
||||
# Verify installation
|
||||
verify_installation() {
|
||||
local install_dir="$1"
|
||||
local installer_path="${install_dir}/installer"
|
||||
|
||||
if [ -f "$installer_path" ] && [ -x "$installer_path" ]; then
|
||||
print_status "Installation successful!"
|
||||
return 0
|
||||
else
|
||||
print_error "Installation failed. Binary not found or not executable."
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
# Main installation process
|
||||
main() {
|
||||
print_status "Installing latest version of installer..."
|
||||
|
||||
# Get latest version
|
||||
print_status "Fetching latest version from GitHub..."
|
||||
VERSION=$(get_latest_version)
|
||||
print_status "Latest version: v${VERSION}"
|
||||
|
||||
# Set base URL with the fetched version
|
||||
BASE_URL="https://github.com/${REPO}/releases/download/${VERSION}"
|
||||
|
||||
# Detect platform
|
||||
PLATFORM=$(detect_platform)
|
||||
print_status "Detected platform: ${PLATFORM}"
|
||||
|
||||
# Get install directory
|
||||
INSTALL_DIR=$(get_install_dir)
|
||||
print_status "Install directory: ${INSTALL_DIR}"
|
||||
|
||||
# Install installer
|
||||
install_installer "$PLATFORM" "$INSTALL_DIR"
|
||||
|
||||
# Verify installation
|
||||
if verify_installation "$INSTALL_DIR"; then
|
||||
print_status "Installer is ready to use!"
|
||||
else
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
# Run main function
|
||||
main "$@"
|
||||
@@ -1,10 +1,10 @@
|
||||
module installer
|
||||
|
||||
go 1.24
|
||||
go 1.24.0
|
||||
|
||||
require (
|
||||
golang.org/x/term v0.33.0
|
||||
golang.org/x/term v0.37.0
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
)
|
||||
|
||||
require golang.org/x/sys v0.34.0 // indirect
|
||||
require golang.org/x/sys v0.38.0 // indirect
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
golang.org/x/sys v0.34.0 h1:H5Y5sJ2L2JRdyv7ROF1he/lPdvFsd0mJHFw2ThKHxLA=
|
||||
golang.org/x/sys v0.34.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||
golang.org/x/term v0.33.0 h1:NuFncQrRcaRvVmgRkvM3j/F00gWIAlcmlB8ACEKmGIg=
|
||||
golang.org/x/term v0.33.0/go.mod h1:s18+ql9tYWp1IfpV9DmCtQDDSRBUjKaw9M1eAv5UeF0=
|
||||
golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc=
|
||||
golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/term v0.37.0 h1:8EGAD0qCmHYZg6J17DvsMy9/wJ7/D/4pV/wfnld5lTU=
|
||||
golang.org/x/term v0.37.0/go.mod h1:5pB4lxRNYYVZuTLmy8oR2BH8dflOR+IbTYFD8fi3254=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
|
||||
307
install/main.go
307
install/main.go
@@ -2,12 +2,14 @@ package main
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"embed"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/fs"
|
||||
"math/rand"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
@@ -15,7 +17,6 @@ import (
|
||||
"strings"
|
||||
"text/template"
|
||||
"time"
|
||||
"net"
|
||||
)
|
||||
|
||||
// DO NOT EDIT THIS FUNCTION; IT MATCHED BY REGEX IN CICD
|
||||
@@ -46,10 +47,8 @@ type Config struct {
|
||||
InstallGerbil bool
|
||||
TraefikBouncerKey string
|
||||
DoCrowdsecInstall bool
|
||||
EnableGeoblocking bool
|
||||
Secret string
|
||||
HybridMode bool
|
||||
HybridId string
|
||||
HybridSecret string
|
||||
}
|
||||
|
||||
type SupportedContainer string
|
||||
@@ -57,6 +56,7 @@ type SupportedContainer string
|
||||
const (
|
||||
Docker SupportedContainer = "docker"
|
||||
Podman SupportedContainer = "podman"
|
||||
Undefined SupportedContainer = "undefined"
|
||||
)
|
||||
|
||||
func main() {
|
||||
@@ -74,7 +74,7 @@ func main() {
|
||||
if err := checkPortsAvailable(p); err != nil {
|
||||
fmt.Fprintln(os.Stderr, err)
|
||||
|
||||
fmt.Printf("Please close any services on ports 80/443 in order to run the installation smoothly")
|
||||
fmt.Printf("Please close any services on ports 80/443 in order to run the installation smoothly. If you already have the Pangolin stack running, shut them down before proceeding.\n")
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
@@ -83,6 +83,7 @@ func main() {
|
||||
reader := bufio.NewReader(os.Stdin)
|
||||
|
||||
var config Config
|
||||
var alreadyInstalled = false
|
||||
|
||||
// check if there is already a config file
|
||||
if _, err := os.Stat("config/config.yml"); err != nil {
|
||||
@@ -94,24 +95,6 @@ func main() {
|
||||
|
||||
fmt.Println("\n=== Generating Configuration Files ===")
|
||||
|
||||
// If the secret and id are not generated then generate them
|
||||
if config.HybridMode && (config.HybridId == "" || config.HybridSecret == "") {
|
||||
// fmt.Println("Requesting hybrid credentials from cloud...")
|
||||
credentials, err := requestHybridCredentials()
|
||||
if err != nil {
|
||||
fmt.Printf("Error requesting hybrid credentials: %v\n", err)
|
||||
fmt.Println("Please obtain credentials manually from the dashboard and run the installer again.")
|
||||
os.Exit(1)
|
||||
}
|
||||
config.HybridId = credentials.RemoteExitNodeId
|
||||
config.HybridSecret = credentials.Secret
|
||||
fmt.Printf("Your managed credentials have been obtained successfully.\n")
|
||||
fmt.Printf(" ID: %s\n", config.HybridId)
|
||||
fmt.Printf(" Secret: %s\n", config.HybridSecret)
|
||||
fmt.Println("Take these to the Pangolin dashboard https://pangolin.fossorial.io to adopt your node.")
|
||||
readBool(reader, "Have you adopted your node?", true)
|
||||
}
|
||||
|
||||
if err := createConfigFiles(config); err != nil {
|
||||
fmt.Printf("Error creating config files: %v\n", err)
|
||||
os.Exit(1)
|
||||
@@ -121,12 +104,21 @@ func main() {
|
||||
|
||||
fmt.Println("\nConfiguration files created successfully!")
|
||||
|
||||
// Download MaxMind database if requested
|
||||
if config.EnableGeoblocking {
|
||||
fmt.Println("\n=== Downloading MaxMind Database ===")
|
||||
if err := downloadMaxMindDatabase(); err != nil {
|
||||
fmt.Printf("Error downloading MaxMind database: %v\n", err)
|
||||
fmt.Println("You can download it manually later if needed.")
|
||||
}
|
||||
}
|
||||
|
||||
fmt.Println("\n=== Starting installation ===")
|
||||
|
||||
if readBool(reader, "Would you like to install and start the containers?", true) {
|
||||
|
||||
config.InstallationContainerType = podmanOrDocker(reader)
|
||||
|
||||
|
||||
if !isDockerInstalled() && runtime.GOOS == "linux" && config.InstallationContainerType == Docker {
|
||||
if readBool(reader, "Docker is not installed. Would you like to install it?", true) {
|
||||
installDocker()
|
||||
@@ -166,10 +158,36 @@ func main() {
|
||||
}
|
||||
|
||||
} else {
|
||||
alreadyInstalled = true
|
||||
fmt.Println("Looks like you already installed Pangolin!")
|
||||
|
||||
// Check if MaxMind database exists and offer to update it
|
||||
fmt.Println("\n=== MaxMind Database Update ===")
|
||||
if _, err := os.Stat("config/GeoLite2-Country.mmdb"); err == nil {
|
||||
fmt.Println("MaxMind GeoLite2 Country database found.")
|
||||
if readBool(reader, "Would you like to update the MaxMind database to the latest version?", false) {
|
||||
if err := downloadMaxMindDatabase(); err != nil {
|
||||
fmt.Printf("Error updating MaxMind database: %v\n", err)
|
||||
fmt.Println("You can try updating it manually later if needed.")
|
||||
}
|
||||
}
|
||||
} else {
|
||||
fmt.Println("MaxMind GeoLite2 Country database not found.")
|
||||
if readBool(reader, "Would you like to download the MaxMind GeoLite2 database for geoblocking functionality?", false) {
|
||||
if err := downloadMaxMindDatabase(); err != nil {
|
||||
fmt.Printf("Error downloading MaxMind database: %v\n", err)
|
||||
fmt.Println("You can try downloading it manually later if needed.")
|
||||
}
|
||||
// Now you need to update your config file accordingly to enable geoblocking
|
||||
fmt.Println("Please remember to update your config/config.yml file to enable geoblocking! \n")
|
||||
// add maxmind_db_path: "./config/GeoLite2-Country.mmdb" under server
|
||||
fmt.Println("Add the following line under the 'server' section:")
|
||||
fmt.Println(" maxmind_db_path: \"./config/GeoLite2-Country.mmdb\"")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !checkIsCrowdsecInstalledInCompose() && !checkIsPangolinInstalledWithHybrid() {
|
||||
if !checkIsCrowdsecInstalledInCompose() {
|
||||
fmt.Println("\n=== CrowdSec Install ===")
|
||||
// check if crowdsec is installed
|
||||
if readBool(reader, "Would you like to install CrowdSec?", false) {
|
||||
@@ -189,7 +207,13 @@ func main() {
|
||||
return
|
||||
}
|
||||
|
||||
config.DashboardDomain = appConfig.DashboardURL
|
||||
parsedURL, err := url.Parse(appConfig.DashboardURL)
|
||||
if err != nil {
|
||||
fmt.Printf("Error parsing URL: %v\n", err)
|
||||
return
|
||||
}
|
||||
|
||||
config.DashboardDomain = parsedURL.Hostname()
|
||||
config.LetsEncryptEmail = traefikConfig.LetsEncryptEmail
|
||||
config.BadgerVersion = traefikConfig.BadgerVersion
|
||||
|
||||
@@ -204,13 +228,21 @@ func main() {
|
||||
}
|
||||
}
|
||||
|
||||
config.InstallationContainerType = podmanOrDocker(reader)
|
||||
|
||||
config.DoCrowdsecInstall = true
|
||||
installCrowdsec(config)
|
||||
err := installCrowdsec(config)
|
||||
if err != nil {
|
||||
fmt.Printf("Error installing CrowdSec: %v\n", err)
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Println("CrowdSec installed successfully!")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !config.HybridMode {
|
||||
if !alreadyInstalled {
|
||||
// Setup Token Section
|
||||
fmt.Println("\n=== Setup Token ===")
|
||||
|
||||
@@ -231,9 +263,7 @@ func main() {
|
||||
|
||||
fmt.Println("\nInstallation complete!")
|
||||
|
||||
if !config.HybridMode && !checkIsPangolinInstalledWithHybrid() {
|
||||
fmt.Printf("\nTo complete the initial setup, please visit:\nhttps://%s/auth/initial-setup\n", config.DashboardDomain)
|
||||
}
|
||||
fmt.Printf("\nTo complete the initial setup, please visit:\nhttps://%s/auth/initial-setup\n", config.DashboardDomain)
|
||||
}
|
||||
|
||||
func podmanOrDocker(reader *bufio.Reader) SupportedContainer {
|
||||
@@ -308,61 +338,38 @@ func collectUserInput(reader *bufio.Reader) Config {
|
||||
|
||||
// Basic configuration
|
||||
fmt.Println("\n=== Basic Configuration ===")
|
||||
for {
|
||||
response := readString(reader, "Do you want to install Pangolin as a cloud-managed (beta) node? (yes/no)", "")
|
||||
if strings.EqualFold(response, "yes") || strings.EqualFold(response, "y") {
|
||||
config.HybridMode = true
|
||||
break
|
||||
} else if strings.EqualFold(response, "no") || strings.EqualFold(response, "n") {
|
||||
config.HybridMode = false
|
||||
break
|
||||
}
|
||||
fmt.Println("Please answer 'yes' or 'no'")
|
||||
|
||||
config.BaseDomain = readString(reader, "Enter your base domain (no subdomain e.g. example.com)", "")
|
||||
|
||||
// Set default dashboard domain after base domain is collected
|
||||
defaultDashboardDomain := ""
|
||||
if config.BaseDomain != "" {
|
||||
defaultDashboardDomain = "pangolin." + config.BaseDomain
|
||||
}
|
||||
config.DashboardDomain = readString(reader, "Enter the domain for the Pangolin dashboard", defaultDashboardDomain)
|
||||
config.LetsEncryptEmail = readString(reader, "Enter email for Let's Encrypt certificates", "")
|
||||
config.InstallGerbil = readBool(reader, "Do you want to use Gerbil to allow tunneled connections", true)
|
||||
|
||||
// Email configuration
|
||||
fmt.Println("\n=== Email Configuration ===")
|
||||
config.EnableEmail = readBool(reader, "Enable email functionality (SMTP)", false)
|
||||
|
||||
if config.EnableEmail {
|
||||
config.EmailSMTPHost = readString(reader, "Enter SMTP host", "")
|
||||
config.EmailSMTPPort = readInt(reader, "Enter SMTP port (default 587)", 587)
|
||||
config.EmailSMTPUser = readString(reader, "Enter SMTP username", "")
|
||||
config.EmailSMTPPass = readString(reader, "Enter SMTP password", "") // Should this be readPassword?
|
||||
config.EmailNoReply = readString(reader, "Enter no-reply email address", "")
|
||||
}
|
||||
|
||||
if config.HybridMode {
|
||||
alreadyHaveCreds := readBool(reader, "Do you already have credentials from the dashboard? If not, we will create them later", false)
|
||||
|
||||
if alreadyHaveCreds {
|
||||
config.HybridId = readString(reader, "Enter your ID", "")
|
||||
config.HybridSecret = readString(reader, "Enter your secret", "")
|
||||
}
|
||||
|
||||
config.DashboardDomain = readString(reader, "The public addressable IP address for this node or a domain pointing to it", "")
|
||||
config.InstallGerbil = true
|
||||
} else {
|
||||
config.BaseDomain = readString(reader, "Enter your base domain (no subdomain e.g. example.com)", "")
|
||||
|
||||
// Set default dashboard domain after base domain is collected
|
||||
defaultDashboardDomain := ""
|
||||
if config.BaseDomain != "" {
|
||||
defaultDashboardDomain = "pangolin." + config.BaseDomain
|
||||
}
|
||||
config.DashboardDomain = readString(reader, "Enter the domain for the Pangolin dashboard", defaultDashboardDomain)
|
||||
config.LetsEncryptEmail = readString(reader, "Enter email for Let's Encrypt certificates", "")
|
||||
config.InstallGerbil = readBool(reader, "Do you want to use Gerbil to allow tunneled connections", true)
|
||||
|
||||
// Email configuration
|
||||
fmt.Println("\n=== Email Configuration ===")
|
||||
config.EnableEmail = readBool(reader, "Enable email functionality (SMTP)", false)
|
||||
|
||||
if config.EnableEmail {
|
||||
config.EmailSMTPHost = readString(reader, "Enter SMTP host", "")
|
||||
config.EmailSMTPPort = readInt(reader, "Enter SMTP port (default 587)", 587)
|
||||
config.EmailSMTPUser = readString(reader, "Enter SMTP username", "")
|
||||
config.EmailSMTPPass = readString(reader, "Enter SMTP password", "") // Should this be readPassword?
|
||||
config.EmailNoReply = readString(reader, "Enter no-reply email address", "")
|
||||
}
|
||||
|
||||
// Validate required fields
|
||||
if config.BaseDomain == "" {
|
||||
fmt.Println("Error: Domain name is required")
|
||||
os.Exit(1)
|
||||
}
|
||||
if config.LetsEncryptEmail == "" {
|
||||
fmt.Println("Error: Let's Encrypt email is required")
|
||||
os.Exit(1)
|
||||
}
|
||||
// Validate required fields
|
||||
if config.BaseDomain == "" {
|
||||
fmt.Println("Error: Domain name is required")
|
||||
os.Exit(1)
|
||||
}
|
||||
if config.LetsEncryptEmail == "" {
|
||||
fmt.Println("Error: Let's Encrypt email is required")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// Advanced configuration
|
||||
@@ -370,6 +377,7 @@ func collectUserInput(reader *bufio.Reader) Config {
|
||||
fmt.Println("\n=== Advanced Configuration ===")
|
||||
|
||||
config.EnableIPv6 = readBool(reader, "Is your server IPv6 capable?", true)
|
||||
config.EnableGeoblocking = readBool(reader, "Do you want to download the MaxMind GeoLite2 database for geoblocking functionality?", true)
|
||||
|
||||
if config.DashboardDomain == "" {
|
||||
fmt.Println("Error: Dashboard Domain name is required")
|
||||
@@ -404,11 +412,6 @@ func createConfigFiles(config Config) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// the hybrid does not need the dynamic config
|
||||
if config.HybridMode && strings.Contains(path, "dynamic_config.yml") {
|
||||
return nil
|
||||
}
|
||||
|
||||
// skip .DS_Store
|
||||
if strings.Contains(path, ".DS_Store") {
|
||||
return nil
|
||||
@@ -522,12 +525,12 @@ func printSetupToken(containerType SupportedContainer, dashboardDomain string) {
|
||||
tokenStart := strings.Index(trimmedLine, "Token:")
|
||||
if tokenStart != -1 {
|
||||
token := strings.TrimSpace(trimmedLine[tokenStart+6:])
|
||||
fmt.Printf("Setup token: %s\n", token)
|
||||
fmt.Println("")
|
||||
fmt.Println("This token is required to register the first admin account in the web UI at:")
|
||||
fmt.Printf("https://%s/auth/initial-setup\n", dashboardDomain)
|
||||
fmt.Println("")
|
||||
fmt.Println("Save this token securely. It will be invalid after the first admin is created.")
|
||||
fmt.Printf("Setup token: %s\n", token)
|
||||
fmt.Println("")
|
||||
fmt.Println("This token is required to register the first admin account in the web UI at:")
|
||||
fmt.Printf("https://%s/auth/initial-setup\n", dashboardDomain)
|
||||
fmt.Println("")
|
||||
fmt.Println("Save this token securely. It will be invalid after the first admin is created.")
|
||||
return
|
||||
}
|
||||
}
|
||||
@@ -541,28 +544,30 @@ func showSetupTokenInstructions(containerType SupportedContainer, dashboardDomai
|
||||
fmt.Println("\n=== Setup Token Instructions ===")
|
||||
fmt.Println("To get your setup token, you need to:")
|
||||
fmt.Println("")
|
||||
fmt.Println("1. Start the containers:")
|
||||
fmt.Println("1. Start the containers")
|
||||
if containerType == Docker {
|
||||
fmt.Println(" docker-compose up -d")
|
||||
} else {
|
||||
fmt.Println(" docker compose up -d")
|
||||
} else if containerType == Podman {
|
||||
fmt.Println(" podman-compose up -d")
|
||||
} else {
|
||||
}
|
||||
fmt.Println("")
|
||||
fmt.Println("2. Wait for the Pangolin container to start and generate the token")
|
||||
fmt.Println("")
|
||||
fmt.Println("3. Check the container logs for the setup token:")
|
||||
fmt.Println("3. Check the container logs for the setup token")
|
||||
if containerType == Docker {
|
||||
fmt.Println(" docker logs pangolin | grep -A 2 -B 2 'SETUP TOKEN'")
|
||||
} else {
|
||||
} else if containerType == Podman {
|
||||
fmt.Println(" podman logs pangolin | grep -A 2 -B 2 'SETUP TOKEN'")
|
||||
} else {
|
||||
}
|
||||
fmt.Println("")
|
||||
fmt.Println("4. Look for output like:")
|
||||
fmt.Println("4. Look for output like")
|
||||
fmt.Println(" === SETUP TOKEN GENERATED ===")
|
||||
fmt.Println(" Token: [your-token-here]")
|
||||
fmt.Println(" Use this token on the initial setup page")
|
||||
fmt.Println("")
|
||||
fmt.Println("5. Use the token to complete initial setup at:")
|
||||
fmt.Println("5. Use the token to complete initial setup at")
|
||||
fmt.Printf(" https://%s/auth/initial-setup\n", dashboardDomain)
|
||||
fmt.Println("")
|
||||
fmt.Println("The setup token is required to register the first admin account.")
|
||||
@@ -584,6 +589,32 @@ func generateRandomSecretKey() string {
|
||||
return string(b)
|
||||
}
|
||||
|
||||
func getPublicIP() string {
|
||||
client := &http.Client{
|
||||
Timeout: 10 * time.Second,
|
||||
}
|
||||
|
||||
resp, err := client.Get("https://ifconfig.io/ip")
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
ip := strings.TrimSpace(string(body))
|
||||
|
||||
// Validate that it's a valid IP address
|
||||
if net.ParseIP(ip) != nil {
|
||||
return ip
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
// Run external commands with stdio/stderr attached.
|
||||
func run(name string, args ...string) error {
|
||||
cmd := exec.Command(name, args...)
|
||||
@@ -593,35 +624,47 @@ func run(name string, args ...string) error {
|
||||
}
|
||||
|
||||
func checkPortsAvailable(port int) error {
|
||||
addr := fmt.Sprintf(":%d", port)
|
||||
ln, err := net.Listen("tcp", addr)
|
||||
if err != nil {
|
||||
return fmt.Errorf(
|
||||
"ERROR: port %d is occupied or cannot be bound: %w\n\n",
|
||||
port, err,
|
||||
)
|
||||
}
|
||||
if closeErr := ln.Close(); closeErr != nil {
|
||||
fmt.Fprintf(os.Stderr,
|
||||
"WARNING: failed to close test listener on port %d: %v\n",
|
||||
port, closeErr,
|
||||
)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func checkIsPangolinInstalledWithHybrid() bool {
|
||||
// Check if config/config.yml exists and contains hybrid section
|
||||
if _, err := os.Stat("config/config.yml"); err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
// Read config file to check for hybrid section
|
||||
content, err := os.ReadFile("config/config.yml")
|
||||
addr := fmt.Sprintf(":%d", port)
|
||||
ln, err := net.Listen("tcp", addr)
|
||||
if err != nil {
|
||||
return false
|
||||
return fmt.Errorf(
|
||||
"ERROR: port %d is occupied or cannot be bound: %w\n\n",
|
||||
port, err,
|
||||
)
|
||||
}
|
||||
|
||||
// Check for hybrid section
|
||||
return bytes.Contains(content, []byte("managed:"))
|
||||
if closeErr := ln.Close(); closeErr != nil {
|
||||
fmt.Fprintf(os.Stderr,
|
||||
"WARNING: failed to close test listener on port %d: %v\n",
|
||||
port, closeErr,
|
||||
)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func downloadMaxMindDatabase() error {
|
||||
fmt.Println("Downloading MaxMind GeoLite2 Country database...")
|
||||
|
||||
// Download the GeoLite2 Country database
|
||||
if err := run("curl", "-L", "-o", "GeoLite2-Country.tar.gz",
|
||||
"https://github.com/GitSquared/node-geolite2-redist/raw/refs/heads/master/redist/GeoLite2-Country.tar.gz"); err != nil {
|
||||
return fmt.Errorf("failed to download GeoLite2 database: %v", err)
|
||||
}
|
||||
|
||||
// Extract the database
|
||||
if err := run("tar", "-xzf", "GeoLite2-Country.tar.gz"); err != nil {
|
||||
return fmt.Errorf("failed to extract GeoLite2 database: %v", err)
|
||||
}
|
||||
|
||||
// Find the .mmdb file and move it to the config directory
|
||||
if err := run("bash", "-c", "mv GeoLite2-Country_*/GeoLite2-Country.mmdb config/"); err != nil {
|
||||
return fmt.Errorf("failed to move GeoLite2 database to config directory: %v", err)
|
||||
}
|
||||
|
||||
// Clean up the downloaded files
|
||||
if err := run("rm", "-rf", "GeoLite2-Country.tar.gz", "GeoLite2-Country_*"); err != nil {
|
||||
fmt.Printf("Warning: failed to clean up temporary files: %v\n", err)
|
||||
}
|
||||
|
||||
fmt.Println("MaxMind GeoLite2 Country database downloaded successfully!")
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -1,110 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
FRONTEND_SECRET_KEY = "af4e4785-7e09-11f0-b93a-74563c4e2a7e"
|
||||
// CLOUD_API_URL = "https://pangolin.fossorial.io/api/v1/remote-exit-node/quick-start"
|
||||
CLOUD_API_URL = "https://pangolin.fossorial.io/api/v1/remote-exit-node/quick-start"
|
||||
)
|
||||
|
||||
// HybridCredentials represents the response from the cloud API
|
||||
type HybridCredentials struct {
|
||||
RemoteExitNodeId string `json:"remoteExitNodeId"`
|
||||
Secret string `json:"secret"`
|
||||
}
|
||||
|
||||
// APIResponse represents the full response structure from the cloud API
|
||||
type APIResponse struct {
|
||||
Data HybridCredentials `json:"data"`
|
||||
}
|
||||
|
||||
// RequestPayload represents the request body structure
|
||||
type RequestPayload struct {
|
||||
Token string `json:"token"`
|
||||
}
|
||||
|
||||
func generateValidationToken() string {
|
||||
timestamp := time.Now().UnixMilli()
|
||||
data := fmt.Sprintf("%s|%d", FRONTEND_SECRET_KEY, timestamp)
|
||||
obfuscated := make([]byte, len(data))
|
||||
for i, char := range []byte(data) {
|
||||
obfuscated[i] = char + 5
|
||||
}
|
||||
return base64.StdEncoding.EncodeToString(obfuscated)
|
||||
}
|
||||
|
||||
// requestHybridCredentials makes an HTTP POST request to the cloud API
|
||||
// to get hybrid credentials (ID and secret)
|
||||
func requestHybridCredentials() (*HybridCredentials, error) {
|
||||
// Generate validation token
|
||||
token := generateValidationToken()
|
||||
|
||||
// Create request payload
|
||||
payload := RequestPayload{
|
||||
Token: token,
|
||||
}
|
||||
|
||||
// Marshal payload to JSON
|
||||
jsonData, err := json.Marshal(payload)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to marshal request payload: %v", err)
|
||||
}
|
||||
|
||||
// Create HTTP request
|
||||
req, err := http.NewRequest("POST", CLOUD_API_URL, bytes.NewBuffer(jsonData))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create HTTP request: %v", err)
|
||||
}
|
||||
|
||||
// Set headers
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
req.Header.Set("X-CSRF-Token", "x-csrf-protection")
|
||||
|
||||
// Create HTTP client with timeout
|
||||
client := &http.Client{
|
||||
Timeout: 30 * time.Second,
|
||||
}
|
||||
|
||||
// Make the request
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to make HTTP request: %v", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
// Check response status
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return nil, fmt.Errorf("API request failed with status code: %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
// Read response body for debugging
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to read response body: %v", err)
|
||||
}
|
||||
|
||||
// Print the raw JSON response for debugging
|
||||
// fmt.Printf("Raw JSON response: %s\n", string(body))
|
||||
|
||||
// Parse response
|
||||
var apiResponse APIResponse
|
||||
if err := json.Unmarshal(body, &apiResponse); err != nil {
|
||||
return nil, fmt.Errorf("failed to decode API response: %v", err)
|
||||
}
|
||||
|
||||
// Validate response data
|
||||
if apiResponse.Data.RemoteExitNodeId == "" || apiResponse.Data.Secret == "" {
|
||||
return nil, fmt.Errorf("invalid response: missing remoteExitNodeId or secret")
|
||||
}
|
||||
|
||||
return &apiResponse.Data, nil
|
||||
}
|
||||
3485
messages/bg-BG.json
3485
messages/bg-BG.json
File diff suppressed because it is too large
Load Diff
3231
messages/cs-CZ.json
3231
messages/cs-CZ.json
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
1034
messages/en-US.json
1034
messages/en-US.json
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -47,9 +47,8 @@
|
||||
"edit": "편집",
|
||||
"siteConfirmDelete": "사이트 삭제 확인",
|
||||
"siteDelete": "사이트 삭제",
|
||||
"siteMessageRemove": "제거되면 사이트에 더 이상 접근할 수 없습니다. 사이트와 관련된 모든 리소스와 대상도 제거됩니다.",
|
||||
"siteMessageConfirm": "확인을 위해 아래에 사이트 이름을 입력해 주세요.",
|
||||
"siteQuestionRemove": "조직에서 사이트 {selectedSite}를 제거하시겠습니까?",
|
||||
"siteMessageRemove": "삭제되면 사이트에 더 이상 액세스할 수 없습니다. 사이트와 연결된 모든 대상도 삭제됩니다.",
|
||||
"siteQuestionRemove": "조직에서 사이트를 제거하시겠습니까?",
|
||||
"siteManageSites": "사이트 관리",
|
||||
"siteDescription": "안전한 터널을 통해 네트워크에 연결할 수 있도록 허용",
|
||||
"siteCreate": "사이트 생성",
|
||||
@@ -96,7 +95,7 @@
|
||||
"siteWgDescription": "모든 WireGuard 클라이언트를 사용하여 터널을 설정하세요. 수동 NAT 설정이 필요합니다.",
|
||||
"siteWgDescriptionSaas": "모든 WireGuard 클라이언트를 사용하여 터널을 설정하세요. 수동 NAT 설정이 필요합니다. 자체 호스팅 노드에서만 작동합니다.",
|
||||
"siteLocalDescription": "로컬 리소스만 사용 가능합니다. 터널링이 없습니다.",
|
||||
"siteLocalDescriptionSaas": "로컬 리소스만. 터널링 없음. 자체 호스팅 노드에서만 작동합니다.",
|
||||
"siteLocalDescriptionSaas": "로컬 리소스 전용. 터널링 금지. 원격 노드에서만 사용 가능합니다.",
|
||||
"siteSeeAll": "모든 사이트 보기",
|
||||
"siteTunnelDescription": "사이트에 연결하는 방법을 결정하세요",
|
||||
"siteNewtCredentials": "Newt 자격 증명",
|
||||
@@ -154,8 +153,7 @@
|
||||
"protected": "보호됨",
|
||||
"notProtected": "보호되지 않음",
|
||||
"resourceMessageRemove": "제거되면 리소스에 더 이상 접근할 수 없습니다. 리소스와 연결된 모든 대상도 제거됩니다.",
|
||||
"resourceMessageConfirm": "확인을 위해 아래에 리소스의 이름을 입력하세요.",
|
||||
"resourceQuestionRemove": "조직에서 리소스 {selectedResource}를 제거하시겠습니까?",
|
||||
"resourceQuestionRemove": "조직에서 리소스를 제거하시겠습니까?",
|
||||
"resourceHTTP": "HTTPS 리소스",
|
||||
"resourceHTTPDescription": "서브도메인 또는 기본 도메인을 사용하여 HTTPS를 통해 앱에 대한 요청을 프록시합니다.",
|
||||
"resourceRaw": "원시 TCP/UDP 리소스",
|
||||
@@ -168,6 +166,9 @@
|
||||
"siteSelect": "사이트 선택",
|
||||
"siteSearch": "사이트 검색",
|
||||
"siteNotFound": "사이트를 찾을 수 없습니다.",
|
||||
"selectCountry": "국가 선택하기",
|
||||
"searchCountries": "국가 검색...",
|
||||
"noCountryFound": "국가를 찾을 수 없습니다.",
|
||||
"siteSelectionDescription": "이 사이트는 대상에 대한 연결을 제공합니다.",
|
||||
"resourceType": "리소스 유형",
|
||||
"resourceTypeDescription": "리소스에 접근하는 방법을 결정하세요",
|
||||
@@ -178,7 +179,7 @@
|
||||
"baseDomain": "기본 도메인",
|
||||
"subdomnainDescription": "리소스에 접근할 수 있는 하위 도메인입니다.",
|
||||
"resourceRawSettings": "TCP/UDP 설정",
|
||||
"resourceRawSettingsDescription": "TCP/UDP를 통해 리소스에 접근하는 방법을 구성하세요.",
|
||||
"resourceRawSettingsDescription": "리소스를 TCP/UDP를 통해 액세스하는 방법을 구성합니다. 리소스를 호스트 Pangolin 서버의 포트에 매핑하여 서버-public-ip:매핑된 포트에서 리소스에 액세스할 수 있습니다.",
|
||||
"protocol": "프로토콜",
|
||||
"protocolSelect": "프로토콜 선택",
|
||||
"resourcePortNumber": "포트 번호",
|
||||
@@ -205,6 +206,7 @@
|
||||
"resourceSetting": "{resourceName} 설정",
|
||||
"alwaysAllow": "항상 허용",
|
||||
"alwaysDeny": "항상 거부",
|
||||
"passToAuth": "인증으로 전달",
|
||||
"orgSettingsDescription": "조직의 일반 설정을 구성하세요",
|
||||
"orgGeneralSettings": "조직 설정",
|
||||
"orgGeneralSettingsDescription": "조직 세부정보 및 구성을 관리하세요.",
|
||||
@@ -216,7 +218,7 @@
|
||||
"orgDeleteConfirm": "조직 삭제 확인",
|
||||
"orgMessageRemove": "이 작업은 되돌릴 수 없으며 모든 관련 데이터를 삭제합니다.",
|
||||
"orgMessageConfirm": "확인을 위해 아래에 조직 이름을 입력하십시오.",
|
||||
"orgQuestionRemove": "조직 {selectedOrg}을(를) 제거하시겠습니까?",
|
||||
"orgQuestionRemove": "조직을 삭제하시겠습니까?",
|
||||
"orgUpdated": "조직이 업데이트되었습니다.",
|
||||
"orgUpdatedDescription": "조직이 업데이트되었습니다.",
|
||||
"orgErrorUpdate": "조직 업데이트에 실패했습니다.",
|
||||
@@ -283,9 +285,8 @@
|
||||
"apiKeysAdd": "API 키 생성",
|
||||
"apiKeysErrorDelete": "API 키 삭제 오류",
|
||||
"apiKeysErrorDeleteMessage": "API 키 삭제 오류",
|
||||
"apiKeysQuestionRemove": "조직에서 API 키 {selectedApiKey}를 제거하시겠습니까?",
|
||||
"apiKeysQuestionRemove": "조직에서 API 키를 제거하시겠습니까?",
|
||||
"apiKeysMessageRemove": "삭제되면 API 키를 더 이상 사용할 수 없습니다.",
|
||||
"apiKeysMessageConfirm": "확인을 위해 아래에 API 키의 이름을 입력해 주세요.",
|
||||
"apiKeysDeleteConfirm": "API 키 삭제 확인",
|
||||
"apiKeysDelete": "API 키 삭제",
|
||||
"apiKeysManage": "API 키 관리",
|
||||
@@ -301,8 +302,7 @@
|
||||
"userDeleteConfirm": "사용자 삭제 확인",
|
||||
"userDeleteServer": "서버에서 사용자 삭제",
|
||||
"userMessageRemove": "사용자가 모든 조직에서 제거되며 서버에서 완전히 삭제됩니다.",
|
||||
"userMessageConfirm": "확인을 위해 아래에 사용자 이름을 입력하십시오.",
|
||||
"userQuestionRemove": "정말로 {selectedUser}를 서버에서 영구적으로 삭제하시겠습니까?",
|
||||
"userQuestionRemove": "서버에서 사용자를 영구적으로 삭제하시겠습니까?",
|
||||
"licenseKey": "라이센스 키",
|
||||
"valid": "유효",
|
||||
"numberOfSites": "사이트 수",
|
||||
@@ -335,7 +335,7 @@
|
||||
"fossorialLicense": "Fossorial 상업 라이선스 및 구독 약관 보기",
|
||||
"licenseMessageRemove": "이 작업은 라이센스 키와 그에 의해 부여된 모든 관련 권한을 제거합니다.",
|
||||
"licenseMessageConfirm": "확인을 위해 아래에 라이센스 키를 입력하세요.",
|
||||
"licenseQuestionRemove": "라이센스 키 {selectedKey}를 삭제하시겠습니까?",
|
||||
"licenseQuestionRemove": "라이선스 키를 삭제하시겠습니까?",
|
||||
"licenseKeyDelete": "라이센스 키 삭제",
|
||||
"licenseKeyDeleteConfirm": "라이센스 키 삭제 확인",
|
||||
"licenseTitle": "라이선스 상태 관리",
|
||||
@@ -368,7 +368,7 @@
|
||||
"inviteRemoveErrorDescription": "초대를 제거하는 동안 오류가 발생했습니다.",
|
||||
"inviteRemoved": "초대가 제거되었습니다.",
|
||||
"inviteRemovedDescription": "{email}에 대한 초대가 삭제되었습니다.",
|
||||
"inviteQuestionRemove": "초대 {email}를 제거하시겠습니까?",
|
||||
"inviteQuestionRemove": "초대를 제거하시겠습니까?",
|
||||
"inviteMessageRemove": "한 번 제거되면 이 초대는 더 이상 유효하지 않습니다. 나중에 사용자를 다시 초대할 수 있습니다.",
|
||||
"inviteMessageConfirm": "확인을 위해 아래 초대의 이메일 주소를 입력해 주세요.",
|
||||
"inviteQuestionRegenerate": "{email}에 대한 초대장을 다시 생성하시겠습니까? 이전 초대장은 취소됩니다.",
|
||||
@@ -394,9 +394,8 @@
|
||||
"userErrorOrgRemoveDescription": "사용자를 제거하는 동안 오류가 발생했습니다.",
|
||||
"userOrgRemoved": "사용자가 제거되었습니다.",
|
||||
"userOrgRemovedDescription": "사용자 {email}가 조직에서 제거되었습니다.",
|
||||
"userQuestionOrgRemove": "{email}을 조직에서 제거하시겠습니까?",
|
||||
"userQuestionOrgRemove": "조직에서 이 사용자를 제거하시겠습니까?",
|
||||
"userMessageOrgRemove": "이 사용자가 제거되면 더 이상 조직에 접근할 수 없습니다. 나중에 다시 초대할 수 있지만, 초대를 다시 수락해야 합니다.",
|
||||
"userMessageOrgConfirm": "확인을 위해 아래에 사용자 이름을 입력하세요.",
|
||||
"userRemoveOrgConfirm": "사용자 제거 확인",
|
||||
"userRemoveOrg": "조직에서 사용자 제거",
|
||||
"users": "사용자",
|
||||
@@ -453,6 +452,8 @@
|
||||
"accessRoleErrorAddDescription": "사용자를 역할에 추가하는 동안 오류가 발생했습니다.",
|
||||
"userSaved": "사용자 저장됨",
|
||||
"userSavedDescription": "사용자가 업데이트되었습니다.",
|
||||
"autoProvisioned": "자동 프로비저닝됨",
|
||||
"autoProvisionedDescription": "이 사용자가 ID 공급자에 의해 자동으로 관리될 수 있도록 허용합니다",
|
||||
"accessControlsDescription": "이 사용자가 조직에서 접근하고 수행할 수 있는 작업을 관리하세요",
|
||||
"accessControlsSubmit": "접근 제어 저장",
|
||||
"roles": "역할",
|
||||
@@ -462,7 +463,10 @@
|
||||
"createdAt": "생성일",
|
||||
"proxyErrorInvalidHeader": "잘못된 사용자 정의 호스트 헤더 값입니다. 도메인 이름 형식을 사용하거나 사용자 정의 호스트 헤더를 해제하려면 비워 두십시오.",
|
||||
"proxyErrorTls": "유효하지 않은 TLS 서버 이름입니다. 도메인 이름 형식을 사용하거나 비워 두어 TLS 서버 이름을 제거하십시오.",
|
||||
"proxyEnableSSL": "SSL 활성화 (https)",
|
||||
"proxyEnableSSL": "SSL 활성화",
|
||||
"proxyEnableSSLDescription": "대상에 대한 안전한 HTTPS 연결을 위해 SSL/TLS 암호화를 활성화하세요.",
|
||||
"target": "대상",
|
||||
"configureTarget": "대상 구성",
|
||||
"targetErrorFetch": "대상 가져오는 데 실패했습니다.",
|
||||
"targetErrorFetchDescription": "대상 가져오는 중 오류가 발생했습니다",
|
||||
"siteErrorFetch": "리소스를 가져오는 데 실패했습니다",
|
||||
@@ -489,7 +493,7 @@
|
||||
"targetTlsSettings": "보안 연결 구성",
|
||||
"targetTlsSettingsDescription": "리소스에 대한 SSL/TLS 설정 구성",
|
||||
"targetTlsSettingsAdvanced": "고급 TLS 설정",
|
||||
"targetTlsSni": "TLS 서버 이름 (SNI)",
|
||||
"targetTlsSni": "TLS 서버 이름",
|
||||
"targetTlsSniDescription": "SNI에 사용할 TLS 서버 이름. 기본값을 사용하려면 비워 두십시오.",
|
||||
"targetTlsSubmit": "설정 저장",
|
||||
"targets": "대상 구성",
|
||||
@@ -498,9 +502,21 @@
|
||||
"targetStickySessionsDescription": "세션 전체 동안 동일한 백엔드 대상을 유지합니다.",
|
||||
"methodSelect": "선택 방법",
|
||||
"targetSubmit": "대상 추가",
|
||||
"targetNoOne": "대상이 없습니다. 양식을 사용하여 대상을 추가하세요.",
|
||||
"targetNoOne": "이 리소스에는 대상이 없습니다. 백엔드로 요청을 보내려면 대상을 추가하세요.",
|
||||
"targetNoOneDescription": "위에 하나 이상의 대상을 추가하면 로드 밸런싱이 활성화됩니다.",
|
||||
"targetsSubmit": "대상 저장",
|
||||
"addTarget": "대상 추가",
|
||||
"targetErrorInvalidIp": "유효하지 않은 IP 주소",
|
||||
"targetErrorInvalidIpDescription": "유효한 IP 주소 또는 호스트 이름을 입력하세요.",
|
||||
"targetErrorInvalidPort": "유효하지 않은 포트",
|
||||
"targetErrorInvalidPortDescription": "유효한 포트 번호를 입력하세요.",
|
||||
"targetErrorNoSite": "선택된 사이트 없음",
|
||||
"targetErrorNoSiteDescription": "대상을 위해 사이트를 선택하세요.",
|
||||
"targetCreated": "대상 생성",
|
||||
"targetCreatedDescription": "대상이 성공적으로 생성되었습니다.",
|
||||
"targetErrorCreate": "대상 생성 실패",
|
||||
"targetErrorCreateDescription": "대상 생성 중 오류가 발생했습니다.",
|
||||
"save": "저장",
|
||||
"proxyAdditional": "추가 프록시 설정",
|
||||
"proxyAdditionalDescription": "리소스가 프록시 설정을 처리하는 방법 구성",
|
||||
"proxyCustomHeader": "사용자 정의 호스트 헤더",
|
||||
@@ -510,6 +526,7 @@
|
||||
"ipAddressErrorInvalidFormat": "잘못된 IP 주소 형식",
|
||||
"ipAddressErrorInvalidOctet": "유효하지 않은 IP 주소 옥텟",
|
||||
"path": "경로",
|
||||
"matchPath": "경로 맞춤",
|
||||
"ipAddressRange": "IP 범위",
|
||||
"rulesErrorFetch": "규칙을 가져오는 데 실패했습니다.",
|
||||
"rulesErrorFetchDescription": "규칙을 가져오는 중 오류가 발생했습니다",
|
||||
@@ -545,6 +562,7 @@
|
||||
"rulesActions": "작업",
|
||||
"rulesActionAlwaysAllow": "항상 허용: 모든 인증 방법 우회",
|
||||
"rulesActionAlwaysDeny": "항상 거부: 모든 요청을 차단합니다. 인증을 시도할 수 없습니다.",
|
||||
"rulesActionPassToAuth": "인증으로 전달: 인증 방법 시도를 허용합니다",
|
||||
"rulesMatchCriteria": "일치 기준",
|
||||
"rulesMatchCriteriaIpAddress": "특정 IP 주소와 일치",
|
||||
"rulesMatchCriteriaIpAddressRange": "CIDR 표기법으로 IP 주소 범위를 일치시킵니다",
|
||||
@@ -707,7 +725,7 @@
|
||||
"pangolinServerAdmin": "서버 관리자 - 판골린",
|
||||
"licenseTierProfessional": "전문 라이센스",
|
||||
"licenseTierEnterprise": "기업 라이선스",
|
||||
"licenseTierCommercial": "상업용 라이선스",
|
||||
"licenseTierPersonal": "개인 라이선스",
|
||||
"licensed": "라이센스",
|
||||
"yes": "예",
|
||||
"no": "아니요",
|
||||
@@ -719,7 +737,7 @@
|
||||
"idpManageDescription": "시스템에서 ID 제공자를 보고 관리합니다",
|
||||
"idpDeletedDescription": "신원 공급자가 성공적으로 삭제되었습니다",
|
||||
"idpOidc": "OAuth2/OIDC",
|
||||
"idpQuestionRemove": "정말로 아이덴티티 공급자 {name}를 영구적으로 삭제하시겠습니까?",
|
||||
"idpQuestionRemove": "아이덴티티 공급자를 영구적으로 삭제하시겠습니까?",
|
||||
"idpMessageRemove": "이 작업은 아이덴티티 공급자와 모든 관련 구성을 제거합니다. 이 공급자를 통해 인증하는 사용자는 더 이상 로그인할 수 없습니다.",
|
||||
"idpMessageConfirm": "확인을 위해 아래에 아이덴티티 제공자의 이름을 입력하세요.",
|
||||
"idpConfirmDelete": "신원 제공자 삭제 확인",
|
||||
@@ -742,7 +760,7 @@
|
||||
"idpDisplayName": "이 신원 공급자를 위한 표시 이름",
|
||||
"idpAutoProvisionUsers": "사용자 자동 프로비저닝",
|
||||
"idpAutoProvisionUsersDescription": "활성화되면 사용자가 첫 로그인 시 시스템에 자동으로 생성되며, 사용자와 역할 및 조직을 매핑할 수 있습니다.",
|
||||
"licenseBadge": "전문가",
|
||||
"licenseBadge": "EE",
|
||||
"idpType": "제공자 유형",
|
||||
"idpTypeDescription": "구성할 ID 공급자의 유형을 선택하십시오.",
|
||||
"idpOidcConfigure": "OAuth2/OIDC 구성",
|
||||
@@ -893,6 +911,18 @@
|
||||
"passwordResetCodeDescription": "재설정 코드를 확인하려면 이메일을 확인하세요.",
|
||||
"passwordNew": "새 비밀번호",
|
||||
"passwordNewConfirm": "새 비밀번호 확인",
|
||||
"changePassword": "비밀번호 변경",
|
||||
"changePasswordDescription": "계정 비밀번호를 업데이트하십시오",
|
||||
"oldPassword": "현재 비밀번호",
|
||||
"newPassword": "새 비밀번호",
|
||||
"confirmNewPassword": "새 비밀번호 확인",
|
||||
"changePasswordError": "비밀번호 변경 실패",
|
||||
"changePasswordErrorDescription": "비밀번호를 변경하는 중 오류가 발생했습니다",
|
||||
"changePasswordSuccess": "비밀번호 변경 완료",
|
||||
"changePasswordSuccessDescription": "비밀번호가 성공적으로 업데이트되었습니다",
|
||||
"passwordExpiryRequired": "비밀번호 만료 필요",
|
||||
"passwordExpiryDescription": "이 조직은 {maxDays}일마다 비밀번호 변경을 요구합니다.",
|
||||
"changePasswordNow": "지금 비밀번호 변경",
|
||||
"pincodeAuth": "인증 코드",
|
||||
"pincodeSubmit2": "코드 제출",
|
||||
"passwordResetSubmit": "재설정 요청",
|
||||
@@ -980,6 +1010,8 @@
|
||||
"licenseTierProfessionalRequired": "전문 에디션이 필요합니다.",
|
||||
"licenseTierProfessionalRequiredDescription": "이 기능은 Professional Edition에서만 사용할 수 있습니다.",
|
||||
"actionGetOrg": "조직 가져오기",
|
||||
"updateOrgUser": "조직 사용자 업데이트",
|
||||
"createOrgUser": "조직 사용자 생성",
|
||||
"actionUpdateOrg": "조직 업데이트",
|
||||
"actionUpdateUser": "사용자 업데이트",
|
||||
"actionGetUser": "사용자 조회",
|
||||
@@ -989,6 +1021,7 @@
|
||||
"actionDeleteSite": "사이트 삭제",
|
||||
"actionGetSite": "사이트 가져오기",
|
||||
"actionListSites": "사이트 목록",
|
||||
"actionApplyBlueprint": "청사진 적용",
|
||||
"setupToken": "설정 토큰",
|
||||
"setupTokenDescription": "서버 콘솔에서 설정 토큰 입력.",
|
||||
"setupTokenRequired": "설정 토큰이 필요합니다",
|
||||
@@ -1057,6 +1090,7 @@
|
||||
"actionGetSiteResource": "사이트 리소스 가져오기",
|
||||
"actionListSiteResources": "사이트 리소스 목록",
|
||||
"actionUpdateSiteResource": "사이트 리소스 업데이트",
|
||||
"actionListInvitations": "초대 목록",
|
||||
"noneSelected": "선택된 항목 없음",
|
||||
"orgNotFound2": "조직이 없습니다.",
|
||||
"searchProgress": "검색...",
|
||||
@@ -1072,7 +1106,6 @@
|
||||
"navbar": "탐색 메뉴",
|
||||
"navbarDescription": "애플리케이션의 주요 탐색 메뉴",
|
||||
"navbarDocsLink": "문서",
|
||||
"commercialEdition": "상업용 에디션",
|
||||
"otpErrorEnable": "2FA를 활성화할 수 없습니다.",
|
||||
"otpErrorEnableDescription": "2FA를 활성화하는 동안 오류가 발생했습니다",
|
||||
"otpSetupCheckCode": "6자리 코드를 입력하세요",
|
||||
@@ -1128,10 +1161,29 @@
|
||||
"sidebarAllUsers": "모든 사용자",
|
||||
"sidebarIdentityProviders": "신원 공급자",
|
||||
"sidebarLicense": "라이선스",
|
||||
"sidebarClients": "클라이언트 (Beta)",
|
||||
"sidebarClients": "클라이언트",
|
||||
"sidebarDomains": "도메인",
|
||||
"enableDockerSocket": "Docker 소켓 활성화",
|
||||
"enableDockerSocketDescription": "컨테이너 정보를 채우기 위해 Docker 소켓 검색을 활성화합니다. 소켓 경로는 Newt에 제공되어야 합니다.",
|
||||
"sidebarBluePrints": "청사진",
|
||||
"blueprints": "청사진",
|
||||
"blueprintsDescription": "선언적 구성을 적용하고 이전 실행을 봅니다",
|
||||
"blueprintAdd": "청사진 추가",
|
||||
"blueprintGoBack": "모든 청사진 보기",
|
||||
"blueprintCreate": "청사진 생성",
|
||||
"blueprintCreateDescription2": "새 청사진을 생성하고 적용하려면 아래 단계를 따르십시오",
|
||||
"blueprintDetails": "청사진 세부사항",
|
||||
"blueprintDetailsDescription": "적용된 청사진의 결과와 발생한 오류를 확인합니다",
|
||||
"blueprintInfo": "청사진 정보",
|
||||
"message": "메시지",
|
||||
"blueprintContentsDescription": "인프라를 설명하는 YAML 콘텐츠를 정의하십시오",
|
||||
"blueprintErrorCreateDescription": "청사진을 적용하는 중 오류가 발생했습니다",
|
||||
"blueprintErrorCreate": "청사진 생성 오류",
|
||||
"searchBlueprintProgress": "청사진 검색...",
|
||||
"appliedAt": "적용 시점",
|
||||
"source": "출처",
|
||||
"contents": "콘텐츠",
|
||||
"parsedContents": "구문 분석된 콘텐츠 (읽기 전용)",
|
||||
"enableDockerSocket": "Docker 청사진 활성화",
|
||||
"enableDockerSocketDescription": "블루프린트 레이블을 위한 Docker 소켓 레이블 수집을 활성화합니다. 소켓 경로는 Newt에 제공되어야 합니다.",
|
||||
"enableDockerSocketLink": "자세히 알아보기",
|
||||
"viewDockerContainers": "도커 컨테이너 보기",
|
||||
"containersIn": "{siteName}의 컨테이너",
|
||||
@@ -1185,9 +1237,8 @@
|
||||
"domainCreate": "도메인 생성",
|
||||
"domainCreatedDescription": "도메인이 성공적으로 생성되었습니다",
|
||||
"domainDeletedDescription": "도메인이 성공적으로 삭제되었습니다",
|
||||
"domainQuestionRemove": "도메인 {domain}을(를) 계정에서 제거하시겠습니까?",
|
||||
"domainQuestionRemove": "계정에서 도메인을 제거하시겠습니까?",
|
||||
"domainMessageRemove": "제거되면 도메인이 더 이상 계정과 연관되지 않습니다.",
|
||||
"domainMessageConfirm": "확인하려면 아래에 도메인명을 입력하세요.",
|
||||
"domainConfirmDelete": "도메인 삭제 확인",
|
||||
"domainDelete": "도메인 삭제",
|
||||
"domain": "도메인",
|
||||
@@ -1228,10 +1279,19 @@
|
||||
"settingsErrorUpdateDescription": "설정을 업데이트하는 동안 오류가 발생했습니다",
|
||||
"sidebarCollapse": "줄이기",
|
||||
"sidebarExpand": "확장하기",
|
||||
"productUpdateMoreInfo": "{noOfUpdates}개의 더 많은 업데이트",
|
||||
"productUpdateInfo": "{noOfUpdates}개 업데이트",
|
||||
"productUpdateWhatsNew": "새로운 기능",
|
||||
"productUpdateTitle": "제품 업데이트",
|
||||
"productUpdateEmpty": "업데이트 없음",
|
||||
"dismissAll": "모두 해제",
|
||||
"pangolinUpdateAvailable": "새 버전 사용 가능",
|
||||
"pangolinUpdateAvailableInfo": "버전 {version}을(를) 설치할 준비가 되었습니다",
|
||||
"pangolinUpdateAvailableReleaseNotes": "릴리스 노트 보기",
|
||||
"newtUpdateAvailable": "업데이트 가능",
|
||||
"newtUpdateAvailableInfo": "뉴트의 새 버전이 출시되었습니다. 최상의 경험을 위해 최신 버전으로 업데이트하세요.",
|
||||
"domainPickerEnterDomain": "도메인",
|
||||
"domainPickerPlaceholder": "myapp.example.com, api.v1.mydomain.com, 또는 그냥 myapp",
|
||||
"domainPickerPlaceholder": "myapp.example.com",
|
||||
"domainPickerDescription": "리소스의 전체 도메인을 입력하여 사용 가능한 옵션을 확인하십시오.",
|
||||
"domainPickerDescriptionSaas": "전체 도메인, 서브도메인 또는 이름을 입력하여 사용 가능한 옵션을 확인하십시오.",
|
||||
"domainPickerTabAll": "모두",
|
||||
@@ -1246,6 +1306,48 @@
|
||||
"domainPickerSubdomain": "서브도메인: {subdomain}",
|
||||
"domainPickerNamespace": "이름 공간: {namespace}",
|
||||
"domainPickerShowMore": "더보기",
|
||||
"regionSelectorTitle": "지역 선택",
|
||||
"regionSelectorInfo": "지역을 선택하면 위치에 따라 더 나은 성능이 제공됩니다. 서버와 같은 지역에 있을 필요는 없습니다.",
|
||||
"regionSelectorPlaceholder": "지역 선택",
|
||||
"regionSelectorComingSoon": "곧 출시 예정",
|
||||
"billingLoadingSubscription": "구독 불러오는 중...",
|
||||
"billingFreeTier": "무료 티어",
|
||||
"billingWarningOverLimit": "경고: 하나 이상의 사용 한도를 초과했습니다. 구독을 수정하거나 사용량을 조정하기 전까지 사이트는 연결되지 않습니다.",
|
||||
"billingUsageLimitsOverview": "사용 한도 개요",
|
||||
"billingMonitorUsage": "설정된 한도에 대한 사용량을 모니터링합니다. 한도를 늘려야 하는 경우 support@pangolin.net로 연락하십시오.",
|
||||
"billingDataUsage": "데이터 사용량",
|
||||
"billingOnlineTime": "사이트 온라인 시간",
|
||||
"billingUsers": "활성 사용자",
|
||||
"billingDomains": "활성 도메인",
|
||||
"billingRemoteExitNodes": "활성 자체 호스팅 노드",
|
||||
"billingNoLimitConfigured": "구성된 한도가 없습니다.",
|
||||
"billingEstimatedPeriod": "예상 청구 기간",
|
||||
"billingIncludedUsage": "포함 사용량",
|
||||
"billingIncludedUsageDescription": "현재 구독 계획에 포함된 사용량",
|
||||
"billingFreeTierIncludedUsage": "무료 티어 사용 허용량",
|
||||
"billingIncluded": "포함됨",
|
||||
"billingEstimatedTotal": "예상 총액:",
|
||||
"billingNotes": "노트",
|
||||
"billingEstimateNote": "현재 사용량을 기반으로 한 추정치입니다.",
|
||||
"billingActualChargesMayVary": "실제 청구 금액은 다를 수 있습니다.",
|
||||
"billingBilledAtEnd": "청구 기간이 끝난 후 청구됩니다.",
|
||||
"billingModifySubscription": "구독 수정",
|
||||
"billingStartSubscription": "구독 시작",
|
||||
"billingRecurringCharge": "반복 요금",
|
||||
"billingManageSubscriptionSettings": "구독 설정 및 기본 설정을 관리합니다",
|
||||
"billingNoActiveSubscription": "활성 구독이 없습니다. 사용 한도를 늘리려면 구독을 시작하십시오.",
|
||||
"billingFailedToLoadSubscription": "구독을 불러오는 데 실패했습니다.",
|
||||
"billingFailedToLoadUsage": "사용량을 불러오는 데 실패했습니다.",
|
||||
"billingFailedToGetCheckoutUrl": "체크아웃 URL을 가져오는 데 실패했습니다.",
|
||||
"billingPleaseTryAgainLater": "나중에 다시 시도하십시오.",
|
||||
"billingCheckoutError": "체크아웃 오류",
|
||||
"billingFailedToGetPortalUrl": "포털 URL을 가져오는 데 실패했습니다.",
|
||||
"billingPortalError": "포털 오류",
|
||||
"billingDataUsageInfo": "클라우드에 연결할 때 보안 터널을 통해 전송된 모든 데이터에 대해 비용이 청구됩니다. 여기에는 모든 사이트의 들어오고 나가는 트래픽이 포함됩니다. 사용량 한도에 도달하면 플랜을 업그레이드하거나 사용량을 줄일 때까지 사이트가 연결 해제됩니다. 노드를 사용하는 경우 데이터는 요금이 청구되지 않습니다.",
|
||||
"billingOnlineTimeInfo": "사이트가 클라우드에 연결된 시간에 따라 요금이 청구됩니다. 예를 들어, 44,640분은 사이트가 한 달 내내 24시간 작동하는 것과 같습니다. 사용량 한도에 도달하면 플랜을 업그레이드하거나 사용량을 줄일 때까지 사이트가 연결 해제됩니다. 노드를 사용할 때 시간은 요금이 청구되지 않습니다.",
|
||||
"billingUsersInfo": "조직의 사용자마다 요금이 청구됩니다. 청구는 조직의 활성 사용자 계정 수에 따라 매일 계산됩니다.",
|
||||
"billingDomainInfo": "조직의 도메인마다 요금이 청구됩니다. 청구는 조직의 활성 도메인 계정 수에 따라 매일 계산됩니다.",
|
||||
"billingRemoteExitNodesInfo": "조직의 관리 노드마다 요금이 청구됩니다. 청구는 조직의 활성 관리 노드 수에 따라 매일 계산됩니다.",
|
||||
"domainNotFound": "도메인을 찾을 수 없습니다",
|
||||
"domainNotFoundDescription": "이 리소스는 도메인이 더 이상 시스템에 존재하지 않아 비활성화되었습니다. 이 리소스에 대한 새 도메인을 설정하세요.",
|
||||
"failed": "실패",
|
||||
@@ -1278,8 +1380,20 @@
|
||||
"securityKeyUnknownError": "보안 키를 사용하는 데 문제가 발생했습니다. 다시 시도하세요.",
|
||||
"twoFactorRequired": "보안 키를 등록하려면 이중 인증이 필요합니다.",
|
||||
"twoFactor": "이중 인증",
|
||||
"twoFactorAuthentication": "이중 인증",
|
||||
"twoFactorDescription": "이 조직은 이중 인증을 요구합니다.",
|
||||
"enableTwoFactor": "이중 인증 활성화",
|
||||
"organizationSecurityPolicy": "조직 보안 정책",
|
||||
"organizationSecurityPolicyDescription": "이 조직에는 접근하기 전에 준수해야 하는 보안 요구 사항이 있습니다",
|
||||
"securityRequirements": "보안 요구 사항",
|
||||
"allRequirementsMet": "모든 요구 사항이 충족되었습니다",
|
||||
"completeRequirementsToContinue": "이 조직에 계속 접근하려면 아래 요구 사항을 완료하십시오",
|
||||
"youCanNowAccessOrganization": "이제 이 조직에 접근할 수 있습니다",
|
||||
"reauthenticationRequired": "세션 길이",
|
||||
"reauthenticationDescription": "이 조직은 {maxDays}일마다 로그인하는 것을 요구합니다.",
|
||||
"reauthenticationDescriptionHours": "이 조직은 {maxHours}시간마다 로그인하는 것을 요구합니다.",
|
||||
"reauthenticateNow": "다시 로그인",
|
||||
"adminEnabled2FaOnYourAccount": "관리자가 {email}에 대한 이중 인증을 활성화했습니다. 계속하려면 설정을 완료하세요.",
|
||||
"continueToApplication": "응용 프로그램으로 계속",
|
||||
"securityKeyAdd": "보안 키 추가",
|
||||
"securityKeyRegisterTitle": "새 보안 키 등록",
|
||||
"securityKeyRegisterDescription": "보안 키를 연결하고 식별할 이름을 입력하세요.",
|
||||
@@ -1309,6 +1423,7 @@
|
||||
"createDomainDnsPropagationDescription": "DNS 변경 사항은 인터넷 전체에 전파되는 데 시간이 걸립니다. DNS 제공자와 TTL 설정에 따라 몇 분에서 48시간까지 걸릴 수 있습니다.",
|
||||
"resourcePortRequired": "HTTP 리소스가 아닌 경우 포트 번호가 필요합니다",
|
||||
"resourcePortNotAllowed": "HTTP 리소스에 대해 포트 번호를 설정하지 마세요",
|
||||
"billingPricingCalculatorLink": "가격 계산기",
|
||||
"signUpTerms": {
|
||||
"IAgreeToThe": "동의합니다",
|
||||
"termsOfService": "서비스 약관",
|
||||
@@ -1356,7 +1471,43 @@
|
||||
"externalProxyEnabled": "외부 프록시 활성화됨",
|
||||
"addNewTarget": "새 대상 추가",
|
||||
"targetsList": "대상 목록",
|
||||
"advancedMode": "고급 모드",
|
||||
"targetErrorDuplicateTargetFound": "중복 대상 발견",
|
||||
"healthCheckHealthy": "정상",
|
||||
"healthCheckUnhealthy": "비정상",
|
||||
"healthCheckUnknown": "알 수 없음",
|
||||
"healthCheck": "상태 확인",
|
||||
"configureHealthCheck": "상태 확인 설정",
|
||||
"configureHealthCheckDescription": "{target}에 대한 상태 모니터링 설정",
|
||||
"enableHealthChecks": "상태 확인 활성화",
|
||||
"enableHealthChecksDescription": "이 대상을 모니터링하여 건강 상태를 확인하세요. 필요에 따라 대상과 다른 엔드포인트를 모니터링할 수 있습니다.",
|
||||
"healthScheme": "방법",
|
||||
"healthSelectScheme": "방법 선택",
|
||||
"healthCheckPath": "경로",
|
||||
"healthHostname": "IP / 호스트",
|
||||
"healthPort": "포트",
|
||||
"healthCheckPathDescription": "상태 확인을 위한 경로입니다.",
|
||||
"healthyIntervalSeconds": "정상 간격",
|
||||
"unhealthyIntervalSeconds": "비정상 간격",
|
||||
"IntervalSeconds": "정상 간격",
|
||||
"timeoutSeconds": "시간 초과",
|
||||
"timeIsInSeconds": "시간은 초 단위입니다",
|
||||
"retryAttempts": "재시도 횟수",
|
||||
"expectedResponseCodes": "예상 응답 코드",
|
||||
"expectedResponseCodesDescription": "정상 상태를 나타내는 HTTP 상태 코드입니다. 비워 두면 200-300이 정상으로 간주됩니다.",
|
||||
"customHeaders": "사용자 정의 헤더",
|
||||
"customHeadersDescription": "헤더는 새 줄로 구분됨: Header-Name: value",
|
||||
"headersValidationError": "헤더는 형식이어야 합니다: 헤더명: 값.",
|
||||
"saveHealthCheck": "상태 확인 저장",
|
||||
"healthCheckSaved": "상태 확인이 저장되었습니다.",
|
||||
"healthCheckSavedDescription": "상태 확인 구성이 성공적으로 저장되었습니다",
|
||||
"healthCheckError": "상태 확인 오류",
|
||||
"healthCheckErrorDescription": "상태 확인 구성을 저장하는 동안 오류가 발생했습니다",
|
||||
"healthCheckPathRequired": "상태 확인 경로는 필수입니다.",
|
||||
"healthCheckMethodRequired": "HTTP 방법은 필수입니다.",
|
||||
"healthCheckIntervalMin": "확인 간격은 최소 5초여야 합니다.",
|
||||
"healthCheckTimeoutMin": "시간 초과는 최소 1초여야 합니다.",
|
||||
"healthCheckRetryMin": "재시도 횟수는 최소 1회여야 합니다.",
|
||||
"httpMethod": "HTTP 메소드",
|
||||
"selectHttpMethod": "HTTP 메소드 선택",
|
||||
"domainPickerSubdomainLabel": "서브도메인",
|
||||
@@ -1370,6 +1521,7 @@
|
||||
"domainPickerEnterSubdomainToSearch": "사용 가능한 무료 도메인에서 검색 및 선택할 서브도메인 입력.",
|
||||
"domainPickerFreeDomains": "무료 도메인",
|
||||
"domainPickerSearchForAvailableDomains": "사용 가능한 도메인 검색",
|
||||
"domainPickerNotWorkSelfHosted": "참고: 무료 제공 도메인은 현재 자체 호스팅 인스턴스에 사용할 수 없습니다.",
|
||||
"resourceDomain": "도메인",
|
||||
"resourceEditDomain": "도메인 수정",
|
||||
"siteName": "사이트 이름",
|
||||
@@ -1382,6 +1534,12 @@
|
||||
"resourcesTableTheseResourcesForUseWith": "이 리소스는 다음과 함께 사용하기 위한 것입니다.",
|
||||
"resourcesTableClients": "클라이언트",
|
||||
"resourcesTableAndOnlyAccessibleInternally": "클라이언트와 연결되었을 때만 내부적으로 접근 가능합니다.",
|
||||
"resourcesTableNoTargets": "대상 없음",
|
||||
"resourcesTableHealthy": "정상",
|
||||
"resourcesTableDegraded": "저하됨",
|
||||
"resourcesTableOffline": "오프라인",
|
||||
"resourcesTableUnknown": "알 수 없음",
|
||||
"resourcesTableNotMonitored": "모니터링되지 않음",
|
||||
"editInternalResourceDialogEditClientResource": "클라이언트 리소스 수정",
|
||||
"editInternalResourceDialogUpdateResourceProperties": "{resourceName}의 리소스 속성과 대상 구성을 업데이트하세요.",
|
||||
"editInternalResourceDialogResourceProperties": "리소스 속성",
|
||||
@@ -1389,8 +1547,6 @@
|
||||
"editInternalResourceDialogProtocol": "프로토콜",
|
||||
"editInternalResourceDialogSitePort": "사이트 포트",
|
||||
"editInternalResourceDialogTargetConfiguration": "대상 구성",
|
||||
"editInternalResourceDialogDestinationIP": "대상 IP",
|
||||
"editInternalResourceDialogDestinationPort": "대상 IP의 포트",
|
||||
"editInternalResourceDialogCancel": "취소",
|
||||
"editInternalResourceDialogSaveResource": "리소스 저장",
|
||||
"editInternalResourceDialogSuccess": "성공",
|
||||
@@ -1421,9 +1577,7 @@
|
||||
"createInternalResourceDialogSitePort": "사이트 포트",
|
||||
"createInternalResourceDialogSitePortDescription": "사이트에 연결되었을 때 리소스에 접근하기 위해 이 포트를 사용합니다.",
|
||||
"createInternalResourceDialogTargetConfiguration": "대상 설정",
|
||||
"createInternalResourceDialogDestinationIP": "대상 IP",
|
||||
"createInternalResourceDialogDestinationIPDescription": "사이트 네트워크의 자원 IP 주소입니다.",
|
||||
"createInternalResourceDialogDestinationPort": "대상 포트",
|
||||
"createInternalResourceDialogDestinationIPDescription": "사이트 네트워크의 자원 IP 또는 호스트 네임 주소입니다.",
|
||||
"createInternalResourceDialogDestinationPortDescription": "대상 IP에서 리소스에 접근할 수 있는 포트입니다.",
|
||||
"createInternalResourceDialogCancel": "취소",
|
||||
"createInternalResourceDialogCreateResource": "리소스 생성",
|
||||
@@ -1455,5 +1609,532 @@
|
||||
"autoLoginRedirecting": "로그인으로 리디렉션 중...",
|
||||
"autoLoginError": "자동 로그인 오류",
|
||||
"autoLoginErrorNoRedirectUrl": "ID 공급자로부터 리디렉션 URL을 받지 못했습니다.",
|
||||
"autoLoginErrorGeneratingUrl": "인증 URL 생성 실패."
|
||||
"autoLoginErrorGeneratingUrl": "인증 URL 생성 실패.",
|
||||
"remoteExitNodeManageRemoteExitNodes": "원격 노드",
|
||||
"remoteExitNodeDescription": "네트워크 연결성을 확장하고 클라우드 의존도를 줄이기 위해 하나 이상의 원격 노드를 자체 호스트하십시오.",
|
||||
"remoteExitNodes": "노드",
|
||||
"searchRemoteExitNodes": "노드 검색...",
|
||||
"remoteExitNodeAdd": "노드 추가",
|
||||
"remoteExitNodeErrorDelete": "노드 삭제 오류",
|
||||
"remoteExitNodeQuestionRemove": "조직에서 노드를 제거하시겠습니까?",
|
||||
"remoteExitNodeMessageRemove": "한 번 제거되면 더 이상 노드에 접근할 수 없습니다.",
|
||||
"remoteExitNodeConfirmDelete": "노드 삭제 확인",
|
||||
"remoteExitNodeDelete": "노드 삭제",
|
||||
"sidebarRemoteExitNodes": "원격 노드",
|
||||
"remoteExitNodeCreate": {
|
||||
"title": "노드 생성",
|
||||
"description": "네트워크 연결성을 확장하기 위해 새 노드를 생성하세요",
|
||||
"viewAllButton": "모든 노드 보기",
|
||||
"strategy": {
|
||||
"title": "생성 전략",
|
||||
"description": "노드를 직접 구성하거나 새 자격 증명을 생성하려면 이것을 선택하세요.",
|
||||
"adopt": {
|
||||
"title": "노드 채택",
|
||||
"description": "이미 노드의 자격 증명이 있는 경우 이것을 선택하세요."
|
||||
},
|
||||
"generate": {
|
||||
"title": "키 생성",
|
||||
"description": "노드에 대한 새 키를 생성하려면 이것을 선택하세요"
|
||||
}
|
||||
},
|
||||
"adopt": {
|
||||
"title": "기존 노드 채택",
|
||||
"description": "채택하려는 기존 노드의 자격 증명을 입력하세요",
|
||||
"nodeIdLabel": "노드 ID",
|
||||
"nodeIdDescription": "채택하려는 기존 노드의 ID",
|
||||
"secretLabel": "비밀",
|
||||
"secretDescription": "기존 노드의 비밀 키",
|
||||
"submitButton": "노드 채택"
|
||||
},
|
||||
"generate": {
|
||||
"title": "생성된 자격 증명",
|
||||
"description": "생성된 자격 증명을 사용하여 노드를 구성하세요",
|
||||
"nodeIdTitle": "노드 ID",
|
||||
"secretTitle": "비밀",
|
||||
"saveCredentialsTitle": "구성에 자격 증명 추가",
|
||||
"saveCredentialsDescription": "연결을 완료하려면 이러한 자격 증명을 자체 호스팅 Pangolin 노드 구성 파일에 추가하십시오.",
|
||||
"submitButton": "노드 생성"
|
||||
},
|
||||
"validation": {
|
||||
"adoptRequired": "기존 노드를 채택하려면 노드 ID와 비밀 키가 필요합니다"
|
||||
},
|
||||
"errors": {
|
||||
"loadDefaultsFailed": "기본값 로드 실패",
|
||||
"defaultsNotLoaded": "기본값 로드되지 않음",
|
||||
"createFailed": "노드 생성 실패"
|
||||
},
|
||||
"success": {
|
||||
"created": "노드가 성공적으로 생성되었습니다"
|
||||
}
|
||||
},
|
||||
"remoteExitNodeSelection": "노드 선택",
|
||||
"remoteExitNodeSelectionDescription": "이 로컬 사이트에서 트래픽을 라우팅할 노드를 선택하세요",
|
||||
"remoteExitNodeRequired": "로컬 사이트에 노드를 선택해야 합니다",
|
||||
"noRemoteExitNodesAvailable": "사용 가능한 노드가 없습니다",
|
||||
"noRemoteExitNodesAvailableDescription": "이 조직에 사용 가능한 노드가 없습니다. 로컬 사이트를 사용하려면 먼저 노드를 생성하세요.",
|
||||
"exitNode": "종단 노드",
|
||||
"country": "국가",
|
||||
"rulesMatchCountry": "현재 소스 IP를 기반으로 합니다",
|
||||
"managedSelfHosted": {
|
||||
"title": "관리 자체 호스팅",
|
||||
"description": "더 신뢰할 수 있고 낮은 유지보수의 자체 호스팅 팡골린 서버, 추가 기능 포함",
|
||||
"introTitle": "관리 자체 호스팅 팡골린",
|
||||
"introDescription": "는 자신의 데이터를 프라이빗하고 자체 호스팅을 유지하면서 더 간단하고 추가적인 신뢰성을 원하는 사람들을 위한 배포 옵션입니다.",
|
||||
"introDetail": "이 옵션을 사용하면 여전히 자신의 팡골린 노드를 운영하고 - 터널, SSL 종료 및 트래픽 모두 서버에 유지됩니다. 차이점은 관리 및 모니터링이 클라우드 대시보드를 통해 처리되어 여러 혜택을 제공합니다.",
|
||||
"benefitSimplerOperations": {
|
||||
"title": "더 간단한 운영",
|
||||
"description": "자체 메일 서버를 운영하거나 복잡한 경고를 설정할 필요가 없습니다. 기본적으로 상태 점검 및 다운타임 경고를 받을 수 있습니다."
|
||||
},
|
||||
"benefitAutomaticUpdates": {
|
||||
"title": "자동 업데이트",
|
||||
"description": "클라우드 대시보드는 빠르게 발전하므로 새로운 기능과 버그 수정 사항을 수동으로 새로운 컨테이너를 가져오지 않고도 받을 수 있습니다."
|
||||
},
|
||||
"benefitLessMaintenance": {
|
||||
"title": "유지보수 감소",
|
||||
"description": "데이터베이스 마이그레이션, 백업 또는 추가 인프라를 관리할 필요가 없습니다. 저희가 클라우드에서 처리합니다."
|
||||
},
|
||||
"benefitCloudFailover": {
|
||||
"title": "클라우드 장애 조치",
|
||||
"description": "노드가 다운되면 터널이 클라우드의 프레즌스 포인트로 임시 전환되어 노드를 다시 온라인으로 가져올 때까지 유지됩니다."
|
||||
},
|
||||
"benefitHighAvailability": {
|
||||
"title": "고가용성 (PoPs)",
|
||||
"description": "계정에 여러 노드를 연결하여 이중성과 성능을 향상시킬 수 있습니다."
|
||||
},
|
||||
"benefitFutureEnhancements": {
|
||||
"title": "향후 개선",
|
||||
"description": "배포를 더욱 견고하게 만들기 위해 더 많은 분석, 경고, 및 관리 도구를 추가할 계획입니다."
|
||||
},
|
||||
"docsAlert": {
|
||||
"text": "관리 자체 호스팅 옵션에 대해 더 알아보세요",
|
||||
"documentation": "문서"
|
||||
},
|
||||
"convertButton": "이 노드를 관리 자체 호스팅으로 변환"
|
||||
},
|
||||
"internationaldomaindetected": "국제 도메인 감지됨",
|
||||
"willbestoredas": "다음으로 저장됩니다:",
|
||||
"roleMappingDescription": "자동 프로비저닝이 활성화되면 사용자가 로그인할 때 역할이 할당되는 방법을 결정합니다.",
|
||||
"selectRole": "역할 선택",
|
||||
"roleMappingExpression": "표현식",
|
||||
"selectRolePlaceholder": "역할 선택",
|
||||
"selectRoleDescription": "이 신원 공급자로부터 모든 사용자에게 할당할 역할을 선택하십시오.",
|
||||
"roleMappingExpressionDescription": "ID 토큰에서 역할 정보를 추출하기 위한 JMESPath 표현식을 입력하세요.",
|
||||
"idpTenantIdRequired": "테넌트 ID가 필요합니다",
|
||||
"invalidValue": "잘못된 값",
|
||||
"idpTypeLabel": "신원 공급자 유형",
|
||||
"roleMappingExpressionPlaceholder": "예: contains(groups, 'admin') && 'Admin' || 'Member'",
|
||||
"idpGoogleConfiguration": "Google 구성",
|
||||
"idpGoogleConfigurationDescription": "Google OAuth2 자격 증명을 구성합니다.",
|
||||
"idpGoogleClientIdDescription": "Google OAuth2 클라이언트 ID",
|
||||
"idpGoogleClientSecretDescription": "Google OAuth2 클라이언트 비밀",
|
||||
"idpAzureConfiguration": "Azure Entra ID 구성",
|
||||
"idpAzureConfigurationDescription": "Azure Entra ID OAuth2 자격 증명을 구성합니다.",
|
||||
"idpTenantId": "테넌트 ID",
|
||||
"idpTenantIdPlaceholder": "your-tenant-id",
|
||||
"idpAzureTenantIdDescription": "Azure 액티브 디렉터리 개요에서 찾은 Azure 테넌트 ID",
|
||||
"idpAzureClientIdDescription": "Azure 앱 등록 클라이언트 ID",
|
||||
"idpAzureClientSecretDescription": "Azure 앱 등록 클라이언트 비밀",
|
||||
"idpGoogleTitle": "구글",
|
||||
"idpGoogleAlt": "구글",
|
||||
"idpAzureTitle": "Azure Entra ID",
|
||||
"idpAzureAlt": "애저",
|
||||
"idpGoogleConfigurationTitle": "Google 구성",
|
||||
"idpAzureConfigurationTitle": "Azure Entra ID 구성",
|
||||
"idpTenantIdLabel": "테넌트 ID",
|
||||
"idpAzureClientIdDescription2": "Azure 앱 등록 클라이언트 ID",
|
||||
"idpAzureClientSecretDescription2": "Azure 앱 등록 클라이언트 비밀",
|
||||
"idpGoogleDescription": "Google OAuth2/OIDC 공급자",
|
||||
"idpAzureDescription": "Microsoft Azure OAuth2/OIDC 공급자",
|
||||
"subnet": "서브넷",
|
||||
"subnetDescription": "이 조직의 네트워크 구성에 대한 서브넷입니다.",
|
||||
"authPage": "인증 페이지",
|
||||
"authPageDescription": "조직에 대한 인증 페이지를 구성합니다.",
|
||||
"authPageDomain": "인증 페이지 도메인",
|
||||
"noDomainSet": "도메인 설정 없음",
|
||||
"changeDomain": "도메인 변경",
|
||||
"selectDomain": "도메인 선택",
|
||||
"restartCertificate": "인증서 재시작",
|
||||
"editAuthPageDomain": "인증 페이지 도메인 편집",
|
||||
"setAuthPageDomain": "인증 페이지 도메인 설정",
|
||||
"failedToFetchCertificate": "인증서 가져오기 실패",
|
||||
"failedToRestartCertificate": "인증서 재시작 실패",
|
||||
"addDomainToEnableCustomAuthPages": "조직의 맞춤 인증 페이지를 활성화하려면 도메인을 추가하세요.",
|
||||
"selectDomainForOrgAuthPage": "조직 인증 페이지에 대한 도메인을 선택하세요.",
|
||||
"domainPickerProvidedDomain": "제공된 도메인",
|
||||
"domainPickerFreeProvidedDomain": "무료 제공된 도메인",
|
||||
"domainPickerVerified": "검증됨",
|
||||
"domainPickerUnverified": "검증되지 않음",
|
||||
"domainPickerInvalidSubdomainStructure": "이 하위 도메인은 잘못된 문자 또는 구조를 포함하고 있습니다. 저장 시 자동으로 정리됩니다.",
|
||||
"domainPickerError": "오류",
|
||||
"domainPickerErrorLoadDomains": "조직 도메인 로드 실패",
|
||||
"domainPickerErrorCheckAvailability": "도메인 가용성 확인 실패",
|
||||
"domainPickerInvalidSubdomain": "잘못된 하위 도메인",
|
||||
"domainPickerInvalidSubdomainRemoved": "입력 \"{sub}\"이(가) 유효하지 않으므로 제거되었습니다.",
|
||||
"domainPickerInvalidSubdomainCannotMakeValid": "\"{sub}\"을(를) {domain}에 대해 유효하게 만들 수 없습니다.",
|
||||
"domainPickerSubdomainSanitized": "하위 도메인 정리됨",
|
||||
"domainPickerSubdomainCorrected": "\"{sub}\"이(가) \"{sanitized}\"로 수정되었습니다",
|
||||
"orgAuthSignInTitle": "조직에 로그인",
|
||||
"orgAuthChooseIdpDescription": "계속하려면 신원 공급자를 선택하세요.",
|
||||
"orgAuthNoIdpConfigured": "이 조직은 구성된 신원 공급자가 없습니다. 대신 Pangolin 아이덴티티로 로그인할 수 있습니다.",
|
||||
"orgAuthSignInWithPangolin": "Pangolin으로 로그인",
|
||||
"subscriptionRequiredToUse": "이 기능을 사용하려면 구독이 필요합니다.",
|
||||
"idpDisabled": "신원 공급자가 비활성화되었습니다.",
|
||||
"orgAuthPageDisabled": "조직 인증 페이지가 비활성화되었습니다.",
|
||||
"domainRestartedDescription": "도메인 인증이 성공적으로 재시작되었습니다.",
|
||||
"resourceAddEntrypointsEditFile": "파일 편집: config/traefik/traefik_config.yml",
|
||||
"resourceExposePortsEditFile": "파일 편집: docker-compose.yml",
|
||||
"emailVerificationRequired": "이메일 인증이 필요합니다. 이 단계를 완료하려면 {dashboardUrl}/auth/login 통해 다시 로그인하십시오. 그런 다음 여기로 돌아오세요.",
|
||||
"twoFactorSetupRequired": "이중 인증 설정이 필요합니다. 이 단계를 완료하려면 {dashboardUrl}/auth/login 통해 다시 로그인하십시오. 그런 다음 여기로 돌아오세요.",
|
||||
"additionalSecurityRequired": "추가 보안 필요",
|
||||
"organizationRequiresAdditionalSteps": "이 조직은 자원에 접근하기 전에 추가 보안 단계를 요구합니다.",
|
||||
"completeTheseSteps": "이 단계를 완료하십시오",
|
||||
"enableTwoFactorAuthentication": "이중 인증 활성화",
|
||||
"completeSecuritySteps": "보안 단계 완료",
|
||||
"securitySettings": "보안 설정",
|
||||
"securitySettingsDescription": "조직에 대한 보안 정책을 구성합니다",
|
||||
"requireTwoFactorForAllUsers": "모든 사용자에 대해 이중 인증 요구",
|
||||
"requireTwoFactorDescription": "활성화되면, 이 조직의 모든 내부 사용자는 조직에 접근하기 위해 이중 인증을 활성화해야 합니다.",
|
||||
"requireTwoFactorDisabledDescription": "이 기능을 사용하려면 유효한 라이선스(Enterprise) 또는 활성 구독(SaaS)가 필요합니다.",
|
||||
"requireTwoFactorCannotEnableDescription": "모든 사용자에게 강제하기 전에 계정에 대해 이중 인증을 활성화해야 합니다",
|
||||
"maxSessionLength": "최대 세션 길이",
|
||||
"maxSessionLengthDescription": "사용자 세션의 최대 지속 시간을 설정합니다. 이 시간이 지나면 사용자는 다시 인증해야 합니다.",
|
||||
"maxSessionLengthDisabledDescription": "이 기능을 사용하려면 유효한 라이선스(Enterprise) 또는 활성 구독(SaaS)가 필요합니다.",
|
||||
"selectSessionLength": "세션 길이 선택",
|
||||
"unenforced": "강제되지 않음",
|
||||
"1Hour": "1 시간",
|
||||
"3Hours": "3 시간",
|
||||
"6Hours": "6 시간",
|
||||
"12Hours": "12 시간",
|
||||
"1DaySession": "1 일",
|
||||
"3Days": "3 일",
|
||||
"7Days": "7 일",
|
||||
"14Days": "14 일",
|
||||
"30DaysSession": "30 일",
|
||||
"90DaysSession": "90 일",
|
||||
"180DaysSession": "180 일",
|
||||
"passwordExpiryDays": "비밀번호 만료",
|
||||
"editPasswordExpiryDescription": "사용자가 비밀번호를 변경해야 하는 날 수를 설정합니다.",
|
||||
"selectPasswordExpiry": "비밀번호 만료 선택",
|
||||
"30Days": "30 일",
|
||||
"1Day": "1 일",
|
||||
"60Days": "60 일",
|
||||
"90Days": "90 일",
|
||||
"180Days": "180 일",
|
||||
"1Year": "1 년",
|
||||
"subscriptionBadge": "구독 필요",
|
||||
"securityPolicyChangeWarning": "보안 정책 변경 경고",
|
||||
"securityPolicyChangeDescription": "보안 정책 설정을 변경하려고 합니다. 저장 후, 정책 업데이트를 준수하기 위해 다시 인증해야 할 수도 있습니다. 규정을 준수하지 않는 모든 사용자도 다시 인증해야 합니다.",
|
||||
"securityPolicyChangeConfirmMessage": "확인합니다",
|
||||
"securityPolicyChangeWarningText": "이 작업은 조직의 모든 사용자에게 영향을 미칩니다",
|
||||
"authPageErrorUpdateMessage": "인증 페이지 설정을 업데이트하는 동안 오류가 발생했습니다",
|
||||
"authPageErrorUpdate": "인증 페이지를 업데이트할 수 없습니다",
|
||||
"authPageUpdated": "인증 페이지가 성공적으로 업데이트되었습니다",
|
||||
"healthCheckNotAvailable": "로컬",
|
||||
"rewritePath": "경로 재작성",
|
||||
"rewritePathDescription": "대상으로 전달하기 전에 경로를 선택적으로 재작성합니다.",
|
||||
"continueToApplication": "응용 프로그램으로 계속",
|
||||
"checkingInvite": "초대 확인 중",
|
||||
"setResourceHeaderAuth": "setResourceHeaderAuth",
|
||||
"resourceHeaderAuthRemove": "헤더 인증 제거",
|
||||
"resourceHeaderAuthRemoveDescription": "헤더 인증이 성공적으로 제거되었습니다.",
|
||||
"resourceErrorHeaderAuthRemove": "헤더 인증 제거 실패",
|
||||
"resourceErrorHeaderAuthRemoveDescription": "리소스의 헤더 인증을 제거할 수 없습니다.",
|
||||
"resourceHeaderAuthProtectionEnabled": "헤더 인증 활성화됨",
|
||||
"resourceHeaderAuthProtectionDisabled": "헤더 인증 비활성화됨",
|
||||
"headerAuthRemove": "헤더 인증 삭제",
|
||||
"headerAuthAdd": "헤더 인증 추가",
|
||||
"resourceErrorHeaderAuthSetup": "헤더 인증 설정 실패",
|
||||
"resourceErrorHeaderAuthSetupDescription": "리소스의 헤더 인증을 설정할 수 없습니다.",
|
||||
"resourceHeaderAuthSetup": "헤더 인증이 성공적으로 설정되었습니다.",
|
||||
"resourceHeaderAuthSetupDescription": "헤더 인증이 성공적으로 설정되었습니다.",
|
||||
"resourceHeaderAuthSetupTitle": "헤더 인증 설정",
|
||||
"resourceHeaderAuthSetupTitleDescription": "이 리소스를 HTTP 헤더 인증으로 보호하기 위해 기본 인증 자격 증명(사용자이름 및 비밀번호)을 설정합니다. 다음과 같은 형식으로 액세스하세요 https://사용자이름:비밀번호@resource.example.com",
|
||||
"resourceHeaderAuthSubmit": "헤더 인증 설정",
|
||||
"actionSetResourceHeaderAuth": "헤더 인증 설정",
|
||||
"enterpriseEdition": "엔터프라이즈 에디션",
|
||||
"unlicensed": "라이선스 없음",
|
||||
"beta": "베타",
|
||||
"manageClients": "클라이언트 관리",
|
||||
"manageClientsDescription": "클라이언트는 당신의 사이트에 연결할 수 있는 디바이스입니다.",
|
||||
"licenseTableValidUntil": "유효 기한",
|
||||
"saasLicenseKeysSettingsTitle": "엔터프라이즈 라이선스",
|
||||
"saasLicenseKeysSettingsDescription": "자체 호스팅된 Pangolin 인스턴스를 위한 엔터프라이즈 라이선스 키를 생성하고 관리합니다.",
|
||||
"sidebarEnterpriseLicenses": "라이선스",
|
||||
"generateLicenseKey": "라이선스 키 생성",
|
||||
"generateLicenseKeyForm": {
|
||||
"validation": {
|
||||
"emailRequired": "유효한 이메일 주소를 입력하세요",
|
||||
"useCaseTypeRequired": "사용 사례 유형을 선택하세요",
|
||||
"firstNameRequired": "이름은 필수입니다.",
|
||||
"lastNameRequired": "성은 필수입니다.",
|
||||
"primaryUseRequired": "주요 용도를 설명하세요",
|
||||
"jobTitleRequiredBusiness": "사업용 직책은 필수입니다.",
|
||||
"industryRequiredBusiness": "사업용 산업은 필수입니다.",
|
||||
"stateProvinceRegionRequired": "주/도/지역이 필수입니다.",
|
||||
"postalZipCodeRequired": "우편번호/ZIP 코드가 필수입니다.",
|
||||
"companyNameRequiredBusiness": "사업용 회사 이름은 필수입니다.",
|
||||
"countryOfResidenceRequiredBusiness": "사업용 거주 국가는 필수입니다.",
|
||||
"countryRequiredPersonal": "개인용 국가가 필수입니다.",
|
||||
"agreeToTermsRequired": "약관에 동의해야 합니다.",
|
||||
"complianceConfirmationRequired": "Fossorial 상용 라이선스를 준수함을 확인해야 합니다."
|
||||
},
|
||||
"useCaseOptions": {
|
||||
"personal": {
|
||||
"title": "개인 용도",
|
||||
"description": "학습, 개인 프로젝트 또는 실험과 같은 개인, 비상업적 용도로 사용합니다."
|
||||
},
|
||||
"business": {
|
||||
"title": "사업 용도",
|
||||
"description": "조직, 회사 또는 수익 창출 활동 내에서 사용됩니다."
|
||||
}
|
||||
},
|
||||
"steps": {
|
||||
"emailLicenseType": {
|
||||
"title": "이메일 및 라이선스 유형",
|
||||
"description": "이메일을 입력하고 라이선스 유형을 선택하세요"
|
||||
},
|
||||
"personalInformation": {
|
||||
"title": "개인 정보",
|
||||
"description": "자기소개를 해주세요"
|
||||
},
|
||||
"contactInformation": {
|
||||
"title": "연락처 정보",
|
||||
"description": "당신의 연락처 정보"
|
||||
},
|
||||
"termsGenerate": {
|
||||
"title": "약관 및 생성",
|
||||
"description": "라이선스를 생성하기 위해 약관을 검토하고 수락하세요"
|
||||
}
|
||||
},
|
||||
"alerts": {
|
||||
"commercialUseDisclosure": {
|
||||
"title": "사용 공개",
|
||||
"description": "당신의 의도된 사용에 정확히 맞는 라이선스 등급을 선택하세요. 개인 라이선스는 연간 총 수익 100,000 USD 이하의 개인, 비상업적 또는 소규모 상업 활동을 위한 소프트웨어의 무료 사용을 허용합니다. 이러한 제한을 넘는 모든 사용 — 비즈니스, 조직 또는 기타 수익 창출 환경 내에서의 사용 — 은 유효한 엔터프라이즈 라이선스 및 해당 라이선스 수수료의 지불이 필요합니다. 개인 또는 기업 사용자는 모두 Fossorial 상용 라이선스 조건을 준수해야 합니다."
|
||||
},
|
||||
"trialPeriodInformation": {
|
||||
"title": "시험 기간 정보",
|
||||
"description": "이 라이선스 키는 엔터프라이즈 기능을 평가판 기간 동안 7일 동안 활성화합니다. 평가 기간 이후 유료 기능에 대한 계속된 액세스는 유효한 개인 또는 엔터프라이즈 라이선스 하에서 활성화가 필요합니다. 엔터프라이즈 라이선스에 대한 정보는 sales@pangolin.net에 문의하세요."
|
||||
}
|
||||
},
|
||||
"form": {
|
||||
"useCaseQuestion": "개인 또는 사업용으로 Pangolin을 사용하시나요?",
|
||||
"firstName": "이름",
|
||||
"lastName": "성",
|
||||
"jobTitle": "직책",
|
||||
"primaryUseQuestion": "Pangolin을 주로 무엇에 사용하려고 계획하시나요?",
|
||||
"industryQuestion": "당신의 산업은 무엇입니까?",
|
||||
"prospectiveUsersQuestion": "예상하는 잠재적 사용자는 몇 명입니까?",
|
||||
"prospectiveSitesQuestion": "예상하는 잠재적 사이트(터널)는 몇 개입니까?",
|
||||
"companyName": "회사 이름",
|
||||
"countryOfResidence": "거주 국가",
|
||||
"stateProvinceRegion": "주 / 도 / 지역",
|
||||
"postalZipCode": "우편번호 / ZIP 코드",
|
||||
"companyWebsite": "회사 웹사이트",
|
||||
"companyPhoneNumber": "회사 전화번호",
|
||||
"country": "국가",
|
||||
"phoneNumberOptional": "전화번호 (선택 항목)",
|
||||
"complianceConfirmation": "제가 제공한 정보가 정확하고 Fossorial 상용 라이선스를 준수하는지 확인합니다. 부정확한 정보를 보고하거나 제품 사용을 실수로 식별하는 것은 라이선스 위반이며, 키가 취소될 수 있습니다."
|
||||
},
|
||||
"buttons": {
|
||||
"close": "닫기",
|
||||
"previous": "이전",
|
||||
"next": "다음",
|
||||
"generateLicenseKey": "라이선스 키 생성"
|
||||
},
|
||||
"toasts": {
|
||||
"success": {
|
||||
"title": "라이선스 키가 성공적으로 생성되었습니다",
|
||||
"description": "당신의 라이선스 키가 생성되었으며 사용 준비가 되었습니다."
|
||||
},
|
||||
"error": {
|
||||
"title": "라이선스 키 생성에 실패했습니다",
|
||||
"description": "라이선스 키를 생성하는 동안 오류가 발생했습니다."
|
||||
}
|
||||
}
|
||||
},
|
||||
"priority": "우선순위",
|
||||
"priorityDescription": "우선 순위가 높은 경로가 먼저 평가됩니다. 우선 순위 = 100은 자동 정렬(시스템 결정)이 의미합니다. 수동 우선 순위를 적용하려면 다른 숫자를 사용하세요.",
|
||||
"instanceName": "인스턴스 이름",
|
||||
"pathMatchModalTitle": "경로 매칭 설정",
|
||||
"pathMatchModalDescription": "경로별로 들어오는 요청을 어떻게 매칭할지 설정합니다.",
|
||||
"pathMatchType": "일치 유형",
|
||||
"pathMatchPrefix": "접두사",
|
||||
"pathMatchExact": "정확하게",
|
||||
"pathMatchRegex": "정규 표현식",
|
||||
"pathMatchValue": "경로 값",
|
||||
"clear": "지우기",
|
||||
"saveChanges": "변경 사항 저장",
|
||||
"pathMatchRegexPlaceholder": "^/api/.*",
|
||||
"pathMatchDefaultPlaceholder": "/경로",
|
||||
"pathMatchPrefixHelp": "예: /api 는 /api, /api/users 등을 매칭합니다.",
|
||||
"pathMatchExactHelp": "예: /api 는 /api 만 매칭합니다.",
|
||||
"pathMatchRegexHelp": "예: ^/api/.* 는 /api/anything를 매칭합니다",
|
||||
"pathRewriteModalTitle": "경로 재작성 설정",
|
||||
"pathRewriteModalDescription": "대상으로 전달하기 전에 매칭된 경로를 변환합니다.",
|
||||
"pathRewriteType": "재작성 유형",
|
||||
"pathRewritePrefixOption": "접두사 - 접두사 대체",
|
||||
"pathRewriteExactOption": "정확 - 전체 경로 대체",
|
||||
"pathRewriteRegexOption": "정규 표현식 - 패턴 대체",
|
||||
"pathRewriteStripPrefixOption": "접두사 제거 - 접두사 삭제",
|
||||
"pathRewriteValue": "재작성 값",
|
||||
"pathRewriteRegexPlaceholder": "/new/$1",
|
||||
"pathRewriteDefaultPlaceholder": "/새-경로",
|
||||
"pathRewritePrefixHelp": "일치하는 접두사를 이 값으로 대체합니다",
|
||||
"pathRewriteExactHelp": "경로가 정확히 일치할 때 전체 경로를 이 값으로 대체합니다",
|
||||
"pathRewriteRegexHelp": "$1, $2와 같은 캡처 그룹을 사용하여 대체",
|
||||
"pathRewriteStripPrefixHelp": "접두사를 제거하거나 새 접두사를 제공하려면 비워 둡니다",
|
||||
"pathRewritePrefix": "접두사",
|
||||
"pathRewriteExact": "정확하게",
|
||||
"pathRewriteRegex": "정규 표현식",
|
||||
"pathRewriteStrip": "제거",
|
||||
"pathRewriteStripLabel": "제거",
|
||||
"sidebarEnableEnterpriseLicense": "엔터프라이즈 라이선스 활성화",
|
||||
"cannotbeUndone": "이 작업은 되돌릴 수 없습니다.",
|
||||
"toConfirm": "확인하려면",
|
||||
"deleteClientQuestion": "고객을 사이트와 조직에서 제거하시겠습니까?",
|
||||
"clientMessageRemove": "제거되면 클라이언트는 사이트에 더 이상 연결할 수 없습니다.",
|
||||
"sidebarLogs": "로그",
|
||||
"request": "요청",
|
||||
"logs": "로그",
|
||||
"logsSettingsDescription": "이 조직에서 수집된 로그를 모니터링합니다",
|
||||
"searchLogs": "로그 검색...",
|
||||
"action": "작업",
|
||||
"actor": "행위자",
|
||||
"timestamp": "타임스탬프",
|
||||
"accessLogs": "접근 로그",
|
||||
"exportCsv": "CSV 내보내기",
|
||||
"actorId": "행위자 ID",
|
||||
"allowedByRule": "룰에 의해 허용됨",
|
||||
"allowedNoAuth": "인증 없음 허용됨",
|
||||
"validAccessToken": "유효한 접근 토큰",
|
||||
"validHeaderAuth": "유효한 헤더 인증",
|
||||
"validPincode": "유효한 핀코드",
|
||||
"validPassword": "유효한 비밀번호",
|
||||
"validEmail": "유효한 이메일",
|
||||
"validSSO": "유효한 SSO",
|
||||
"resourceBlocked": "리소스 차단됨",
|
||||
"droppedByRule": "룰에 의해 드롭됨",
|
||||
"noSessions": "세션 없음",
|
||||
"temporaryRequestToken": "임시 요청 토큰",
|
||||
"noMoreAuthMethods": "유효한 인증 없음",
|
||||
"ip": "IP",
|
||||
"reason": "이유",
|
||||
"requestLogs": "요청 로그",
|
||||
"host": "호스트",
|
||||
"location": "위치",
|
||||
"actionLogs": "작업 로그",
|
||||
"sidebarLogsRequest": "요청 로그",
|
||||
"sidebarLogsAccess": "접근 로그",
|
||||
"sidebarLogsAction": "작업 로그",
|
||||
"logRetention": "로그 보관",
|
||||
"logRetentionDescription": "다양한 유형의 로그를 이 조직에 대해 얼마나 오래 보관할지 관리하거나 비활성화합니다",
|
||||
"requestLogsDescription": "이 조직의 자원에 대한 상세한 요청 로그를 봅니다",
|
||||
"logRetentionRequestLabel": "요청 로그 보관",
|
||||
"logRetentionRequestDescription": "요청 로그를 얼마나 오래 보관할지",
|
||||
"logRetentionAccessLabel": "접근 로그 보관",
|
||||
"logRetentionAccessDescription": "접근 로그를 얼마나 오래 보관할지",
|
||||
"logRetentionActionLabel": "작업 로그 보관",
|
||||
"logRetentionActionDescription": "작업 로그를 얼마나 오래 보관할지",
|
||||
"logRetentionDisabled": "비활성화됨",
|
||||
"logRetention3Days": "3 일",
|
||||
"logRetention7Days": "7 일",
|
||||
"logRetention14Days": "14 일",
|
||||
"logRetention30Days": "30 일",
|
||||
"logRetention90Days": "90 일",
|
||||
"logRetentionForever": "영구",
|
||||
"actionLogsDescription": "이 조직에서 수행된 작업의 기록을 봅니다",
|
||||
"accessLogsDescription": "이 조직의 자원에 대한 접근 인증 요청을 확인합니다",
|
||||
"licenseRequiredToUse": "이 기능을 사용하려면 Enterprise 라이선스가 필요합니다.",
|
||||
"certResolver": "인증서 해결사",
|
||||
"certResolverDescription": "이 리소스에 사용할 인증서 해결사를 선택하세요.",
|
||||
"selectCertResolver": "인증서 해결사 선택",
|
||||
"enterCustomResolver": "사용자 정의 해결사 입력",
|
||||
"preferWildcardCert": "와일드카드 인증서 선호",
|
||||
"unverified": "검증되지 않음",
|
||||
"domainSetting": "도메인 설정",
|
||||
"domainSettingDescription": "도메인에 대한 설정을 구성하세요.",
|
||||
"preferWildcardCertDescription": "와일드카드 인증서를 생성하려고 시도합니다 (올바르게 구성된 인증서 해결사가 필요합니다).",
|
||||
"recordName": "레코드 이름",
|
||||
"auto": "자동",
|
||||
"TTL": "TTL",
|
||||
"howToAddRecords": "레코드 추가 방법",
|
||||
"dnsRecord": "DNS 레코드",
|
||||
"required": "필수",
|
||||
"domainSettingsUpdated": "도메인 설정이 성공적으로 업데이트되었습니다",
|
||||
"orgOrDomainIdMissing": "조직 ID 또는 도메인 ID가 누락되었습니다",
|
||||
"loadingDNSRecords": "DNS 레코드를 로드하는 중...",
|
||||
"olmUpdateAvailableInfo": "올름의 새 버전이 이용 가능합니다. 최상의 경험을 위해 최신 버전으로 업데이트하세요.",
|
||||
"client": "클라이언트",
|
||||
"proxyProtocol": "프록시 프로토콜 설정",
|
||||
"proxyProtocolDescription": "프록시 프로토콜을 구성하여 TCP/UDP 서비스에 대한 클라이언트 IP 주소를 보존하십시오.",
|
||||
"enableProxyProtocol": "프록시 프로토콜 활성화",
|
||||
"proxyProtocolInfo": "TCP/UDP 백엔드의 클라이언트 IP 주소를 보존합니다",
|
||||
"proxyProtocolVersion": "프록시 프로토콜 버전",
|
||||
"version1": " 버전 1 (추천)",
|
||||
"version2": "버전 2",
|
||||
"versionDescription": "버전 1은 텍스트 기반으로 널리 지원됩니다. 버전 2는 이진 기반으로 더 효율적이지만 호환성이 낮습니다.",
|
||||
"warning": "경고",
|
||||
"proxyProtocolWarning": "백엔드 애플리케이션이 프록시 프로토콜 연결을 허용하도록 구성되어야 합니다. 백엔드가 프록시 프로토콜을 지원하지 않으면, 이를 활성화하면 모든 연결이 끊어집니다. 트래픽에서 온 프록시 프로토콜 헤더를 백엔드가 신뢰하도록 구성하십시오.",
|
||||
"restarting": "재시작 중...",
|
||||
"manual": "수동",
|
||||
"messageSupport": "지원 메시지",
|
||||
"supportNotAvailableTitle": "지원 불가",
|
||||
"supportNotAvailableDescription": "현재 지원을 받을 수 없습니다. support@pangolin.net으로 이메일을 보낼 수 있습니다.",
|
||||
"supportRequestSentTitle": "지원 요청 전송 완료",
|
||||
"supportRequestSentDescription": "메시지가 성공적으로 전송되었습니다.",
|
||||
"supportRequestFailedTitle": "요청 전송 실패",
|
||||
"supportRequestFailedDescription": "지원 요청을 보내는 중 오류가 발생했습니다.",
|
||||
"supportSubjectRequired": "제목은 필수입니다",
|
||||
"supportSubjectMaxLength": "제목은 255자 이내여야 합니다",
|
||||
"supportMessageRequired": "메시지는 필수입니다",
|
||||
"supportReplyTo": "회신",
|
||||
"supportSubject": "제목",
|
||||
"supportSubjectPlaceholder": "제목 입력",
|
||||
"supportMessage": "메시지",
|
||||
"supportMessagePlaceholder": "메시지를 입력하십시오",
|
||||
"supportSending": "발송 중...",
|
||||
"supportSend": "보내기",
|
||||
"supportMessageSent": "메시지 전송 완료!",
|
||||
"supportWillContact": "곧 연락드리겠습니다!",
|
||||
"selectLogRetention": "로그 보존 선택",
|
||||
"showColumns": "열 표시",
|
||||
"hideColumns": "열 숨기기",
|
||||
"columnVisibility": "열 가시성",
|
||||
"toggleColumn": "{columnName} 열 토글",
|
||||
"allColumns": "모든 열",
|
||||
"defaultColumns": "기본 열",
|
||||
"customizeView": "보기 사용자 지정",
|
||||
"viewOptions": "보기 옵션",
|
||||
"selectAll": "모두 선택",
|
||||
"selectNone": "선택하지 않음",
|
||||
"selectedResources": "선택된 리소스",
|
||||
"enableSelected": "선택된 항목 활성화",
|
||||
"disableSelected": "선택된 항목 비활성화",
|
||||
"checkSelectedStatus": "선택된 항목 상태 확인",
|
||||
"credentials": "자격 증명",
|
||||
"savecredentials": "자격 증명 저장",
|
||||
"regeneratecredentials": "재생성",
|
||||
"regenerateCredentials": "자격 증명을 재생성하고 저장합니다",
|
||||
"generatedcredentials": "생성된 자격 증명",
|
||||
"copyandsavethesecredentials": "이 자격 증명을 복사하여 저장합니다",
|
||||
"copyandsavethesecredentialsdescription": "이 페이지를 떠난 후에는 자격 증명이 다시 표시되지 않습니다. 지금 안전하게 저장하십시오.",
|
||||
"credentialsSaved": "자격 증명 저장됨",
|
||||
"credentialsSavedDescription": "자격 증명이 성공적으로 재생성 및 저장되었습니다.",
|
||||
"credentialsSaveError": "자격 증명 저장 오류",
|
||||
"credentialsSaveErrorDescription": "자격 증명을 재생성하고 저장하는 동안 오류가 발생했습니다.",
|
||||
"regenerateCredentialsWarning": "자격 증명을 재생성하면 이전 자격 증명이 무효화됩니다. 이 자격 증명을 사용하는 모든 구성을 업데이트하십시오.",
|
||||
"confirm": "확인",
|
||||
"regenerateCredentialsConfirmation": "자격 증명을 재생성하시겠습니까?",
|
||||
"endpoint": "엔드포인트",
|
||||
"Id": "아이디",
|
||||
"SecretKey": "비밀 키",
|
||||
"featureDisabledTooltip": "이 기능은 엔터프라이즈 플랜에서만 사용할 수 있으며 라이센스가 필요합니다.",
|
||||
"niceId": "예쁜 ID",
|
||||
"niceIdUpdated": "예쁜 ID 업데이트됨",
|
||||
"niceIdUpdatedSuccessfully": "예쁜 ID가 성공적으로 업데이트되었습니다",
|
||||
"niceIdUpdateError": "예쁜 ID 업데이트 오류",
|
||||
"niceIdUpdateErrorDescription": "예쁜 ID를 업데이트하는 동안 오류가 발생했습니다.",
|
||||
"niceIdCannotBeEmpty": "예쁜 ID는 비워둘 수 없습니다",
|
||||
"enterIdentifier": "식별자 입력",
|
||||
"identifier": "식별자"
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
1009
messages/pt-PT.json
1009
messages/pt-PT.json
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -47,9 +47,8 @@
|
||||
"edit": "Düzenle",
|
||||
"siteConfirmDelete": "Site Silmeyi Onayla",
|
||||
"siteDelete": "Siteyi Sil",
|
||||
"siteMessageRemove": "Kaldırıldıktan sonra site artık erişilebilir olmayacak. Siteyle ilişkili tüm kaynaklar ve hedefler de kaldırılacaktır.",
|
||||
"siteMessageConfirm": "Onaylamak için lütfen aşağıya sitenin adını yazın.",
|
||||
"siteQuestionRemove": "{selectedSite} sitesini organizasyondan kaldırmak istediğinizden emin misiniz?",
|
||||
"siteMessageRemove": "Kaldırıldıktan sonra site artık erişilebilir olmayacaktır. Siteyle ilişkilendirilmiş tüm hedefler de kaldırılacaktır.",
|
||||
"siteQuestionRemove": "Siteyi organizasyondan kaldırmak istediğinizden emin misiniz?",
|
||||
"siteManageSites": "Siteleri Yönet",
|
||||
"siteDescription": "Ağınıza güvenli tüneller üzerinden bağlantı izni verin",
|
||||
"siteCreate": "Site Oluştur",
|
||||
@@ -96,7 +95,7 @@
|
||||
"siteWgDescription": "Bir tünel oluşturmak için herhangi bir WireGuard istemcisi kullanın. Manuel NAT kurulumu gereklidir.",
|
||||
"siteWgDescriptionSaas": "Bir tünel oluşturmak için herhangi bir WireGuard istemcisi kullanın. Manuel NAT kurulumu gereklidir. YALNIZCA SELF HOSTED DÜĞÜMLERDE ÇALIŞIR",
|
||||
"siteLocalDescription": "Yalnızca yerel kaynaklar. Tünelleme yok.",
|
||||
"siteLocalDescriptionSaas": "Yalnızca yerel kaynaklar. Tünel yok. YALNIZCA SELF HOSTED DÜĞÜMLERDE ÇALIŞIR",
|
||||
"siteLocalDescriptionSaas": "Yerel kaynaklar yalnızca. Tünel oluşturma yok. Yalnızca uzak düğümlerde mevcuttur.",
|
||||
"siteSeeAll": "Tüm Siteleri Gör",
|
||||
"siteTunnelDescription": "Sitenize nasıl bağlanmak istediğinizi belirleyin",
|
||||
"siteNewtCredentials": "Newt Kimlik Bilgileri",
|
||||
@@ -154,8 +153,7 @@
|
||||
"protected": "Korunan",
|
||||
"notProtected": "Korunmayan",
|
||||
"resourceMessageRemove": "Kaldırıldıktan sonra kaynak artık erişilebilir olmayacaktır. Kaynakla ilişkili tüm hedefler de kaldırılacaktır.",
|
||||
"resourceMessageConfirm": "Onaylamak için lütfen aşağıya kaynağın adını yazın.",
|
||||
"resourceQuestionRemove": "{selectedResource} kaynağını organizasyondan kaldırmak istediğinizden emin misiniz?",
|
||||
"resourceQuestionRemove": "Kaynağı organizasyondan kaldırmak istediğinizden emin misiniz?",
|
||||
"resourceHTTP": "HTTPS Kaynağı",
|
||||
"resourceHTTPDescription": "Bir alt alan adı veya temel alan adı kullanarak uygulamanıza HTTPS üzerinden vekil istek gönderin.",
|
||||
"resourceRaw": "Ham TCP/UDP Kaynağı",
|
||||
@@ -168,6 +166,9 @@
|
||||
"siteSelect": "Site seç",
|
||||
"siteSearch": "Site ara",
|
||||
"siteNotFound": "Herhangi bir site bulunamadı.",
|
||||
"selectCountry": "Ülke Seç",
|
||||
"searchCountries": "Ülkeleri ara...",
|
||||
"noCountryFound": "Ülke bulunamadı.",
|
||||
"siteSelectionDescription": "Bu site hedefe bağlantı sağlayacaktır.",
|
||||
"resourceType": "Kaynak Türü",
|
||||
"resourceTypeDescription": "Kaynağınıza nasıl erişmek istediğinizi belirleyin",
|
||||
@@ -178,7 +179,7 @@
|
||||
"baseDomain": "Temel Alan Adı",
|
||||
"subdomnainDescription": "Kaynağınızın erişilebileceği alt alan adı.",
|
||||
"resourceRawSettings": "TCP/UDP Ayarları",
|
||||
"resourceRawSettingsDescription": "Kaynağınıza TCP/UDP üzerinden erişimin nasıl sağlanacağını yapılandırın",
|
||||
"resourceRawSettingsDescription": "Kaynağınızın TCP/UDP üzerinden nasıl erişileceğini yapılandırın. Kaynağı, sunucudan erişebilmeniz için bir ana bilgisayar Pangolin sunucusundaki bir bağlantı noktasına eşlersiniz: sunucu genel-IP: eşlenen-bağlantı-noktası.",
|
||||
"protocol": "Protokol",
|
||||
"protocolSelect": "Bir protokol seçin",
|
||||
"resourcePortNumber": "Port Numarası",
|
||||
@@ -205,6 +206,7 @@
|
||||
"resourceSetting": "{resourceName} Ayarları",
|
||||
"alwaysAllow": "Her Zaman İzin Ver",
|
||||
"alwaysDeny": "Her Zaman Reddet",
|
||||
"passToAuth": "Kimlik Doğrulamasına Geç",
|
||||
"orgSettingsDescription": "Organizasyonunuzun genel ayarlarını yapılandırın",
|
||||
"orgGeneralSettings": "Organizasyon Ayarları",
|
||||
"orgGeneralSettingsDescription": "Organizasyon detaylarınızı ve yapılandırmanızı yönetin",
|
||||
@@ -216,7 +218,7 @@
|
||||
"orgDeleteConfirm": "Organizasyon Silmeyi Onayla",
|
||||
"orgMessageRemove": "Bu işlem geri alınamaz ve tüm ilişkili verileri silecektir.",
|
||||
"orgMessageConfirm": "Onaylamak için lütfen aşağıya organizasyonun adını yazın.",
|
||||
"orgQuestionRemove": "{selectedOrg} organizasyonunu kaldırmak istediğinizden emin misiniz?",
|
||||
"orgQuestionRemove": "Organizasyondan kaldırmak istediğinizden emin misiniz?",
|
||||
"orgUpdated": "Organizasyon güncellendi",
|
||||
"orgUpdatedDescription": "Organizasyon güncellendi.",
|
||||
"orgErrorUpdate": "Organizasyon güncellenemedi",
|
||||
@@ -283,9 +285,8 @@
|
||||
"apiKeysAdd": "API Anahtarı Oluştur",
|
||||
"apiKeysErrorDelete": "API anahtarı silinirken bir hata oluştu",
|
||||
"apiKeysErrorDeleteMessage": "API anahtarı silinirken bir hata oluştu",
|
||||
"apiKeysQuestionRemove": "{selectedApiKey} API anahtarını organizasyondan kaldırmak istediğinizden emin misiniz?",
|
||||
"apiKeysQuestionRemove": "API anahtarını organizasyondan kaldırmak istediğinizden emin misiniz?",
|
||||
"apiKeysMessageRemove": "Kaldırıldığında, API anahtarı artık kullanılamayacaktır.",
|
||||
"apiKeysMessageConfirm": "Onaylamak için lütfen aşağıya API anahtarının adını yazın.",
|
||||
"apiKeysDeleteConfirm": "API Anahtarının Silinmesini Onaylayın",
|
||||
"apiKeysDelete": "API Anahtarını Sil",
|
||||
"apiKeysManage": "API Anahtarlarını Yönet",
|
||||
@@ -301,8 +302,7 @@
|
||||
"userDeleteConfirm": "Kullanıcı Silinmesini Onayla",
|
||||
"userDeleteServer": "Kullanıcıyı Sunucudan Sil",
|
||||
"userMessageRemove": "Kullanıcı tüm organizasyonlardan çıkarılacak ve tamamen sunucudan kaldırılacaktır.",
|
||||
"userMessageConfirm": "Onaylamak için lütfen aşağıya kullanıcının adını yazın.",
|
||||
"userQuestionRemove": "{selectedUser} kullanıcısını sunucudan kalıcı olarak silmek istediğinizden emin misiniz?",
|
||||
"userQuestionRemove": "Kullanıcıyı sunucudan kalıcı olarak silmek istediğinizden emin misiniz?",
|
||||
"licenseKey": "Lisans Anahtarı",
|
||||
"valid": "Geçerli",
|
||||
"numberOfSites": "Site Sayısı",
|
||||
@@ -335,7 +335,7 @@
|
||||
"fossorialLicense": "Fossorial Ticari Lisans ve Abonelik Koşullarını Gör",
|
||||
"licenseMessageRemove": "Bu, lisans anahtarını ve onun tarafından verilen tüm izinleri kaldıracaktır.",
|
||||
"licenseMessageConfirm": "Onaylamak için lütfen aşağıya lisans anahtarını yazın.",
|
||||
"licenseQuestionRemove": "{selectedKey} lisans anahtarını silmek istediğinizden emin misiniz?",
|
||||
"licenseQuestionRemove": "Lisans anahtarını silmek istediğinizden emin misiniz?",
|
||||
"licenseKeyDelete": "Lisans Anahtarını Sil",
|
||||
"licenseKeyDeleteConfirm": "Lisans Anahtarının Silinmesini Onaylayın",
|
||||
"licenseTitle": "Lisans Durumunu Yönet",
|
||||
@@ -368,7 +368,7 @@
|
||||
"inviteRemoveErrorDescription": "Daveti kaldırırken bir hata oluştu.",
|
||||
"inviteRemoved": "Davetiye kaldırıldı",
|
||||
"inviteRemovedDescription": "{email} için olan davetiye kaldırıldı.",
|
||||
"inviteQuestionRemove": "{email} davetini kaldırmak istediğinizden emin misiniz?",
|
||||
"inviteQuestionRemove": "Davetiyeyi kaldırmak istediğinizden emin misiniz?",
|
||||
"inviteMessageRemove": "Kaldırıldıktan sonra bu davetiye artık geçerli olmayacak. Kullanıcı tekrar davet edilebilir.",
|
||||
"inviteMessageConfirm": "Onaylamak için lütfen aşağıya davetiyenin e-posta adresini yazın.",
|
||||
"inviteQuestionRegenerate": "Are you sure you want to regenerate the invitation for{email, plural, ='' {}, other { for #}}? This will revoke the previous invitation.",
|
||||
@@ -394,9 +394,8 @@
|
||||
"userErrorOrgRemoveDescription": "Kullanıcı kaldırılırken bir hata oluştu.",
|
||||
"userOrgRemoved": "Kullanıcı kaldırıldı",
|
||||
"userOrgRemovedDescription": "{email} kullanıcı organizasyondan kaldırılmıştır.",
|
||||
"userQuestionOrgRemove": "{email} adresini organizasyondan kaldırmak istediğinizden emin misiniz?",
|
||||
"userQuestionOrgRemove": "Kullanıcıyı organizasyondan kaldırmak istediğinizden emin misiniz?",
|
||||
"userMessageOrgRemove": "Kaldırıldığında, bu kullanıcı organizasyona artık erişim sağlayamayacak. Kullanıcı tekrar davet edilebilir, ancak daveti kabul etmesi gerekecek.",
|
||||
"userMessageOrgConfirm": "Onaylamak için lütfen aşağıya kullanıcının adını yazın.",
|
||||
"userRemoveOrgConfirm": "Kullanıcıyı Kaldırmayı Onayla",
|
||||
"userRemoveOrg": "Kullanıcıyı Organizasyondan Kaldır",
|
||||
"users": "Kullanıcılar",
|
||||
@@ -453,6 +452,8 @@
|
||||
"accessRoleErrorAddDescription": "Kullanıcı role eklenirken bir hata oluştu.",
|
||||
"userSaved": "Kullanıcı kaydedildi",
|
||||
"userSavedDescription": "Kullanıcı güncellenmiştir.",
|
||||
"autoProvisioned": "Otomatik Sağlandı",
|
||||
"autoProvisionedDescription": "Bu kullanıcının kimlik sağlayıcısı tarafından otomatik olarak yönetilmesine izin ver",
|
||||
"accessControlsDescription": "Bu kullanıcının organizasyonda neleri erişebileceğini ve yapabileceğini yönetin",
|
||||
"accessControlsSubmit": "Erişim Kontrollerini Kaydet",
|
||||
"roles": "Roller",
|
||||
@@ -462,7 +463,10 @@
|
||||
"createdAt": "Oluşturulma Tarihi",
|
||||
"proxyErrorInvalidHeader": "Geçersiz özel Ana Bilgisayar Başlığı değeri. Alan adı formatını kullanın veya özel Ana Bilgisayar Başlığını ayarlamak için boş bırakın.",
|
||||
"proxyErrorTls": "Geçersiz TLS Sunucu Adı. Alan adı formatını kullanın veya TLS Sunucu Adını kaldırmak için boş bırakılsın.",
|
||||
"proxyEnableSSL": "SSL'yi Etkinleştir (https)",
|
||||
"proxyEnableSSL": "SSL Etkinleştir",
|
||||
"proxyEnableSSLDescription": "Hedeflerinize güvenli HTTPS bağlantıları için SSL/TLS şifrelemesi etkinleştirin.",
|
||||
"target": "Hedef",
|
||||
"configureTarget": "Hedefleri Yapılandır",
|
||||
"targetErrorFetch": "Hedefleri alamadı",
|
||||
"targetErrorFetchDescription": "Hedefler alınırken bir hata oluştu",
|
||||
"siteErrorFetch": "kaynağa ulaşılamadı",
|
||||
@@ -489,7 +493,7 @@
|
||||
"targetTlsSettings": "HTTPS & TLS Settings",
|
||||
"targetTlsSettingsDescription": "Configure TLS settings for your resource",
|
||||
"targetTlsSettingsAdvanced": "Gelişmiş TLS Ayarları",
|
||||
"targetTlsSni": "TLS Sunucu Adı (SNI)",
|
||||
"targetTlsSni": "TLS Sunucu Adı",
|
||||
"targetTlsSniDescription": "SNI için kullanılacak TLS Sunucu Adı'",
|
||||
"targetTlsSubmit": "Ayarları Kaydet",
|
||||
"targets": "Hedefler Konfigürasyonu",
|
||||
@@ -498,9 +502,21 @@
|
||||
"targetStickySessionsDescription": "Bağlantıları oturum süresince aynı arka uç hedef üzerinde tutun.",
|
||||
"methodSelect": "Yöntemi Seç",
|
||||
"targetSubmit": "Hedef Ekle",
|
||||
"targetNoOne": "Hiçbir hedef yok. Formu kullanarak bir hedef ekleyin.",
|
||||
"targetNoOne": "Bu kaynağın hedefi yok. Arka planınıza istek göndereceğiniz bir hedef yapılandırmak için hedef ekleyin.",
|
||||
"targetNoOneDescription": "Yukarıdaki birden fazla hedef ekleyerek yük dengeleme etkinleştirilecektir.",
|
||||
"targetsSubmit": "Hedefleri Kaydet",
|
||||
"addTarget": "Hedef Ekle",
|
||||
"targetErrorInvalidIp": "Geçersiz IP adresi",
|
||||
"targetErrorInvalidIpDescription": "Lütfen geçerli bir IP adresi veya host adı girin",
|
||||
"targetErrorInvalidPort": "Geçersiz port",
|
||||
"targetErrorInvalidPortDescription": "Lütfen geçerli bir port numarası girin",
|
||||
"targetErrorNoSite": "Hiçbir site seçili değil",
|
||||
"targetErrorNoSiteDescription": "Lütfen hedef için bir site seçin",
|
||||
"targetCreated": "Hedef oluşturuldu",
|
||||
"targetCreatedDescription": "Hedef başarıyla oluşturuldu",
|
||||
"targetErrorCreate": "Hedef oluşturma başarısız oldu",
|
||||
"targetErrorCreateDescription": "Hedef oluşturulurken bir hata oluştu",
|
||||
"save": "Kaydet",
|
||||
"proxyAdditional": "Ek Proxy Ayarları",
|
||||
"proxyAdditionalDescription": "Kaynağınızın proxy ayarlarını nasıl yöneteceğini yapılandırın",
|
||||
"proxyCustomHeader": "Özel Ana Bilgisayar Başlığı",
|
||||
@@ -510,6 +526,7 @@
|
||||
"ipAddressErrorInvalidFormat": "Geçersiz IP adresi formatı",
|
||||
"ipAddressErrorInvalidOctet": "Geçersiz IP adresi okteti",
|
||||
"path": "Yol",
|
||||
"matchPath": "Yol Eşleştir",
|
||||
"ipAddressRange": "IP Aralığı",
|
||||
"rulesErrorFetch": "Kurallar alınamadı",
|
||||
"rulesErrorFetchDescription": "Kurallar alınırken bir hata oluştu",
|
||||
@@ -545,6 +562,7 @@
|
||||
"rulesActions": "Aksiyonlar",
|
||||
"rulesActionAlwaysAllow": "Her Zaman İzin Ver: Tüm kimlik doğrulama yöntemlerini atlayın",
|
||||
"rulesActionAlwaysDeny": "Her Zaman Reddedin: Tüm istekleri engelleyin; kimlik doğrulaması yapılamaz",
|
||||
"rulesActionPassToAuth": "Kimlik Doğrulamasına Geç: Kimlik doğrulama yöntemlerinin denenmesine izin ver",
|
||||
"rulesMatchCriteria": "Eşleşme Kriterleri",
|
||||
"rulesMatchCriteriaIpAddress": "Belirli bir IP adresi ile eşleşme",
|
||||
"rulesMatchCriteriaIpAddressRange": "CIDR gösteriminde bir IP adresi aralığı ile eşleşme",
|
||||
@@ -707,7 +725,7 @@
|
||||
"pangolinServerAdmin": "Sunucu Yöneticisi - Pangolin",
|
||||
"licenseTierProfessional": "Profesyonel Lisans",
|
||||
"licenseTierEnterprise": "Kurumsal Lisans",
|
||||
"licenseTierCommercial": "Ticari Lisans",
|
||||
"licenseTierPersonal": "Kişisel Lisans",
|
||||
"licensed": "Lisanslı",
|
||||
"yes": "Evet",
|
||||
"no": "Hayır",
|
||||
@@ -719,7 +737,7 @@
|
||||
"idpManageDescription": "Sistem içindeki kimlik sağlayıcıları görün ve yönetin",
|
||||
"idpDeletedDescription": "Kimlik sağlayıcı başarıyla silindi",
|
||||
"idpOidc": "OAuth2/OIDC",
|
||||
"idpQuestionRemove": "Kimlik sağlayıcıyı kalıcı olarak silmek istediğinizden emin misiniz? {name}",
|
||||
"idpQuestionRemove": "Kimlik sağlayıcısını kalıcı olarak silmek istediğinizden emin misiniz?",
|
||||
"idpMessageRemove": "Bu, kimlik sağlayıcıyı ve tüm ilişkili yapılandırmaları kaldıracaktır. Bu sağlayıcıdan kimlik doğrulayan kullanıcılar artık giriş yapamayacaktır.",
|
||||
"idpMessageConfirm": "Onaylamak için lütfen aşağıya kimlik sağlayıcının adını yazın.",
|
||||
"idpConfirmDelete": "Kimlik Sağlayıcıyı Silme Onayı",
|
||||
@@ -742,7 +760,7 @@
|
||||
"idpDisplayName": "Bu kimlik sağlayıcı için bir görüntü adı",
|
||||
"idpAutoProvisionUsers": "Kullanıcıları Otomatik Sağla",
|
||||
"idpAutoProvisionUsersDescription": "Etkinleştirildiğinde, kullanıcılar rol ve organizasyonlara eşleme yeteneğiyle birlikte sistemde otomatik olarak oluşturulacak.",
|
||||
"licenseBadge": "Profesyonel",
|
||||
"licenseBadge": " ",
|
||||
"idpType": "Sağlayıcı Türü",
|
||||
"idpTypeDescription": "Yapılandırmak istediğiniz kimlik sağlayıcısı türünü seçin",
|
||||
"idpOidcConfigure": "OAuth2/OIDC Yapılandırması",
|
||||
@@ -809,7 +827,7 @@
|
||||
"redirectUrl": "Yönlendirme URL'si",
|
||||
"redirectUrlAbout": "Yönlendirme URL'si Hakkında",
|
||||
"redirectUrlAboutDescription": "Bu, kimlik doğrulamasından sonra kullanıcıların yönlendirileceği URL'dir. Bu URL'yi kimlik sağlayıcınızın ayarlarında yapılandırmanız gerekir.",
|
||||
"pangolinAuth": "Auth - Pangolin",
|
||||
"pangolinAuth": "Yetkilendirme - Pangolin",
|
||||
"verificationCodeLengthRequirements": "Doğrulama kodunuz 8 karakter olmalıdır.",
|
||||
"errorOccurred": "Bir hata oluştu",
|
||||
"emailErrorVerify": "E-posta doğrulanamadı: ",
|
||||
@@ -893,6 +911,18 @@
|
||||
"passwordResetCodeDescription": "E-posta gelen kutunuzda sıfırlama kodunu kontrol edin.",
|
||||
"passwordNew": "Yeni Şifre",
|
||||
"passwordNewConfirm": "Yeni Şifreyi Onayla",
|
||||
"changePassword": "Parola Değiştir",
|
||||
"changePasswordDescription": "Hesap şifrenizi güncelleyin",
|
||||
"oldPassword": "Mevcut Şifre",
|
||||
"newPassword": "Yeni Şifre",
|
||||
"confirmNewPassword": "Yeni Şifreyi Onayla",
|
||||
"changePasswordError": "Parola değiştirme başarısız oldu",
|
||||
"changePasswordErrorDescription": "Parolanız değiştiriliyor.",
|
||||
"changePasswordSuccess": "Şifre Başarıyla Değiştirildi",
|
||||
"changePasswordSuccessDescription": "Parolanız başarıyla güncellendi",
|
||||
"passwordExpiryRequired": "Şifre Süresi Gereklidir",
|
||||
"passwordExpiryDescription": "Bu kuruluş, parolanızı {maxDays} günde bir değiştirmenizi gerektirir.",
|
||||
"changePasswordNow": "Şifrenizi Şimdi Değiştirin",
|
||||
"pincodeAuth": "Kimlik Doğrulama Kodu",
|
||||
"pincodeSubmit2": "Kodu Gönder",
|
||||
"passwordResetSubmit": "Sıfırlama İsteği",
|
||||
@@ -980,6 +1010,8 @@
|
||||
"licenseTierProfessionalRequired": "Profesyonel Sürüme Gereklidir",
|
||||
"licenseTierProfessionalRequiredDescription": "Bu özellik yalnızca Professional Edition'da kullanılabilir.",
|
||||
"actionGetOrg": "Kuruluşu Al",
|
||||
"updateOrgUser": "Organizasyon Kullanıcısını Güncelle",
|
||||
"createOrgUser": "Organizasyon Kullanıcısı Oluştur",
|
||||
"actionUpdateOrg": "Kuruluşu Güncelle",
|
||||
"actionUpdateUser": "Kullanıcıyı Güncelle",
|
||||
"actionGetUser": "Kullanıcıyı Getir",
|
||||
@@ -989,6 +1021,7 @@
|
||||
"actionDeleteSite": "Siteyi Sil",
|
||||
"actionGetSite": "Siteyi Al",
|
||||
"actionListSites": "Siteleri Listele",
|
||||
"actionApplyBlueprint": "Planı Uygula",
|
||||
"setupToken": "Kurulum Simgesi",
|
||||
"setupTokenDescription": "Sunucu konsolundan kurulum simgesini girin.",
|
||||
"setupTokenRequired": "Kurulum simgesi gerekli",
|
||||
@@ -1057,6 +1090,7 @@
|
||||
"actionGetSiteResource": "Site Kaynağını Al",
|
||||
"actionListSiteResources": "Site Kaynaklarını Listele",
|
||||
"actionUpdateSiteResource": "Site Kaynağını Güncelle",
|
||||
"actionListInvitations": "Davetiyeleri Listele",
|
||||
"noneSelected": "Hiçbiri seçili değil",
|
||||
"orgNotFound2": "Hiçbir organizasyon bulunamadı.",
|
||||
"searchProgress": "Ara...",
|
||||
@@ -1072,7 +1106,6 @@
|
||||
"navbar": "Navigasyon Menüsü",
|
||||
"navbarDescription": "Uygulamanın ana navigasyon menüsü",
|
||||
"navbarDocsLink": "Dokümantasyon",
|
||||
"commercialEdition": "Ticari Sürüm",
|
||||
"otpErrorEnable": "2FA etkinleştirilemedi",
|
||||
"otpErrorEnableDescription": "2FA etkinleştirilirken bir hata oluştu",
|
||||
"otpSetupCheckCode": "6 haneli bir kod girin",
|
||||
@@ -1128,10 +1161,29 @@
|
||||
"sidebarAllUsers": "Tüm Kullanıcılar",
|
||||
"sidebarIdentityProviders": "Kimlik Sağlayıcılar",
|
||||
"sidebarLicense": "Lisans",
|
||||
"sidebarClients": "Müşteriler (Beta)",
|
||||
"sidebarClients": "İstemciler",
|
||||
"sidebarDomains": "Alan Adları",
|
||||
"enableDockerSocket": "Docker Soketi Etkinleştir",
|
||||
"enableDockerSocketDescription": "Konteyner bilgilerini doldurmak için Docker Socket keşfini etkinleştirin. Socket yolu Newt'e sağlanmalıdır.",
|
||||
"sidebarBluePrints": "Planlar",
|
||||
"blueprints": "Planlar",
|
||||
"blueprintsDescription": "Deklaratif yapılandırmaları uygulayın ve önceki çalışmaları görüntüleyin",
|
||||
"blueprintAdd": "Plan Ekle",
|
||||
"blueprintGoBack": "Tüm Planları Gör",
|
||||
"blueprintCreate": "Plan Oluştur",
|
||||
"blueprintCreateDescription2": "Yeni bir plan oluşturup uygulamak için aşağıdaki adımları izleyin",
|
||||
"blueprintDetails": "Mavi Yazılım Detayları",
|
||||
"blueprintDetailsDescription": "Uygulanan mavi yazılımın sonucunu ve oluşan hataları görün",
|
||||
"blueprintInfo": "Plan Bilgileri",
|
||||
"message": "Mesaj",
|
||||
"blueprintContentsDescription": "Altyapınızı tanımlayan YAML içeriğini tanımlayın",
|
||||
"blueprintErrorCreateDescription": "Plan uygulanırken bir hata oluştu",
|
||||
"blueprintErrorCreate": "Plan oluşturulurken hata oluştu",
|
||||
"searchBlueprintProgress": "Planlarda ara...",
|
||||
"appliedAt": "Uygulama Zamanı",
|
||||
"source": "Kaynak",
|
||||
"contents": "İçerik",
|
||||
"parsedContents": "Verilerin Ayrıştırılmış İçeriği (Salt Okunur)",
|
||||
"enableDockerSocket": "Docker Soketini Etkinleştir",
|
||||
"enableDockerSocketDescription": "Plan etiketleri için Docker Socket etiket toplamasını etkinleştirin. Newt'e soket yolu sağlanmalıdır.",
|
||||
"enableDockerSocketLink": "Daha fazla bilgi",
|
||||
"viewDockerContainers": "Docker Konteynerlerini Görüntüle",
|
||||
"containersIn": "{siteName} içindeki konteynerler",
|
||||
@@ -1185,9 +1237,8 @@
|
||||
"domainCreate": "Alan Adı Oluştur",
|
||||
"domainCreatedDescription": "Alan adı başarıyla oluşturuldu",
|
||||
"domainDeletedDescription": "Alan adı başarıyla silindi",
|
||||
"domainQuestionRemove": "{domain} alan adını hesabınızdan kaldırmak istediğinizden emin misiniz?",
|
||||
"domainQuestionRemove": "Alan adını hesabınızdan kaldırmak istediğinizden emin misiniz?",
|
||||
"domainMessageRemove": "Kaldırıldığında, alan adı hesabınızla ilişkilendirilmeyecek.",
|
||||
"domainMessageConfirm": "Onaylamak için lütfen aşağıya alan adını yazın.",
|
||||
"domainConfirmDelete": "Alan Adı Silinmesini Onayla",
|
||||
"domainDelete": "Alan Adını Sil",
|
||||
"domain": "Alan Adı",
|
||||
@@ -1228,10 +1279,19 @@
|
||||
"settingsErrorUpdateDescription": "Ayarları güncellerken bir hata oluştu",
|
||||
"sidebarCollapse": "Daralt",
|
||||
"sidebarExpand": "Genişlet",
|
||||
"productUpdateMoreInfo": "{noOfUpdates} daha fazla güncelleme",
|
||||
"productUpdateInfo": "{noOfUpdates} güncellemeler",
|
||||
"productUpdateWhatsNew": "Neler Yeni",
|
||||
"productUpdateTitle": "Ürün Güncellemeleri",
|
||||
"productUpdateEmpty": "Güncelleme yok",
|
||||
"dismissAll": "Hepsini Kapat",
|
||||
"pangolinUpdateAvailable": "Yeni sürüm mevcut",
|
||||
"pangolinUpdateAvailableInfo": "Sürüm {version} yüklenmeye hazır",
|
||||
"pangolinUpdateAvailableReleaseNotes": "Sürüm notlarını görüntüleyin",
|
||||
"newtUpdateAvailable": "Güncelleme Mevcut",
|
||||
"newtUpdateAvailableInfo": "Newt'in yeni bir versiyonu mevcut. En iyi deneyim için lütfen en son sürüme güncelleyin.",
|
||||
"domainPickerEnterDomain": "Domain",
|
||||
"domainPickerPlaceholder": "myapp.example.com, api.v1.mydomain.com veya sadece myapp",
|
||||
"domainPickerEnterDomain": "Alan Adı",
|
||||
"domainPickerPlaceholder": "myapp.example.com",
|
||||
"domainPickerDescription": "Mevcut seçenekleri görmek için kaynağın tam etki alanını girin.",
|
||||
"domainPickerDescriptionSaas": "Mevcut seçenekleri görmek için tam etki alanı, alt etki alanı veya sadece bir isim girin",
|
||||
"domainPickerTabAll": "Tümü",
|
||||
@@ -1246,6 +1306,48 @@
|
||||
"domainPickerSubdomain": "Alt Alan: {subdomain}",
|
||||
"domainPickerNamespace": "Ad Alanı: {namespace}",
|
||||
"domainPickerShowMore": "Daha Fazla Göster",
|
||||
"regionSelectorTitle": "Bölge Seç",
|
||||
"regionSelectorInfo": "Bir bölge seçmek, konumunuz için daha iyi performans sağlamamıza yardımcı olur. Sunucunuzla aynı bölgede olmanıza gerek yoktur.",
|
||||
"regionSelectorPlaceholder": "Bölge Seçin",
|
||||
"regionSelectorComingSoon": "Yakında Geliyor",
|
||||
"billingLoadingSubscription": "Abonelik yükleniyor...",
|
||||
"billingFreeTier": "Ücretsiz Dilim",
|
||||
"billingWarningOverLimit": "Uyarı: Bir veya daha fazla kullanım limitini aştınız. Aboneliğinizi değiştirmediğiniz veya kullanımı ayarlamadığınız sürece siteleriniz bağlanmayacaktır.",
|
||||
"billingUsageLimitsOverview": "Kullanım Limitleri Genel Görünümü",
|
||||
"billingMonitorUsage": "Kullanımınızı yapılandırılmış limitlerle karşılaştırın. Limitlerin artırılmasına ihtiyacınız varsa, lütfen support@pangolin.net adresinden bizimle iletişime geçin.",
|
||||
"billingDataUsage": "Veri Kullanımı",
|
||||
"billingOnlineTime": "Site Çevrimiçi Süresi",
|
||||
"billingUsers": "Aktif Kullanıcılar",
|
||||
"billingDomains": "Aktif Alanlar",
|
||||
"billingRemoteExitNodes": "Aktif Öz-Host Düğümleri",
|
||||
"billingNoLimitConfigured": "Hiçbir limit yapılandırılmadı",
|
||||
"billingEstimatedPeriod": "Tahmini Fatura Dönemi",
|
||||
"billingIncludedUsage": "Dahil Kullanım",
|
||||
"billingIncludedUsageDescription": "Mevcut abonelik planınıza bağlı kullanım",
|
||||
"billingFreeTierIncludedUsage": "Ücretsiz dilim kullanım hakları",
|
||||
"billingIncluded": "dahil",
|
||||
"billingEstimatedTotal": "Tahmini Toplam:",
|
||||
"billingNotes": "Notlar",
|
||||
"billingEstimateNote": "Bu, mevcut kullanımınıza dayalı bir tahmindir.",
|
||||
"billingActualChargesMayVary": "Asıl ücretler farklılık gösterebilir.",
|
||||
"billingBilledAtEnd": "Fatura döneminin sonunda fatura düzenlenecektir.",
|
||||
"billingModifySubscription": "Aboneliği Düzenle",
|
||||
"billingStartSubscription": "Aboneliği Başlat",
|
||||
"billingRecurringCharge": "Yinelenen Ücret",
|
||||
"billingManageSubscriptionSettings": "Abonelik ayarlarınızı ve tercihlerinizi yönetin",
|
||||
"billingNoActiveSubscription": "Aktif bir aboneliğiniz yok. Kullanım limitlerini artırmak için aboneliğinizi başlatın.",
|
||||
"billingFailedToLoadSubscription": "Abonelik yüklenemedi",
|
||||
"billingFailedToLoadUsage": "Kullanım yüklenemedi",
|
||||
"billingFailedToGetCheckoutUrl": "Ödeme URL'si alınamadı",
|
||||
"billingPleaseTryAgainLater": "Lütfen daha sonra tekrar deneyin.",
|
||||
"billingCheckoutError": "Ödeme Hatası",
|
||||
"billingFailedToGetPortalUrl": "Portal URL'si alınamadı",
|
||||
"billingPortalError": "Portal Hatası",
|
||||
"billingDataUsageInfo": "Buluta bağlandığınızda, güvenli tünellerinizden aktarılan tüm verilerden ücret alınırsınız. Bu, tüm sitelerinizdeki gelen ve giden trafiği içerir. Limitinize ulaştığınızda, planınızı yükseltmeli veya kullanımı azaltmalısınız, aksi takdirde siteleriniz bağlantıyı keser. Düğümler kullanırken verilerden ücret alınmaz.",
|
||||
"billingOnlineTimeInfo": "Sitelerinizin buluta ne kadar süre bağlı kaldığına göre ücretlendirilirsiniz. Örneğin, 44,640 dakika, bir sitenin 24/7 boyunca tam bir ay boyunca çalışması anlamına gelir. Limitinize ulaştığınızda, planınızı yükseltmeyip kullanımı azaltmazsanız siteleriniz bağlantıyı keser. Düğümler kullanırken zamandan ücret alınmaz.",
|
||||
"billingUsersInfo": "Kuruluşunuzdaki her kullanıcı için ücretlendirilirsiniz. Faturalandırma, hesabınızdaki aktif kullanıcı hesaplarının sayısına göre günlük olarak hesaplanır.",
|
||||
"billingDomainInfo": "Kuruluşunuzdaki her alan adı için ücretlendirilirsiniz. Faturalandırma, hesabınızdaki aktif alan adları hesaplarının sayısına göre günlük olarak hesaplanır.",
|
||||
"billingRemoteExitNodesInfo": "Kuruluşunuzdaki her yönetilen Düğüm için ücretlendirilirsiniz. Faturalandırma, hesabınızdaki aktif yönetilen Düğümler sayısına göre günlük olarak hesaplanır.",
|
||||
"domainNotFound": "Alan Adı Bulunamadı",
|
||||
"domainNotFoundDescription": "Bu kaynak devre dışıdır çünkü alan adı sistemimizde artık mevcut değil. Bu kaynak için yeni bir alan adı belirleyin.",
|
||||
"failed": "Başarısız",
|
||||
@@ -1278,8 +1380,20 @@
|
||||
"securityKeyUnknownError": "Güvenlik anahtarınızı kullanırken bir sorun oluştu. Lütfen tekrar deneyin.",
|
||||
"twoFactorRequired": "Güvenlik anahtarını kaydetmek için iki faktörlü kimlik doğrulama gereklidir.",
|
||||
"twoFactor": "İki Faktörlü Kimlik Doğrulama",
|
||||
"twoFactorAuthentication": "İki Faktörlü Kimlik Doğrulama",
|
||||
"twoFactorDescription": "Bu kuruluş iki faktörlü kimlik doğrulama gerektirir.",
|
||||
"enableTwoFactor": "İki Faktörlü Kimlik Doğrulamayı Etkinleştir",
|
||||
"organizationSecurityPolicy": "Kuruluş Güvenlik Politikası",
|
||||
"organizationSecurityPolicyDescription": "Bu kuruluşun güvenlik gereksinimlerine erişmeden önce karşılanması gereken güvenlik gereksinimleri vardır.",
|
||||
"securityRequirements": "Güvenlik Gereksinimleri",
|
||||
"allRequirementsMet": "Tüm gereksinimler karşılandı",
|
||||
"completeRequirementsToContinue": "Bu kuruluşa erişmeye devam etmek için aşağıdaki gereksinimleri tamamlayın",
|
||||
"youCanNowAccessOrganization": "Artık bu kuruluşa erişebilirsiniz",
|
||||
"reauthenticationRequired": "Oturum Süresi",
|
||||
"reauthenticationDescription": "Bu kuruluş, {maxDays} günde bir oturum açmanızı gerektirir.",
|
||||
"reauthenticationDescriptionHours": "Bu kuruluş, {maxHours} saatte bir oturum açmanızı gerektirir.",
|
||||
"reauthenticateNow": "Tekrar Giriş Yap",
|
||||
"adminEnabled2FaOnYourAccount": "Yöneticiniz {email} için iki faktörlü kimlik doğrulamayı etkinleştirdi. Devam etmek için kurulum işlemini tamamlayın.",
|
||||
"continueToApplication": "Uygulamaya Devam Et",
|
||||
"securityKeyAdd": "Güvenlik Anahtarı Ekle",
|
||||
"securityKeyRegisterTitle": "Yeni Güvenlik Anahtarı Kaydet",
|
||||
"securityKeyRegisterDescription": "Güvenlik anahtarınızı bağlayın ve tanımlamak için bir ad girin",
|
||||
@@ -1309,6 +1423,7 @@
|
||||
"createDomainDnsPropagationDescription": "DNS değişikliklerinin internet genelinde yayılması zaman alabilir. DNS sağlayıcınız ve TTL ayarlarına bağlı olarak bu birkaç dakika ile 48 saat arasında değişebilir.",
|
||||
"resourcePortRequired": "HTTP dışı kaynaklar için bağlantı noktası numarası gereklidir",
|
||||
"resourcePortNotAllowed": "HTTP kaynakları için bağlantı noktası numarası ayarlanmamalı",
|
||||
"billingPricingCalculatorLink": "Fiyat Hesaplayıcı",
|
||||
"signUpTerms": {
|
||||
"IAgreeToThe": "Kabul ediyorum",
|
||||
"termsOfService": "hizmet şartları",
|
||||
@@ -1356,7 +1471,43 @@
|
||||
"externalProxyEnabled": "Dış Proxy Etkinleştirildi",
|
||||
"addNewTarget": "Yeni Hedef Ekle",
|
||||
"targetsList": "Hedefler Listesi",
|
||||
"advancedMode": "Gelişmiş Mod",
|
||||
"targetErrorDuplicateTargetFound": "Yinelenen hedef bulundu",
|
||||
"healthCheckHealthy": "Sağlıklı",
|
||||
"healthCheckUnhealthy": "Sağlıksız",
|
||||
"healthCheckUnknown": "Bilinmiyor",
|
||||
"healthCheck": "Sağlık Kontrolü",
|
||||
"configureHealthCheck": "Sağlık Kontrolünü Yapılandır",
|
||||
"configureHealthCheckDescription": "{hedef} için sağlık izleme kurun",
|
||||
"enableHealthChecks": "Sağlık Kontrollerini Etkinleştir",
|
||||
"enableHealthChecksDescription": "Bu hedefin sağlığını izleyin. Gerekirse hedef dışındaki bir son noktayı izleyebilirsiniz.",
|
||||
"healthScheme": "Yöntem",
|
||||
"healthSelectScheme": "Yöntem Seç",
|
||||
"healthCheckPath": "Yol",
|
||||
"healthHostname": "IP / Hostname",
|
||||
"healthPort": "Bağlantı Noktası",
|
||||
"healthCheckPathDescription": "Sağlık durumunu kontrol etmek için yol.",
|
||||
"healthyIntervalSeconds": "Sağlıklı Aralık",
|
||||
"unhealthyIntervalSeconds": "Sağlıksız Aralık",
|
||||
"IntervalSeconds": "Sağlıklı Aralık",
|
||||
"timeoutSeconds": "Zaman Aşımı",
|
||||
"timeIsInSeconds": "Zaman saniye cinsindendir",
|
||||
"retryAttempts": "Tekrar Deneme Girişimleri",
|
||||
"expectedResponseCodes": "Beklenen Yanıt Kodları",
|
||||
"expectedResponseCodesDescription": "Sağlıklı durumu gösteren HTTP durum kodu. Boş bırakılırsa, 200-300 arası sağlıklı kabul edilir.",
|
||||
"customHeaders": "Özel Başlıklar",
|
||||
"customHeadersDescription": "Başlıklar yeni satırla ayrılmış: Başlık-Adı: değer",
|
||||
"headersValidationError": "Başlıklar şu formatta olmalıdır: Başlık-Adı: değer.",
|
||||
"saveHealthCheck": "Sağlık Kontrolünü Kaydet",
|
||||
"healthCheckSaved": "Sağlık Kontrolü Kaydedildi",
|
||||
"healthCheckSavedDescription": "Sağlık kontrol yapılandırması başarıyla kaydedildi",
|
||||
"healthCheckError": "Sağlık Kontrol Hatası",
|
||||
"healthCheckErrorDescription": "Sağlık kontrol yapılandırması kaydedilirken bir hata oluştu",
|
||||
"healthCheckPathRequired": "Sağlık kontrol yolu gereklidir",
|
||||
"healthCheckMethodRequired": "HTTP yöntemi gereklidir",
|
||||
"healthCheckIntervalMin": "Kontrol aralığı en az 5 saniye olmalıdır",
|
||||
"healthCheckTimeoutMin": "Zaman aşımı en az 1 saniye olmalıdır",
|
||||
"healthCheckRetryMin": "Tekrar deneme girişimleri en az 1 olmalıdır",
|
||||
"httpMethod": "HTTP Yöntemi",
|
||||
"selectHttpMethod": "HTTP yöntemini seçin",
|
||||
"domainPickerSubdomainLabel": "Alt Alan Adı",
|
||||
@@ -1370,6 +1521,7 @@
|
||||
"domainPickerEnterSubdomainToSearch": "Mevcut ücretsiz alan adları arasından aramak ve seçmek için bir alt alan adı girin.",
|
||||
"domainPickerFreeDomains": "Ücretsiz Alan Adları",
|
||||
"domainPickerSearchForAvailableDomains": "Mevcut alan adlarını ara",
|
||||
"domainPickerNotWorkSelfHosted": "Not: Ücretsiz sağlanan alan adları şu anda öz-host edilmiş örnekler için kullanılabilir değildir.",
|
||||
"resourceDomain": "Alan Adı",
|
||||
"resourceEditDomain": "Alan Adını Düzenle",
|
||||
"siteName": "Site Adı",
|
||||
@@ -1382,6 +1534,12 @@
|
||||
"resourcesTableTheseResourcesForUseWith": "Bu kaynaklar ile kullanılmak için",
|
||||
"resourcesTableClients": "İstemciler",
|
||||
"resourcesTableAndOnlyAccessibleInternally": "veyalnızca bir istemci ile bağlandığında dahili olarak erişilebilir.",
|
||||
"resourcesTableNoTargets": "Hedef yok",
|
||||
"resourcesTableHealthy": "Sağlıklı",
|
||||
"resourcesTableDegraded": "Düşük Performanslı",
|
||||
"resourcesTableOffline": "Çevrimdışı",
|
||||
"resourcesTableUnknown": "Bilinmiyor",
|
||||
"resourcesTableNotMonitored": "İzlenmiyor",
|
||||
"editInternalResourceDialogEditClientResource": "İstemci Kaynağı Düzenleyin",
|
||||
"editInternalResourceDialogUpdateResourceProperties": "{resourceName} için kaynak özelliklerini ve hedef yapılandırmasını güncelleyin.",
|
||||
"editInternalResourceDialogResourceProperties": "Kaynak Özellikleri",
|
||||
@@ -1389,8 +1547,6 @@
|
||||
"editInternalResourceDialogProtocol": "Protokol",
|
||||
"editInternalResourceDialogSitePort": "Site Bağlantı Noktası",
|
||||
"editInternalResourceDialogTargetConfiguration": "Hedef Yapılandırma",
|
||||
"editInternalResourceDialogDestinationIP": "Hedef IP",
|
||||
"editInternalResourceDialogDestinationPort": "Hedef Bağlantı Noktası",
|
||||
"editInternalResourceDialogCancel": "İptal",
|
||||
"editInternalResourceDialogSaveResource": "Kaynağı Kaydet",
|
||||
"editInternalResourceDialogSuccess": "Başarı",
|
||||
@@ -1421,9 +1577,7 @@
|
||||
"createInternalResourceDialogSitePort": "Site Bağlantı Noktası",
|
||||
"createInternalResourceDialogSitePortDescription": "İstemci ile bağlanıldığında site üzerindeki kaynağa erişmek için bu bağlantı noktasını kullanın.",
|
||||
"createInternalResourceDialogTargetConfiguration": "Hedef Yapılandırma",
|
||||
"createInternalResourceDialogDestinationIP": "Hedef IP",
|
||||
"createInternalResourceDialogDestinationIPDescription": "Site ağındaki kaynağın IP adresi.",
|
||||
"createInternalResourceDialogDestinationPort": "Hedef Bağlantı Noktası",
|
||||
"createInternalResourceDialogDestinationIPDescription": "Kaynağın site ağındaki IP veya ana bilgisayar adresi.",
|
||||
"createInternalResourceDialogDestinationPortDescription": "Kaynağa erişilebilecek hedef IP üzerindeki bağlantı noktası.",
|
||||
"createInternalResourceDialogCancel": "İptal",
|
||||
"createInternalResourceDialogCreateResource": "Kaynak Oluştur",
|
||||
@@ -1455,5 +1609,532 @@
|
||||
"autoLoginRedirecting": "Girişe yönlendiriliyorsunuz...",
|
||||
"autoLoginError": "Otomatik Giriş Hatası",
|
||||
"autoLoginErrorNoRedirectUrl": "Kimlik sağlayıcıdan yönlendirme URL'si alınamadı.",
|
||||
"autoLoginErrorGeneratingUrl": "Kimlik doğrulama URL'si oluşturulamadı."
|
||||
"autoLoginErrorGeneratingUrl": "Kimlik doğrulama URL'si oluşturulamadı.",
|
||||
"remoteExitNodeManageRemoteExitNodes": "Uzak Düğümler",
|
||||
"remoteExitNodeDescription": "Kendi konum ağlarınızdan bir veya daha fazlasını barındırarak, bağlantı kurulumları için buluta bağımlılığı azaltın.",
|
||||
"remoteExitNodes": "Düğümler",
|
||||
"searchRemoteExitNodes": "Düğüm ara...",
|
||||
"remoteExitNodeAdd": "Düğüm Ekle",
|
||||
"remoteExitNodeErrorDelete": "Düğüm silinirken hata oluştu",
|
||||
"remoteExitNodeQuestionRemove": "Düğümü organizasyondan kaldırmak istediğinizden emin misiniz?",
|
||||
"remoteExitNodeMessageRemove": "Kaldırıldığında, düğüm artık erişilebilir olmayacaktır.",
|
||||
"remoteExitNodeConfirmDelete": "Düğüm Silmeyi Onayla",
|
||||
"remoteExitNodeDelete": "Düğümü Sil",
|
||||
"sidebarRemoteExitNodes": "Uzak Düğümler",
|
||||
"remoteExitNodeCreate": {
|
||||
"title": "Düğüm Oluştur",
|
||||
"description": "Ağ bağlantınızı genişletmek için yeni bir düğüm oluşturun",
|
||||
"viewAllButton": "Tüm Düğümleri Gör",
|
||||
"strategy": {
|
||||
"title": "Oluşturma Stratejisi",
|
||||
"description": "Düğümünüzü manuel olarak yapılandırmak veya yeni kimlik bilgileri oluşturmak için bunu seçin.",
|
||||
"adopt": {
|
||||
"title": "Düğüm Benimse",
|
||||
"description": "Zaten düğüm için kimlik bilgilerine sahipseniz bunu seçin."
|
||||
},
|
||||
"generate": {
|
||||
"title": "Anahtarları Oluştur",
|
||||
"description": "Düğüm için yeni anahtarlar oluşturmak istiyorsanız bunu seçin"
|
||||
}
|
||||
},
|
||||
"adopt": {
|
||||
"title": "Mevcut Düğümü Benimse",
|
||||
"description": "Adayacağınız mevcut düğümün kimlik bilgilerini girin",
|
||||
"nodeIdLabel": "Düğüm ID",
|
||||
"nodeIdDescription": "Adayacağınız mevcut düğümün ID'si",
|
||||
"secretLabel": "Gizli",
|
||||
"secretDescription": "Mevcut düğümün gizli anahtarı",
|
||||
"submitButton": "Düğümü Benimse"
|
||||
},
|
||||
"generate": {
|
||||
"title": "Oluşturulan Kimlik Bilgileri",
|
||||
"description": "Düğümünüzü yapılandırmak için oluşturulan bu kimlik bilgilerini kullanın",
|
||||
"nodeIdTitle": "Düğüm ID",
|
||||
"secretTitle": "Gizli",
|
||||
"saveCredentialsTitle": "Kimlik Bilgilerini Yapılandırmaya Ekle",
|
||||
"saveCredentialsDescription": "Bağlantıyı tamamlamak için bu kimlik bilgilerini öz-host Pangolin düğüm yapılandırma dosyanıza ekleyin.",
|
||||
"submitButton": "Düğüm Oluştur"
|
||||
},
|
||||
"validation": {
|
||||
"adoptRequired": "Mevcut bir düğümü benimserken Düğüm ID ve Gizli anahtar gereklidir"
|
||||
},
|
||||
"errors": {
|
||||
"loadDefaultsFailed": "Varsayılanlar yüklenemedi",
|
||||
"defaultsNotLoaded": "Varsayılanlar yüklenmedi",
|
||||
"createFailed": "Düğüm oluşturulamadı"
|
||||
},
|
||||
"success": {
|
||||
"created": "Düğüm başarıyla oluşturuldu"
|
||||
}
|
||||
},
|
||||
"remoteExitNodeSelection": "Düğüm Seçimi",
|
||||
"remoteExitNodeSelectionDescription": "Yerel site için trafiği yönlendirecek düğümü seçin",
|
||||
"remoteExitNodeRequired": "Yerel siteler için bir düğüm seçilmelidir",
|
||||
"noRemoteExitNodesAvailable": "Düğüm Bulunamadı",
|
||||
"noRemoteExitNodesAvailableDescription": "Bu organizasyon için düğüm mevcut değil. Yerel siteleri kullanmak için önce bir düğüm oluşturun.",
|
||||
"exitNode": "Çıkış Düğümü",
|
||||
"country": "Ülke",
|
||||
"rulesMatchCountry": "Şu anda kaynak IP'ye dayanarak",
|
||||
"managedSelfHosted": {
|
||||
"title": "Yönetilen Self-Hosted",
|
||||
"description": "Daha güvenilir ve düşük bakım gerektiren, ekstra özelliklere sahip kendi kendine barındırabileceğiniz Pangolin sunucusu",
|
||||
"introTitle": "Yönetilen Kendi Kendine Barındırılan Pangolin",
|
||||
"introDescription": "Bu, basitlik ve ekstra güvenilirlik arayan, ancak verilerini gizli tutmak ve kendi sunucularında barındırmak isteyen kişiler için tasarlanmış bir dağıtım seçeneğidir.",
|
||||
"introDetail": "Bu seçenekle, kendi Pangolin düğümünüzü çalıştırmaya devam edersiniz — tünelleriniz, SSL bitişiniz ve trafiğiniz tamamen sunucunuzda kalır. Fark, yönetim ve izlemeyi bulut panomuz üzerinden gerçekleştiririz, bu da bir dizi avantaj sağlar:",
|
||||
"benefitSimplerOperations": {
|
||||
"title": "Daha basit işlemler",
|
||||
"description": "Kendi e-posta sunucunuzu çalıştırmanıza veya karmaşık uyarılar kurmanıza gerek yok. Sağlık kontrolleri ve kesinti uyarılarını kutudan çıktığı gibi alırsınız."
|
||||
},
|
||||
"benefitAutomaticUpdates": {
|
||||
"title": "Otomatik güncellemeler",
|
||||
"description": "Bulut panosu hızla gelişir, böylece her seferinde yeni konteynerler manuel olarak çekmeden yeni özellikler ve hata düzeltmeleri alırsınız."
|
||||
},
|
||||
"benefitLessMaintenance": {
|
||||
"title": "Daha az bakım",
|
||||
"description": "Veritabanı geçişleri, yedeklemeler veya ekstra altyapı yönetimi yok. Biz bunu bulutta hallederiz."
|
||||
},
|
||||
"benefitCloudFailover": {
|
||||
"title": "Bulut yedekleme",
|
||||
"description": "Düğümünüz kapandığında, tünelleriniz geçici olarak bulut bağlantı noktalarımıza geçebilir, böylece tekrar çevrimiçi hale getirene kadar tünelleriniz kesintiye uğramaz."
|
||||
},
|
||||
"benefitHighAvailability": {
|
||||
"title": "Yüksek kullanılabilirlik (Bağlantı Noktaları)",
|
||||
"description": "Yedeklilik ve daha iyi performans için hesabınıza birden fazla düğüm bağlayabilirsiniz."
|
||||
},
|
||||
"benefitFutureEnhancements": {
|
||||
"title": "Gelecek iyileştirmeler",
|
||||
"description": "Dağıtımınızı daha sağlam hale getirmek amacıyla daha fazla analiz, uyarı ve yönetim aracı eklemeyi planlıyoruz."
|
||||
},
|
||||
"docsAlert": {
|
||||
"text": "Yönetilen Kendi Kendine Barındırılan seçeneği hakkında daha fazla bilgi edinin",
|
||||
"documentation": "dokümantasyon"
|
||||
},
|
||||
"convertButton": "Bu Düğümü Yönetilen Kendi Kendine Barındırma Dönüştürün"
|
||||
},
|
||||
"internationaldomaindetected": "Uluslararası Alan Adı Tespit Edildi",
|
||||
"willbestoredas": "Şu şekilde depolanacak:",
|
||||
"roleMappingDescription": "Otomatik Sağlama etkinleştirildiğinde kullanıcıların oturum açarken rollerin nasıl atandığını belirleyin.",
|
||||
"selectRole": "Bir Rol Seçin",
|
||||
"roleMappingExpression": "İfade",
|
||||
"selectRolePlaceholder": "Bir rol seçin",
|
||||
"selectRoleDescription": "Bu kimlik sağlayıcısından tüm kullanıcılara atanacak bir rol seçin",
|
||||
"roleMappingExpressionDescription": "Rol bilgilerini ID tokeninden çıkarmak için bir JMESPath ifadesi girin",
|
||||
"idpTenantIdRequired": "Kiracı Kimliği gereklidir",
|
||||
"invalidValue": "Geçersiz değer",
|
||||
"idpTypeLabel": "Kimlik Sağlayıcı Türü",
|
||||
"roleMappingExpressionPlaceholder": "örn., contains(gruplar, 'yönetici') && 'Yönetici' || 'Üye'",
|
||||
"idpGoogleConfiguration": "Google Yapılandırması",
|
||||
"idpGoogleConfigurationDescription": "Google OAuth2 kimlik bilgilerinizi yapılandırın",
|
||||
"idpGoogleClientIdDescription": "Google OAuth2 İstemci Kimliğiniz",
|
||||
"idpGoogleClientSecretDescription": "Google OAuth2 İstemci Sırrınız",
|
||||
"idpAzureConfiguration": "Azure Entra ID Yapılandırması",
|
||||
"idpAzureConfigurationDescription": "Azure Entra ID OAuth2 kimlik bilgilerinizi yapılandırın",
|
||||
"idpTenantId": "Kiracı Kimliği",
|
||||
"idpTenantIdPlaceholder": "kiraci-kimliginiz",
|
||||
"idpAzureTenantIdDescription": "Azure kiracı kimliğiniz (Azure Active Directory genel bakışında bulunur)",
|
||||
"idpAzureClientIdDescription": "Azure Uygulama Kaydı İstemci Kimliğiniz",
|
||||
"idpAzureClientSecretDescription": "Azure Uygulama Kaydı İstemci Sırrınız",
|
||||
"idpGoogleTitle": "Google",
|
||||
"idpGoogleAlt": "Google",
|
||||
"idpAzureTitle": "Azure Entra ID",
|
||||
"idpAzureAlt": "Azure",
|
||||
"idpGoogleConfigurationTitle": "Google Yapılandırması",
|
||||
"idpAzureConfigurationTitle": "Azure Entra ID Yapılandırması",
|
||||
"idpTenantIdLabel": "Kiracı Kimliği",
|
||||
"idpAzureClientIdDescription2": "Azure Uygulama Kaydı İstemci Kimliğiniz",
|
||||
"idpAzureClientSecretDescription2": "Azure Uygulama Kaydı İstemci Sırrınız",
|
||||
"idpGoogleDescription": "Google OAuth2/OIDC sağlayıcısı",
|
||||
"idpAzureDescription": "Microsoft Azure OAuth2/OIDC sağlayıcısı",
|
||||
"subnet": "Alt ağ",
|
||||
"subnetDescription": "Bu organizasyonun ağ yapılandırması için alt ağ.",
|
||||
"authPage": "Yetkilendirme Sayfası",
|
||||
"authPageDescription": "Kuruluşunuz için yetkilendirme sayfasını yapılandırın",
|
||||
"authPageDomain": "Yetkilendirme Sayfası Alanı",
|
||||
"noDomainSet": "Alan belirlenmedi",
|
||||
"changeDomain": "Alanı Değiştir",
|
||||
"selectDomain": "Alan Seçin",
|
||||
"restartCertificate": "Sertifikayı Yenile",
|
||||
"editAuthPageDomain": "Yetkilendirme Sayfası Alanını Düzenle",
|
||||
"setAuthPageDomain": "Yetkilendirme Sayfası Alanını Ayarla",
|
||||
"failedToFetchCertificate": "Sertifika getirilemedi",
|
||||
"failedToRestartCertificate": "Sertifika yeniden başlatılamadı",
|
||||
"addDomainToEnableCustomAuthPages": "Kuruluşunuz için özel kimlik doğrulama sayfalarını etkinleştirmek için bir alan ekleyin",
|
||||
"selectDomainForOrgAuthPage": "Kuruluşun kimlik doğrulama sayfası için bir alan seçin",
|
||||
"domainPickerProvidedDomain": "Sağlanan Alan Adı",
|
||||
"domainPickerFreeProvidedDomain": "Ücretsiz Sağlanan Alan Adı",
|
||||
"domainPickerVerified": "Doğrulandı",
|
||||
"domainPickerUnverified": "Doğrulanmadı",
|
||||
"domainPickerInvalidSubdomainStructure": "Bu alt alan adı geçersiz karakterler veya yapı içeriyor. Kaydettiğinizde otomatik olarak temizlenecektir.",
|
||||
"domainPickerError": "Hata",
|
||||
"domainPickerErrorLoadDomains": "Organizasyon alan adları yüklenemedi",
|
||||
"domainPickerErrorCheckAvailability": "Alan adı kullanılabilirliği kontrol edilemedi",
|
||||
"domainPickerInvalidSubdomain": "Geçersiz alt alan adı",
|
||||
"domainPickerInvalidSubdomainRemoved": "Girdi \"{sub}\" geçersiz olduğu için kaldırıldı.",
|
||||
"domainPickerInvalidSubdomainCannotMakeValid": "\"{sub}\" {domain} için geçerli yapılamadı.",
|
||||
"domainPickerSubdomainSanitized": "Alt alan adı temizlendi",
|
||||
"domainPickerSubdomainCorrected": "\"{sub}\" \"{sanitized}\" olarak düzeltildi",
|
||||
"orgAuthSignInTitle": "Kuruluşunuza giriş yapın",
|
||||
"orgAuthChooseIdpDescription": "Devam etmek için kimlik sağlayıcınızı seçin",
|
||||
"orgAuthNoIdpConfigured": "Bu kuruluşta yapılandırılmış kimlik sağlayıcı yok. Bunun yerine Pangolin kimliğinizle giriş yapabilirsiniz.",
|
||||
"orgAuthSignInWithPangolin": "Pangolin ile Giriş Yap",
|
||||
"subscriptionRequiredToUse": "Bu özelliği kullanmak için abonelik gerekmektedir.",
|
||||
"idpDisabled": "Kimlik sağlayıcılar devre dışı bırakılmıştır.",
|
||||
"orgAuthPageDisabled": "Kuruluş kimlik doğrulama sayfası devre dışı bırakılmıştır.",
|
||||
"domainRestartedDescription": "Alan doğrulaması başarıyla yeniden başlatıldı",
|
||||
"resourceAddEntrypointsEditFile": "Dosyayı düzenle: config/traefik/traefik_config.yml",
|
||||
"resourceExposePortsEditFile": "Dosyayı düzenle: docker-compose.yml",
|
||||
"emailVerificationRequired": "E-posta doğrulaması gereklidir. Bu adımı tamamlamak için lütfen tekrar {dashboardUrl}/auth/login üzerinden oturum açın. Sonra buraya geri dönün.",
|
||||
"twoFactorSetupRequired": "İki faktörlü kimlik doğrulama ayarı gereklidir. Bu adımı tamamlamak için lütfen tekrar {dashboardUrl}/auth/login üzerinden oturum açın. Sonra buraya geri dönün.",
|
||||
"additionalSecurityRequired": "Ek Güvenlik Gereklidir",
|
||||
"organizationRequiresAdditionalSteps": "Bu kuruluş, kaynaklara erişmeden önce ek güvenlik adımları gerektirir.",
|
||||
"completeTheseSteps": "Bu adımları tamamlayın",
|
||||
"enableTwoFactorAuthentication": "İki faktörlü kimlik doğrulamayı etkinleştir",
|
||||
"completeSecuritySteps": "Güvenlik Adımlarını Tamamla",
|
||||
"securitySettings": "Güvenlik Ayarları",
|
||||
"securitySettingsDescription": "Kuruluşunuz için güvenlik politikalarını yapılandırın",
|
||||
"requireTwoFactorForAllUsers": "Tüm Kullanıcılar için İki Faktörlü Kimlik Doğrulama Gerektir",
|
||||
"requireTwoFactorDescription": "Etkinleştirildiğinde, bu kuruluştaki tüm dahili kullanıcıların, kuruluşa erişmek için iki faktörlü kimlik doğrulama etkinleştirilmiş olmalıdır.",
|
||||
"requireTwoFactorDisabledDescription": "Bu özellik, geçerli bir lisans (Kurumsal) veya aktif bir abonelik (SaaS) gerektirir",
|
||||
"requireTwoFactorCannotEnableDescription": "Tüm kullanıcılar için etkinleştirilmeden önce hesabınızda iki faktörlü kimlik doğrulamayı etkinleştirmeniz gerekir",
|
||||
"maxSessionLength": "Maksimum Oturum Süresi",
|
||||
"maxSessionLengthDescription": "Kullanıcı oturumları için maksimum süreyi ayarlayın. Bu süre sonra kullanıcıların tekrar kimlik doğrulaması gerekecektir.",
|
||||
"maxSessionLengthDisabledDescription": "Bu özellik, geçerli bir lisans (Kurumsal) veya aktif bir abonelik (SaaS) gerektirir",
|
||||
"selectSessionLength": "Oturum süresini seçin",
|
||||
"unenforced": "Zorunlu Değil",
|
||||
"1Hour": "1 saat",
|
||||
"3Hours": "3 saat",
|
||||
"6Hours": "6 saat",
|
||||
"12Hours": "12 saat",
|
||||
"1DaySession": "1 gün",
|
||||
"3Days": "3 gün",
|
||||
"7Days": "7 gün",
|
||||
"14Days": "14 gün",
|
||||
"30DaysSession": "30 gün",
|
||||
"90DaysSession": "90 gün",
|
||||
"180DaysSession": "180 gün",
|
||||
"passwordExpiryDays": "Şifre Sona Ermesi",
|
||||
"editPasswordExpiryDescription": "Kullanıcıların parolalarını değiştirmeleri gereken gün sayısını ayarlayın.",
|
||||
"selectPasswordExpiry": "Şifre sona ermesini seçin",
|
||||
"30Days": "30 gün",
|
||||
"1Day": "1 gün",
|
||||
"60Days": "60 gün",
|
||||
"90Days": "90 gün",
|
||||
"180Days": "180 gün",
|
||||
"1Year": "1 yıl",
|
||||
"subscriptionBadge": "Abonelik Gerekiyor",
|
||||
"securityPolicyChangeWarning": "Güvenlik Politikası Değişiklik Uyarısı",
|
||||
"securityPolicyChangeDescription": "Güvenlik politikası ayarlarını değiştirmek üzeresiniz. Değişiklikleri kaydettikten sonra, bu politika güncellemelerine uyum sağlamak amacıyla tekrar kimlik doğrulamanız gerekebilir. Uyum sağlamayan tüm kullanıcıların da tekrar kimlik doğrulaması gerekecektir.",
|
||||
"securityPolicyChangeConfirmMessage": "Onaylıyorum",
|
||||
"securityPolicyChangeWarningText": "Bu, organizasyondaki tüm kullanıcıları etkileyecektir",
|
||||
"authPageErrorUpdateMessage": "Kimlik doğrulama sayfası ayarları güncellenirken bir hata oluştu.",
|
||||
"authPageErrorUpdate": "Kimlik doğrulama sayfası güncellenemedi",
|
||||
"authPageUpdated": "Kimlik doğrulama sayfası başarıyla güncellendi",
|
||||
"healthCheckNotAvailable": "Yerel",
|
||||
"rewritePath": "Yolu Yeniden Yaz",
|
||||
"rewritePathDescription": "Seçenek olarak hedefe iletmeden önce yolu yeniden yazın.",
|
||||
"continueToApplication": "Uygulamaya Devam Et",
|
||||
"checkingInvite": "Davet Kontrol Ediliyor",
|
||||
"setResourceHeaderAuth": "setResourceHeaderAuth",
|
||||
"resourceHeaderAuthRemove": "Başlık Kimlik Doğrulama Kaldır",
|
||||
"resourceHeaderAuthRemoveDescription": "Başlık kimlik doğrulama başarıyla kaldırıldı.",
|
||||
"resourceErrorHeaderAuthRemove": "Başlık Kimlik Doğrulama kaldırılamadı",
|
||||
"resourceErrorHeaderAuthRemoveDescription": "Kaynak için başlık kimlik doğrulaması kaldırılamadı.",
|
||||
"resourceHeaderAuthProtectionEnabled": "Başlık Doğrulaması Etkin",
|
||||
"resourceHeaderAuthProtectionDisabled": "Başlık Doğrulaması Devre Dışı",
|
||||
"headerAuthRemove": "Başlık Doğrulaması Kaldır",
|
||||
"headerAuthAdd": "Başlık Doğrulaması Ekle",
|
||||
"resourceErrorHeaderAuthSetup": "Başlık Kimlik Doğrulama ayarlanamadı",
|
||||
"resourceErrorHeaderAuthSetupDescription": "Kaynak için başlık kimlik doğrulaması ayarlanamadı.",
|
||||
"resourceHeaderAuthSetup": "Başlık Kimlik Doğrulama başarıyla ayarlandı",
|
||||
"resourceHeaderAuthSetupDescription": "Başlık kimlik doğrulaması başarıyla ayarlandı.",
|
||||
"resourceHeaderAuthSetupTitle": "Başlık Kimlik Doğrulama Ayarla",
|
||||
"resourceHeaderAuthSetupTitleDescription": "Bu kaynağı HTTP Başlık Kimlik Doğrulaması ile korumak için temel kimlik bilgilerini (kullanıcı adı ve şifre) ayarlayın. Kaynağa erişim için https://username:password@resource.example.com formatını kullanın.",
|
||||
"resourceHeaderAuthSubmit": "Başlık Kimlik Doğrulama Ayarla",
|
||||
"actionSetResourceHeaderAuth": "Başlık Kimlik Doğrulama Ayarla",
|
||||
"enterpriseEdition": "Kurumsal Sürüm",
|
||||
"unlicensed": "Lisansız",
|
||||
"beta": "Beta",
|
||||
"manageClients": "Müşteri Yönetimi",
|
||||
"manageClientsDescription": "Müşteriler, sitelerinize bağlanabilen cihazlardır.",
|
||||
"licenseTableValidUntil": "Geçerli İki Tarih Kadar",
|
||||
"saasLicenseKeysSettingsTitle": "Kurumsal Lisanslar",
|
||||
"saasLicenseKeysSettingsDescription": "Kendi barındırdığınız Pangolin örnekleri için kurumsal lisans anahtarları oluşturun ve yönetin.",
|
||||
"sidebarEnterpriseLicenses": "Lisanslar",
|
||||
"generateLicenseKey": "Lisans Anahtarı Oluştur",
|
||||
"generateLicenseKeyForm": {
|
||||
"validation": {
|
||||
"emailRequired": "Lütfen geçerli bir e-posta adresi girin.",
|
||||
"useCaseTypeRequired": "Lütfen bir kullanım alanı türü seçin.",
|
||||
"firstNameRequired": "İsim gereklidir.",
|
||||
"lastNameRequired": "Soyisim gereklidir.",
|
||||
"primaryUseRequired": "Lütfen öncelikli kullanımınızı açıklayın.",
|
||||
"jobTitleRequiredBusiness": "İş kullanımı için iş unvanı gereklidir.",
|
||||
"industryRequiredBusiness": "İş kullanımı için sektör gereklidir.",
|
||||
"stateProvinceRegionRequired": "Eyalet/İl/Bölge gereklidir.",
|
||||
"postalZipCodeRequired": "Posta/ZIP kodu gereklidir.",
|
||||
"companyNameRequiredBusiness": "İş kullanımı için şirket ismi gereklidir.",
|
||||
"countryOfResidenceRequiredBusiness": "İş kullanımı için ikamet edilen ülke gereklidir.",
|
||||
"countryRequiredPersonal": "Kişisel kullanım için ülke gereklidir.",
|
||||
"agreeToTermsRequired": "Şartları kabul etmelisiniz.",
|
||||
"complianceConfirmationRequired": "Fossorial Ticari Lisans ile uyumluluğu doğrulamalısınız."
|
||||
},
|
||||
"useCaseOptions": {
|
||||
"personal": {
|
||||
"title": "Kişisel Kullanım",
|
||||
"description": "Bireysel, ticari olmayan kullanım için öğrenme, kişisel projeler veya denemeler gibi."
|
||||
},
|
||||
"business": {
|
||||
"title": "İş Kullanımı",
|
||||
"description": "Kuruluşlar, şirketler veya ticari veya gelir getirici faaliyetler bünyesinde kullanım için."
|
||||
}
|
||||
},
|
||||
"steps": {
|
||||
"emailLicenseType": {
|
||||
"title": "E-posta ve Lisans Türü",
|
||||
"description": "E-posta adresinizi girin ve lisans türünüzü seçin."
|
||||
},
|
||||
"personalInformation": {
|
||||
"title": "Kişisel Bilgiler",
|
||||
"description": "Kendinizden bahsedin."
|
||||
},
|
||||
"contactInformation": {
|
||||
"title": "İletişim Bilgileri",
|
||||
"description": "İletişim detaylarınız."
|
||||
},
|
||||
"termsGenerate": {
|
||||
"title": "Şartlar ve Lisans Üret",
|
||||
"description": "Lisansınızı oluşturmak için şartları inceleyin ve kabul edin."
|
||||
}
|
||||
},
|
||||
"alerts": {
|
||||
"commercialUseDisclosure": {
|
||||
"title": "Kullanım Açıklaması",
|
||||
"description": "Kullanım amacınızı doğru bir şekilde yansıtan lisans seviyesini seçin. Kişisel Lisans, yazılımın bireysel, ticari olmayan veya yıllık geliri 100,000 ABD Dolarının altında olan küçük ölçekli ticari faaliyetlerde ücretsiz kullanılmasına izin verir. Bu sınırların ötesinde kullanım — bir işletme, organizasyon veya diğer gelir getirici ortamlarda kullanım dahil olmak üzere — geçerli bir Kurumsal Lisans ve ilgili lisans ücretinin ödenmesini gerektirir. Tüm kullanıcılar, ister Kişisel ister Kurumsal, Fossorial Ticari Lisans Şartlarına uymalıdır."
|
||||
},
|
||||
"trialPeriodInformation": {
|
||||
"title": "Deneme Süresi Bilgileri",
|
||||
"description": "Bu Lisans Anahtarı, Kurumsal özellikleri 7 günlük bir değerlendirme süresi için etkinleştirir. Değerlendirme süresi bittikten sonra, Ücretli Özelliklere devam eden erişim, geçerli bir Kişisel veya Kurumsal Lisans altında etkinleşmeyi gerektirir. Kurumsal lisanslama için sales@pangolin.net ile iletişime geçin."
|
||||
}
|
||||
},
|
||||
"form": {
|
||||
"useCaseQuestion": "Pangolin'i kişisel veya iş kullanımı için mi kullanıyorsunuz?",
|
||||
"firstName": "İsim",
|
||||
"lastName": "Soyisim",
|
||||
"jobTitle": "İş Unvanı",
|
||||
"primaryUseQuestion": "Pangolin'i öncelikli olarak ne amacıyla kullanmayı planlıyorsunuz?",
|
||||
"industryQuestion": "Hangi sektördesiniz?",
|
||||
"prospectiveUsersQuestion": "Kaç potansiyel kullanıcı öngörüyorsunuz?",
|
||||
"prospectiveSitesQuestion": "Kaç potansiyel site (tünel) öngörüyorsunuz?",
|
||||
"companyName": "Şirket İsmi",
|
||||
"countryOfResidence": "İkamet edilen ülke",
|
||||
"stateProvinceRegion": "Eyalet / İl / Bölge",
|
||||
"postalZipCode": "Posta / ZIP Kodu",
|
||||
"companyWebsite": "Şirket web sitesi",
|
||||
"companyPhoneNumber": "Şirket telefon numarası",
|
||||
"country": "Ülke",
|
||||
"phoneNumberOptional": "Telefon numarası (isteğe bağlı)",
|
||||
"complianceConfirmation": "Sağladığım bilgilerin doğru olduğunu ve Fossorial Ticari Lisans ile uyumlu olduğumu teyit ederim. Yanlış bilgi raporlamak veya ürün kullanımını yanlış tanımlamak lisans ihlalidir ve anahtarınızın iptal edilmesine neden olabilir."
|
||||
},
|
||||
"buttons": {
|
||||
"close": "Kapat",
|
||||
"previous": "Önceki",
|
||||
"next": "Sonraki",
|
||||
"generateLicenseKey": "Lisans Anahtarı Oluştur"
|
||||
},
|
||||
"toasts": {
|
||||
"success": {
|
||||
"title": "Lisans anahtarı başarıyla oluşturuldu",
|
||||
"description": "Lisans anahtarınız oluşturuldu ve kullanıma hazır."
|
||||
},
|
||||
"error": {
|
||||
"title": "Lisans anahtarı oluşturulamadı",
|
||||
"description": "Lisans anahtarı oluşturulurken bir hata oluştu."
|
||||
}
|
||||
}
|
||||
},
|
||||
"priority": "Öncelik",
|
||||
"priorityDescription": "Daha yüksek öncelikli rotalar önce değerlendirilir. Öncelik = 100, otomatik sıralama anlamına gelir (sistem karar verir). Manuel öncelik uygulamak için başka bir numara kullanın.",
|
||||
"instanceName": "Örnek İsmi",
|
||||
"pathMatchModalTitle": "Yol Eşleşmesini Yapılandır",
|
||||
"pathMatchModalDescription": "Gelen isteklerin yolu temel alarak nasıl eşleştirilmesi gerektiğini ayarlayın.",
|
||||
"pathMatchType": "Eşleşme Türü",
|
||||
"pathMatchPrefix": "Önek",
|
||||
"pathMatchExact": "Tam",
|
||||
"pathMatchRegex": "Regex",
|
||||
"pathMatchValue": "Yol Değeri",
|
||||
"clear": "Temizle",
|
||||
"saveChanges": "Değişiklikleri Kaydet",
|
||||
"pathMatchRegexPlaceholder": "^/api/.*",
|
||||
"pathMatchDefaultPlaceholder": "/path",
|
||||
"pathMatchPrefixHelp": "Örnek: /api, /api/users vb.'ni eşleştirir.",
|
||||
"pathMatchExactHelp": "Örnek: /api yalnızca /api'yi eşleştirir",
|
||||
"pathMatchRegexHelp": "Örnek: ^/api/.* her şeyi eşleştirir /api/anything",
|
||||
"pathRewriteModalTitle": "Yolu Yeniden Yazmayı Yapılandır",
|
||||
"pathRewriteModalDescription": "Hedefe iletilmeden önce eşleşen yolu dönüştürün.",
|
||||
"pathRewriteType": "Yeniden Yazma Türü",
|
||||
"pathRewritePrefixOption": "Önek - Ön ek değiştirme",
|
||||
"pathRewriteExactOption": "Tam - Tüm yolu değiştir",
|
||||
"pathRewriteRegexOption": "Regex - Desen değiştirme",
|
||||
"pathRewriteStripPrefixOption": "Ön Ek Kaldır - Ön eki sil",
|
||||
"pathRewriteValue": "Yeniden Yazma Değeri",
|
||||
"pathRewriteRegexPlaceholder": "/new/$1",
|
||||
"pathRewriteDefaultPlaceholder": "/new-path",
|
||||
"pathRewritePrefixHelp": "Eşleşen öneki bu değerle değiştir",
|
||||
"pathRewriteExactHelp": "Yol tam olarak eşleştiğinde, yolun tamamını bu değerle değiştir",
|
||||
"pathRewriteRegexHelp": "Değiştirme için $1, $2 gibi yakalama gruplarını kullan",
|
||||
"pathRewriteStripPrefixHelp": "Öneki silmek veya yeni bir ön ek sağlamak için boş bırakın",
|
||||
"pathRewritePrefix": "Önek",
|
||||
"pathRewriteExact": "Tam",
|
||||
"pathRewriteRegex": "Regex",
|
||||
"pathRewriteStrip": "Sil",
|
||||
"pathRewriteStripLabel": "sil",
|
||||
"sidebarEnableEnterpriseLicense": "Kurumsal Lisans Etkinleştir",
|
||||
"cannotbeUndone": "Bu geri alınamaz.",
|
||||
"toConfirm": "doğrulamak için",
|
||||
"deleteClientQuestion": "Müşteriyi siteden ve organizasyondan kaldırmak istediğinizden emin misiniz?",
|
||||
"clientMessageRemove": "Kaldırıldıktan sonra müşteri siteye bağlanamayacaktır.",
|
||||
"sidebarLogs": "Kayıtlar",
|
||||
"request": "İstek",
|
||||
"logs": "Günlükler",
|
||||
"logsSettingsDescription": "Bu organizasyondan toplanan günlükleri izleyin",
|
||||
"searchLogs": "Günlüklerde ara...",
|
||||
"action": "Eylem",
|
||||
"actor": "Aktör",
|
||||
"timestamp": "Zaman damgası",
|
||||
"accessLogs": "Erişim Günlükleri",
|
||||
"exportCsv": "CSV Dışa Aktar",
|
||||
"actorId": "Aktör Kimliği",
|
||||
"allowedByRule": "Kurallara Göre İzin Verildi",
|
||||
"allowedNoAuth": "Kimlik Doğrulama Yok İzin Verildi",
|
||||
"validAccessToken": "Geçerli Erişim Jetonu",
|
||||
"validHeaderAuth": "Geçerli Başlık Doğrulama",
|
||||
"validPincode": "Geçerli Pincode",
|
||||
"validPassword": "Geçerli Şifre",
|
||||
"validEmail": "Geçerli E-posta",
|
||||
"validSSO": "Geçerli SSO",
|
||||
"resourceBlocked": "Kaynak Engellendi",
|
||||
"droppedByRule": "Kurallara Göre Çıkartıldı",
|
||||
"noSessions": "Oturum Yok",
|
||||
"temporaryRequestToken": "Geçici İstek Jetonu",
|
||||
"noMoreAuthMethods": "Daha Fazla Kimlik Doğrulama Yöntemi Yok",
|
||||
"ip": "IP",
|
||||
"reason": "Sebep",
|
||||
"requestLogs": "İstek Günlükleri",
|
||||
"host": "Sunucu",
|
||||
"location": "Konum",
|
||||
"actionLogs": "Eylem Günlükleri",
|
||||
"sidebarLogsRequest": "İstek Günlükleri",
|
||||
"sidebarLogsAccess": "Erişim Günlükleri",
|
||||
"sidebarLogsAction": "Eylem Günlükleri",
|
||||
"logRetention": "Kayıt Saklama",
|
||||
"logRetentionDescription": "Bu organizasyon için farklı türdeki günlüklerin ne kadar süre saklanacağını yönetin veya devre dışı bırakın",
|
||||
"requestLogsDescription": "Bu organizasyondaki kaynaklar için ayrıntılı istek günlüklerini görüntüleyin",
|
||||
"logRetentionRequestLabel": "İstek Günlüğü Saklama",
|
||||
"logRetentionRequestDescription": "İstek günlüklerini ne kadar süre tutacağını belirle",
|
||||
"logRetentionAccessLabel": "Erişim Günlüğü Saklama",
|
||||
"logRetentionAccessDescription": "Erişim günlüklerini ne kadar süre tutacağını belirle",
|
||||
"logRetentionActionLabel": "Eylem Günlüğü Saklama",
|
||||
"logRetentionActionDescription": "Eylem günlüklerini ne kadar süre tutacağını belirle",
|
||||
"logRetentionDisabled": "Devre Dışı",
|
||||
"logRetention3Days": "3 gün",
|
||||
"logRetention7Days": "7 gün",
|
||||
"logRetention14Days": "14 gün",
|
||||
"logRetention30Days": "30 gün",
|
||||
"logRetention90Days": "90 gün",
|
||||
"logRetentionForever": "Sonsuza kadar",
|
||||
"actionLogsDescription": "Bu organizasyondaki eylemler geçmişini görüntüleyin",
|
||||
"accessLogsDescription": "Bu organizasyondaki kaynaklar için erişim kimlik doğrulama isteklerini görüntüleyin",
|
||||
"licenseRequiredToUse": "Bu özelliği kullanmak için bir kurumsal lisans gereklidir.",
|
||||
"certResolver": "Sertifika Çözücü",
|
||||
"certResolverDescription": "Bu kaynak için kullanılacak sertifika çözücüsünü seçin.",
|
||||
"selectCertResolver": "Sertifika Çözücü Seçin",
|
||||
"enterCustomResolver": "Özel Çözücü Girin",
|
||||
"preferWildcardCert": "Joker Sertifikayı Tercih Et",
|
||||
"unverified": "Doğrulanmadı",
|
||||
"domainSetting": "Alan Adı Ayarları",
|
||||
"domainSettingDescription": "Alan adınız için ayarları yapılandırın",
|
||||
"preferWildcardCertDescription": "Joker sertifika üretmeye çalışın (doğru yapılandırılmış bir sertifika çözücü gereklidir).",
|
||||
"recordName": "Kayıt Adı",
|
||||
"auto": "Otomatik",
|
||||
"TTL": "TTL",
|
||||
"howToAddRecords": "Kayıtları Nasıl Ekleyebilirsiniz",
|
||||
"dnsRecord": "DNS Kayıtları",
|
||||
"required": "Gerekli",
|
||||
"domainSettingsUpdated": "Alan adına yönelik ayarlar başarıyla güncellendi",
|
||||
"orgOrDomainIdMissing": "Organizasyon veya Alan Adı Kimliği eksik",
|
||||
"loadingDNSRecords": "DNS kayıtları yükleniyor...",
|
||||
"olmUpdateAvailableInfo": "Olm'nin güncellenmiş bir sürümü mevcut. En iyi deneyim için lütfen en son sürüme güncelleyin.",
|
||||
"client": "İstemci",
|
||||
"proxyProtocol": "Proxy Protokol Ayarları",
|
||||
"proxyProtocolDescription": "TCP/UDP hizmetleri için istemci IP adreslerini korumak için Proxy Protokolünü yapılandırın.",
|
||||
"enableProxyProtocol": "Proxy Protokolünü Etkinleştir",
|
||||
"proxyProtocolInfo": "TCP/UDP arka uçları için istemci IP adreslerini koruyun",
|
||||
"proxyProtocolVersion": "Proxy Protokol Versiyonu",
|
||||
"version1": " Versiyon 1 (Önerilen)",
|
||||
"version2": "Versiyon 2",
|
||||
"versionDescription": "Versiyon 1 metin tabanlı ve yaygın olarak desteklenir. Versiyon 2 ise ikili ve daha verimlidir ama daha az uyumludur.",
|
||||
"warning": "Uyarı",
|
||||
"proxyProtocolWarning": "Arka uç uygulamanız, Proxy Protokol bağlantılarını kabul etmek üzere yapılandırılmalıdır. Arka ucunuz Proxy Protokolünü desteklemiyorsa, bunu etkinleştirmek tüm bağlantıları koparır. Traefik'ten gelen Proxy Protokol başlıklarına güvenecek şekilde arka ucunuzu yapılandırdığınızdan emin olun.",
|
||||
"restarting": "Yeniden Başlatılıyor...",
|
||||
"manual": "Manuel",
|
||||
"messageSupport": "Destek Mesajı Gönder",
|
||||
"supportNotAvailableTitle": "Destek Yok",
|
||||
"supportNotAvailableDescription": "Destek şu anda mevcut değil. Destek'e bir e-posta gönderebilirsiniz: support@pangolin.net.",
|
||||
"supportRequestSentTitle": "Destek İsteği Gönderildi",
|
||||
"supportRequestSentDescription": "Mesajınız başarıyla gönderildi.",
|
||||
"supportRequestFailedTitle": "İsteği Gönderme Başarısız",
|
||||
"supportRequestFailedDescription": "Destek isteği gönderilirken bir hata oluştu.",
|
||||
"supportSubjectRequired": "Konu gerekli",
|
||||
"supportSubjectMaxLength": "Konu en fazla 255 karakter olabilir",
|
||||
"supportMessageRequired": "Mesaj gerekli",
|
||||
"supportReplyTo": "Yanıtla",
|
||||
"supportSubject": "Konu",
|
||||
"supportSubjectPlaceholder": "Konu girin",
|
||||
"supportMessage": "Mesaj",
|
||||
"supportMessagePlaceholder": "Mesajınızı girin",
|
||||
"supportSending": "Gönderiliyor...",
|
||||
"supportSend": "Gönder",
|
||||
"supportMessageSent": "Mesaj Gönderildi!",
|
||||
"supportWillContact": "En kısa sürede size geri döneceğiz!",
|
||||
"selectLogRetention": "Kayıt saklama seç",
|
||||
"showColumns": "Sütunları Göster",
|
||||
"hideColumns": "Sütunları Gizle",
|
||||
"columnVisibility": "Sütun Görünürlüğü",
|
||||
"toggleColumn": "{columnName} sütununu aç/kapat",
|
||||
"allColumns": "Tüm Sütunlar",
|
||||
"defaultColumns": "Varsayılan Sütunlar",
|
||||
"customizeView": "Görünümü Özelleştir",
|
||||
"viewOptions": "Görünüm Seçenekleri",
|
||||
"selectAll": "Tümünü Seç",
|
||||
"selectNone": "Hiçbirini Seçme",
|
||||
"selectedResources": "Seçilen Kaynaklar",
|
||||
"enableSelected": "Seçilenleri Etkinleştir",
|
||||
"disableSelected": "Seçilenleri Devre Dışı Bırak",
|
||||
"checkSelectedStatus": "Seçilenlerin Durumunu Kontrol Et",
|
||||
"credentials": "Kimlik Bilgileri",
|
||||
"savecredentials": "Kimlik Bilgilerini Kaydet",
|
||||
"regeneratecredentials": "Yeniden Anahtarla",
|
||||
"regenerateCredentials": "Kimlik bilgilerinizi yeniden oluşturun ve kaydedin",
|
||||
"generatedcredentials": "Oluşturulan Kimlik Bilgileri",
|
||||
"copyandsavethesecredentials": "Bu kimlik bilgilerini kopyalayın ve kaydedin",
|
||||
"copyandsavethesecredentialsdescription": "Bu sayfadan ayrıldıktan sonra bu kimlik bilgileri tekrar gösterilmeyecek. Onları şimdi güvenli bir şekilde saklayın.",
|
||||
"credentialsSaved": "Kimlik Bilgileri Kaydedildi",
|
||||
"credentialsSavedDescription": "Kimlik bilgileri başarılı bir şekilde yeniden oluşturuldu ve kaydedildi.",
|
||||
"credentialsSaveError": "Kimlik Bilgileri Kayıt Hatası",
|
||||
"credentialsSaveErrorDescription": "Kimlik bilgilerini yeniden oluştururken ve kaydederken bir hata oluştu.",
|
||||
"regenerateCredentialsWarning": "Kimlik bilgilerini yeniden oluşturmak önceki bilgileri geçersiz kılacaktır. Bu kimlik bilgilerini kullanan tüm yapılandırmaları güncellediğinizden emin olun.",
|
||||
"confirm": "Onayla",
|
||||
"regenerateCredentialsConfirmation": "Kimlik bilgilerini yeniden oluşturmak istediğinizden emin misiniz?",
|
||||
"endpoint": "Uç Nokta",
|
||||
"Id": "Kimlik",
|
||||
"SecretKey": "Gizli Anahtar",
|
||||
"featureDisabledTooltip": "Bu özellik yalnızca kurumsal planda mevcuttur ve kullanmak için lisans gerektirir.",
|
||||
"niceId": "Güzel Kimlik",
|
||||
"niceIdUpdated": "Güzel Kimlik Güncellendi",
|
||||
"niceIdUpdatedSuccessfully": "Güzel Kimlik Başarıyla Güncellendi",
|
||||
"niceIdUpdateError": "Güzel Kimlik güncellenirken hata",
|
||||
"niceIdUpdateErrorDescription": "Güzel Kimlik güncellenirken bir hata oluştu.",
|
||||
"niceIdCannotBeEmpty": "Güzel Kimlik boş olamaz",
|
||||
"enterIdentifier": "Tanımlayıcıyı girin",
|
||||
"identifier": "Tanımlayıcı"
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
2099
messages/zh-TW.json
Normal file
2099
messages/zh-TW.json
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,12 +1,15 @@
|
||||
import type { NextConfig } from "next";
|
||||
import createNextIntlPlugin from "next-intl/plugin";
|
||||
|
||||
const withNextIntl = createNextIntlPlugin();
|
||||
|
||||
/** @type {import("next").NextConfig} */
|
||||
const nextConfig = {
|
||||
const nextConfig: NextConfig = {
|
||||
eslint: {
|
||||
ignoreDuringBuilds: true
|
||||
},
|
||||
experimental: {
|
||||
reactCompiler: true
|
||||
},
|
||||
output: "standalone"
|
||||
};
|
||||
|
||||
15732
package-lock.json
generated
15732
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
137
package.json
137
package.json
@@ -19,47 +19,58 @@
|
||||
"db:sqlite:studio": "drizzle-kit studio --config=./drizzle.sqlite.config.ts",
|
||||
"db:pg:studio": "drizzle-kit studio --config=./drizzle.pg.config.ts",
|
||||
"db:clear-migrations": "rm -rf server/migrations",
|
||||
"set:oss": "echo 'export const build = \"oss\" as any;' > server/build.ts && cp tsconfig.oss.json tsconfig.json",
|
||||
"set:saas": "echo 'export const build = \"saas\" as any;' > server/build.ts && cp tsconfig.saas.json tsconfig.json",
|
||||
"set:enterprise": "echo 'export const build = \"enterprise\" as any;' > server/build.ts && cp tsconfig.enterprise.json tsconfig.json",
|
||||
"set:sqlite": "echo 'export * from \"./sqlite\";\nexport const driver: \"pg\" | \"sqlite\" = \"sqlite\";' > server/db/index.ts",
|
||||
"set:pg": "echo 'export * from \"./pg\";\nexport const driver: \"pg\" | \"sqlite\" = \"pg\";' > server/db/index.ts",
|
||||
"next:build": "next build",
|
||||
"build:sqlite": "mkdir -p dist && next build && node esbuild.mjs -e server/index.ts -o dist/server.mjs && node esbuild.mjs -e server/setup/migrationsSqlite.ts -o dist/migrations.mjs",
|
||||
"build: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": "DB_TYPE=sqlite NODE_OPTIONS=--enable-source-maps NODE_ENV=development ENVIRONMENT=prod sh -c 'node dist/migrations.mjs && node dist/server.mjs'",
|
||||
"start": "ENVIRONMENT=prod node dist/migrations.mjs && ENVIRONMENT=prod NODE_ENV=development node --enable-source-maps dist/server.mjs",
|
||||
"email": "email dev --dir server/emails/templates --port 3005",
|
||||
"build:cli": "node esbuild.mjs -e cli/index.ts -o dist/cli.mjs"
|
||||
},
|
||||
"dependencies": {
|
||||
"@asteasolutions/zod-to-openapi": "^7.3.4",
|
||||
"@hookform/resolvers": "3.9.1",
|
||||
"@asteasolutions/zod-to-openapi": "8.1.0",
|
||||
"@aws-sdk/client-s3": "3.922.0",
|
||||
"@faker-js/faker": "^10.1.0",
|
||||
"@headlessui/react": "^2.2.9",
|
||||
"@hookform/resolvers": "5.2.2",
|
||||
"@monaco-editor/react": "^4.7.0",
|
||||
"@node-rs/argon2": "^2.0.2",
|
||||
"@oslojs/crypto": "1.0.1",
|
||||
"@oslojs/encoding": "1.1.0",
|
||||
"@radix-ui/react-avatar": "1.1.10",
|
||||
"@radix-ui/react-avatar": "1.1.11",
|
||||
"@radix-ui/react-checkbox": "1.3.3",
|
||||
"@radix-ui/react-collapsible": "1.1.12",
|
||||
"@radix-ui/react-dialog": "1.1.15",
|
||||
"@radix-ui/react-dropdown-menu": "2.1.16",
|
||||
"@radix-ui/react-icons": "1.3.2",
|
||||
"@radix-ui/react-label": "2.1.7",
|
||||
"@radix-ui/react-label": "2.1.8",
|
||||
"@radix-ui/react-popover": "1.1.15",
|
||||
"@radix-ui/react-progress": "^1.1.7",
|
||||
"@radix-ui/react-progress": "^1.1.8",
|
||||
"@radix-ui/react-radio-group": "1.3.8",
|
||||
"@radix-ui/react-scroll-area": "^1.2.10",
|
||||
"@radix-ui/react-select": "2.2.6",
|
||||
"@radix-ui/react-separator": "1.1.7",
|
||||
"@radix-ui/react-slot": "1.2.3",
|
||||
"@radix-ui/react-separator": "1.1.8",
|
||||
"@radix-ui/react-slot": "1.2.4",
|
||||
"@radix-ui/react-switch": "1.2.6",
|
||||
"@radix-ui/react-tabs": "1.1.13",
|
||||
"@radix-ui/react-toast": "1.2.15",
|
||||
"@radix-ui/react-tooltip": "^1.2.8",
|
||||
"@react-email/components": "0.5.0",
|
||||
"@react-email/render": "^1.2.0",
|
||||
"@react-email/components": "0.5.7",
|
||||
"@react-email/render": "^1.3.2",
|
||||
"@react-email/tailwind": "1.2.2",
|
||||
"@simplewebauthn/browser": "^13.1.0",
|
||||
"@simplewebauthn/server": "^9.0.3",
|
||||
"@simplewebauthn/browser": "^13.2.2",
|
||||
"@simplewebauthn/server": "^13.2.2",
|
||||
"@tailwindcss/forms": "^0.5.10",
|
||||
"@tanstack/react-query": "^5.90.6",
|
||||
"@tanstack/react-table": "8.21.3",
|
||||
"arctic": "^3.7.0",
|
||||
"axios": "1.11.0",
|
||||
"axios": "^1.13.2",
|
||||
"better-sqlite3": "11.7.0",
|
||||
"canvas-confetti": "1.9.3",
|
||||
"canvas-confetti": "1.9.4",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "2.1.1",
|
||||
"cmdk": "1.1.1",
|
||||
@@ -68,83 +79,103 @@
|
||||
"cookies": "^0.9.1",
|
||||
"cors": "2.8.5",
|
||||
"crypto-js": "^4.2.0",
|
||||
"drizzle-orm": "0.44.4",
|
||||
"eslint": "9.33.0",
|
||||
"eslint-config-next": "15.4.6",
|
||||
"d3": "^7.9.0",
|
||||
"date-fns": "4.1.0",
|
||||
"drizzle-orm": "0.44.7",
|
||||
"eslint": "9.39.1",
|
||||
"eslint-config-next": "16.0.3",
|
||||
"express": "5.1.0",
|
||||
"express-rate-limit": "8.0.1",
|
||||
"glob": "11.0.3",
|
||||
"express-rate-limit": "8.2.1",
|
||||
"glob": "11.1.0",
|
||||
"helmet": "8.1.0",
|
||||
"http-errors": "2.0.0",
|
||||
"i": "^0.3.7",
|
||||
"input-otp": "1.4.2",
|
||||
"ioredis": "5.8.2",
|
||||
"jmespath": "^0.16.0",
|
||||
"js-yaml": "4.1.0",
|
||||
"js-yaml": "4.1.1",
|
||||
"jsonwebtoken": "^9.0.2",
|
||||
"lucide-react": "0.539.0",
|
||||
"lucide-react": "^0.552.0",
|
||||
"maxmind": "5.0.1",
|
||||
"moment": "2.30.1",
|
||||
"next": "15.4.6",
|
||||
"next-intl": "^4.3.4",
|
||||
"next": "15.5.7",
|
||||
"next-intl": "^4.4.0",
|
||||
"next-themes": "0.4.6",
|
||||
"nextjs-toploader": "^3.9.17",
|
||||
"node-cache": "5.1.2",
|
||||
"node-fetch": "3.3.2",
|
||||
"nodemailer": "7.0.5",
|
||||
"npm": "^11.5.2",
|
||||
"nodemailer": "7.0.10",
|
||||
"npm": "^11.6.4",
|
||||
"nprogress": "^0.2.0",
|
||||
"oslo": "1.2.1",
|
||||
"pg": "^8.16.2",
|
||||
"posthog-node": "^5.7.0",
|
||||
"posthog-node": "^5.11.2",
|
||||
"qrcode.react": "4.2.0",
|
||||
"react": "19.1.1",
|
||||
"react-dom": "19.1.1",
|
||||
"react-easy-sort": "^1.6.0",
|
||||
"react-hook-form": "7.62.0",
|
||||
"react": "19.2.1",
|
||||
"react-day-picker": "9.11.1",
|
||||
"react-dom": "19.2.1",
|
||||
"react-easy-sort": "^1.8.0",
|
||||
"react-hook-form": "7.66.0",
|
||||
"react-icons": "^5.5.0",
|
||||
"rebuild": "0.1.2",
|
||||
"semver": "^7.7.2",
|
||||
"recharts": "^2.15.4",
|
||||
"reodotdev": "^1.0.0",
|
||||
"resend": "^6.4.2",
|
||||
"semver": "^7.7.3",
|
||||
"stripe": "18.2.1",
|
||||
"swagger-ui-express": "^5.0.1",
|
||||
"tailwind-merge": "3.3.1",
|
||||
"tw-animate-css": "^1.3.7",
|
||||
"uuid": "^11.1.0",
|
||||
"topojson-client": "^3.1.0",
|
||||
"tw-animate-css": "^1.3.8",
|
||||
"uuid": "^13.0.0",
|
||||
"vaul": "1.1.2",
|
||||
"winston": "3.17.0",
|
||||
"visionscarto-world-atlas": "^1.0.0",
|
||||
"winston": "3.18.3",
|
||||
"winston-daily-rotate-file": "5.0.0",
|
||||
"ws": "8.18.3",
|
||||
"yaml": "^2.8.1",
|
||||
"yargs": "18.0.0",
|
||||
"zod": "3.25.76",
|
||||
"zod-validation-error": "3.5.2"
|
||||
"zod": "4.1.12",
|
||||
"zod-validation-error": "5.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@dotenvx/dotenvx": "1.49.0",
|
||||
"@dotenvx/dotenvx": "1.51.1",
|
||||
"@esbuild-plugins/tsconfig-paths": "0.1.2",
|
||||
"@tailwindcss/postcss": "^4.1.12",
|
||||
"@react-email/preview-server": "4.3.2",
|
||||
"@tailwindcss/postcss": "^4.1.17",
|
||||
"@tanstack/react-query-devtools": "^5.90.2",
|
||||
"@types/better-sqlite3": "7.6.12",
|
||||
"@types/cookie-parser": "1.4.9",
|
||||
"@types/cookie-parser": "1.4.10",
|
||||
"@types/cors": "2.8.19",
|
||||
"@types/crypto-js": "^4.2.2",
|
||||
"@types/express": "5.0.3",
|
||||
"@types/d3": "^7.4.3",
|
||||
"@types/express": "5.0.5",
|
||||
"@types/express-session": "^1.18.2",
|
||||
"@types/jmespath": "^0.15.2",
|
||||
"@types/js-yaml": "4.0.9",
|
||||
"@types/jsonwebtoken": "^9.0.10",
|
||||
"@types/node": "^24",
|
||||
"@types/nodemailer": "6.4.17",
|
||||
"@types/pg": "8.15.5",
|
||||
"@types/react": "19.1.10",
|
||||
"@types/react-dom": "19.1.7",
|
||||
"@types/semver": "^7.7.0",
|
||||
"@types/node": "24.10.1",
|
||||
"@types/nodemailer": "7.0.3",
|
||||
"@types/nprogress": "^0.2.3",
|
||||
"@types/pg": "8.15.6",
|
||||
"@types/react": "19.2.2",
|
||||
"@types/react-dom": "19.2.2",
|
||||
"@types/semver": "^7.7.1",
|
||||
"@types/swagger-ui-express": "^4.1.8",
|
||||
"@types/topojson-client": "^3.1.5",
|
||||
"@types/ws": "8.18.1",
|
||||
"@types/yargs": "17.0.33",
|
||||
"drizzle-kit": "0.31.4",
|
||||
"esbuild": "0.25.9",
|
||||
"esbuild-node-externals": "1.18.0",
|
||||
"@types/yargs": "17.0.34",
|
||||
"babel-plugin-react-compiler": "^1.0.0",
|
||||
"drizzle-kit": "0.31.6",
|
||||
"esbuild": "0.27.0",
|
||||
"esbuild-node-externals": "1.19.1",
|
||||
"postcss": "^8",
|
||||
"react-email": "4.2.8",
|
||||
"react-email": "4.3.2",
|
||||
"tailwindcss": "^4.1.4",
|
||||
"tsc-alias": "1.8.16",
|
||||
"tsx": "4.20.4",
|
||||
"tsx": "4.20.6",
|
||||
"typescript": "^5",
|
||||
"typescript-eslint": "^8.40.0"
|
||||
"typescript-eslint": "^8.46.3"
|
||||
},
|
||||
"overrides": {
|
||||
"emblor": {
|
||||
|
||||
BIN
public/idp/azure.png
Normal file
BIN
public/idp/azure.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 65 KiB |
BIN
public/idp/google.png
Normal file
BIN
public/idp/google.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 46 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 34 KiB After Width: | Height: | Size: 13 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 33 KiB After Width: | Height: | Size: 13 KiB |
@@ -7,16 +7,21 @@ import {
|
||||
errorHandlerMiddleware,
|
||||
notFoundMiddleware
|
||||
} from "@server/middlewares";
|
||||
import { authenticated, unauthenticated } from "@server/routers/external";
|
||||
import { router as wsRouter, handleWSUpgrade } from "@server/routers/ws";
|
||||
import { authenticated, unauthenticated } from "#dynamic/routers/external";
|
||||
import { router as wsRouter, handleWSUpgrade } from "#dynamic/routers/ws";
|
||||
import { logIncomingMiddleware } from "./middlewares/logIncoming";
|
||||
import { csrfProtectionMiddleware } from "./middlewares/csrfProtection";
|
||||
import helmet from "helmet";
|
||||
import { build } from "./build";
|
||||
import rateLimit, { ipKeyGenerator } from "express-rate-limit";
|
||||
import createHttpError from "http-errors";
|
||||
import HttpCode from "./types/HttpCode";
|
||||
import requestTimeoutMiddleware from "./middlewares/requestTimeout";
|
||||
import { createStore } from "./lib/rateLimitStore";
|
||||
import { createStore } from "#dynamic/lib/rateLimitStore";
|
||||
import { stripDuplicateSesions } from "./middlewares/stripDuplicateSessions";
|
||||
import { corsWithLoginPageSupport } from "@server/lib/corsWithLoginPage";
|
||||
import { hybridRouter } from "#dynamic/routers/hybrid";
|
||||
import { billingWebhookHandler } from "#dynamic/routers/billing/webhooks";
|
||||
|
||||
const dev = config.isDev;
|
||||
const externalPort = config.getRawConfig().server.external_port;
|
||||
@@ -30,8 +35,15 @@ export function createApiServer() {
|
||||
apiServer.set("trust proxy", trustProxy);
|
||||
}
|
||||
|
||||
const corsConfig = config.getRawConfig().server.cors;
|
||||
if (build == "saas") {
|
||||
apiServer.post(
|
||||
`${prefix}/billing/webhooks`,
|
||||
express.raw({ type: "application/json" }),
|
||||
billingWebhookHandler
|
||||
);
|
||||
}
|
||||
|
||||
const corsConfig = config.getRawConfig().server.cors;
|
||||
const options = {
|
||||
...(corsConfig?.origins
|
||||
? { origin: corsConfig.origins }
|
||||
@@ -47,21 +59,32 @@ export function createApiServer() {
|
||||
credentials: !(corsConfig?.credentials === false)
|
||||
};
|
||||
|
||||
logger.debug("Using CORS options", options);
|
||||
|
||||
apiServer.use(cors(options));
|
||||
if (build == "oss" || !corsConfig) {
|
||||
logger.debug("Using CORS options", options);
|
||||
apiServer.use(cors(options));
|
||||
} else if (corsConfig) {
|
||||
// Use the custom CORS middleware with loginPage support
|
||||
apiServer.use(corsWithLoginPageSupport(corsConfig));
|
||||
}
|
||||
|
||||
if (!dev) {
|
||||
apiServer.use(helmet());
|
||||
apiServer.use(csrfProtectionMiddleware);
|
||||
}
|
||||
|
||||
apiServer.use(stripDuplicateSesions);
|
||||
apiServer.use(cookieParser());
|
||||
apiServer.use(express.json());
|
||||
|
||||
// Add request timeout middleware
|
||||
apiServer.use(requestTimeoutMiddleware(60000)); // 60 second timeout
|
||||
|
||||
apiServer.use(logIncomingMiddleware);
|
||||
|
||||
if (build !== "oss") {
|
||||
apiServer.use(`${prefix}/hybrid`, hybridRouter); // put before rate limiting because we will rate limit there separately because some of the routes are heavily used
|
||||
}
|
||||
|
||||
if (!dev) {
|
||||
apiServer.use(
|
||||
rateLimit({
|
||||
@@ -70,7 +93,8 @@ export function createApiServer() {
|
||||
60 *
|
||||
1000,
|
||||
max: config.getRawConfig().rate_limits.global.max_requests,
|
||||
keyGenerator: (req) => `apiServerGlobal:${ipKeyGenerator(req.ip || "")}:${req.path}`,
|
||||
keyGenerator: (req) =>
|
||||
`apiServerGlobal:${ipKeyGenerator(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(
|
||||
@@ -83,7 +107,6 @@ export function createApiServer() {
|
||||
}
|
||||
|
||||
// API routes
|
||||
apiServer.use(logIncomingMiddleware);
|
||||
apiServer.use(prefix, unauthenticated);
|
||||
apiServer.use(prefix, authenticated);
|
||||
|
||||
|
||||
@@ -19,6 +19,7 @@ export enum ActionsEnum {
|
||||
getSite = "getSite",
|
||||
listSites = "listSites",
|
||||
updateSite = "updateSite",
|
||||
reGenerateSecret = "reGenerateSecret",
|
||||
createResource = "createResource",
|
||||
deleteResource = "deleteResource",
|
||||
getResource = "getResource",
|
||||
@@ -60,6 +61,7 @@ export enum ActionsEnum {
|
||||
getUser = "getUser",
|
||||
setResourcePassword = "setResourcePassword",
|
||||
setResourcePincode = "setResourcePincode",
|
||||
setResourceHeaderAuth = "setResourceHeaderAuth",
|
||||
setResourceWhitelist = "setResourceWhitelist",
|
||||
getResourceWhitelist = "getResourceWhitelist",
|
||||
generateAccessToken = "generateAccessToken",
|
||||
@@ -80,7 +82,11 @@ export enum ActionsEnum {
|
||||
listClients = "listClients",
|
||||
getClient = "getClient",
|
||||
listOrgDomains = "listOrgDomains",
|
||||
getDomain = "getDomain",
|
||||
updateOrgDomain = "updateOrgDomain",
|
||||
getDNSRecords = "getDNSRecords",
|
||||
createNewt = "createNewt",
|
||||
createOlm = "createOlm",
|
||||
createIdp = "createIdp",
|
||||
updateIdp = "updateIdp",
|
||||
deleteIdp = "deleteIdp",
|
||||
@@ -98,9 +104,28 @@ export enum ActionsEnum {
|
||||
listApiKeyActions = "listApiKeyActions",
|
||||
listApiKeys = "listApiKeys",
|
||||
getApiKey = "getApiKey",
|
||||
getCertificate = "getCertificate",
|
||||
restartCertificate = "restartCertificate",
|
||||
billing = "billing",
|
||||
createOrgDomain = "createOrgDomain",
|
||||
deleteOrgDomain = "deleteOrgDomain",
|
||||
restartOrgDomain = "restartOrgDomain"
|
||||
restartOrgDomain = "restartOrgDomain",
|
||||
sendUsageNotification = "sendUsageNotification",
|
||||
createRemoteExitNode = "createRemoteExitNode",
|
||||
updateRemoteExitNode = "updateRemoteExitNode",
|
||||
getRemoteExitNode = "getRemoteExitNode",
|
||||
listRemoteExitNode = "listRemoteExitNode",
|
||||
deleteRemoteExitNode = "deleteRemoteExitNode",
|
||||
updateOrgUser = "updateOrgUser",
|
||||
createLoginPage = "createLoginPage",
|
||||
updateLoginPage = "updateLoginPage",
|
||||
getLoginPage = "getLoginPage",
|
||||
deleteLoginPage = "deleteLoginPage",
|
||||
listBlueprints = "listBlueprints",
|
||||
getBlueprint = "getBlueprint",
|
||||
applyBlueprint = "applyBlueprint",
|
||||
viewLogs = "viewLogs",
|
||||
exportLogs = "exportLogs"
|
||||
}
|
||||
|
||||
export async function checkUserActionPermission(
|
||||
@@ -177,8 +202,6 @@ export async function checkUserActionPermission(
|
||||
.limit(1);
|
||||
|
||||
return roleActionPermission.length > 0;
|
||||
|
||||
return false;
|
||||
} catch (error) {
|
||||
console.error("Error checking user action permission:", error);
|
||||
throw createHttpError(
|
||||
|
||||
@@ -3,13 +3,7 @@ import {
|
||||
encodeHexLowerCase
|
||||
} from "@oslojs/encoding";
|
||||
import { sha256 } from "@oslojs/crypto/sha2";
|
||||
import {
|
||||
resourceSessions,
|
||||
Session,
|
||||
sessions,
|
||||
User,
|
||||
users
|
||||
} from "@server/db";
|
||||
import { resourceSessions, Session, sessions, User, users } from "@server/db";
|
||||
import { db } from "@server/db";
|
||||
import { eq, inArray } from "drizzle-orm";
|
||||
import config from "@server/lib/config";
|
||||
@@ -24,8 +18,9 @@ export const SESSION_COOKIE_EXPIRES =
|
||||
60 *
|
||||
60 *
|
||||
config.getRawConfig().server.dashboard_session_length_hours;
|
||||
export const COOKIE_DOMAIN = config.getRawConfig().app.dashboard_url ?
|
||||
"." + new URL(config.getRawConfig().app.dashboard_url!).hostname : undefined;
|
||||
export const COOKIE_DOMAIN = config.getRawConfig().app.dashboard_url
|
||||
? new URL(config.getRawConfig().app.dashboard_url!).hostname
|
||||
: undefined;
|
||||
|
||||
export function generateSessionToken(): string {
|
||||
const bytes = new Uint8Array(20);
|
||||
@@ -41,12 +36,15 @@ export async function createSession(
|
||||
const sessionId = encodeHexLowerCase(
|
||||
sha256(new TextEncoder().encode(token))
|
||||
);
|
||||
const session: Session = {
|
||||
sessionId: sessionId,
|
||||
userId,
|
||||
expiresAt: new Date(Date.now() + SESSION_COOKIE_EXPIRES).getTime()
|
||||
};
|
||||
await db.insert(sessions).values(session);
|
||||
const [session] = await db
|
||||
.insert(sessions)
|
||||
.values({
|
||||
sessionId: sessionId,
|
||||
userId,
|
||||
expiresAt: new Date(Date.now() + SESSION_COOKIE_EXPIRES).getTime(),
|
||||
issuedAt: new Date().getTime()
|
||||
})
|
||||
.returning();
|
||||
return session;
|
||||
}
|
||||
|
||||
@@ -98,8 +96,8 @@ export async function invalidateSession(sessionId: string): Promise<void> {
|
||||
try {
|
||||
await db.transaction(async (trx) => {
|
||||
await trx
|
||||
.delete(resourceSessions)
|
||||
.where(eq(resourceSessions.userSessionId, sessionId));
|
||||
.delete(resourceSessions)
|
||||
.where(eq(resourceSessions.userSessionId, sessionId));
|
||||
await trx.delete(sessions).where(eq(sessions.sessionId, sessionId));
|
||||
});
|
||||
} catch (e) {
|
||||
@@ -111,9 +109,9 @@ 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));
|
||||
.select()
|
||||
.from(sessions)
|
||||
.where(eq(sessions.userId, userId));
|
||||
await trx.delete(resourceSessions).where(
|
||||
inArray(
|
||||
resourceSessions.userSessionId,
|
||||
|
||||
@@ -4,9 +4,6 @@ import { resourceSessions, ResourceSession } from "@server/db";
|
||||
import { db } from "@server/db";
|
||||
import { eq, and } from "drizzle-orm";
|
||||
import config from "@server/lib/config";
|
||||
import axios from "axios";
|
||||
import logger from "@server/logger";
|
||||
import { tokenManager } from "@server/lib/tokenManager";
|
||||
|
||||
export const SESSION_COOKIE_NAME =
|
||||
config.getRawConfig().server.session_cookie_name;
|
||||
@@ -53,7 +50,8 @@ export async function createResourceSession(opts: {
|
||||
doNotExtend: opts.doNotExtend || false,
|
||||
accessTokenId: opts.accessTokenId || null,
|
||||
isRequestToken: opts.isRequestToken || false,
|
||||
userSessionId: opts.userSessionId || null
|
||||
userSessionId: opts.userSessionId || null,
|
||||
issuedAt: new Date().getTime()
|
||||
};
|
||||
|
||||
await db.insert(resourceSessions).values(session);
|
||||
@@ -65,29 +63,6 @@ export async function validateResourceSessionToken(
|
||||
token: string,
|
||||
resourceId: number
|
||||
): Promise<ResourceSessionValidationResult> {
|
||||
if (config.isManagedMode()) {
|
||||
try {
|
||||
const response = await axios.post(`${config.getRawConfig().managed?.endpoint}/api/v1/hybrid/resource/${resourceId}/session/validate`, {
|
||||
token: token
|
||||
}, await tokenManager.getAuthHeader());
|
||||
return response.data.data;
|
||||
} catch (error) {
|
||||
if (axios.isAxiosError(error)) {
|
||||
logger.error("Error validating resource session token in hybrid mode:", {
|
||||
message: error.message,
|
||||
code: error.code,
|
||||
status: error.response?.status,
|
||||
statusText: error.response?.statusText,
|
||||
url: error.config?.url,
|
||||
method: error.config?.method
|
||||
});
|
||||
} else {
|
||||
logger.error("Error validating resource session token in hybrid mode:", error);
|
||||
}
|
||||
return { resourceSession: null };
|
||||
}
|
||||
}
|
||||
|
||||
const sessionId = encodeHexLowerCase(
|
||||
sha256(new TextEncoder().encode(token))
|
||||
);
|
||||
@@ -199,14 +174,14 @@ export function serializeResourceSessionCookie(
|
||||
const now = new Date().getTime();
|
||||
if (!isHttp) {
|
||||
if (expiresAt === undefined) {
|
||||
return `${cookieName}_s.${now}=${token}; HttpOnly; SameSite=Lax; Path=/; Secure; Domain=${"." + domain}`;
|
||||
return `${cookieName}_s.${now}=${token}; HttpOnly; SameSite=Lax; Path=/; Secure; Domain=${domain}`;
|
||||
}
|
||||
return `${cookieName}_s.${now}=${token}; HttpOnly; SameSite=Lax; Expires=${expiresAt.toUTCString()}; Path=/; Secure; Domain=${"." + domain}`;
|
||||
return `${cookieName}_s.${now}=${token}; HttpOnly; SameSite=Lax; Expires=${expiresAt.toUTCString()}; Path=/; Secure; Domain=${domain}`;
|
||||
} else {
|
||||
if (expiresAt === undefined) {
|
||||
return `${cookieName}.${now}=${token}; HttpOnly; SameSite=Lax; Path=/; Domain=${"." + domain}`;
|
||||
return `${cookieName}.${now}=${token}; HttpOnly; SameSite=Lax; Path=/; Domain=$domain}`;
|
||||
}
|
||||
return `${cookieName}.${now}=${token}; HttpOnly; SameSite=Lax; Expires=${expiresAt.toUTCString()}; Path=/; Domain=${"." + domain}`;
|
||||
return `${cookieName}.${now}=${token}; HttpOnly; SameSite=Lax; Expires=${expiresAt.toUTCString()}; Path=/; Domain=${domain}`;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -216,9 +191,9 @@ export function createBlankResourceSessionTokenCookie(
|
||||
isHttp: boolean = false
|
||||
): string {
|
||||
if (!isHttp) {
|
||||
return `${cookieName}_s=; HttpOnly; SameSite=Lax; Max-Age=0; Path=/; Secure; Domain=${"." + domain}`;
|
||||
return `${cookieName}_s=; HttpOnly; SameSite=Lax; Max-Age=0; Path=/; Secure; Domain=${domain}`;
|
||||
} else {
|
||||
return `${cookieName}=; HttpOnly; SameSite=Lax; Max-Age=0; Path=/; Domain=${"." + domain}`;
|
||||
return `${cookieName}=; HttpOnly; SameSite=Lax; Max-Age=0; Path=/; Domain=${domain}`;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,9 +1,43 @@
|
||||
import { Request } from "express";
|
||||
import { validateSessionToken, SESSION_COOKIE_NAME } from "@server/auth/sessions/app";
|
||||
import {
|
||||
validateSessionToken,
|
||||
SESSION_COOKIE_NAME
|
||||
} from "@server/auth/sessions/app";
|
||||
|
||||
export async function verifySession(req: Request) {
|
||||
export async function verifySession(req: Request, forceLogin?: boolean) {
|
||||
const res = await validateSessionToken(
|
||||
req.cookies[SESSION_COOKIE_NAME] ?? "",
|
||||
req.cookies[SESSION_COOKIE_NAME] ?? ""
|
||||
);
|
||||
|
||||
if (!forceLogin) {
|
||||
return res;
|
||||
}
|
||||
if (!res.session || !res.user) {
|
||||
return {
|
||||
session: null,
|
||||
user: null
|
||||
};
|
||||
}
|
||||
if (res.session.deviceAuthUsed) {
|
||||
return {
|
||||
session: null,
|
||||
user: null
|
||||
};
|
||||
}
|
||||
if (!res.session.issuedAt) {
|
||||
return {
|
||||
session: null,
|
||||
user: null
|
||||
};
|
||||
}
|
||||
const mins = 5 * 60 * 1000;
|
||||
const now = new Date().getTime();
|
||||
if (now - res.session.issuedAt > mins) {
|
||||
return {
|
||||
session: null,
|
||||
user: null
|
||||
};
|
||||
}
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
export const build = "oss" as any;
|
||||
13
server/cleanup.ts
Normal file
13
server/cleanup.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import { cleanup as wsCleanup } from "@server/routers/ws";
|
||||
|
||||
async function cleanup() {
|
||||
await wsCleanup();
|
||||
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
export async function initCleanup() {
|
||||
// Handle process termination
|
||||
process.on("SIGTERM", () => cleanup());
|
||||
process.on("SIGINT", () => cleanup());
|
||||
}
|
||||
1014
server/db/countries.ts
Normal file
1014
server/db/countries.ts
Normal file
File diff suppressed because it is too large
Load Diff
13
server/db/maxmind.ts
Normal file
13
server/db/maxmind.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import maxmind, { CountryResponse, Reader } from "maxmind";
|
||||
import config from "@server/lib/config";
|
||||
|
||||
let maxmindLookup: Reader<CountryResponse> | null;
|
||||
if (config.getRawConfig().server.maxmind_db_path) {
|
||||
maxmindLookup = await maxmind.open<CountryResponse>(
|
||||
config.getRawConfig().server.maxmind_db_path!
|
||||
);
|
||||
} else {
|
||||
maxmindLookup = null;
|
||||
}
|
||||
|
||||
export { maxmindLookup };
|
||||
@@ -1,6 +1,6 @@
|
||||
import { join } from "path";
|
||||
import { readFileSync } from "fs";
|
||||
import { db } from "@server/db";
|
||||
import { db, resources, siteResources } from "@server/db";
|
||||
import { exitNodes, sites } from "@server/db";
|
||||
import { eq, and } from "drizzle-orm";
|
||||
import { __DIRNAME } from "@server/lib/consts";
|
||||
@@ -34,6 +34,56 @@ export async function getUniqueSiteName(orgId: string): Promise<string> {
|
||||
}
|
||||
}
|
||||
|
||||
export async function getUniqueResourceName(orgId: string): Promise<string> {
|
||||
let loops = 0;
|
||||
while (true) {
|
||||
if (loops > 100) {
|
||||
throw new Error("Could not generate a unique name");
|
||||
}
|
||||
|
||||
const name = generateName();
|
||||
const [resourceCount, siteResourceCount] = await Promise.all([
|
||||
db
|
||||
.select({ niceId: resources.niceId, orgId: resources.orgId })
|
||||
.from(resources)
|
||||
.where(and(eq(resources.niceId, name), eq(resources.orgId, orgId))),
|
||||
db
|
||||
.select({ niceId: siteResources.niceId, orgId: siteResources.orgId })
|
||||
.from(siteResources)
|
||||
.where(and(eq(siteResources.niceId, name), eq(siteResources.orgId, orgId)))
|
||||
]);
|
||||
if (resourceCount.length === 0 && siteResourceCount.length === 0) {
|
||||
return name;
|
||||
}
|
||||
loops++;
|
||||
}
|
||||
}
|
||||
|
||||
export async function getUniqueSiteResourceName(orgId: string): Promise<string> {
|
||||
let loops = 0;
|
||||
while (true) {
|
||||
if (loops > 100) {
|
||||
throw new Error("Could not generate a unique name");
|
||||
}
|
||||
|
||||
const name = generateName();
|
||||
const [resourceCount, siteResourceCount] = await Promise.all([
|
||||
db
|
||||
.select({ niceId: resources.niceId, orgId: resources.orgId })
|
||||
.from(resources)
|
||||
.where(and(eq(resources.niceId, name), eq(resources.orgId, orgId))),
|
||||
db
|
||||
.select({ niceId: siteResources.niceId, orgId: siteResources.orgId })
|
||||
.from(siteResources)
|
||||
.where(and(eq(siteResources.niceId, name), eq(siteResources.orgId, orgId)))
|
||||
]);
|
||||
if (resourceCount.length === 0 && siteResourceCount.length === 0) {
|
||||
return name;
|
||||
}
|
||||
loops++;
|
||||
}
|
||||
}
|
||||
|
||||
export async function getUniqueExitNodeEndpointName(): Promise<string> {
|
||||
let loops = 0;
|
||||
const count = await db
|
||||
|
||||
@@ -7,9 +7,25 @@ function createDb() {
|
||||
const config = readConfigFile();
|
||||
|
||||
if (!config.postgres) {
|
||||
throw new Error(
|
||||
"Postgres configuration is missing in the configuration file."
|
||||
);
|
||||
// check the environment variables for postgres config
|
||||
if (process.env.POSTGRES_CONNECTION_STRING) {
|
||||
config.postgres = {
|
||||
connection_string: process.env.POSTGRES_CONNECTION_STRING
|
||||
};
|
||||
if (process.env.POSTGRES_REPLICA_CONNECTION_STRINGS) {
|
||||
const replicas =
|
||||
process.env.POSTGRES_REPLICA_CONNECTION_STRINGS.split(
|
||||
","
|
||||
).map((conn) => ({
|
||||
connection_string: conn.trim()
|
||||
}));
|
||||
config.postgres.replicas = replicas;
|
||||
}
|
||||
} else {
|
||||
throw new Error(
|
||||
"Postgres configuration is missing in the configuration file."
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const connectionString = config.postgres?.connection_string;
|
||||
@@ -22,31 +38,49 @@ function createDb() {
|
||||
}
|
||||
|
||||
// Create connection pools instead of individual connections
|
||||
const poolConfig = config.postgres.pool;
|
||||
const primaryPool = new Pool({
|
||||
connectionString,
|
||||
max: 20,
|
||||
idleTimeoutMillis: 30000,
|
||||
connectionTimeoutMillis: 2000,
|
||||
max: poolConfig?.max_connections || 20,
|
||||
idleTimeoutMillis: poolConfig?.idle_timeout_ms || 30000,
|
||||
connectionTimeoutMillis: poolConfig?.connection_timeout_ms || 5000
|
||||
});
|
||||
|
||||
const replicas = [];
|
||||
|
||||
if (!replicaConnections.length) {
|
||||
replicas.push(DrizzlePostgres(primaryPool));
|
||||
replicas.push(
|
||||
DrizzlePostgres(primaryPool, {
|
||||
logger: process.env.NODE_ENV === "development"
|
||||
})
|
||||
);
|
||||
} else {
|
||||
for (const conn of replicaConnections) {
|
||||
const replicaPool = new Pool({
|
||||
connectionString: conn.connection_string,
|
||||
max: 10,
|
||||
idleTimeoutMillis: 30000,
|
||||
connectionTimeoutMillis: 2000,
|
||||
max: poolConfig?.max_replica_connections || 20,
|
||||
idleTimeoutMillis: poolConfig?.idle_timeout_ms || 30000,
|
||||
connectionTimeoutMillis:
|
||||
poolConfig?.connection_timeout_ms || 5000
|
||||
});
|
||||
replicas.push(DrizzlePostgres(replicaPool));
|
||||
replicas.push(
|
||||
DrizzlePostgres(replicaPool, {
|
||||
logger: process.env.NODE_ENV === "development"
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return withReplicas(DrizzlePostgres(primaryPool), replicas as any);
|
||||
return withReplicas(
|
||||
DrizzlePostgres(primaryPool, {
|
||||
logger: process.env.QUERY_LOGGING === "true"
|
||||
}),
|
||||
replicas as any
|
||||
);
|
||||
}
|
||||
|
||||
export const db = createDb();
|
||||
export default db;
|
||||
export type Transaction = Parameters<
|
||||
Parameters<(typeof db)["transaction"]>[0]
|
||||
>[0];
|
||||
|
||||
@@ -1,2 +1,3 @@
|
||||
export * from "./driver";
|
||||
export * from "./schema";
|
||||
export * from "./schema/schema";
|
||||
export * from "./schema/privateSchema";
|
||||
|
||||
@@ -11,6 +11,7 @@ const runMigrations = async () => {
|
||||
migrationsFolder: migrationsFolder
|
||||
});
|
||||
console.log("Migrations completed successfully.");
|
||||
process.exit(0);
|
||||
} catch (error) {
|
||||
console.error("Error running migrations:", error);
|
||||
process.exit(1);
|
||||
|
||||
273
server/db/pg/schema/privateSchema.ts
Normal file
273
server/db/pg/schema/privateSchema.ts
Normal file
@@ -0,0 +1,273 @@
|
||||
import {
|
||||
pgTable,
|
||||
serial,
|
||||
varchar,
|
||||
boolean,
|
||||
integer,
|
||||
bigint,
|
||||
real,
|
||||
text,
|
||||
index
|
||||
} from "drizzle-orm/pg-core";
|
||||
import { InferSelectModel } from "drizzle-orm";
|
||||
import { domains, orgs, targets, users, exitNodes, sessions } from "./schema";
|
||||
|
||||
export const certificates = pgTable("certificates", {
|
||||
certId: serial("certId").primaryKey(),
|
||||
domain: varchar("domain", { length: 255 }).notNull().unique(),
|
||||
domainId: varchar("domainId").references(() => domains.domainId, {
|
||||
onDelete: "cascade"
|
||||
}),
|
||||
wildcard: boolean("wildcard").default(false),
|
||||
status: varchar("status", { length: 50 }).notNull().default("pending"), // pending, requested, valid, expired, failed
|
||||
expiresAt: bigint("expiresAt", { mode: "number" }),
|
||||
lastRenewalAttempt: bigint("lastRenewalAttempt", { mode: "number" }),
|
||||
createdAt: bigint("createdAt", { mode: "number" }).notNull(),
|
||||
updatedAt: bigint("updatedAt", { mode: "number" }).notNull(),
|
||||
orderId: varchar("orderId", { length: 500 }),
|
||||
errorMessage: text("errorMessage"),
|
||||
renewalCount: integer("renewalCount").default(0),
|
||||
certFile: text("certFile"),
|
||||
keyFile: text("keyFile")
|
||||
});
|
||||
|
||||
export const dnsChallenge = pgTable("dnsChallenges", {
|
||||
dnsChallengeId: serial("dnsChallengeId").primaryKey(),
|
||||
domain: varchar("domain", { length: 255 }).notNull(),
|
||||
token: varchar("token", { length: 255 }).notNull(),
|
||||
keyAuthorization: varchar("keyAuthorization", { length: 1000 }).notNull(),
|
||||
createdAt: bigint("createdAt", { mode: "number" }).notNull(),
|
||||
expiresAt: bigint("expiresAt", { mode: "number" }).notNull(),
|
||||
completed: boolean("completed").default(false)
|
||||
});
|
||||
|
||||
export const account = pgTable("account", {
|
||||
accountId: serial("accountId").primaryKey(),
|
||||
userId: varchar("userId")
|
||||
.notNull()
|
||||
.references(() => users.userId, { onDelete: "cascade" })
|
||||
});
|
||||
|
||||
export const customers = pgTable("customers", {
|
||||
customerId: varchar("customerId", { length: 255 }).primaryKey().notNull(),
|
||||
orgId: varchar("orgId", { length: 255 })
|
||||
.notNull()
|
||||
.references(() => orgs.orgId, { onDelete: "cascade" }),
|
||||
// accountId: integer("accountId")
|
||||
// .references(() => account.accountId, { onDelete: "cascade" }), // Optional, if using accounts
|
||||
email: varchar("email", { length: 255 }),
|
||||
name: varchar("name", { length: 255 }),
|
||||
phone: varchar("phone", { length: 50 }),
|
||||
address: text("address"),
|
||||
createdAt: bigint("createdAt", { mode: "number" }).notNull(),
|
||||
updatedAt: bigint("updatedAt", { mode: "number" }).notNull()
|
||||
});
|
||||
|
||||
export const subscriptions = pgTable("subscriptions", {
|
||||
subscriptionId: varchar("subscriptionId", { length: 255 })
|
||||
.primaryKey()
|
||||
.notNull(),
|
||||
customerId: varchar("customerId", { length: 255 })
|
||||
.notNull()
|
||||
.references(() => customers.customerId, { onDelete: "cascade" }),
|
||||
status: varchar("status", { length: 50 }).notNull().default("active"), // active, past_due, canceled, unpaid
|
||||
canceledAt: bigint("canceledAt", { mode: "number" }),
|
||||
createdAt: bigint("createdAt", { mode: "number" }).notNull(),
|
||||
updatedAt: bigint("updatedAt", { mode: "number" }),
|
||||
billingCycleAnchor: bigint("billingCycleAnchor", { mode: "number" })
|
||||
});
|
||||
|
||||
export const subscriptionItems = pgTable("subscriptionItems", {
|
||||
subscriptionItemId: serial("subscriptionItemId").primaryKey(),
|
||||
subscriptionId: varchar("subscriptionId", { length: 255 })
|
||||
.notNull()
|
||||
.references(() => subscriptions.subscriptionId, {
|
||||
onDelete: "cascade"
|
||||
}),
|
||||
planId: varchar("planId", { length: 255 }).notNull(),
|
||||
priceId: varchar("priceId", { length: 255 }),
|
||||
meterId: varchar("meterId", { length: 255 }),
|
||||
unitAmount: real("unitAmount"),
|
||||
tiers: text("tiers"),
|
||||
interval: varchar("interval", { length: 50 }),
|
||||
currentPeriodStart: bigint("currentPeriodStart", { mode: "number" }),
|
||||
currentPeriodEnd: bigint("currentPeriodEnd", { mode: "number" }),
|
||||
name: varchar("name", { length: 255 })
|
||||
});
|
||||
|
||||
export const accountDomains = pgTable("accountDomains", {
|
||||
accountId: integer("accountId")
|
||||
.notNull()
|
||||
.references(() => account.accountId, { onDelete: "cascade" }),
|
||||
domainId: varchar("domainId")
|
||||
.notNull()
|
||||
.references(() => domains.domainId, { onDelete: "cascade" })
|
||||
});
|
||||
|
||||
export const usage = pgTable("usage", {
|
||||
usageId: varchar("usageId", { length: 255 }).primaryKey(),
|
||||
featureId: varchar("featureId", { length: 255 }).notNull(),
|
||||
orgId: varchar("orgId")
|
||||
.references(() => orgs.orgId, { onDelete: "cascade" })
|
||||
.notNull(),
|
||||
meterId: varchar("meterId", { length: 255 }),
|
||||
instantaneousValue: real("instantaneousValue"),
|
||||
latestValue: real("latestValue").notNull(),
|
||||
previousValue: real("previousValue"),
|
||||
updatedAt: bigint("updatedAt", { mode: "number" }).notNull(),
|
||||
rolledOverAt: bigint("rolledOverAt", { mode: "number" }),
|
||||
nextRolloverAt: bigint("nextRolloverAt", { mode: "number" })
|
||||
});
|
||||
|
||||
export const limits = pgTable("limits", {
|
||||
limitId: varchar("limitId", { length: 255 }).primaryKey(),
|
||||
featureId: varchar("featureId", { length: 255 }).notNull(),
|
||||
orgId: varchar("orgId")
|
||||
.references(() => orgs.orgId, {
|
||||
onDelete: "cascade"
|
||||
})
|
||||
.notNull(),
|
||||
value: real("value"),
|
||||
description: text("description")
|
||||
});
|
||||
|
||||
export const usageNotifications = pgTable("usageNotifications", {
|
||||
notificationId: serial("notificationId").primaryKey(),
|
||||
orgId: varchar("orgId")
|
||||
.notNull()
|
||||
.references(() => orgs.orgId, { onDelete: "cascade" }),
|
||||
featureId: varchar("featureId", { length: 255 }).notNull(),
|
||||
limitId: varchar("limitId", { length: 255 }).notNull(),
|
||||
notificationType: varchar("notificationType", { length: 50 }).notNull(),
|
||||
sentAt: bigint("sentAt", { mode: "number" }).notNull()
|
||||
});
|
||||
|
||||
export const domainNamespaces = pgTable("domainNamespaces", {
|
||||
domainNamespaceId: varchar("domainNamespaceId", {
|
||||
length: 255
|
||||
}).primaryKey(),
|
||||
domainId: varchar("domainId")
|
||||
.references(() => domains.domainId, {
|
||||
onDelete: "set null"
|
||||
})
|
||||
.notNull()
|
||||
});
|
||||
|
||||
export const exitNodeOrgs = pgTable("exitNodeOrgs", {
|
||||
exitNodeId: integer("exitNodeId")
|
||||
.notNull()
|
||||
.references(() => exitNodes.exitNodeId, { onDelete: "cascade" }),
|
||||
orgId: text("orgId")
|
||||
.notNull()
|
||||
.references(() => orgs.orgId, { onDelete: "cascade" })
|
||||
});
|
||||
|
||||
export const remoteExitNodes = pgTable("remoteExitNode", {
|
||||
remoteExitNodeId: varchar("id").primaryKey(),
|
||||
secretHash: varchar("secretHash").notNull(),
|
||||
dateCreated: varchar("dateCreated").notNull(),
|
||||
version: varchar("version"),
|
||||
secondaryVersion: varchar("secondaryVersion"), // This is to detect the new nodes after the transition to pangolin-node
|
||||
exitNodeId: integer("exitNodeId").references(() => exitNodes.exitNodeId, {
|
||||
onDelete: "cascade"
|
||||
})
|
||||
});
|
||||
|
||||
export const remoteExitNodeSessions = pgTable("remoteExitNodeSession", {
|
||||
sessionId: varchar("id").primaryKey(),
|
||||
remoteExitNodeId: varchar("remoteExitNodeId")
|
||||
.notNull()
|
||||
.references(() => remoteExitNodes.remoteExitNodeId, {
|
||||
onDelete: "cascade"
|
||||
}),
|
||||
expiresAt: bigint("expiresAt", { mode: "number" }).notNull()
|
||||
});
|
||||
|
||||
export const loginPage = pgTable("loginPage", {
|
||||
loginPageId: serial("loginPageId").primaryKey(),
|
||||
subdomain: varchar("subdomain"),
|
||||
fullDomain: varchar("fullDomain"),
|
||||
exitNodeId: integer("exitNodeId").references(() => exitNodes.exitNodeId, {
|
||||
onDelete: "set null"
|
||||
}),
|
||||
domainId: varchar("domainId").references(() => domains.domainId, {
|
||||
onDelete: "set null"
|
||||
})
|
||||
});
|
||||
|
||||
export const loginPageOrg = pgTable("loginPageOrg", {
|
||||
loginPageId: integer("loginPageId")
|
||||
.notNull()
|
||||
.references(() => loginPage.loginPageId, { onDelete: "cascade" }),
|
||||
orgId: varchar("orgId")
|
||||
.notNull()
|
||||
.references(() => orgs.orgId, { onDelete: "cascade" })
|
||||
});
|
||||
|
||||
export const sessionTransferToken = pgTable("sessionTransferToken", {
|
||||
token: varchar("token").primaryKey(),
|
||||
sessionId: varchar("sessionId")
|
||||
.notNull()
|
||||
.references(() => sessions.sessionId, {
|
||||
onDelete: "cascade"
|
||||
}),
|
||||
encryptedSession: text("encryptedSession").notNull(),
|
||||
expiresAt: bigint("expiresAt", { mode: "number" }).notNull()
|
||||
});
|
||||
|
||||
export const actionAuditLog = pgTable("actionAuditLog", {
|
||||
id: serial("id").primaryKey(),
|
||||
timestamp: bigint("timestamp", { mode: "number" }).notNull(), // this is EPOCH time in seconds
|
||||
orgId: varchar("orgId")
|
||||
.notNull()
|
||||
.references(() => orgs.orgId, { onDelete: "cascade" }),
|
||||
actorType: varchar("actorType", { length: 50 }).notNull(),
|
||||
actor: varchar("actor", { length: 255 }).notNull(),
|
||||
actorId: varchar("actorId", { length: 255 }).notNull(),
|
||||
action: varchar("action", { length: 100 }).notNull(),
|
||||
metadata: text("metadata")
|
||||
}, (table) => ([
|
||||
index("idx_actionAuditLog_timestamp").on(table.timestamp),
|
||||
index("idx_actionAuditLog_org_timestamp").on(table.orgId, table.timestamp)
|
||||
]));
|
||||
|
||||
export const accessAuditLog = pgTable("accessAuditLog", {
|
||||
id: serial("id").primaryKey(),
|
||||
timestamp: bigint("timestamp", { mode: "number" }).notNull(), // this is EPOCH time in seconds
|
||||
orgId: varchar("orgId")
|
||||
.notNull()
|
||||
.references(() => orgs.orgId, { onDelete: "cascade" }),
|
||||
actorType: varchar("actorType", { length: 50 }),
|
||||
actor: varchar("actor", { length: 255 }),
|
||||
actorId: varchar("actorId", { length: 255 }),
|
||||
resourceId: integer("resourceId"),
|
||||
ip: varchar("ip", { length: 45 }),
|
||||
type: varchar("type", { length: 100 }).notNull(),
|
||||
action: boolean("action").notNull(),
|
||||
location: text("location"),
|
||||
userAgent: text("userAgent"),
|
||||
metadata: text("metadata")
|
||||
}, (table) => ([
|
||||
index("idx_identityAuditLog_timestamp").on(table.timestamp),
|
||||
index("idx_identityAuditLog_org_timestamp").on(table.orgId, table.timestamp)
|
||||
]));
|
||||
|
||||
export type Limit = InferSelectModel<typeof limits>;
|
||||
export type Account = InferSelectModel<typeof account>;
|
||||
export type Certificate = InferSelectModel<typeof certificates>;
|
||||
export type DnsChallenge = InferSelectModel<typeof dnsChallenge>;
|
||||
export type Customer = InferSelectModel<typeof customers>;
|
||||
export type Subscription = InferSelectModel<typeof subscriptions>;
|
||||
export type SubscriptionItem = InferSelectModel<typeof subscriptionItems>;
|
||||
export type Usage = InferSelectModel<typeof usage>;
|
||||
export type UsageLimit = InferSelectModel<typeof limits>;
|
||||
export type AccountDomain = InferSelectModel<typeof accountDomains>;
|
||||
export type UsageNotification = InferSelectModel<typeof usageNotifications>;
|
||||
export type RemoteExitNode = InferSelectModel<typeof remoteExitNodes>;
|
||||
export type RemoteExitNodeSession = InferSelectModel<
|
||||
typeof remoteExitNodeSessions
|
||||
>;
|
||||
export type ExitNodeOrg = InferSelectModel<typeof exitNodeOrgs>;
|
||||
export type LoginPage = InferSelectModel<typeof loginPage>;
|
||||
export type ActionAuditLog = InferSelectModel<typeof actionAuditLog>;
|
||||
export type AccessAuditLog = InferSelectModel<typeof accessAuditLog>;
|
||||
@@ -6,9 +6,12 @@ import {
|
||||
integer,
|
||||
bigint,
|
||||
real,
|
||||
text
|
||||
text,
|
||||
index
|
||||
} from "drizzle-orm/pg-core";
|
||||
import { InferSelectModel } from "drizzle-orm";
|
||||
import { randomUUID } from "crypto";
|
||||
import { alias } from "yargs";
|
||||
|
||||
export const domains = pgTable("domains", {
|
||||
domainId: varchar("domainId").primaryKey(),
|
||||
@@ -17,14 +20,41 @@ export const domains = pgTable("domains", {
|
||||
type: varchar("type"), // "ns", "cname", "wildcard"
|
||||
verified: boolean("verified").notNull().default(false),
|
||||
failed: boolean("failed").notNull().default(false),
|
||||
tries: integer("tries").notNull().default(0)
|
||||
tries: integer("tries").notNull().default(0),
|
||||
certResolver: varchar("certResolver"),
|
||||
customCertResolver: varchar("customCertResolver"),
|
||||
preferWildcardCert: boolean("preferWildcardCert")
|
||||
});
|
||||
|
||||
export const dnsRecords = pgTable("dnsRecords", {
|
||||
id: serial("id").primaryKey(),
|
||||
domainId: varchar("domainId")
|
||||
.notNull()
|
||||
.references(() => domains.domainId, { onDelete: "cascade" }),
|
||||
recordType: varchar("recordType").notNull(), // "NS" | "CNAME" | "A" | "TXT"
|
||||
baseDomain: varchar("baseDomain"),
|
||||
value: varchar("value").notNull(),
|
||||
verified: boolean("verified").notNull().default(false)
|
||||
});
|
||||
|
||||
export const orgs = pgTable("orgs", {
|
||||
orgId: varchar("orgId").primaryKey(),
|
||||
name: varchar("name").notNull(),
|
||||
subnet: varchar("subnet"),
|
||||
createdAt: text("createdAt")
|
||||
utilitySubnet: varchar("utilitySubnet"), // this is the subnet for utility addresses
|
||||
createdAt: text("createdAt"),
|
||||
requireTwoFactor: boolean("requireTwoFactor"),
|
||||
maxSessionLengthHours: integer("maxSessionLengthHours"),
|
||||
passwordExpiryDays: integer("passwordExpiryDays"),
|
||||
settingsLogRetentionDaysRequest: integer("settingsLogRetentionDaysRequest") // where 0 = dont keep logs and -1 = keep forever
|
||||
.notNull()
|
||||
.default(7),
|
||||
settingsLogRetentionDaysAccess: integer("settingsLogRetentionDaysAccess")
|
||||
.notNull()
|
||||
.default(0),
|
||||
settingsLogRetentionDaysAction: integer("settingsLogRetentionDaysAction")
|
||||
.notNull()
|
||||
.default(0)
|
||||
});
|
||||
|
||||
export const orgDomains = pgTable("orgDomains", {
|
||||
@@ -60,17 +90,21 @@ export const sites = pgTable("sites", {
|
||||
publicKey: varchar("publicKey"),
|
||||
lastHolePunch: bigint("lastHolePunch", { mode: "number" }),
|
||||
listenPort: integer("listenPort"),
|
||||
dockerSocketEnabled: boolean("dockerSocketEnabled").notNull().default(true),
|
||||
remoteSubnets: text("remoteSubnets") // comma-separated list of subnets that this site can access
|
||||
dockerSocketEnabled: boolean("dockerSocketEnabled").notNull().default(true)
|
||||
});
|
||||
|
||||
export const resources = pgTable("resources", {
|
||||
resourceId: serial("resourceId").primaryKey(),
|
||||
resourceGuid: varchar("resourceGuid", { length: 36 })
|
||||
.unique()
|
||||
.notNull()
|
||||
.$defaultFn(() => randomUUID()),
|
||||
orgId: varchar("orgId")
|
||||
.references(() => orgs.orgId, {
|
||||
onDelete: "cascade"
|
||||
})
|
||||
.notNull(),
|
||||
niceId: text("niceId").notNull(),
|
||||
name: varchar("name").notNull(),
|
||||
subdomain: varchar("subdomain"),
|
||||
fullDomain: varchar("fullDomain"),
|
||||
@@ -93,8 +127,11 @@ export const resources = pgTable("resources", {
|
||||
setHostHeader: varchar("setHostHeader"),
|
||||
enableProxy: boolean("enableProxy").default(true),
|
||||
skipToIdpId: integer("skipToIdpId").references(() => idp.idpId, {
|
||||
onDelete: "cascade"
|
||||
onDelete: "set null"
|
||||
}),
|
||||
headers: text("headers"), // comma-separated list of headers to add to the request
|
||||
proxyProtocol: boolean("proxyProtocol").notNull().default(false),
|
||||
proxyProtocolVersion: integer("proxyProtocolVersion").default(1)
|
||||
});
|
||||
|
||||
export const targets = pgTable("targets", {
|
||||
@@ -113,7 +150,34 @@ export const targets = pgTable("targets", {
|
||||
method: varchar("method"),
|
||||
port: integer("port").notNull(),
|
||||
internalPort: integer("internalPort"),
|
||||
enabled: boolean("enabled").notNull().default(true)
|
||||
enabled: boolean("enabled").notNull().default(true),
|
||||
path: text("path"),
|
||||
pathMatchType: text("pathMatchType"), // exact, prefix, regex
|
||||
rewritePath: text("rewritePath"), // if set, rewrites the path to this value before sending to the target
|
||||
rewritePathType: text("rewritePathType"), // exact, prefix, regex, stripPrefix
|
||||
priority: integer("priority").notNull().default(100)
|
||||
});
|
||||
|
||||
export const targetHealthCheck = pgTable("targetHealthCheck", {
|
||||
targetHealthCheckId: serial("targetHealthCheckId").primaryKey(),
|
||||
targetId: integer("targetId")
|
||||
.notNull()
|
||||
.references(() => targets.targetId, { onDelete: "cascade" }),
|
||||
hcEnabled: boolean("hcEnabled").notNull().default(false),
|
||||
hcPath: varchar("hcPath"),
|
||||
hcScheme: varchar("hcScheme"),
|
||||
hcMode: varchar("hcMode").default("http"),
|
||||
hcHostname: varchar("hcHostname"),
|
||||
hcPort: integer("hcPort"),
|
||||
hcInterval: integer("hcInterval").default(30), // in seconds
|
||||
hcUnhealthyInterval: integer("hcUnhealthyInterval").default(30), // in seconds
|
||||
hcTimeout: integer("hcTimeout").default(5), // in seconds
|
||||
hcHeaders: varchar("hcHeaders"),
|
||||
hcFollowRedirects: boolean("hcFollowRedirects").default(true),
|
||||
hcMethod: varchar("hcMethod").default("GET"),
|
||||
hcStatus: integer("hcStatus"), // http code
|
||||
hcHealth: text("hcHealth").default("unknown"), // "unknown", "healthy", "unhealthy"
|
||||
hcTlsServerName: text("hcTlsServerName"),
|
||||
});
|
||||
|
||||
export const exitNodes = pgTable("exitNodes", {
|
||||
@@ -127,10 +191,12 @@ export const exitNodes = pgTable("exitNodes", {
|
||||
maxConnections: integer("maxConnections"),
|
||||
online: boolean("online").notNull().default(false),
|
||||
lastPing: integer("lastPing"),
|
||||
type: text("type").default("gerbil") // gerbil, remoteExitNode
|
||||
type: text("type").default("gerbil"), // gerbil, remoteExitNode
|
||||
region: varchar("region")
|
||||
});
|
||||
|
||||
export const siteResources = pgTable("siteResources", { // this is for the clients
|
||||
export const siteResources = pgTable("siteResources", {
|
||||
// this is for the clients
|
||||
siteResourceId: serial("siteResourceId").primaryKey(),
|
||||
siteId: integer("siteId")
|
||||
.notNull()
|
||||
@@ -138,12 +204,43 @@ export const siteResources = pgTable("siteResources", { // this is for the clien
|
||||
orgId: varchar("orgId")
|
||||
.notNull()
|
||||
.references(() => orgs.orgId, { onDelete: "cascade" }),
|
||||
niceId: varchar("niceId").notNull(),
|
||||
name: varchar("name").notNull(),
|
||||
protocol: varchar("protocol").notNull(),
|
||||
proxyPort: integer("proxyPort").notNull(),
|
||||
destinationPort: integer("destinationPort").notNull(),
|
||||
destinationIp: varchar("destinationIp").notNull(),
|
||||
mode: varchar("mode").notNull(), // "host" | "cidr" | "port"
|
||||
protocol: varchar("protocol"), // only for port mode
|
||||
proxyPort: integer("proxyPort"), // only for port mode
|
||||
destinationPort: integer("destinationPort"), // only for port mode
|
||||
destination: varchar("destination").notNull(), // ip, cidr, hostname; validate against the mode
|
||||
enabled: boolean("enabled").notNull().default(true),
|
||||
alias: varchar("alias"),
|
||||
aliasAddress: varchar("aliasAddress")
|
||||
});
|
||||
|
||||
export const clientSiteResources = pgTable("clientSiteResources", {
|
||||
clientId: integer("clientId")
|
||||
.notNull()
|
||||
.references(() => clients.clientId, { onDelete: "cascade" }),
|
||||
siteResourceId: integer("siteResourceId")
|
||||
.notNull()
|
||||
.references(() => siteResources.siteResourceId, { onDelete: "cascade" })
|
||||
});
|
||||
|
||||
export const roleSiteResources = pgTable("roleSiteResources", {
|
||||
roleId: integer("roleId")
|
||||
.notNull()
|
||||
.references(() => roles.roleId, { onDelete: "cascade" }),
|
||||
siteResourceId: integer("siteResourceId")
|
||||
.notNull()
|
||||
.references(() => siteResources.siteResourceId, { onDelete: "cascade" })
|
||||
});
|
||||
|
||||
export const userSiteResources = pgTable("userSiteResources", {
|
||||
userId: varchar("userId")
|
||||
.notNull()
|
||||
.references(() => users.userId, { onDelete: "cascade" }),
|
||||
siteResourceId: integer("siteResourceId")
|
||||
.notNull()
|
||||
.references(() => siteResources.siteResourceId, { onDelete: "cascade" })
|
||||
});
|
||||
|
||||
export const users = pgTable("user", {
|
||||
@@ -163,7 +260,8 @@ export const users = pgTable("user", {
|
||||
dateCreated: varchar("dateCreated").notNull(),
|
||||
termsAcceptedTimestamp: varchar("termsAcceptedTimestamp"),
|
||||
termsVersion: varchar("termsVersion"),
|
||||
serverAdmin: boolean("serverAdmin").notNull().default(false)
|
||||
serverAdmin: boolean("serverAdmin").notNull().default(false),
|
||||
lastPasswordChange: bigint("lastPasswordChange", { mode: "number" })
|
||||
});
|
||||
|
||||
export const newts = pgTable("newt", {
|
||||
@@ -189,7 +287,9 @@ export const sessions = pgTable("session", {
|
||||
userId: varchar("userId")
|
||||
.notNull()
|
||||
.references(() => users.userId, { onDelete: "cascade" }),
|
||||
expiresAt: bigint("expiresAt", { mode: "number" }).notNull()
|
||||
expiresAt: bigint("expiresAt", { mode: "number" }).notNull(),
|
||||
issuedAt: bigint("issuedAt", { mode: "number" }),
|
||||
deviceAuthUsed: boolean("deviceAuthUsed").notNull().default(false)
|
||||
});
|
||||
|
||||
export const newtSessions = pgTable("newtSession", {
|
||||
@@ -212,7 +312,8 @@ export const userOrgs = pgTable("userOrgs", {
|
||||
roleId: integer("roleId")
|
||||
.notNull()
|
||||
.references(() => roles.roleId),
|
||||
isOwner: boolean("isOwner").notNull().default(false)
|
||||
isOwner: boolean("isOwner").notNull().default(false),
|
||||
autoProvisioned: boolean("autoProvisioned").default(false)
|
||||
});
|
||||
|
||||
export const emailVerificationCodes = pgTable("emailVerificationCodes", {
|
||||
@@ -343,6 +444,14 @@ export const resourcePassword = pgTable("resourcePassword", {
|
||||
passwordHash: varchar("passwordHash").notNull()
|
||||
});
|
||||
|
||||
export const resourceHeaderAuth = pgTable("resourceHeaderAuth", {
|
||||
headerAuthId: serial("headerAuthId").primaryKey(),
|
||||
resourceId: integer("resourceId")
|
||||
.notNull()
|
||||
.references(() => resources.resourceId, { onDelete: "cascade" }),
|
||||
headerAuthHash: varchar("headerAuthHash").notNull()
|
||||
});
|
||||
|
||||
export const resourceAccessToken = pgTable("resourceAccessToken", {
|
||||
accessTokenId: varchar("accessTokenId").primaryKey(),
|
||||
orgId: varchar("orgId")
|
||||
@@ -397,7 +506,8 @@ export const resourceSessions = pgTable("resourceSessions", {
|
||||
{
|
||||
onDelete: "cascade"
|
||||
}
|
||||
)
|
||||
),
|
||||
issuedAt: bigint("issuedAt", { mode: "number" })
|
||||
});
|
||||
|
||||
export const resourceWhitelist = pgTable("resourceWhitelist", {
|
||||
@@ -430,7 +540,7 @@ export const resourceRules = pgTable("resourceRules", {
|
||||
.references(() => resources.resourceId, { onDelete: "cascade" }),
|
||||
enabled: boolean("enabled").notNull().default(true),
|
||||
priority: integer("priority").notNull(),
|
||||
action: varchar("action").notNull(), // ACCEPT, DROP
|
||||
action: varchar("action").notNull(), // ACCEPT, DROP, PASS
|
||||
match: varchar("match").notNull(), // CIDR, PATH, IP
|
||||
value: varchar("value").notNull()
|
||||
});
|
||||
@@ -458,6 +568,7 @@ export const idpOidcConfig = pgTable("idpOidcConfig", {
|
||||
idpId: integer("idpId")
|
||||
.notNull()
|
||||
.references(() => idp.idpId, { onDelete: "cascade" }),
|
||||
variant: varchar("variant").notNull().default("oidc"),
|
||||
clientId: varchar("clientId").notNull(),
|
||||
clientSecret: varchar("clientSecret").notNull(),
|
||||
authUrl: varchar("authUrl").notNull(),
|
||||
@@ -520,7 +631,7 @@ export const idpOrg = pgTable("idpOrg", {
|
||||
});
|
||||
|
||||
export const clients = pgTable("clients", {
|
||||
clientId: serial("id").primaryKey(),
|
||||
clientId: serial("clientId").primaryKey(),
|
||||
orgId: varchar("orgId")
|
||||
.references(() => orgs.orgId, {
|
||||
onDelete: "cascade"
|
||||
@@ -529,6 +640,11 @@ export const clients = pgTable("clients", {
|
||||
exitNodeId: integer("exitNode").references(() => exitNodes.exitNodeId, {
|
||||
onDelete: "set null"
|
||||
}),
|
||||
userId: text("userId").references(() => users.userId, {
|
||||
// optionally tied to a user and in this case delete when the user deletes
|
||||
onDelete: "cascade"
|
||||
}),
|
||||
olmId: text("olmId"), // to lock it to a specific olm optionally
|
||||
name: varchar("name").notNull(),
|
||||
pubKey: varchar("pubKey"),
|
||||
subnet: varchar("subnet").notNull(),
|
||||
@@ -543,23 +659,40 @@ export const clients = pgTable("clients", {
|
||||
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),
|
||||
endpoint: varchar("endpoint")
|
||||
});
|
||||
export const clientSitesAssociationsCache = pgTable(
|
||||
"clientSitesAssociationsCache",
|
||||
{
|
||||
clientId: integer("clientId") // not a foreign key here so after its deleted the rebuild function can delete it and send the message
|
||||
.notNull(),
|
||||
siteId: integer("siteId").notNull(),
|
||||
isRelayed: boolean("isRelayed").notNull().default(false),
|
||||
endpoint: varchar("endpoint"),
|
||||
publicKey: varchar("publicKey") // this will act as the session's public key for hole punching so we can track when it changes
|
||||
}
|
||||
);
|
||||
|
||||
export const clientSiteResourcesAssociationsCache = pgTable(
|
||||
"clientSiteResourcesAssociationsCache",
|
||||
{
|
||||
clientId: integer("clientId") // not a foreign key here so after its deleted the rebuild function can delete it and send the message
|
||||
.notNull(),
|
||||
siteResourceId: integer("siteResourceId").notNull()
|
||||
}
|
||||
);
|
||||
|
||||
export const olms = pgTable("olms", {
|
||||
olmId: varchar("id").primaryKey(),
|
||||
secretHash: varchar("secretHash").notNull(),
|
||||
dateCreated: varchar("dateCreated").notNull(),
|
||||
version: text("version"),
|
||||
agent: text("agent"),
|
||||
name: varchar("name"),
|
||||
clientId: integer("clientId").references(() => clients.clientId, {
|
||||
// we will switch this depending on the current org it wants to connect to
|
||||
onDelete: "set null"
|
||||
}),
|
||||
userId: text("userId").references(() => users.userId, {
|
||||
// optionally tied to a user and in this case delete when the user deletes
|
||||
onDelete: "cascade"
|
||||
})
|
||||
});
|
||||
@@ -624,6 +757,72 @@ export const setupTokens = pgTable("setupTokens", {
|
||||
dateUsed: varchar("dateUsed")
|
||||
});
|
||||
|
||||
// Blueprint runs
|
||||
export const blueprints = pgTable("blueprints", {
|
||||
blueprintId: serial("blueprintId").primaryKey(),
|
||||
orgId: text("orgId")
|
||||
.references(() => orgs.orgId, {
|
||||
onDelete: "cascade"
|
||||
})
|
||||
.notNull(),
|
||||
name: varchar("name").notNull(),
|
||||
source: varchar("source").notNull(),
|
||||
createdAt: integer("createdAt").notNull(),
|
||||
succeeded: boolean("succeeded").notNull(),
|
||||
contents: text("contents").notNull(),
|
||||
message: text("message")
|
||||
});
|
||||
export const requestAuditLog = pgTable(
|
||||
"requestAuditLog",
|
||||
{
|
||||
id: serial("id").primaryKey(),
|
||||
timestamp: integer("timestamp").notNull(), // this is EPOCH time in seconds
|
||||
orgId: text("orgId").references(() => orgs.orgId, {
|
||||
onDelete: "cascade"
|
||||
}),
|
||||
action: boolean("action").notNull(),
|
||||
reason: integer("reason").notNull(),
|
||||
actorType: text("actorType"),
|
||||
actor: text("actor"),
|
||||
actorId: text("actorId"),
|
||||
resourceId: integer("resourceId"),
|
||||
ip: text("ip"),
|
||||
location: text("location"),
|
||||
userAgent: text("userAgent"),
|
||||
metadata: text("metadata"),
|
||||
headers: text("headers"), // JSON blob
|
||||
query: text("query"), // JSON blob
|
||||
originalRequestURL: text("originalRequestURL"),
|
||||
scheme: text("scheme"),
|
||||
host: text("host"),
|
||||
path: text("path"),
|
||||
method: text("method"),
|
||||
tls: boolean("tls")
|
||||
},
|
||||
(table) => [
|
||||
index("idx_requestAuditLog_timestamp").on(table.timestamp),
|
||||
index("idx_requestAuditLog_org_timestamp").on(
|
||||
table.orgId,
|
||||
table.timestamp
|
||||
)
|
||||
]
|
||||
);
|
||||
|
||||
export const deviceWebAuthCodes = pgTable("deviceWebAuthCodes", {
|
||||
codeId: serial("codeId").primaryKey(),
|
||||
code: text("code").notNull().unique(),
|
||||
ip: text("ip"),
|
||||
city: text("city"),
|
||||
deviceName: text("deviceName"),
|
||||
applicationName: text("applicationName").notNull(),
|
||||
expiresAt: bigint("expiresAt", { mode: "number" }).notNull(),
|
||||
createdAt: bigint("createdAt", { mode: "number" }).notNull(),
|
||||
verified: boolean("verified").notNull().default(false),
|
||||
userId: varchar("userId").references(() => users.userId, {
|
||||
onDelete: "cascade"
|
||||
})
|
||||
});
|
||||
|
||||
export type Org = InferSelectModel<typeof orgs>;
|
||||
export type User = InferSelectModel<typeof users>;
|
||||
export type Site = InferSelectModel<typeof sites>;
|
||||
@@ -651,6 +850,7 @@ export type UserOrg = InferSelectModel<typeof userOrgs>;
|
||||
export type ResourceSession = InferSelectModel<typeof resourceSessions>;
|
||||
export type ResourcePincode = InferSelectModel<typeof resourcePincode>;
|
||||
export type ResourcePassword = InferSelectModel<typeof resourcePassword>;
|
||||
export type ResourceHeaderAuth = InferSelectModel<typeof resourceHeaderAuth>;
|
||||
export type ResourceOtp = InferSelectModel<typeof resourceOtp>;
|
||||
export type ResourceAccessToken = InferSelectModel<typeof resourceAccessToken>;
|
||||
export type ResourceWhitelist = InferSelectModel<typeof resourceWhitelist>;
|
||||
@@ -663,7 +863,7 @@ 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 ClientSite = InferSelectModel<typeof clientSitesAssociationsCache>;
|
||||
export type Olm = InferSelectModel<typeof olms>;
|
||||
export type OlmSession = InferSelectModel<typeof olmSessions>;
|
||||
export type UserClient = InferSelectModel<typeof userClients>;
|
||||
@@ -672,3 +872,11 @@ export type OrgDomains = InferSelectModel<typeof orgDomains>;
|
||||
export type SiteResource = InferSelectModel<typeof siteResources>;
|
||||
export type SetupToken = InferSelectModel<typeof setupTokens>;
|
||||
export type HostMeta = InferSelectModel<typeof hostMeta>;
|
||||
export type TargetHealthCheck = InferSelectModel<typeof targetHealthCheck>;
|
||||
export type IdpOidcConfig = InferSelectModel<typeof idpOidcConfig>;
|
||||
export type Blueprint = InferSelectModel<typeof blueprints>;
|
||||
export type LicenseKey = InferSelectModel<typeof licenseKey>;
|
||||
export type SecurityKey = InferSelectModel<typeof securityKeys>;
|
||||
export type WebauthnChallenge = InferSelectModel<typeof webauthnChallenge>;
|
||||
export type DeviceWebAuthCode = InferSelectModel<typeof deviceWebAuthCodes>;
|
||||
export type RequestAuditLog = InferSelectModel<typeof requestAuditLog>;
|
||||
@@ -1,4 +1,4 @@
|
||||
import { db } from "@server/db";
|
||||
import { db, loginPage, LoginPage, loginPageOrg, Org, orgs } from "@server/db";
|
||||
import {
|
||||
Resource,
|
||||
ResourcePassword,
|
||||
@@ -6,6 +6,8 @@ import {
|
||||
ResourceRule,
|
||||
resourcePassword,
|
||||
resourcePincode,
|
||||
resourceHeaderAuth,
|
||||
ResourceHeaderAuth,
|
||||
resourceRules,
|
||||
resources,
|
||||
roleResources,
|
||||
@@ -15,15 +17,13 @@ import {
|
||||
users
|
||||
} from "@server/db";
|
||||
import { and, eq } from "drizzle-orm";
|
||||
import axios from "axios";
|
||||
import config from "@server/lib/config";
|
||||
import logger from "@server/logger";
|
||||
import { tokenManager } from "@server/lib/tokenManager";
|
||||
|
||||
export type ResourceWithAuth = {
|
||||
resource: Resource | null;
|
||||
pincode: ResourcePincode | null;
|
||||
password: ResourcePassword | null;
|
||||
headerAuth: ResourceHeaderAuth | null;
|
||||
org: Org;
|
||||
};
|
||||
|
||||
export type UserSessionWithUser = {
|
||||
@@ -37,27 +37,6 @@ export type UserSessionWithUser = {
|
||||
export async function getResourceByDomain(
|
||||
domain: string
|
||||
): Promise<ResourceWithAuth | null> {
|
||||
if (config.isManagedMode()) {
|
||||
try {
|
||||
const response = await axios.get(`${config.getRawConfig().managed?.endpoint}/api/v1/hybrid/resource/domain/${domain}`, await tokenManager.getAuthHeader());
|
||||
return response.data.data;
|
||||
} catch (error) {
|
||||
if (axios.isAxiosError(error)) {
|
||||
logger.error("Error fetching config in verify session:", {
|
||||
message: error.message,
|
||||
code: error.code,
|
||||
status: error.response?.status,
|
||||
statusText: error.response?.statusText,
|
||||
url: error.config?.url,
|
||||
method: error.config?.method
|
||||
});
|
||||
} else {
|
||||
logger.error("Error fetching config in verify session:", error);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
const [result] = await db
|
||||
.select()
|
||||
.from(resources)
|
||||
@@ -69,6 +48,14 @@ export async function getResourceByDomain(
|
||||
resourcePassword,
|
||||
eq(resourcePassword.resourceId, resources.resourceId)
|
||||
)
|
||||
.leftJoin(
|
||||
resourceHeaderAuth,
|
||||
eq(resourceHeaderAuth.resourceId, resources.resourceId)
|
||||
)
|
||||
.innerJoin(
|
||||
orgs,
|
||||
eq(orgs.orgId, resources.orgId)
|
||||
)
|
||||
.where(eq(resources.fullDomain, domain))
|
||||
.limit(1);
|
||||
|
||||
@@ -79,7 +66,9 @@ export async function getResourceByDomain(
|
||||
return {
|
||||
resource: result.resources,
|
||||
pincode: result.resourcePincode,
|
||||
password: result.resourcePassword
|
||||
password: result.resourcePassword,
|
||||
headerAuth: result.resourceHeaderAuth,
|
||||
org: result.orgs
|
||||
};
|
||||
}
|
||||
|
||||
@@ -89,27 +78,6 @@ export async function getResourceByDomain(
|
||||
export async function getUserSessionWithUser(
|
||||
userSessionId: string
|
||||
): Promise<UserSessionWithUser | null> {
|
||||
if (config.isManagedMode()) {
|
||||
try {
|
||||
const response = await axios.get(`${config.getRawConfig().managed?.endpoint}/api/v1/hybrid/session/${userSessionId}`, await tokenManager.getAuthHeader());
|
||||
return response.data.data;
|
||||
} catch (error) {
|
||||
if (axios.isAxiosError(error)) {
|
||||
logger.error("Error fetching config in verify session:", {
|
||||
message: error.message,
|
||||
code: error.code,
|
||||
status: error.response?.status,
|
||||
statusText: error.response?.statusText,
|
||||
url: error.config?.url,
|
||||
method: error.config?.method
|
||||
});
|
||||
} else {
|
||||
logger.error("Error fetching config in verify session:", error);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
const [res] = await db
|
||||
.select()
|
||||
.from(sessions)
|
||||
@@ -130,36 +98,10 @@ export async function getUserSessionWithUser(
|
||||
* Get user organization role
|
||||
*/
|
||||
export async function getUserOrgRole(userId: string, orgId: string) {
|
||||
if (config.isManagedMode()) {
|
||||
try {
|
||||
const response = await axios.get(`${config.getRawConfig().managed?.endpoint}/api/v1/hybrid/user/${userId}/org/${orgId}/role`, await tokenManager.getAuthHeader());
|
||||
return response.data.data;
|
||||
} catch (error) {
|
||||
if (axios.isAxiosError(error)) {
|
||||
logger.error("Error fetching config in verify session:", {
|
||||
message: error.message,
|
||||
code: error.code,
|
||||
status: error.response?.status,
|
||||
statusText: error.response?.statusText,
|
||||
url: error.config?.url,
|
||||
method: error.config?.method
|
||||
});
|
||||
} else {
|
||||
logger.error("Error fetching config in verify session:", error);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
const userOrgRole = await db
|
||||
.select()
|
||||
.from(userOrgs)
|
||||
.where(
|
||||
and(
|
||||
eq(userOrgs.userId, userId),
|
||||
eq(userOrgs.orgId, orgId)
|
||||
)
|
||||
)
|
||||
.where(and(eq(userOrgs.userId, userId), eq(userOrgs.orgId, orgId)))
|
||||
.limit(1);
|
||||
|
||||
return userOrgRole.length > 0 ? userOrgRole[0] : null;
|
||||
@@ -168,28 +110,10 @@ export async function getUserOrgRole(userId: string, orgId: string) {
|
||||
/**
|
||||
* Check if role has access to resource
|
||||
*/
|
||||
export async function getRoleResourceAccess(resourceId: number, roleId: number) {
|
||||
if (config.isManagedMode()) {
|
||||
try {
|
||||
const response = await axios.get(`${config.getRawConfig().managed?.endpoint}/api/v1/hybrid/role/${roleId}/resource/${resourceId}/access`, await tokenManager.getAuthHeader());
|
||||
return response.data.data;
|
||||
} catch (error) {
|
||||
if (axios.isAxiosError(error)) {
|
||||
logger.error("Error fetching config in verify session:", {
|
||||
message: error.message,
|
||||
code: error.code,
|
||||
status: error.response?.status,
|
||||
statusText: error.response?.statusText,
|
||||
url: error.config?.url,
|
||||
method: error.config?.method
|
||||
});
|
||||
} else {
|
||||
logger.error("Error fetching config in verify session:", error);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export async function getRoleResourceAccess(
|
||||
resourceId: number,
|
||||
roleId: number
|
||||
) {
|
||||
const roleResourceAccess = await db
|
||||
.select()
|
||||
.from(roleResources)
|
||||
@@ -207,28 +131,10 @@ export async function getRoleResourceAccess(resourceId: number, roleId: number)
|
||||
/**
|
||||
* Check if user has direct access to resource
|
||||
*/
|
||||
export async function getUserResourceAccess(userId: string, resourceId: number) {
|
||||
if (config.isManagedMode()) {
|
||||
try {
|
||||
const response = await axios.get(`${config.getRawConfig().managed?.endpoint}/api/v1/hybrid/user/${userId}/resource/${resourceId}/access`, await tokenManager.getAuthHeader());
|
||||
return response.data.data;
|
||||
} catch (error) {
|
||||
if (axios.isAxiosError(error)) {
|
||||
logger.error("Error fetching config in verify session:", {
|
||||
message: error.message,
|
||||
code: error.code,
|
||||
status: error.response?.status,
|
||||
statusText: error.response?.statusText,
|
||||
url: error.config?.url,
|
||||
method: error.config?.method
|
||||
});
|
||||
} else {
|
||||
logger.error("Error fetching config in verify session:", error);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export async function getUserResourceAccess(
|
||||
userId: string,
|
||||
resourceId: number
|
||||
) {
|
||||
const userResourceAccess = await db
|
||||
.select()
|
||||
.from(userResources)
|
||||
@@ -246,28 +152,9 @@ export async function getUserResourceAccess(userId: string, resourceId: number)
|
||||
/**
|
||||
* Get resource rules for a given resource
|
||||
*/
|
||||
export async function getResourceRules(resourceId: number): Promise<ResourceRule[]> {
|
||||
if (config.isManagedMode()) {
|
||||
try {
|
||||
const response = await axios.get(`${config.getRawConfig().managed?.endpoint}/api/v1/hybrid/resource/${resourceId}/rules`, await tokenManager.getAuthHeader());
|
||||
return response.data.data;
|
||||
} catch (error) {
|
||||
if (axios.isAxiosError(error)) {
|
||||
logger.error("Error fetching config in verify session:", {
|
||||
message: error.message,
|
||||
code: error.code,
|
||||
status: error.response?.status,
|
||||
statusText: error.response?.statusText,
|
||||
url: error.config?.url,
|
||||
method: error.config?.method
|
||||
});
|
||||
} else {
|
||||
logger.error("Error fetching config in verify session:", error);
|
||||
}
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
export async function getResourceRules(
|
||||
resourceId: number
|
||||
): Promise<ResourceRule[]> {
|
||||
const rules = await db
|
||||
.select()
|
||||
.from(resourceRules)
|
||||
@@ -275,3 +162,26 @@ export async function getResourceRules(resourceId: number): Promise<ResourceRule
|
||||
|
||||
return rules;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get organization login page
|
||||
*/
|
||||
export async function getOrgLoginPage(
|
||||
orgId: string
|
||||
): Promise<LoginPage | null> {
|
||||
const [result] = await db
|
||||
.select()
|
||||
.from(loginPageOrg)
|
||||
.where(eq(loginPageOrg.orgId, orgId))
|
||||
.innerJoin(
|
||||
loginPage,
|
||||
eq(loginPageOrg.loginPageId, loginPage.loginPageId)
|
||||
)
|
||||
.limit(1);
|
||||
|
||||
if (!result) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return result?.loginPage;
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user